|
@@ -24,6 +24,7 @@ import com.carrotsearch.hppc.ObjectObjectOpenHashMap;
|
|
|
import com.carrotsearch.randomizedtesting.generators.RandomInts;
|
|
|
import org.apache.lucene.analysis.MockAnalyzer;
|
|
|
import org.apache.lucene.document.Document;
|
|
|
+import org.apache.lucene.document.DoubleField;
|
|
|
import org.apache.lucene.document.Field;
|
|
|
import org.apache.lucene.document.StringField;
|
|
|
import org.apache.lucene.index.*;
|
|
@@ -34,11 +35,17 @@ import org.apache.lucene.util.FixedBitSet;
|
|
|
import org.apache.lucene.util.LuceneTestCase;
|
|
|
import org.elasticsearch.common.lease.Releasables;
|
|
|
import org.elasticsearch.common.lucene.search.NotFilter;
|
|
|
+import org.elasticsearch.common.lucene.search.Queries;
|
|
|
import org.elasticsearch.common.lucene.search.XFilteredQuery;
|
|
|
+import org.elasticsearch.common.lucene.search.function.FieldValueFactorFunction;
|
|
|
+import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery;
|
|
|
import org.elasticsearch.index.engine.Engine;
|
|
|
import org.elasticsearch.index.cache.fixedbitset.FixedBitSetFilter;
|
|
|
+import org.elasticsearch.index.fielddata.IndexNumericFieldData;
|
|
|
import org.elasticsearch.index.fielddata.plain.ParentChildIndexFieldData;
|
|
|
+import org.elasticsearch.index.mapper.FieldMapper;
|
|
|
import org.elasticsearch.index.mapper.Uid;
|
|
|
+import org.elasticsearch.index.mapper.internal.IdFieldMapper;
|
|
|
import org.elasticsearch.index.mapper.internal.ParentFieldMapper;
|
|
|
import org.elasticsearch.index.mapper.internal.TypeFieldMapper;
|
|
|
import org.elasticsearch.index.mapper.internal.UidFieldMapper;
|
|
@@ -56,6 +63,9 @@ import java.util.NavigableMap;
|
|
|
import java.util.Random;
|
|
|
import java.util.TreeMap;
|
|
|
|
|
|
+import static org.hamcrest.Matchers.is;
|
|
|
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
|
|
|
+
|
|
|
public class ChildrenQueryTests extends AbstractChildTests {
|
|
|
|
|
|
@BeforeClass
|
|
@@ -128,7 +138,7 @@ public class ChildrenQueryTests extends AbstractChildTests {
|
|
|
String childValue = childValues[random().nextInt(childValues.length)];
|
|
|
|
|
|
document = new Document();
|
|
|
- document.add(new StringField(UidFieldMapper.NAME, Uid.createUid("child", Integer.toString(childDocId)), Field.Store.NO));
|
|
|
+ document.add(new StringField(UidFieldMapper.NAME, Uid.createUid("child", Integer.toString(childDocId++)), Field.Store.NO));
|
|
|
document.add(new StringField(TypeFieldMapper.NAME, "child", Field.Store.NO));
|
|
|
document.add(new StringField(ParentFieldMapper.NAME, Uid.createUid("parent", parent), Field.Store.NO));
|
|
|
document.add(new StringField("field1", childValue, Field.Store.NO));
|
|
@@ -264,4 +274,140 @@ public class ChildrenQueryTests extends AbstractChildTests {
|
|
|
directory.close();
|
|
|
}
|
|
|
|
|
|
+ @Test
|
|
|
+ public void testMinScoreMode() throws IOException {
|
|
|
+ assertScoreType(ScoreType.MIN);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testMaxScoreMode() throws IOException {
|
|
|
+ assertScoreType(ScoreType.MAX);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testAvgScoreMode() throws IOException {
|
|
|
+ assertScoreType(ScoreType.AVG);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testSumScoreMode() throws IOException {
|
|
|
+ assertScoreType(ScoreType.SUM);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Assert that the {@code scoreType} operates as expected and parents are found in the expected order.
|
|
|
+ * <p />
|
|
|
+ * This will use the test index's parent/child types to create parents with multiple children. Each child will have
|
|
|
+ * a randomly generated scored stored in {@link #CHILD_SCORE_NAME}, which is used to score based on the
|
|
|
+ * {@code scoreType} by using a {@link MockScorer} to determine the expected scores.
|
|
|
+ * @param scoreType The score type to use within the query to score parents relative to their children.
|
|
|
+ * @throws IOException if any unexpected error occurs
|
|
|
+ */
|
|
|
+ private void assertScoreType(ScoreType scoreType) throws IOException {
|
|
|
+ SearchContext context = SearchContext.current();
|
|
|
+ Directory directory = newDirectory();
|
|
|
+ IndexWriter writer = new IndexWriter(directory, newIndexWriterConfig(new MockAnalyzer(random())));
|
|
|
+
|
|
|
+ // calculates the expected score per parent
|
|
|
+ MockScorer scorer = new MockScorer(scoreType);
|
|
|
+ scorer.scores = new FloatArrayList(10);
|
|
|
+
|
|
|
+ // number of parents to generate
|
|
|
+ int parentDocs = scaledRandomIntBetween(2, 10);
|
|
|
+ // unique child ID
|
|
|
+ int childDocId = 0;
|
|
|
+
|
|
|
+ // Parent ID to expected score
|
|
|
+ Map<String, Float> parentScores = new TreeMap<>();
|
|
|
+
|
|
|
+ // Add a few random parents to ensure that the children's score is appropriately taken into account
|
|
|
+ for (int parentDocId = 0; parentDocId < parentDocs; ++parentDocId) {
|
|
|
+ String parent = Integer.toString(parentDocId);
|
|
|
+
|
|
|
+ // Create the parent
|
|
|
+ Document parentDocument = new Document();
|
|
|
+
|
|
|
+ parentDocument.add(new StringField(UidFieldMapper.NAME, Uid.createUid("parent", parent), Field.Store.YES));
|
|
|
+ parentDocument.add(new StringField(IdFieldMapper.NAME, parent, Field.Store.YES));
|
|
|
+ parentDocument.add(new StringField(TypeFieldMapper.NAME, "parent", Field.Store.NO));
|
|
|
+
|
|
|
+ // add the parent to the index
|
|
|
+ writer.addDocument(parentDocument);
|
|
|
+
|
|
|
+ int numChildDocs = scaledRandomIntBetween(1, 10);
|
|
|
+
|
|
|
+ // forget any parent's previous scores
|
|
|
+ scorer.scores.clear();
|
|
|
+
|
|
|
+ // associate children with the parent
|
|
|
+ for (int i = 0; i < numChildDocs; ++i) {
|
|
|
+ int childScore = random().nextInt(128);
|
|
|
+
|
|
|
+ Document childDocument = new Document();
|
|
|
+
|
|
|
+ childDocument.add(new StringField(UidFieldMapper.NAME, Uid.createUid("child", Integer.toString(childDocId++)), Field.Store.NO));
|
|
|
+ childDocument.add(new StringField(TypeFieldMapper.NAME, "child", Field.Store.NO));
|
|
|
+ // parent association:
|
|
|
+ childDocument.add(new StringField(ParentFieldMapper.NAME, Uid.createUid("parent", parent), Field.Store.NO));
|
|
|
+ childDocument.add(new DoubleField(CHILD_SCORE_NAME, childScore, Field.Store.NO));
|
|
|
+
|
|
|
+ // remember the score to be calculated
|
|
|
+ scorer.scores.add(childScore);
|
|
|
+
|
|
|
+ // add the associated child to the index
|
|
|
+ writer.addDocument(childDocument);
|
|
|
+ }
|
|
|
+
|
|
|
+ // this score that should be returned for this parent
|
|
|
+ parentScores.put(parent, scorer.score());
|
|
|
+ }
|
|
|
+
|
|
|
+ writer.commit();
|
|
|
+
|
|
|
+ IndexReader reader = DirectoryReader.open(writer, true);
|
|
|
+ IndexSearcher searcher = new IndexSearcher(reader);
|
|
|
+
|
|
|
+ // setup to read the parent/child map
|
|
|
+ Engine.SimpleSearcher engineSearcher = new Engine.SimpleSearcher(ChildrenQueryTests.class.getSimpleName(), searcher);
|
|
|
+ ((TestSearchContext)context).setSearcher(new ContextIndexSearcher(context, engineSearcher));
|
|
|
+ ParentFieldMapper parentFieldMapper = context.mapperService().documentMapper("child").parentFieldMapper();
|
|
|
+ ParentChildIndexFieldData parentChildIndexFieldData = context.fieldData().getForField(parentFieldMapper);
|
|
|
+ FixedBitSetFilter parentFilter = wrap(new TermFilter(new Term(TypeFieldMapper.NAME, "parent")));
|
|
|
+
|
|
|
+ // child query that returns the score as the value of "childScore" for each child document,
|
|
|
+ // with the parent's score determined by the score type
|
|
|
+ FieldMapper fieldMapper = context.mapperService().smartNameFieldMapper(CHILD_SCORE_NAME);
|
|
|
+ IndexNumericFieldData fieldData = context.fieldData().getForField(fieldMapper);
|
|
|
+ FieldValueFactorFunction fieldScore = new FieldValueFactorFunction(CHILD_SCORE_NAME, 1, FieldValueFactorFunction.Modifier.NONE, fieldData);
|
|
|
+ Query childQuery = new FunctionScoreQuery(new FilteredQuery(Queries.newMatchAllQuery(), new TermFilter(new Term(TypeFieldMapper.NAME, "child"))), fieldScore);
|
|
|
+
|
|
|
+ // Perform the search for the documents using the selected score type
|
|
|
+ TopDocs docs =
|
|
|
+ searcher.search(
|
|
|
+ new ChildrenQuery(parentChildIndexFieldData, "parent", "child", parentFilter, childQuery, scoreType, 0, 0, parentDocs, null),
|
|
|
+ parentDocs);
|
|
|
+
|
|
|
+ assertThat("Expected all parents", docs.totalHits, is(parentDocs));
|
|
|
+
|
|
|
+ // score should be descending (just a sanity check)
|
|
|
+ float topScore = docs.scoreDocs[0].score;
|
|
|
+
|
|
|
+ // ensure each score is returned as expected
|
|
|
+ for (int i = 0; i < parentDocs; ++i) {
|
|
|
+ ScoreDoc scoreDoc = docs.scoreDocs[i];
|
|
|
+ // get the ID from the document to get its expected score; remove it so we cannot double-count it
|
|
|
+ float score = parentScores.remove(reader.document(scoreDoc.doc).get(IdFieldMapper.NAME));
|
|
|
+
|
|
|
+ // expect exact match
|
|
|
+ assertThat("Unexpected score", scoreDoc.score, is(score));
|
|
|
+ assertThat("Not descending", score, lessThanOrEqualTo(topScore));
|
|
|
+
|
|
|
+ // it had better keep descending
|
|
|
+ topScore = score;
|
|
|
+ }
|
|
|
+
|
|
|
+ reader.close();
|
|
|
+ writer.close();
|
|
|
+ directory.close();
|
|
|
+ }
|
|
|
}
|