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));
+ }
}