From 36acada76293302d83cab60c086a70f8c17e17e6 Mon Sep 17 00:00:00 2001
From: Martijn van Groningen DocTermOrds
instance is empty.
+ */
+ public boolean isEmpty() {
+ return index == null;
+ }
+
/** Subclass can override this */
protected void visitTerm(TermsEnum te, int termNum) throws IOException {
}
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 05586cd6ab9..9502d04223c 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
@@ -38,12 +38,24 @@ public final class JoinUtil {
*
* Execute the returned query with a {@link IndexSearcher} to retrieve all documents that have the same terms in the
* to field that match with documents matching the specified fromQuery and have the same terms in the from field.
+ *
+ * In the case a single document relates to more than one document the multipleValuesPerDocument
option
+ * should be set to true. When the multipleValuesPerDocument
is set to true
only the
+ * the score from the first encountered join value originating from the 'from' side is mapped into the 'to' side.
+ * Even in the case when a second join value related to a specific document yields a higher score. Obviously this
+ * doesn't apply in the case that {@link ScoreMode#None} is used, since no scores are computed at all.
+ *
The query time joining is index term based and implemented as two pass search. The first pass collects all the terms from a fromField @@ -68,22 +68,26 @@
fromField
: The from field to join from.
fromQuery
: The query executed to collect the from terms. This is usually the user specified query.
multipleValuesPerDocument
: Whether the fromField contains more than one value per document
+ scoreMode
: Defines how scores are translated to the other join side. If you don't care about scoring
+ use {@link org.apache.lucene.search.join.ScoreMode#None} mode. This will disable scoring and is therefore more
+ efficient (requires less memory and is faster).
toField
: The to field to join to
Basically the query-time joining is accessible from one static method. The user of this method supplies the method
with the described input and a IndexSearcher
where the from terms need to be collected from. The returned
query can be executed with the same IndexSearcher
, but also with another IndexSearcher
.
- Example usage of the {@link org.apache.lucene.search.join.JoinUtil#createJoinQuery(String, boolean, String, org.apache.lucene.search.Query, org.apache.lucene.search.IndexSearcher)
+ Example usage of the {@link org.apache.lucene.search.join.JoinUtil#createJoinQuery(String, boolean, String, org.apache.lucene.search.Query, org.apache.lucene.search.IndexSearcher, org.apache.lucene.search.join.ScoreMode)
JoinUtil.createJoinQuery()} :
String fromField = "from"; // Name of the from field boolean multipleValuesPerDocument = false; // Set only yo true in the case when your fromField has multiple values per document in your index String toField = "to"; // Name of the to field + ScoreMode scoreMode = ScoreMode.Max // Defines how the scores are translated into the other side of the join. Query fromQuery = new TermQuery(new Term("content", searchTerm)); // Query executed to collect from values to join to the to values - Query joinQuery = JoinUtil.createJoinQuery(fromField, multipleValuesPerDocument, toField, fromQuery, fromSearcher); + Query joinQuery = JoinUtil.createJoinQuery(fromField, multipleValuesPerDocument, toField, fromQuery, fromSearcher, scoreMode); TopDocs topDocs = toSearcher.search(joinQuery, 10); // Note: toSearcher can be the same as the fromSearcher // Render topDocs...diff --git a/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoin.java b/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoin.java index 34f7b656119..c81117702b9 100644 --- a/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoin.java +++ b/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoin.java @@ -96,7 +96,7 @@ public class TestBlockJoin extends LuceneTestCase { // Wrap the child document query to 'join' any matches // up to corresponding parent: - ToParentBlockJoinQuery childJoinQuery = new ToParentBlockJoinQuery(childQuery, parentsFilter, ToParentBlockJoinQuery.ScoreMode.Avg); + ToParentBlockJoinQuery childJoinQuery = new ToParentBlockJoinQuery(childQuery, parentsFilter, ScoreMode.Avg); // Combine the parent and nested child queries into a single query for a candidate BooleanQuery fullQuery = new BooleanQuery(); @@ -198,7 +198,7 @@ public class TestBlockJoin extends LuceneTestCase { // Wrap the child document query to 'join' any matches // up to corresponding parent: - ToParentBlockJoinQuery childJoinQuery = new ToParentBlockJoinQuery(childQuery, parentsFilter, ToParentBlockJoinQuery.ScoreMode.Avg); + ToParentBlockJoinQuery childJoinQuery = new ToParentBlockJoinQuery(childQuery, parentsFilter, ScoreMode.Avg); assertEquals("no filter - both passed", 2, s.search(childJoinQuery, 10).totalHits); @@ -259,7 +259,7 @@ public class TestBlockJoin extends LuceneTestCase { w.close(); IndexSearcher s = newSearcher(r); - ToParentBlockJoinQuery q = new ToParentBlockJoinQuery(new MatchAllDocsQuery(), new QueryWrapperFilter(new MatchAllDocsQuery()), ToParentBlockJoinQuery.ScoreMode.Avg); + ToParentBlockJoinQuery q = new ToParentBlockJoinQuery(new MatchAllDocsQuery(), new QueryWrapperFilter(new MatchAllDocsQuery()), ScoreMode.Avg); s.search(q, 10); BooleanQuery bq = new BooleanQuery(); bq.setBoost(2f); // we boost the BQ @@ -493,15 +493,15 @@ public class TestBlockJoin extends LuceneTestCase { } final int x = random().nextInt(4); - final ToParentBlockJoinQuery.ScoreMode agg; + final ScoreMode agg; if (x == 0) { - agg = ToParentBlockJoinQuery.ScoreMode.None; + agg = ScoreMode.None; } else if (x == 1) { - agg = ToParentBlockJoinQuery.ScoreMode.Max; + agg = ScoreMode.Max; } else if (x == 2) { - agg = ToParentBlockJoinQuery.ScoreMode.Total; + agg = ScoreMode.Total; } else { - agg = ToParentBlockJoinQuery.ScoreMode.Avg; + agg = ScoreMode.Avg; } final ToParentBlockJoinQuery childJoinQuery = new ToParentBlockJoinQuery(childQuery, parentsFilter, agg); @@ -584,7 +584,7 @@ public class TestBlockJoin extends LuceneTestCase { final boolean trackScores; final boolean trackMaxScore; - if (agg == ToParentBlockJoinQuery.ScoreMode.None) { + if (agg == ScoreMode.None) { trackScores = false; trackMaxScore = false; } else { @@ -881,8 +881,8 @@ public class TestBlockJoin extends LuceneTestCase { // Wrap the child document query to 'join' any matches // up to corresponding parent: - ToParentBlockJoinQuery childJobJoinQuery = new ToParentBlockJoinQuery(childJobQuery, parentsFilter, ToParentBlockJoinQuery.ScoreMode.Avg); - ToParentBlockJoinQuery childQualificationJoinQuery = new ToParentBlockJoinQuery(childQualificationQuery, parentsFilter, ToParentBlockJoinQuery.ScoreMode.Avg); + ToParentBlockJoinQuery childJobJoinQuery = new ToParentBlockJoinQuery(childJobQuery, parentsFilter, ScoreMode.Avg); + ToParentBlockJoinQuery childQualificationJoinQuery = new ToParentBlockJoinQuery(childQualificationQuery, parentsFilter, ScoreMode.Avg); // Combine the parent and nested child queries into a single query for a candidate BooleanQuery fullQuery = new BooleanQuery(); @@ -952,7 +952,7 @@ public class TestBlockJoin extends LuceneTestCase { new QueryWrapperFilter( new TermQuery(new Term("parent", "1")))); - ToParentBlockJoinQuery q = new ToParentBlockJoinQuery(tq, parentFilter, ToParentBlockJoinQuery.ScoreMode.Avg); + ToParentBlockJoinQuery q = new ToParentBlockJoinQuery(tq, parentFilter, ScoreMode.Avg); Weight weight = s.createNormalizedWeight(q); DocIdSetIterator disi = weight.scorer(s.getIndexReader().getTopReaderContext().leaves()[0], true, true, null); assertEquals(1, disi.advance(1)); @@ -986,7 +986,7 @@ public class TestBlockJoin extends LuceneTestCase { new QueryWrapperFilter( new TermQuery(new Term("isparent", "yes")))); - ToParentBlockJoinQuery q = new ToParentBlockJoinQuery(tq, parentFilter, ToParentBlockJoinQuery.ScoreMode.Avg); + ToParentBlockJoinQuery q = new ToParentBlockJoinQuery(tq, parentFilter, ScoreMode.Avg); Weight weight = s.createNormalizedWeight(q); DocIdSetIterator disi = weight.scorer(s.getIndexReader().getTopReaderContext().leaves()[0], true, true, null); assertEquals(2, disi.advance(0)); 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 8040ded1176..7c7ca12c668 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 @@ -22,8 +22,26 @@ import org.apache.lucene.analysis.MockTokenizer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.TextField; -import org.apache.lucene.index.*; -import org.apache.lucene.search.*; +import org.apache.lucene.index.AtomicReaderContext; +import org.apache.lucene.index.DocTermOrds; +import org.apache.lucene.index.DocsEnum; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.MultiFields; +import org.apache.lucene.index.RandomIndexWriter; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermsEnum; +import org.apache.lucene.search.Collector; +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.FieldCache; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.search.TopScoreDocCollector; import org.apache.lucene.store.Directory; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.FixedBitSet; @@ -49,45 +67,45 @@ public class TestJoinUtil extends LuceneTestCase { // 0 Document doc = new Document(); - doc.add(new Field("description", "random text", TextField.TYPE_STORED)); - doc.add(new Field("name", "name1", TextField.TYPE_STORED)); - doc.add(new Field(idField, "1", TextField.TYPE_STORED)); + doc.add(new Field("description", "random text", TextField.TYPE_UNSTORED)); + doc.add(new Field("name", "name1", TextField.TYPE_UNSTORED)); + doc.add(new Field(idField, "1", TextField.TYPE_UNSTORED)); w.addDocument(doc); // 1 doc = new Document(); - doc.add(new Field("price", "10.0", TextField.TYPE_STORED)); - doc.add(new Field(idField, "2", TextField.TYPE_STORED)); - doc.add(new Field(toField, "1", TextField.TYPE_STORED)); + doc.add(new Field("price", "10.0", TextField.TYPE_UNSTORED)); + doc.add(new Field(idField, "2", TextField.TYPE_UNSTORED)); + doc.add(new Field(toField, "1", TextField.TYPE_UNSTORED)); w.addDocument(doc); // 2 doc = new Document(); - doc.add(new Field("price", "20.0", TextField.TYPE_STORED)); - doc.add(new Field(idField, "3", TextField.TYPE_STORED)); - doc.add(new Field(toField, "1", TextField.TYPE_STORED)); + doc.add(new Field("price", "20.0", TextField.TYPE_UNSTORED)); + doc.add(new Field(idField, "3", TextField.TYPE_UNSTORED)); + doc.add(new Field(toField, "1", TextField.TYPE_UNSTORED)); w.addDocument(doc); // 3 doc = new Document(); - doc.add(new Field("description", "more random text", TextField.TYPE_STORED)); - doc.add(new Field("name", "name2", TextField.TYPE_STORED)); - doc.add(new Field(idField, "4", TextField.TYPE_STORED)); + doc.add(new Field("description", "more random text", TextField.TYPE_UNSTORED)); + doc.add(new Field("name", "name2", TextField.TYPE_UNSTORED)); + doc.add(new Field(idField, "4", TextField.TYPE_UNSTORED)); w.addDocument(doc); w.commit(); // 4 doc = new Document(); - doc.add(new Field("price", "10.0", TextField.TYPE_STORED)); - doc.add(new Field(idField, "5", TextField.TYPE_STORED)); - doc.add(new Field(toField, "4", TextField.TYPE_STORED)); + doc.add(new Field("price", "10.0", TextField.TYPE_UNSTORED)); + doc.add(new Field(idField, "5", TextField.TYPE_UNSTORED)); + doc.add(new Field(toField, "4", TextField.TYPE_UNSTORED)); w.addDocument(doc); // 5 doc = new Document(); - doc.add(new Field("price", "20.0", TextField.TYPE_STORED)); - doc.add(new Field(idField, "6", TextField.TYPE_STORED)); - doc.add(new Field(toField, "4", TextField.TYPE_STORED)); + doc.add(new Field("price", "20.0", TextField.TYPE_UNSTORED)); + doc.add(new Field(idField, "6", TextField.TYPE_UNSTORED)); + doc.add(new Field(toField, "4", TextField.TYPE_UNSTORED)); w.addDocument(doc); IndexSearcher indexSearcher = new IndexSearcher(w.getReader()); @@ -95,21 +113,21 @@ public class TestJoinUtil extends LuceneTestCase { // Search for product Query joinQuery = - JoinUtil.createJoinQuery(idField, false, toField, new TermQuery(new Term("name", "name2")), indexSearcher); + JoinUtil.createJoinQuery(idField, false, toField, new TermQuery(new Term("name", "name2")), indexSearcher, ScoreMode.None); TopDocs result = indexSearcher.search(joinQuery, 10); assertEquals(2, result.totalHits); assertEquals(4, result.scoreDocs[0].doc); assertEquals(5, result.scoreDocs[1].doc); - joinQuery = JoinUtil.createJoinQuery(idField, false, toField, new TermQuery(new Term("name", "name1")), indexSearcher); + joinQuery = JoinUtil.createJoinQuery(idField, false, toField, new TermQuery(new Term("name", "name1")), indexSearcher, ScoreMode.None); result = indexSearcher.search(joinQuery, 10); assertEquals(2, result.totalHits); assertEquals(1, result.scoreDocs[0].doc); assertEquals(2, result.scoreDocs[1].doc); // Search for offer - joinQuery = JoinUtil.createJoinQuery(toField, false, idField, new TermQuery(new Term("id", "5")), indexSearcher); + joinQuery = JoinUtil.createJoinQuery(toField, false, idField, new TermQuery(new Term("id", "5")), indexSearcher, ScoreMode.None); result = indexSearcher.search(joinQuery, 10); assertEquals(1, result.totalHits); assertEquals(3, result.scoreDocs[0].doc); @@ -118,6 +136,96 @@ public class TestJoinUtil extends LuceneTestCase { dir.close(); } + public void testSimpleWithScoring() throws Exception { + final String idField = "id"; + final String toField = "movieId"; + + Directory dir = newDirectory(); + RandomIndexWriter w = new RandomIndexWriter( + random(), + dir, + newIndexWriterConfig(TEST_VERSION_CURRENT, + new MockAnalyzer(random())).setMergePolicy(newLogMergePolicy())); + + // 0 + Document doc = new Document(); + doc.add(new Field("description", "A random movie", TextField.TYPE_UNSTORED)); + doc.add(new Field("name", "Movie 1", TextField.TYPE_UNSTORED)); + doc.add(new Field(idField, "1", TextField.TYPE_UNSTORED)); + w.addDocument(doc); + + // 1 + doc = new Document(); + doc.add(new Field("subtitle", "The first subtitle of this movie", TextField.TYPE_UNSTORED)); + doc.add(new Field(idField, "2", TextField.TYPE_UNSTORED)); + doc.add(new Field(toField, "1", TextField.TYPE_UNSTORED)); + w.addDocument(doc); + + // 2 + doc = new Document(); + doc.add(new Field("subtitle", "random subtitle; random event movie", TextField.TYPE_UNSTORED)); + doc.add(new Field(idField, "3", TextField.TYPE_UNSTORED)); + doc.add(new Field(toField, "1", TextField.TYPE_UNSTORED)); + w.addDocument(doc); + + // 3 + doc = new Document(); + doc.add(new Field("description", "A second random movie", TextField.TYPE_UNSTORED)); + doc.add(new Field("name", "Movie 2", TextField.TYPE_UNSTORED)); + doc.add(new Field(idField, "4", TextField.TYPE_UNSTORED)); + w.addDocument(doc); + w.commit(); + + // 4 + doc = new Document(); + doc.add(new Field("subtitle", "a very random event happened during christmas night", TextField.TYPE_UNSTORED)); + doc.add(new Field(idField, "5", TextField.TYPE_UNSTORED)); + doc.add(new Field(toField, "4", TextField.TYPE_UNSTORED)); + w.addDocument(doc); + + // 5 + doc = new Document(); + doc.add(new Field("subtitle", "movie end movie test 123 test 123 random", TextField.TYPE_UNSTORED)); + doc.add(new Field(idField, "6", TextField.TYPE_UNSTORED)); + doc.add(new Field(toField, "4", TextField.TYPE_UNSTORED)); + w.addDocument(doc); + + IndexSearcher indexSearcher = new IndexSearcher(w.getReader()); + w.close(); + + // Search for movie via subtitle + Query joinQuery = + JoinUtil.createJoinQuery(toField, false, idField, new TermQuery(new Term("subtitle", "random")), indexSearcher, ScoreMode.Max); + TopDocs result = indexSearcher.search(joinQuery, 10); + assertEquals(2, result.totalHits); + assertEquals(0, result.scoreDocs[0].doc); + assertEquals(3, result.scoreDocs[1].doc); + + // Score mode max. + joinQuery = JoinUtil.createJoinQuery(toField, false, idField, new TermQuery(new Term("subtitle", "movie")), indexSearcher, ScoreMode.Max); + result = indexSearcher.search(joinQuery, 10); + assertEquals(2, result.totalHits); + assertEquals(3, result.scoreDocs[0].doc); + assertEquals(0, result.scoreDocs[1].doc); + + // Score mode total + joinQuery = JoinUtil.createJoinQuery(toField, false, idField, new TermQuery(new Term("subtitle", "movie")), indexSearcher, ScoreMode.Total); + result = indexSearcher.search(joinQuery, 10); + assertEquals(2, result.totalHits); + assertEquals(0, result.scoreDocs[0].doc); + assertEquals(3, result.scoreDocs[1].doc); + + //Score mode avg + joinQuery = JoinUtil.createJoinQuery(toField, false, idField, new TermQuery(new Term("subtitle", "movie")), indexSearcher, ScoreMode.Avg); + result = indexSearcher.search(joinQuery, 10); + assertEquals(2, result.totalHits); + assertEquals(3, result.scoreDocs[0].doc); + assertEquals(0, result.scoreDocs[1].doc); + + indexSearcher.getIndexReader().close(); + dir.close(); + } + @Test public void testSingleValueRandomJoin() throws Exception { int maxIndexIter = _TestUtil.nextInt(random(), 6, 12); @@ -160,15 +268,20 @@ public class TestJoinUtil extends LuceneTestCase { String randomValue = context.randomUniqueValues[r]; FixedBitSet expectedResult = createExpectedResult(randomValue, from, indexSearcher.getIndexReader(), context); - Query actualQuery = new TermQuery(new Term("value", randomValue)); + final Query actualQuery = new TermQuery(new Term("value", randomValue)); if (VERBOSE) { System.out.println("actualQuery=" + actualQuery); } - Query joinQuery; + final ScoreMode scoreMode = ScoreMode.values()[random().nextInt(ScoreMode.values().length)]; + if (VERBOSE) { + System.out.println("scoreMode=" + scoreMode); + } + + final Query joinQuery; if (from) { - joinQuery = JoinUtil.createJoinQuery("from", multipleValuesPerDocument, "to", actualQuery, indexSearcher); + joinQuery = JoinUtil.createJoinQuery("from", multipleValuesPerDocument, "to", actualQuery, indexSearcher, scoreMode); } else { - joinQuery = JoinUtil.createJoinQuery("to", multipleValuesPerDocument, "from", actualQuery, indexSearcher); + joinQuery = JoinUtil.createJoinQuery("to", multipleValuesPerDocument, "from", actualQuery, indexSearcher, scoreMode); } if (VERBOSE) { System.out.println("joinQuery=" + joinQuery); @@ -176,26 +289,30 @@ public class TestJoinUtil extends LuceneTestCase { // Need to know all documents that have matches. TopDocs doesn't give me that and then I'd be also testing TopDocsCollector... final FixedBitSet actualResult = new FixedBitSet(indexSearcher.getIndexReader().maxDoc()); + final TopScoreDocCollector topScoreDocCollector = TopScoreDocCollector.create(10, false); indexSearcher.search(joinQuery, new Collector() { int docBase; public void collect(int doc) throws IOException { actualResult.set(doc + docBase); + topScoreDocCollector.collect(doc); } public void setNextReader(AtomicReaderContext context) throws IOException { docBase = context.docBase; + topScoreDocCollector.setNextReader(context); } public void setScorer(Scorer scorer) throws IOException { + topScoreDocCollector.setScorer(scorer); } public boolean acceptsDocsOutOfOrder() { - return true; + return topScoreDocCollector.acceptsDocsOutOfOrder(); } }); - + // Asserting bit set... if (VERBOSE) { System.out.println("expected cardinality:" + expectedResult.cardinality()); DocIdSetIterator iterator = expectedResult.iterator(); @@ -208,8 +325,28 @@ public class TestJoinUtil extends LuceneTestCase { System.out.println(String.format("Actual doc[%d] with id value %s", doc, indexSearcher.doc(doc).get("id"))); } } - assertEquals(expectedResult, actualResult); + + // Asserting TopDocs... + TopDocs expectedTopDocs = createExpectedTopDocs(randomValue, from, scoreMode, context); + TopDocs actualTopDocs = topScoreDocCollector.topDocs(); + assertEquals(expectedTopDocs.totalHits, actualTopDocs.totalHits); + assertEquals(expectedTopDocs.scoreDocs.length, actualTopDocs.scoreDocs.length); + if (scoreMode == ScoreMode.None) { + continue; + } + + assertEquals(expectedTopDocs.getMaxScore(), actualTopDocs.getMaxScore(), 0.0f); + for (int i = 0; i < expectedTopDocs.scoreDocs.length; i++) { + if (VERBOSE) { + System.out.printf("Expected doc: %d | Actual doc: %d\n", expectedTopDocs.scoreDocs[i].doc, actualTopDocs.scoreDocs[i].doc); + System.out.printf("Expected score: %f | Actual score: %f\n", expectedTopDocs.scoreDocs[i].score, actualTopDocs.scoreDocs[i].score); + } + assertEquals(expectedTopDocs.scoreDocs[i].doc, actualTopDocs.scoreDocs[i].doc); + assertEquals(expectedTopDocs.scoreDocs[i].score, actualTopDocs.scoreDocs[i].score, 0.0f); + Explanation explanation = indexSearcher.explain(joinQuery, expectedTopDocs.scoreDocs[i].doc); + assertEquals(expectedTopDocs.scoreDocs[i].score, explanation.getValue(), 0.0f); + } } topLevelReader.close(); dir.close(); @@ -238,20 +375,21 @@ public class TestJoinUtil extends LuceneTestCase { context.randomUniqueValues[i] = uniqueRandomValue; } + RandomDoc[] docs = new RandomDoc[nDocs]; for (int i = 0; i < nDocs; i++) { String id = Integer.toString(i); int randomI = random().nextInt(context.randomUniqueValues.length); String value = context.randomUniqueValues[randomI]; Document document = new Document(); - document.add(newField(random(), "id", id, TextField.TYPE_STORED)); - document.add(newField(random(), "value", value, TextField.TYPE_STORED)); + document.add(newField(random(), "id", id, TextField.TYPE_UNSTORED)); + document.add(newField(random(), "value", value, TextField.TYPE_UNSTORED)); boolean from = context.randomFrom[randomI]; int numberOfLinkValues = multipleValuesPerDocument ? 2 + random().nextInt(10) : 1; - RandomDoc doc = new RandomDoc(id, numberOfLinkValues, value); + docs[i] = new RandomDoc(id, numberOfLinkValues, value, from); for (int j = 0; j < numberOfLinkValues; j++) { String linkValue = context.randomUniqueValues[random().nextInt(context.randomUniqueValues.length)]; - doc.linkValues.add(linkValue); + docs[i].linkValues.add(linkValue); if (from) { if (!context.fromDocuments.containsKey(linkValue)) { context.fromDocuments.put(linkValue, new ArrayList