You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by sh...@apache.org on 2022/07/31 05:28:27 UTC

[lucene] branch branch_9x updated: [LUCENE-10629]: Add fast match query support to FacetSets (#1015) (#1055)

This is an automated email from the ASF dual-hosted git repository.

shaie pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/lucene.git


The following commit(s) were added to refs/heads/branch_9x by this push:
     new d192cc0de24 [LUCENE-10629]: Add fast match query support to FacetSets (#1015) (#1055)
d192cc0de24 is described below

commit d192cc0de245cff1cdc0e9f9b52c146da7815241
Author: Shai Erera <se...@gmail.com>
AuthorDate: Sun Jul 31 08:28:20 2022 +0300

    [LUCENE-10629]: Add fast match query support to FacetSets (#1015) (#1055)
---
 lucene/CHANGES.txt                                 |   2 +-
 .../lucene/demo/facet/CustomFacetSetExample.java   |  83 +++++++++++++++
 .../demo/facet/TestCustomFacetSetExample.java      |  13 +++
 lucene/facet/docs/FacetSets.adoc                   |   9 +-
 .../lucene/facet/FacetCountsWithFilterQuery.java   |  91 +++++++++++++++++
 .../facet/facetset/MatchingFacetSetsCounts.java    |  25 ++++-
 .../lucene/facet/range/RangeFacetCounts.java       |  51 +---------
 .../facet/facetset/TestExactFacetSetMatcher.java   |  98 ++++++++++++++++++
 .../facet/facetset/TestRangeFacetSetMatcher.java   | 112 +++++++++++++++++++++
 9 files changed, 430 insertions(+), 54 deletions(-)

diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index d540a5c5256..af67906dd2f 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -11,7 +11,7 @@ API Changes
 
 New Features
 ---------------------
-(No changes)
+* LUCENE-10629: Support match set filtering with a query in MatchingFacetSetCounts. (Stefan Vodita, Shai Erera)
 
 Improvements
 ---------------------
diff --git a/lucene/demo/src/java/org/apache/lucene/demo/facet/CustomFacetSetExample.java b/lucene/demo/src/java/org/apache/lucene/demo/facet/CustomFacetSetExample.java
index c688c17d17f..b66196c3236 100644
--- a/lucene/demo/src/java/org/apache/lucene/demo/facet/CustomFacetSetExample.java
+++ b/lucene/demo/src/java/org/apache/lucene/demo/facet/CustomFacetSetExample.java
@@ -19,6 +19,7 @@ package org.apache.lucene.demo.facet;
 import java.io.IOException;
 import java.time.LocalDate;
 import java.time.ZoneOffset;
+import java.util.Arrays;
 import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
 import org.apache.lucene.document.Document;
 import org.apache.lucene.document.Field;
@@ -42,8 +43,12 @@ import org.apache.lucene.index.DirectoryReader;
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.index.IndexWriterConfig;
 import org.apache.lucene.index.IndexWriterConfig.OpenMode;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermInSetQuery;
 import org.apache.lucene.store.ByteBuffersDirectory;
 import org.apache.lucene.store.Directory;
 import org.apache.lucene.util.BytesRef;
@@ -85,6 +90,7 @@ public class CustomFacetSetExample {
             new TemperatureReadingFacetSet(MAY_SECOND_2022, HUNDRED_DEGREES),
             new TemperatureReadingFacetSet(JUNE_SECOND_2022, EIGHTY_DEGREES),
             new TemperatureReadingFacetSet(JULY_SECOND_2022, HUNDRED_TWENTY_DEGREES)));
+    addFastMatchFields(doc);
     indexWriter.addDocument(doc);
 
     doc = new Document();
@@ -95,11 +101,24 @@ public class CustomFacetSetExample {
             new TemperatureReadingFacetSet(MAY_SECOND_2022, EIGHTY_DEGREES),
             new TemperatureReadingFacetSet(JUNE_SECOND_2022, HUNDRED_DEGREES),
             new TemperatureReadingFacetSet(JULY_SECOND_2022, HUNDRED_TWENTY_DEGREES)));
+    addFastMatchFields(doc);
     indexWriter.addDocument(doc);
 
     indexWriter.close();
   }
 
+  private void addFastMatchFields(Document doc) {
+    // day field
+    doc.add(new StringField("day", String.valueOf(MAY_SECOND_2022), Field.Store.NO));
+    doc.add(new StringField("day", String.valueOf(JUNE_SECOND_2022), Field.Store.NO));
+    doc.add(new StringField("day", String.valueOf(JULY_SECOND_2022), Field.Store.NO));
+
+    // temp field
+    doc.add(new StringField("temp", String.valueOf(EIGHTY_DEGREES), Field.Store.NO));
+    doc.add(new StringField("temp", String.valueOf(HUNDRED_DEGREES), Field.Store.NO));
+    doc.add(new StringField("temp", String.valueOf(HUNDRED_TWENTY_DEGREES), Field.Store.NO));
+  }
+
   /** Counting documents which exactly match a given {@link FacetSet}. */
   private FacetResult exactMatching() throws IOException {
     try (DirectoryReader indexReader = DirectoryReader.open(indexDir)) {
@@ -128,6 +147,59 @@ public class CustomFacetSetExample {
     }
   }
 
+  /**
+   * Counting documents which exactly match a given {@link FacetSet}. This example also demonstrates
+   * how to use a fast match query to improve the counting efficiency by skipping over documents
+   * which cannot possibly match a set.
+   */
+  private FacetResult exactMatchingWithFastMatchQuery() throws IOException {
+    try (DirectoryReader indexReader = DirectoryReader.open(indexDir)) {
+      IndexSearcher searcher = new IndexSearcher(indexReader);
+
+      // MatchAllDocsQuery is for "browsing" (counts facets
+      // for all non-deleted docs in the index); normally
+      // you'd use a "normal" query:
+      FacetsCollector fc = searcher.search(new MatchAllDocsQuery(), new FacetsCollectorManager());
+
+      // Match documents whose "day" field is either "May 2022" or "July 2022"
+      Query dateQuery =
+          new TermInSetQuery(
+              "day",
+              Arrays.asList(
+                  new BytesRef(String.valueOf(MAY_SECOND_2022)),
+                  new BytesRef(String.valueOf(JULY_SECOND_2022))));
+      // Match documents whose "temp" field is either "80" or "120" degrees
+      Query temperatureQuery =
+          new TermInSetQuery(
+              "temp",
+              Arrays.asList(
+                  new BytesRef(String.valueOf(HUNDRED_DEGREES)),
+                  new BytesRef(String.valueOf(HUNDRED_TWENTY_DEGREES))));
+      // Documents must match both clauses
+      Query fastMatchQuery =
+          new BooleanQuery.Builder()
+              .add(dateQuery, BooleanClause.Occur.MUST)
+              .add(temperatureQuery, BooleanClause.Occur.MUST)
+              .build();
+
+      // Count both "May 2022, 100 degrees" and "July 2022, 120 degrees" dimensions
+      Facets facets =
+          new MatchingFacetSetsCounts(
+              "temperature",
+              fc,
+              TemperatureReadingFacetSet::decodeTemperatureReading,
+              fastMatchQuery,
+              new ExactFacetSetMatcher(
+                  "May 2022 (100f)",
+                  new TemperatureReadingFacetSet(MAY_SECOND_2022, HUNDRED_DEGREES)),
+              new ExactFacetSetMatcher(
+                  "July 2022 (120f)",
+                  new TemperatureReadingFacetSet(JULY_SECOND_2022, HUNDRED_TWENTY_DEGREES)));
+
+      // Retrieve results
+      return facets.getAllChildren("temperature");
+    }
+  }
   /** Counting documents which match a certain degrees value for any date. */
   private FacetResult rangeMatching() throws IOException {
     try (DirectoryReader indexReader = DirectoryReader.open(indexDir)) {
@@ -197,6 +269,12 @@ public class CustomFacetSetExample {
     return exactMatching();
   }
 
+  /** Runs the exact matching with fast match query example. */
+  public FacetResult runExactMatchingWithFastMatchQuery() throws IOException {
+    index();
+    return exactMatchingWithFastMatchQuery();
+  }
+
   /** Runs the range matching example. */
   public FacetResult runRangeMatching() throws IOException {
     index();
@@ -218,6 +296,11 @@ public class CustomFacetSetExample {
     FacetResult result = example.runExactMatching();
     System.out.println("Temperature Reading: " + result);
 
+    System.out.println("Exact Facet Set matching with fast match query example:");
+    System.out.println("-----------------------");
+    result = example.runExactMatchingWithFastMatchQuery();
+    System.out.println("Temperature Reading: " + result);
+
     System.out.println("Range Facet Set matching example:");
     System.out.println("-----------------------");
     result = example.runRangeMatching();
diff --git a/lucene/demo/src/test/org/apache/lucene/demo/facet/TestCustomFacetSetExample.java b/lucene/demo/src/test/org/apache/lucene/demo/facet/TestCustomFacetSetExample.java
index 31bacffa51c..eee950ec482 100644
--- a/lucene/demo/src/test/org/apache/lucene/demo/facet/TestCustomFacetSetExample.java
+++ b/lucene/demo/src/test/org/apache/lucene/demo/facet/TestCustomFacetSetExample.java
@@ -36,6 +36,19 @@ public class TestCustomFacetSetExample extends LuceneTestCase {
     assertEquals(new LabelAndValue("July 2022 (120f)", 2), result.labelValues[1]);
   }
 
+  @Test
+  public void testExactMatchingWithFastMatchQuery() throws Exception {
+    FacetResult result = new CustomFacetSetExample().runExactMatchingWithFastMatchQuery();
+
+    assertEquals("temperature", result.dim);
+    assertEquals(0, result.path.length);
+    assertEquals(2, result.value);
+    assertEquals(2, result.childCount);
+
+    assertEquals(new LabelAndValue("May 2022 (100f)", 1), result.labelValues[0]);
+    assertEquals(new LabelAndValue("July 2022 (120f)", 2), result.labelValues[1]);
+  }
+
   @Test
   public void testRangeMatching() throws Exception {
     FacetResult result = new CustomFacetSetExample().runRangeMatching();
diff --git a/lucene/facet/docs/FacetSets.adoc b/lucene/facet/docs/FacetSets.adoc
index 90ad52d7d93..c95a25b1bda 100644
--- a/lucene/facet/docs/FacetSets.adoc
+++ b/lucene/facet/docs/FacetSets.adoc
@@ -118,7 +118,14 @@ off-the-shelf `Int/FloatFacetSet` can use `FacetSetDecoder::decodeInts` and `Lon
 
 A `Facets` implementation which counts how many documents are matched by each of a list of `FacetSetMatcher`, in
 addition to counting the total number of documents that were matched. It uses the `FacetSetDecoder` to decode the
-dimension values.
+dimension values. When there are many possible facet set combinations, you can improve the counting efficiency by
+passing a `fastMatchQuery` which will skip over documents which cannot be matched by any of the facet set matchers,
+e.g. because the target dimension values were not indexed for a document at all. For instance, in the <<movie-actors>>
+example there are many possible combinations of indexed `genre` and `year` facet sets. If one only wants to count the
+combinations of "Thriller + 2010", "Drama + 2010" and "Comedy + 2010", one can pass a `fastMatchQuery` which will match
+documents whose `genre` field contains one of the genres and their `releaseYear` field contains the value "2010". That
+way documents which match none of the "genre" values or the desired "releaseYear", will not even be evaluated by the
+facet set matchers.
 
 == FacetSets Under the Hood
 
diff --git a/lucene/facet/src/java/org/apache/lucene/facet/FacetCountsWithFilterQuery.java b/lucene/facet/src/java/org/apache/lucene/facet/FacetCountsWithFilterQuery.java
new file mode 100644
index 00000000000..9d3ff5aefdd
--- /dev/null
+++ b/lucene/facet/src/java/org/apache/lucene/facet/FacetCountsWithFilterQuery.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.facet;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import org.apache.lucene.index.IndexReaderContext;
+import org.apache.lucene.index.ReaderUtil;
+import org.apache.lucene.search.ConjunctionUtils;
+import org.apache.lucene.search.DocIdSetIterator;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.ScoreMode;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.Weight;
+
+/**
+ * Base class for facet counts. It allows for a query to be passed in to filter the match set.
+ *
+ * @lucene.experimental
+ */
+public abstract class FacetCountsWithFilterQuery extends Facets {
+
+  /**
+   * Optional: if specified, we first test this Query to see whether the document should be checked
+   * for matching ranges. If this is null, all documents are checked.
+   */
+  protected final Query fastMatchQuery;
+
+  /** Create {@code FacetCounts} */
+  protected FacetCountsWithFilterQuery(Query fastMatchQuery) {
+    this.fastMatchQuery = fastMatchQuery;
+  }
+
+  /**
+   * Create a {@link DocIdSetIterator} from the provided {@code hits} that relies on {@code
+   * fastMatchQuery} if available for first-pass filtering. If {@code iterators} is not empty then
+   * all iterators are intersected. If any of the iterators is null, it indicates no documents will
+   * be matched by it, and therefore no documents will be matched overall. A null response indicates
+   * no documents will match.
+   */
+  protected DocIdSetIterator createIterator(
+      FacetsCollector.MatchingDocs hits, DocIdSetIterator... iterators) throws IOException {
+    List<DocIdSetIterator> allIterators = new ArrayList<>();
+    allIterators.add(hits.bits.iterator());
+    allIterators.addAll(Arrays.asList(iterators));
+    if (allIterators.stream().anyMatch(Objects::isNull)) {
+      // if any of the iterators are null, there are no matching docs
+      return null;
+    }
+
+    if (fastMatchQuery != null) {
+      final IndexReaderContext topLevelContext = ReaderUtil.getTopLevelContext(hits.context);
+      final IndexSearcher searcher = new IndexSearcher(topLevelContext);
+      searcher.setQueryCache(null);
+      final Weight fastMatchWeight =
+          searcher.createWeight(searcher.rewrite(fastMatchQuery), ScoreMode.COMPLETE_NO_SCORES, 1);
+      final Scorer s = fastMatchWeight.scorer(hits.context);
+      DocIdSetIterator fastMatchQueryIterator = s.iterator();
+      if (fastMatchQueryIterator == null) {
+        // no matching docs by the fast match query
+        return null;
+      } else {
+        allIterators.add(fastMatchQueryIterator);
+      }
+    }
+
+    if (allIterators.size() == 1) {
+      return allIterators.get(0);
+    } else {
+      return ConjunctionUtils.intersectIterators(allIterators);
+    }
+  }
+}
diff --git a/lucene/facet/src/java/org/apache/lucene/facet/facetset/MatchingFacetSetsCounts.java b/lucene/facet/src/java/org/apache/lucene/facet/facetset/MatchingFacetSetsCounts.java
index 07a61d69e53..e6c8e67e288 100644
--- a/lucene/facet/src/java/org/apache/lucene/facet/facetset/MatchingFacetSetsCounts.java
+++ b/lucene/facet/src/java/org/apache/lucene/facet/facetset/MatchingFacetSetsCounts.java
@@ -21,14 +21,14 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import org.apache.lucene.document.IntPoint;
+import org.apache.lucene.facet.FacetCountsWithFilterQuery;
 import org.apache.lucene.facet.FacetResult;
-import org.apache.lucene.facet.Facets;
 import org.apache.lucene.facet.FacetsCollector;
 import org.apache.lucene.facet.LabelAndValue;
 import org.apache.lucene.index.BinaryDocValues;
 import org.apache.lucene.index.DocValues;
-import org.apache.lucene.search.ConjunctionUtils;
 import org.apache.lucene.search.DocIdSetIterator;
+import org.apache.lucene.search.Query;
 import org.apache.lucene.util.BytesRef;
 
 /**
@@ -36,7 +36,7 @@ import org.apache.lucene.util.BytesRef;
  *
  * @lucene.experimental
  */
-public class MatchingFacetSetsCounts extends Facets {
+public class MatchingFacetSetsCounts extends FacetCountsWithFilterQuery {
 
   private final FacetSetMatcher[] facetSetMatchers;
   private final int[] counts;
@@ -54,6 +54,22 @@ public class MatchingFacetSetsCounts extends Facets {
       FacetSetDecoder facetSetDecoder,
       FacetSetMatcher... facetSetMatchers)
       throws IOException {
+    this(field, hits, facetSetDecoder, null, facetSetMatchers);
+  }
+
+  /**
+   * Constructs a new instance of matching facet set counts which calculates the counts for each
+   * given facet set matcher. If {@code fastMatchQuery} is not {@code null}, then only documents
+   * which are matched by it will be counted.
+   */
+  public MatchingFacetSetsCounts(
+      String field,
+      FacetsCollector hits,
+      FacetSetDecoder facetSetDecoder,
+      Query fastMatchQuery,
+      FacetSetMatcher... facetSetMatchers)
+      throws IOException {
+    super(fastMatchQuery);
     if (facetSetMatchers == null || facetSetMatchers.length == 0) {
       throw new IllegalArgumentException("facetSetMatchers cannot be null or empty");
     }
@@ -76,8 +92,7 @@ public class MatchingFacetSetsCounts extends Facets {
 
       BinaryDocValues binaryDocValues = DocValues.getBinary(hits.context.reader(), field);
 
-      final DocIdSetIterator it =
-          ConjunctionUtils.intersectIterators(Arrays.asList(hits.bits.iterator(), binaryDocValues));
+      final DocIdSetIterator it = createIterator(hits, binaryDocValues);
       if (it == null) {
         continue;
       }
diff --git a/lucene/facet/src/java/org/apache/lucene/facet/range/RangeFacetCounts.java b/lucene/facet/src/java/org/apache/lucene/facet/range/RangeFacetCounts.java
index 1e93ad802b0..df227941813 100644
--- a/lucene/facet/src/java/org/apache/lucene/facet/range/RangeFacetCounts.java
+++ b/lucene/facet/src/java/org/apache/lucene/facet/range/RangeFacetCounts.java
@@ -17,44 +17,30 @@
 package org.apache.lucene.facet.range;
 
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import org.apache.lucene.facet.FacetCountsWithFilterQuery;
 import org.apache.lucene.facet.FacetResult;
-import org.apache.lucene.facet.Facets;
 import org.apache.lucene.facet.FacetsCollector;
 import org.apache.lucene.facet.LabelAndValue;
 import org.apache.lucene.index.DocValues;
-import org.apache.lucene.index.IndexReaderContext;
 import org.apache.lucene.index.NumericDocValues;
-import org.apache.lucene.index.ReaderUtil;
 import org.apache.lucene.index.SortedNumericDocValues;
-import org.apache.lucene.search.ConjunctionUtils;
 import org.apache.lucene.search.DocIdSetIterator;
-import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
-import org.apache.lucene.search.ScoreMode;
-import org.apache.lucene.search.Scorer;
-import org.apache.lucene.search.Weight;
 
 /**
  * Base class for range faceting.
  *
  * @lucene.experimental
  */
-abstract class RangeFacetCounts extends Facets {
+abstract class RangeFacetCounts extends FacetCountsWithFilterQuery {
   /** Ranges passed to constructor. */
   protected final Range[] ranges;
 
   /** Counts, initialized in by subclass. */
   protected final int[] counts;
 
-  /**
-   * Optional: if specified, we first test this Query to see whether the document should be checked
-   * for matching ranges. If this is null, all documents are checked.
-   */
-  protected final Query fastMatchQuery;
-
   /** Our field name. */
   protected final String field;
 
@@ -62,42 +48,13 @@ abstract class RangeFacetCounts extends Facets {
   protected int totCount;
 
   /** Create {@code RangeFacetCounts} */
-  protected RangeFacetCounts(String field, Range[] ranges, Query fastMatchQuery)
-      throws IOException {
+  protected RangeFacetCounts(String field, Range[] ranges, Query fastMatchQuery) {
+    super(fastMatchQuery);
     this.field = field;
     this.ranges = ranges;
-    this.fastMatchQuery = fastMatchQuery;
     counts = new int[ranges.length];
   }
 
-  /**
-   * Create a {@link org.apache.lucene.search.DocIdSetIterator} from the provided {@code hits} that
-   * relies on {@code fastMatchQuery} if available for first-pass filtering. A null response
-   * indicates no documents will match.
-   */
-  protected DocIdSetIterator createIterator(FacetsCollector.MatchingDocs hits) throws IOException {
-
-    if (fastMatchQuery != null) {
-
-      final IndexReaderContext topLevelContext = ReaderUtil.getTopLevelContext(hits.context);
-      final IndexSearcher searcher = new IndexSearcher(topLevelContext);
-      searcher.setQueryCache(null);
-      final Weight fastMatchWeight =
-          searcher.createWeight(searcher.rewrite(fastMatchQuery), ScoreMode.COMPLETE_NO_SCORES, 1);
-      final Scorer s = fastMatchWeight.scorer(hits.context);
-      if (s == null) {
-        return null; // no hits from the fastMatchQuery; return null
-      } else {
-        DocIdSetIterator fastMatchDocs = s.iterator();
-        return ConjunctionUtils.intersectIterators(
-            Arrays.asList(hits.bits.iterator(), fastMatchDocs));
-      }
-
-    } else {
-      return hits.bits.iterator();
-    }
-  }
-
   protected abstract LongRange[] getLongRanges();
 
   protected long mapDocValue(long l) {
diff --git a/lucene/facet/src/test/org/apache/lucene/facet/facetset/TestExactFacetSetMatcher.java b/lucene/facet/src/test/org/apache/lucene/facet/facetset/TestExactFacetSetMatcher.java
index 9751822273a..3fbb86503ce 100644
--- a/lucene/facet/src/test/org/apache/lucene/facet/facetset/TestExactFacetSetMatcher.java
+++ b/lucene/facet/src/test/org/apache/lucene/facet/facetset/TestExactFacetSetMatcher.java
@@ -17,16 +17,25 @@
 package org.apache.lucene.facet.facetset;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.StringField;
 import org.apache.lucene.facet.*;
 import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermInSetQuery;
 import org.apache.lucene.store.Directory;
 import org.apache.lucene.tests.index.RandomIndexWriter;
 import org.apache.lucene.tests.util.TestUtil;
+import org.apache.lucene.util.BytesRef;
 
 public class TestExactFacetSetMatcher extends FacetTestCase {
 
@@ -330,4 +339,93 @@ public class TestExactFacetSetMatcher extends FacetTestCase {
     r.close();
     d.close();
   }
+
+  public void testLongFacetSetMatchingWithFastMatchQuery() throws Exception {
+    Directory d = newDirectory();
+    RandomIndexWriter w = new RandomIndexWriter(random(), d);
+
+    List<LongFacetSet> allSets = new ArrayList<>();
+    for (int manufacturerOrd : MANUFACTURER_ORDS) {
+      for (int year : YEARS) {
+        allSets.add(new LongFacetSet(manufacturerOrd, year));
+      }
+    }
+
+    int numFord2010 = 0;
+    int numChevy2011 = 0;
+    int numMatchingDocs = 0;
+    for (int i = 0; i < 100; i++) {
+      Document doc = new Document();
+      int numSets = TestUtil.nextInt(random(), 1, 4);
+      Collections.shuffle(allSets, random());
+      LongFacetSet[] facetSets = allSets.subList(0, numSets).toArray(LongFacetSet[]::new);
+      boolean matchingDoc = false;
+      for (LongFacetSet facetSet : facetSets) {
+        if (FORD_ORD == facetSet.values[0] && facetSet.values[1] == 2010) {
+          ++numFord2010;
+          matchingDoc = true;
+        } else if (CHEVY_ORD == facetSet.values[0] && facetSet.values[1] == 2011) {
+          ++numChevy2011;
+          matchingDoc = true;
+        }
+      }
+      numMatchingDocs += matchingDoc ? 1 : 0;
+      doc.add(FacetSetsField.create("field", facetSets)); // field for aggregation
+      // add fields for drill-down + fast matching
+      addFastMatchField("manufacturer", doc, facetSets, 0);
+      addFastMatchField("year", doc, facetSets, 1);
+      w.addDocument(doc);
+    }
+
+    IndexReader r = w.getReader();
+    w.close();
+
+    IndexSearcher s = newSearcher(r);
+    FacetsCollector fc = s.search(new MatchAllDocsQuery(), new FacetsCollectorManager());
+
+    Query fastMatchQuery =
+        new BooleanQuery.Builder()
+            .add(
+                createFastMatchQuery("manufacturer", FORD_ORD, CHEVY_ORD), BooleanClause.Occur.MUST)
+            .add(createFastMatchQuery("year", 2010, 2011), BooleanClause.Occur.MUST)
+            .build();
+    Facets facets =
+        new MatchingFacetSetsCounts(
+            "field",
+            fc,
+            FacetSetDecoder::decodeLongs,
+            fastMatchQuery,
+            new ExactFacetSetMatcher("Ford 2010", new LongFacetSet(FORD_ORD, 2010)),
+            new ExactFacetSetMatcher("Chevy 2011", new LongFacetSet(CHEVY_ORD, 2011)));
+
+    FacetResult result = facets.getAllChildren("field");
+
+    assertEquals("field", result.dim);
+    assertEquals(0, result.path.length);
+    assertEquals(numMatchingDocs, result.value);
+    assertEquals(2, result.childCount);
+
+    assertEquals(new LabelAndValue("Ford 2010", numFord2010), result.labelValues[0]);
+    assertEquals(new LabelAndValue("Chevy 2011", numChevy2011), result.labelValues[1]);
+
+    r.close();
+    d.close();
+  }
+
+  private static Query createFastMatchQuery(String field, int... values) {
+    return new TermInSetQuery(
+        field,
+        Arrays.stream(values)
+            .mapToObj(String::valueOf)
+            .map(BytesRef::new)
+            .collect(Collectors.toList()));
+  }
+
+  private static void addFastMatchField(
+      String field, Document doc, LongFacetSet[] facetSets, int index) {
+    Arrays.stream(facetSets)
+        .map(facetSet -> facetSet.values[index])
+        .distinct()
+        .forEach(value -> doc.add(new StringField(field, String.valueOf(value), Field.Store.NO)));
+  }
 }
diff --git a/lucene/facet/src/test/org/apache/lucene/facet/facetset/TestRangeFacetSetMatcher.java b/lucene/facet/src/test/org/apache/lucene/facet/facetset/TestRangeFacetSetMatcher.java
index ec055d900df..fcae6841691 100644
--- a/lucene/facet/src/test/org/apache/lucene/facet/facetset/TestRangeFacetSetMatcher.java
+++ b/lucene/facet/src/test/org/apache/lucene/facet/facetset/TestRangeFacetSetMatcher.java
@@ -17,13 +17,19 @@
 package org.apache.lucene.facet.facetset;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import org.apache.lucene.document.Document;
+import org.apache.lucene.document.LongPoint;
+import org.apache.lucene.document.LongRange;
 import org.apache.lucene.facet.*;
 import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.Query;
 import org.apache.lucene.store.Directory;
 import org.apache.lucene.tests.index.RandomIndexWriter;
 import org.apache.lucene.tests.util.TestUtil;
@@ -389,6 +395,99 @@ public class TestRangeFacetSetMatcher extends FacetTestCase {
     d.close();
   }
 
+  public void testLongRangeFacetSetMatchingWithFastMatchQuery() throws Exception {
+    Directory d = newDirectory();
+    RandomIndexWriter w = new RandomIndexWriter(random(), d);
+
+    List<LongFacetSet> allSets = new ArrayList<>();
+    for (int manufacturerOrd : MANUFACTURER_ORDS) {
+      for (int year : YEARS) {
+        allSets.add(new LongFacetSet(manufacturerOrd, year));
+      }
+    }
+
+    int numFord2011_2013 = 0;
+    int numFord2010_2014 = 0;
+    int numFord2011_2014 = 0;
+    int numFord2010_2013 = 0;
+    int numMatchingDocs = 0;
+    for (int i = 0; i < 100; i++) {
+      Document doc = new Document();
+      int numSets = TestUtil.nextInt(random(), 1, 4);
+      Collections.shuffle(allSets, random());
+      LongFacetSet[] facetSets = allSets.subList(0, numSets).toArray(LongFacetSet[]::new);
+      boolean matchingDoc = false;
+      for (LongFacetSet facetSet : facetSets) {
+        if (FORD_ORD != facetSet.values[0]) {
+          continue;
+        }
+        long year = facetSet.values[1];
+        if (year > 2010 && year < 2014) {
+          ++numFord2010_2013;
+          ++numFord2010_2014;
+          ++numFord2011_2013;
+          ++numFord2011_2014;
+          matchingDoc = true;
+        } else if (year == 2014) {
+          ++numFord2010_2014;
+          ++numFord2011_2014;
+          matchingDoc = true;
+        } else if (year == 2010) {
+          ++numFord2010_2014;
+          ++numFord2010_2013;
+          matchingDoc = true;
+        }
+      }
+      numMatchingDocs += matchingDoc ? 1 : 0;
+      doc.add(FacetSetsField.create("field", facetSets));
+      // add fields for drill-down + fast matching
+      addFastMatchField("manufacturer", doc, facetSets, 0);
+      addFastMatchField("year", doc, facetSets, 1);
+      w.addDocument(doc);
+    }
+
+    IndexReader r = w.getReader();
+    w.close();
+
+    IndexSearcher s = newSearcher(r);
+    FacetsCollector fc = s.search(new MatchAllDocsQuery(), new FacetsCollectorManager());
+
+    Query fastMatchQuery =
+        new BooleanQuery.Builder()
+            .add(createFastMatchQuery("manufacturer", FORD_ORD, FORD_ORD), BooleanClause.Occur.MUST)
+            .add(createFastMatchQuery("year", 2010, 2014), BooleanClause.Occur.MUST)
+            .build();
+    Facets facets =
+        new MatchingFacetSetsCounts(
+            "field",
+            fc,
+            FacetSetDecoder::decodeLongs,
+            fastMatchQuery,
+            new RangeFacetSetMatcher(
+                "Ford [2010-2014]", singleLong(FORD_ORD), longRange(2010, true, 2014, true)),
+            new RangeFacetSetMatcher(
+                "Ford (2010-2014]", singleLong(FORD_ORD), longRange(2010, false, 2014, true)),
+            new RangeFacetSetMatcher(
+                "Ford [2010-2014)", singleLong(FORD_ORD), longRange(2010, true, 2014, false)),
+            new RangeFacetSetMatcher(
+                "Ford (2010-2014)", singleLong(FORD_ORD), longRange(2010, false, 2014, false)));
+
+    FacetResult result = facets.getAllChildren("field");
+
+    assertEquals("field", result.dim);
+    assertEquals(0, result.path.length);
+    assertEquals(numMatchingDocs, result.value);
+    assertEquals(4, result.childCount);
+
+    assertEquals(new LabelAndValue("Ford [2010-2014]", numFord2010_2014), result.labelValues[0]);
+    assertEquals(new LabelAndValue("Ford (2010-2014]", numFord2011_2014), result.labelValues[1]);
+    assertEquals(new LabelAndValue("Ford [2010-2014)", numFord2010_2013), result.labelValues[2]);
+    assertEquals(new LabelAndValue("Ford (2010-2014)", numFord2011_2013), result.labelValues[3]);
+
+    r.close();
+    d.close();
+  }
+
   private static DimRange singleLong(long value) {
     return DimRange.fromLongs(value, true, value, true);
   }
@@ -415,4 +514,17 @@ public class TestRangeFacetSetMatcher extends FacetTestCase {
       float min, boolean minExclusive, float max, boolean maxExclusive) {
     return DimRange.fromFloats(min, minExclusive, max, maxExclusive);
   }
+
+  private static Query createFastMatchQuery(String field, long min, long max) {
+    return LongRange.newIntersectsQuery(field, new long[] {min}, new long[] {max});
+  }
+
+  private static void addFastMatchField(
+      String field, Document doc, LongFacetSet[] facetSets, int index) {
+    long min =
+        Arrays.stream(facetSets).mapToLong(facetSet -> facetSet.values[index]).min().orElseThrow();
+    long max =
+        Arrays.stream(facetSets).mapToLong(facetSet -> facetSet.values[index]).max().orElseThrow();
+    doc.add(new LongPoint(field, min, max));
+  }
 }