diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 0f373ba4235..81a5d60386b 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -54,9 +54,9 @@ New Features queries, and supports two-phased iterators to avoid loading positions when possible. (Paul Elschot via Robert Muir) -* LUCENE-6352: Added a new query time join to the join module that uses - global ordinals, which is faster for subsequent joins between reopens. - (Martijn van Groningen, Adrien Grand) +* LUCENE-6352, LUCENE-6472: Added a new query time join to the join module + that uses global ordinals, which is faster for subsequent joins between + reopens. (Martijn van Groningen, Adrien Grand) * LUCENE-5879: Added experimental auto-prefix terms to BlockTree terms dictionary, exposed as AutoPrefixPostingsFormat (Adrien Grand, diff --git a/lucene/join/src/java/org/apache/lucene/search/join/BaseGlobalOrdinalScorer.java b/lucene/join/src/java/org/apache/lucene/search/join/BaseGlobalOrdinalScorer.java index 4d81d58f92a..e04e275eca9 100644 --- a/lucene/join/src/java/org/apache/lucene/search/join/BaseGlobalOrdinalScorer.java +++ b/lucene/join/src/java/org/apache/lucene/search/join/BaseGlobalOrdinalScorer.java @@ -28,15 +28,13 @@ import java.io.IOException; abstract class BaseGlobalOrdinalScorer extends Scorer { - final LongBitSet foundOrds; final SortedDocValues values; final Scorer approximationScorer; float score; - public BaseGlobalOrdinalScorer(Weight weight, LongBitSet foundOrds, SortedDocValues values, Scorer approximationScorer) { + public BaseGlobalOrdinalScorer(Weight weight, SortedDocValues values, Scorer approximationScorer) { super(weight); - this.foundOrds = foundOrds; this.values = values; this.approximationScorer = approximationScorer; } diff --git a/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsQuery.java b/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsQuery.java index f292f53e9b4..19b908f7902 100644 --- a/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsQuery.java +++ b/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsQuery.java @@ -160,11 +160,13 @@ final class GlobalOrdinalsQuery extends Query { final static class OrdinalMapScorer extends BaseGlobalOrdinalScorer { + final LongBitSet foundOrds; final LongValues segmentOrdToGlobalOrdLookup; public OrdinalMapScorer(Weight weight, float score, LongBitSet foundOrds, SortedDocValues values, Scorer approximationScorer, LongValues segmentOrdToGlobalOrdLookup) { - super(weight, foundOrds, values, approximationScorer); + super(weight, values, approximationScorer); this.score = score; + this.foundOrds = foundOrds; this.segmentOrdToGlobalOrdLookup = segmentOrdToGlobalOrdLookup; } @@ -203,9 +205,12 @@ final class GlobalOrdinalsQuery extends Query { final static class SegmentOrdinalScorer extends BaseGlobalOrdinalScorer { + final LongBitSet foundOrds; + public SegmentOrdinalScorer(Weight weight, float score, LongBitSet foundOrds, SortedDocValues values, Scorer approximationScorer) { - super(weight, foundOrds, values, approximationScorer); + super(weight, values, approximationScorer); this.score = score; + this.foundOrds = foundOrds; } @Override diff --git a/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsWithScoreCollector.java b/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsWithScoreCollector.java index 9252b56b12c..b02d6e59dfd 100644 --- a/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsWithScoreCollector.java +++ b/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsWithScoreCollector.java @@ -33,23 +33,48 @@ import java.util.Arrays; abstract class GlobalOrdinalsWithScoreCollector implements Collector { final String field; + final boolean doMinMax; + final int min; + final int max; final MultiDocValues.OrdinalMap ordinalMap; final LongBitSet collectedOrds; - protected final Scores scores; - GlobalOrdinalsWithScoreCollector(String field, MultiDocValues.OrdinalMap ordinalMap, long valueCount) { + protected final Scores scores; + protected final Occurrences occurrences; + + GlobalOrdinalsWithScoreCollector(String field, MultiDocValues.OrdinalMap ordinalMap, long valueCount, ScoreMode scoreMode, int min, int max) { if (valueCount > Integer.MAX_VALUE) { // We simply don't support more than throw new IllegalStateException("Can't collect more than [" + Integer.MAX_VALUE + "] ids"); } this.field = field; + this.doMinMax = !(min <= 0 && max == Integer.MAX_VALUE); + this.min = min; + this.max = max;; this.ordinalMap = ordinalMap; this.collectedOrds = new LongBitSet(valueCount); - this.scores = new Scores(valueCount, unset()); + if (scoreMode != ScoreMode.None) { + this.scores = new Scores(valueCount, unset()); + } else { + this.scores = null; + } + if (scoreMode == ScoreMode.Avg || doMinMax) { + this.occurrences = new Occurrences(valueCount); + } else { + this.occurrences = null; + } } - public LongBitSet getCollectorOrdinals() { - return collectedOrds; + public boolean match(int globalOrd) { + if (collectedOrds.get(globalOrd)) { + if (doMinMax) { + final int occurrence = occurrences.getOccurrence(globalOrd); + return occurrence >= min && occurrence <= max; + } else { + return true; + } + } + return false; } public float score(int globalOrdinal) { @@ -96,6 +121,9 @@ abstract class GlobalOrdinalsWithScoreCollector implements Collector { float existingScore = scores.getScore(globalOrd); float newScore = scorer.score(); doScore(globalOrd, existingScore, newScore); + if (occurrences != null) { + occurrences.increment(globalOrd); + } } } @@ -122,6 +150,9 @@ abstract class GlobalOrdinalsWithScoreCollector implements Collector { float existingScore = scores.getScore(segmentOrd); float newScore = scorer.score(); doScore(segmentOrd, existingScore, newScore); + if (occurrences != null) { + occurrences.increment(segmentOrd); + } } } @@ -133,8 +164,8 @@ abstract class GlobalOrdinalsWithScoreCollector implements Collector { static final class Min extends GlobalOrdinalsWithScoreCollector { - public Min(String field, MultiDocValues.OrdinalMap ordinalMap, long valueCount) { - super(field, ordinalMap, valueCount); + public Min(String field, MultiDocValues.OrdinalMap ordinalMap, long valueCount, int min, int max) { + super(field, ordinalMap, valueCount, ScoreMode.Min, min, max); } @Override @@ -150,8 +181,8 @@ abstract class GlobalOrdinalsWithScoreCollector implements Collector { static final class Max extends GlobalOrdinalsWithScoreCollector { - public Max(String field, MultiDocValues.OrdinalMap ordinalMap, long valueCount) { - super(field, ordinalMap, valueCount); + public Max(String field, MultiDocValues.OrdinalMap ordinalMap, long valueCount, int min, int max) { + super(field, ordinalMap, valueCount, ScoreMode.Max, min, max); } @Override @@ -167,8 +198,8 @@ abstract class GlobalOrdinalsWithScoreCollector implements Collector { static final class Sum extends GlobalOrdinalsWithScoreCollector { - public Sum(String field, MultiDocValues.OrdinalMap ordinalMap, long valueCount) { - super(field, ordinalMap, valueCount); + public Sum(String field, MultiDocValues.OrdinalMap ordinalMap, long valueCount, int min, int max) { + super(field, ordinalMap, valueCount, ScoreMode.Total, min, max); } @Override @@ -184,16 +215,12 @@ abstract class GlobalOrdinalsWithScoreCollector implements Collector { static final class Avg extends GlobalOrdinalsWithScoreCollector { - private final Occurrences occurrences; - - public Avg(String field, MultiDocValues.OrdinalMap ordinalMap, long valueCount) { - super(field, ordinalMap, valueCount); - this.occurrences = new Occurrences(valueCount); + public Avg(String field, MultiDocValues.OrdinalMap ordinalMap, long valueCount, int min, int max) { + super(field, ordinalMap, valueCount, ScoreMode.Avg, min, max); } @Override protected void doScore(int globalOrd, float existingScore, float newScore) { - occurrences.increment(globalOrd); scores.setScore(globalOrd, existingScore + newScore); } @@ -208,6 +235,71 @@ abstract class GlobalOrdinalsWithScoreCollector implements Collector { } } + static final class NoScore extends GlobalOrdinalsWithScoreCollector { + + public NoScore(String field, MultiDocValues.OrdinalMap ordinalMap, long valueCount, int min, int max) { + super(field, ordinalMap, valueCount, ScoreMode.None, min, max); + } + + @Override + public LeafCollector getLeafCollector(LeafReaderContext context) throws IOException { + SortedDocValues docTermOrds = DocValues.getSorted(context.reader(), field); + if (ordinalMap != null) { + LongValues segmentOrdToGlobalOrdLookup = ordinalMap.getGlobalOrds(context.ord); + return new LeafCollector() { + + @Override + public void setScorer(Scorer scorer) throws IOException { + } + + @Override + public void collect(int doc) throws IOException { + final long segmentOrd = docTermOrds.getOrd(doc); + if (segmentOrd != -1) { + final int globalOrd = (int) segmentOrdToGlobalOrdLookup.get(segmentOrd); + collectedOrds.set(globalOrd); + occurrences.increment(globalOrd); + } + } + }; + } else { + return new LeafCollector() { + @Override + public void setScorer(Scorer scorer) throws IOException { + } + + @Override + public void collect(int doc) throws IOException { + final int segmentOrd = docTermOrds.getOrd(doc); + if (segmentOrd != -1) { + collectedOrds.set(segmentOrd); + occurrences.increment(segmentOrd); + } + } + }; + } + } + + @Override + protected void doScore(int globalOrd, float existingScore, float newScore) { + } + + @Override + public float score(int globalOrdinal) { + return 1f; + } + + @Override + protected float unset() { + return 0f; + } + + @Override + public boolean needsScores() { + return false; + } + } + // Because the global ordinal is directly used as a key to a score we should be somewhat smart about allocation // the scores array. Most of the times not all docs match so splitting the scores array up in blocks can prevent creation of huge arrays. // Also working with smaller arrays is supposed to be more gc friendly diff --git a/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsWithScoreQuery.java b/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsWithScoreQuery.java index 093475b8f53..c1bcb646063 100644 --- a/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsWithScoreQuery.java +++ b/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsWithScoreQuery.java @@ -17,9 +17,6 @@ package org.apache.lucene.search.join; * limitations under the License. */ -import java.io.IOException; -import java.util.Set; - import org.apache.lucene.index.DocValues; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.LeafReaderContext; @@ -37,6 +34,9 @@ import org.apache.lucene.util.Bits; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.LongValues; +import java.io.IOException; +import java.util.Set; + final class GlobalOrdinalsWithScoreQuery extends Query { private final GlobalOrdinalsWithScoreCollector collector; @@ -47,14 +47,18 @@ final class GlobalOrdinalsWithScoreQuery extends Query { // just for hashcode and equals: private final Query fromQuery; + private final int min; + private final int max; private final IndexReader indexReader; - GlobalOrdinalsWithScoreQuery(GlobalOrdinalsWithScoreCollector collector, String joinField, MultiDocValues.OrdinalMap globalOrds, Query toQuery, Query fromQuery, IndexReader indexReader) { + GlobalOrdinalsWithScoreQuery(GlobalOrdinalsWithScoreCollector collector, String joinField, MultiDocValues.OrdinalMap globalOrds, Query toQuery, Query fromQuery, int min, int max, IndexReader indexReader) { this.collector = collector; this.joinField = joinField; this.globalOrds = globalOrds; this.toQuery = toQuery; this.fromQuery = fromQuery; + this.min = min; + this.max = max; this.indexReader = indexReader; } @@ -71,8 +75,10 @@ final class GlobalOrdinalsWithScoreQuery extends Query { GlobalOrdinalsWithScoreQuery that = (GlobalOrdinalsWithScoreQuery) o; - if (!fromQuery.equals(that.fromQuery)) return false; + if (min != that.min) return false; + if (max != that.max) return false; if (!joinField.equals(that.joinField)) return false; + if (!fromQuery.equals(that.fromQuery)) return false; if (!toQuery.equals(that.toQuery)) return false; if (!indexReader.equals(that.indexReader)) return false; @@ -85,6 +91,8 @@ final class GlobalOrdinalsWithScoreQuery extends Query { result = 31 * result + joinField.hashCode(); result = 31 * result + toQuery.hashCode(); result = 31 * result + fromQuery.hashCode(); + result = 31 * result + min; + result = 31 * result + max; result = 31 * result + indexReader.hashCode(); return result; } @@ -92,7 +100,10 @@ final class GlobalOrdinalsWithScoreQuery extends Query { @Override public String toString(String field) { return "GlobalOrdinalsQuery{" + - "joinField=" + joinField + + "joinField=" + joinField + + "min=" + min + + "max=" + max + + "fromQuery=" + fromQuery + '}'; } @@ -168,7 +179,7 @@ final class GlobalOrdinalsWithScoreQuery extends Query { final GlobalOrdinalsWithScoreCollector collector; public OrdinalMapScorer(Weight weight, GlobalOrdinalsWithScoreCollector collector, SortedDocValues values, Scorer approximationScorer, LongValues segmentOrdToGlobalOrdLookup) { - super(weight, collector.getCollectorOrdinals(), values, approximationScorer); + super(weight, values, approximationScorer); this.segmentOrdToGlobalOrdLookup = segmentOrdToGlobalOrdLookup; this.collector = collector; } @@ -178,9 +189,9 @@ final class GlobalOrdinalsWithScoreQuery extends Query { for (int docID = approximationScorer.advance(target); docID < NO_MORE_DOCS; docID = approximationScorer.nextDoc()) { final long segmentOrd = values.getOrd(docID); if (segmentOrd != -1) { - final long globalOrd = segmentOrdToGlobalOrdLookup.get(segmentOrd); - if (foundOrds.get(globalOrd)) { - score = collector.score((int) globalOrd); + final int globalOrd = (int) segmentOrdToGlobalOrdLookup.get(segmentOrd); + if (collector.match(globalOrd)) { + score = collector.score(globalOrd); return docID; } } @@ -196,9 +207,9 @@ final class GlobalOrdinalsWithScoreQuery extends Query { public boolean matches() throws IOException { final long segmentOrd = values.getOrd(approximationScorer.docID()); if (segmentOrd != -1) { - final long globalOrd = segmentOrdToGlobalOrdLookup.get(segmentOrd); - if (foundOrds.get(globalOrd)) { - score = collector.score((int) globalOrd); + final int globalOrd = (int) segmentOrdToGlobalOrdLookup.get(segmentOrd); + if (collector.match(globalOrd)) { + score = collector.score(globalOrd); return true; } } @@ -214,7 +225,7 @@ final class GlobalOrdinalsWithScoreQuery extends Query { final GlobalOrdinalsWithScoreCollector collector; public SegmentOrdinalScorer(Weight weight, GlobalOrdinalsWithScoreCollector collector, SortedDocValues values, Scorer approximationScorer) { - super(weight, collector.getCollectorOrdinals(), values, approximationScorer); + super(weight, values, approximationScorer); this.collector = collector; } @@ -223,7 +234,7 @@ final class GlobalOrdinalsWithScoreQuery extends Query { for (int docID = approximationScorer.advance(target); docID < NO_MORE_DOCS; docID = approximationScorer.nextDoc()) { final int segmentOrd = values.getOrd(docID); if (segmentOrd != -1) { - if (foundOrds.get(segmentOrd)) { + if (collector.match(segmentOrd)) { score = collector.score(segmentOrd); return docID; } @@ -240,7 +251,7 @@ final class GlobalOrdinalsWithScoreQuery extends Query { public boolean matches() throws IOException { final int segmentOrd = values.getOrd(approximationScorer.docID()); if (segmentOrd != -1) { - if (foundOrds.get(segmentOrd)) { + if (collector.match(segmentOrd)) { score = collector.score(segmentOrd); return true; } diff --git a/lucene/join/src/java/org/apache/lucene/search/join/JoinUtil.java b/lucene/join/src/java/org/apache/lucene/search/join/JoinUtil.java index 5c1dc65c3f1..5ab2430122d 100644 --- a/lucene/join/src/java/org/apache/lucene/search/join/JoinUtil.java +++ b/lucene/join/src/java/org/apache/lucene/search/join/JoinUtil.java @@ -29,7 +29,7 @@ import java.io.IOException; import java.util.Locale; /** - * Utility for query time joining using TermsQuery and TermsCollector. + * Utility for query time joining. * * @lucene.experimental */ @@ -97,17 +97,10 @@ public final class JoinUtil { } /** - * A query time join using global ordinals over a dedicated join field. + * Delegates to {@link #createJoinQuery(String, Query, Query, IndexSearcher, ScoreMode, MultiDocValues.OrdinalMap, int, int)}, + * but disables the min and max filtering. * - * This join has certain restrictions and requirements: - * 1) A document can only refer to one other document. (but can be referred by one or more documents) - * 2) Documents on each side of the join must be distinguishable. Typically this can be done by adding an extra field - * that identifies the "from" and "to" side and then the fromQuery and toQuery must take the this into account. - * 3) There must be a single sorted doc values join field used by both the "from" and "to" documents. This join field - * should store the join values as UTF-8 strings. - * 4) An ordinal map must be provided that is created on top of the join field. - * - * @param joinField The {@link org.apache.lucene.index.SortedDocValues} field containing the join values + * @param joinField The {@link SortedDocValues} field containing the join values * @param fromQuery The query containing the actual user query. Also the fromQuery can only match "from" documents. * @param toQuery The query identifying all documents on the "to" side. * @param searcher The index searcher used to execute the from query @@ -123,6 +116,47 @@ public final class JoinUtil { IndexSearcher searcher, ScoreMode scoreMode, MultiDocValues.OrdinalMap ordinalMap) throws IOException { + return createJoinQuery(joinField, fromQuery, toQuery, searcher, scoreMode, ordinalMap, 0, Integer.MAX_VALUE); + } + + /** + * A query time join using global ordinals over a dedicated join field. + * + * This join has certain restrictions and requirements: + * 1) A document can only refer to one other document. (but can be referred by one or more documents) + * 2) Documents on each side of the join must be distinguishable. Typically this can be done by adding an extra field + * that identifies the "from" and "to" side and then the fromQuery and toQuery must take the this into account. + * 3) There must be a single sorted doc values join field used by both the "from" and "to" documents. This join field + * should store the join values as UTF-8 strings. + * 4) An ordinal map must be provided that is created on top of the join field. + * + * Note: min and max filtering and the avg score mode will require this join to keep track of the number of times + * a document matches per join value. This will increase the per join cost in terms of execution time and memory. + * + * @param joinField The {@link SortedDocValues} field containing the join values + * @param fromQuery The query containing the actual user query. Also the fromQuery can only match "from" documents. + * @param toQuery The query identifying all documents on the "to" side. + * @param searcher The index searcher used to execute the from query + * @param scoreMode Instructs how scores from the fromQuery are mapped to the returned query + * @param ordinalMap The ordinal map constructed over the joinField. In case of a single segment index, no ordinal map + * needs to be provided. + * @param min Optionally the minimum number of "from" documents that are required to match for a "to" document + * to be a match. The min is inclusive. Setting min to 0 and max to Interger.MAX_VALUE + * disables the min and max "from" documents filtering + * @param max Optionally the maximum number of "from" documents that are allowed to match for a "to" document + * to be a match. The max is inclusive. Setting min to 0 and max to Interger.MAX_VALUE + * disables the min and max "from" documents filtering + * @return a {@link Query} instance that can be used to join documents based on the join field + * @throws IOException If I/O related errors occur + */ + public static Query createJoinQuery(String joinField, + Query fromQuery, + Query toQuery, + IndexSearcher searcher, + ScoreMode scoreMode, + MultiDocValues.OrdinalMap ordinalMap, + int min, + int max) throws IOException { IndexReader indexReader = searcher.getIndexReader(); int numSegments = indexReader.leaves().size(); final long valueCount; @@ -146,31 +180,34 @@ public final class JoinUtil { } final Query rewrittenFromQuery = searcher.rewrite(fromQuery); - if (scoreMode == ScoreMode.None) { - GlobalOrdinalsCollector globalOrdinalsCollector = new GlobalOrdinalsCollector(joinField, ordinalMap, valueCount); - searcher.search(rewrittenFromQuery, globalOrdinalsCollector); - return new GlobalOrdinalsQuery(globalOrdinalsCollector.getCollectorOrdinals(), joinField, ordinalMap, toQuery, rewrittenFromQuery, indexReader); - } - GlobalOrdinalsWithScoreCollector globalOrdinalsWithScoreCollector; switch (scoreMode) { case Total: - globalOrdinalsWithScoreCollector = new GlobalOrdinalsWithScoreCollector.Sum(joinField, ordinalMap, valueCount); + globalOrdinalsWithScoreCollector = new GlobalOrdinalsWithScoreCollector.Sum(joinField, ordinalMap, valueCount, min, max); break; case Min: - globalOrdinalsWithScoreCollector = new GlobalOrdinalsWithScoreCollector.Min(joinField, ordinalMap, valueCount); + globalOrdinalsWithScoreCollector = new GlobalOrdinalsWithScoreCollector.Min(joinField, ordinalMap, valueCount, min, max); break; case Max: - globalOrdinalsWithScoreCollector = new GlobalOrdinalsWithScoreCollector.Max(joinField, ordinalMap, valueCount); + globalOrdinalsWithScoreCollector = new GlobalOrdinalsWithScoreCollector.Max(joinField, ordinalMap, valueCount, min, max); break; case Avg: - globalOrdinalsWithScoreCollector = new GlobalOrdinalsWithScoreCollector.Avg(joinField, ordinalMap, valueCount); + globalOrdinalsWithScoreCollector = new GlobalOrdinalsWithScoreCollector.Avg(joinField, ordinalMap, valueCount, min, max); break; + case None: + if (min <= 0 && max == Integer.MAX_VALUE) { + GlobalOrdinalsCollector globalOrdinalsCollector = new GlobalOrdinalsCollector(joinField, ordinalMap, valueCount); + searcher.search(rewrittenFromQuery, globalOrdinalsCollector); + return new GlobalOrdinalsQuery(globalOrdinalsCollector.getCollectorOrdinals(), joinField, ordinalMap, toQuery, rewrittenFromQuery, indexReader); + } else { + globalOrdinalsWithScoreCollector = new GlobalOrdinalsWithScoreCollector.NoScore(joinField, ordinalMap, valueCount, min, max); + break; + } default: throw new IllegalArgumentException(String.format(Locale.ROOT, "Score mode %s isn't supported.", scoreMode)); } searcher.search(rewrittenFromQuery, globalOrdinalsWithScoreCollector); - return new GlobalOrdinalsWithScoreQuery(globalOrdinalsWithScoreCollector, joinField, ordinalMap, toQuery, rewrittenFromQuery, indexReader); + return new GlobalOrdinalsWithScoreQuery(globalOrdinalsWithScoreCollector, joinField, ordinalMap, toQuery, rewrittenFromQuery, min, max, indexReader); } } diff --git a/lucene/join/src/test/org/apache/lucene/search/join/TestJoinUtil.java b/lucene/join/src/test/org/apache/lucene/search/join/TestJoinUtil.java index dd7e3f7dfc8..2e9f0fb1f58 100644 --- a/lucene/join/src/test/org/apache/lucene/search/join/TestJoinUtil.java +++ b/lucene/join/src/test/org/apache/lucene/search/join/TestJoinUtil.java @@ -61,6 +61,7 @@ import org.apache.lucene.search.SimpleCollector; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.TopScoreDocCollector; +import org.apache.lucene.search.TotalHitCountCollector; import org.apache.lucene.search.Weight; import org.apache.lucene.store.Directory; import org.apache.lucene.util.BitSet; @@ -412,7 +413,7 @@ public class TestJoinUtil extends LuceneTestCase { String childId = Integer.toString(p + c); Document childDoc = new Document(); childDoc.add(new StringField("id", childId, Field.Store.YES)); - parentDoc.add(new StringField("type", "from", Field.Store.NO)); + childDoc.add(new StringField("type", "from", Field.Store.NO)); childDoc.add(new SortedDocValuesField("join_field", new BytesRef(parentId))); int price = random().nextInt(1000); childDoc.add(new NumericDocValuesField(priceField, price)); @@ -459,6 +460,76 @@ public class TestJoinUtil extends LuceneTestCase { dir.close(); } + public void testMinMaxDocs() throws Exception { + Directory dir = newDirectory(); + RandomIndexWriter iw = new RandomIndexWriter( + random(), + dir, + newIndexWriterConfig(new MockAnalyzer(random(), MockTokenizer.KEYWORD, false)) + ); + + int minChildDocsPerParent = 2; + int maxChildDocsPerParent = 16; + int numParents = RandomInts.randomIntBetween(random(), 16, 64); + int[] childDocsPerParent = new int[numParents]; + for (int p = 0; p < numParents; p++) { + String parentId = Integer.toString(p); + Document parentDoc = new Document(); + parentDoc.add(new StringField("id", parentId, Field.Store.YES)); + parentDoc.add(new StringField("type", "to", Field.Store.NO)); + parentDoc.add(new SortedDocValuesField("join_field", new BytesRef(parentId))); + iw.addDocument(parentDoc); + int numChildren = RandomInts.randomIntBetween(random(), minChildDocsPerParent, maxChildDocsPerParent); + childDocsPerParent[p] = numChildren; + for (int c = 0; c < numChildren; c++) { + String childId = Integer.toString(p + c); + Document childDoc = new Document(); + childDoc.add(new StringField("id", childId, Field.Store.YES)); + childDoc.add(new StringField("type", "from", Field.Store.NO)); + childDoc.add(new SortedDocValuesField("join_field", new BytesRef(parentId))); + iw.addDocument(childDoc); + } + } + iw.close(); + + IndexSearcher searcher = new IndexSearcher(DirectoryReader.open(dir)); + SortedDocValues[] values = new SortedDocValues[searcher.getIndexReader().leaves().size()]; + for (LeafReaderContext leadContext : searcher.getIndexReader().leaves()) { + values[leadContext.ord] = DocValues.getSorted(leadContext.reader(), "join_field"); + } + MultiDocValues.OrdinalMap ordinalMap = MultiDocValues.OrdinalMap.build( + searcher.getIndexReader().getCoreCacheKey(), values, PackedInts.DEFAULT + ); + Query fromQuery = new TermQuery(new Term("type", "from")); + Query toQuery = new TermQuery(new Term("type", "to")); + + int iters = RandomInts.randomIntBetween(random(), 3, 9); + for (int i = 1; i <= iters; i++) { + final ScoreMode scoreMode = ScoreMode.values()[random().nextInt(ScoreMode.values().length)]; + int min = RandomInts.randomIntBetween(random(), minChildDocsPerParent, maxChildDocsPerParent - 1); + int max = RandomInts.randomIntBetween(random(), min, maxChildDocsPerParent); + if (VERBOSE) { + System.out.println("iter=" + i); + System.out.println("scoreMode=" + scoreMode); + System.out.println("min=" + min); + System.out.println("max=" + max); + } + Query joinQuery = JoinUtil.createJoinQuery("join_field", fromQuery, toQuery, searcher, scoreMode, ordinalMap, min, max); + TotalHitCountCollector collector = new TotalHitCountCollector(); + searcher.search(joinQuery, collector); + int expectedCount = 0; + for (int numChildDocs : childDocsPerParent) { + if (numChildDocs >= min && numChildDocs <= max) { + expectedCount++; + } + } + assertEquals(expectedCount, collector.getTotalHits()); + } + + searcher.getIndexReader().close(); + dir.close(); + } + // TermsWithScoreCollector.MV.Avg forgets to grow beyond TermsWithScoreCollector.INITIAL_ARRAY_SIZE public void testOverflowTermsWithScoreCollector() throws Exception { test300spartans(true, ScoreMode.Avg);