| /* |
| * 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.search.grouping; |
| |
| import org.apache.lucene.search.ScoreDoc; |
| import org.apache.lucene.search.Sort; |
| import org.apache.lucene.search.TotalHits; |
| import org.apache.lucene.util.LuceneTestCase; |
| |
| public class TopGroupsTest extends LuceneTestCase { |
| |
| public void testAllGroupsEmptyInSecondPass() { |
| narrativeMergeTestImplementation(false, false, false, false); |
| } |
| |
| public void testSomeGroupsEmptyInSecondPass() { |
| narrativeMergeTestImplementation(false, false, false, true); |
| narrativeMergeTestImplementation(false, false, true, false); |
| narrativeMergeTestImplementation(false, false, true, true); |
| |
| narrativeMergeTestImplementation(false, true, false, false); |
| narrativeMergeTestImplementation(false, true, false, true); |
| narrativeMergeTestImplementation(false, true, true, false); |
| narrativeMergeTestImplementation(false, true, true, true); |
| |
| narrativeMergeTestImplementation(true, false, false, false); |
| narrativeMergeTestImplementation(true, false, false, true); |
| narrativeMergeTestImplementation(true, false, true, false); |
| narrativeMergeTestImplementation(true, false, true, true); |
| |
| narrativeMergeTestImplementation(true, true, false, false); |
| narrativeMergeTestImplementation(true, true, false, true); |
| narrativeMergeTestImplementation(true, true, true, false); |
| } |
| |
| public void testNoGroupsEmptyInSecondPass() { |
| narrativeMergeTestImplementation(true, true, true, true); |
| } |
| |
| /* |
| * This method implements tests for the <code>TopGroup.merge</code> method |
| * using a narrative approach. Use of a creative narrative may seem unusual |
| * or even silly but the idea behind it is to make it hopefully easier to |
| * reason about the documents and groups and scores in the test whilst testing |
| * several scenario permutations. |
| * |
| * Imagine: |
| * |
| * Each document represents (say) a picture book of an animal. |
| * We are searching for two books and wish to draw a picture of our own, inspired by the books. |
| * We think that large animals are easier to draw and therefore order the books by the featured animal's size. |
| * We think that different colors would make for a good drawing and therefore group the books by the featured animal's color. |
| * |
| * Index content: |
| * |
| * The documents are in 2 groups ("blue" and "red") and there are 4 documents across 2 shards: |
| * shard 1 (blue whale, red ant) and shard 2 (blue dragonfly, red squirrel). |
| * |
| * If all documents are present the "blue whale" and the "red squirrel" documents would be returned |
| * for our drawing since they are the largest animals in their respective groups. |
| * |
| * Test permutations (haveBlueWhale, haveRedAnt, haveBlueDragonfly, haveRedSquirrel) arise because |
| * in the first pass of the search all documents can be present, but |
| * in the second pass of the search some documents could be missing |
| * if they have been deleted 'just so' between the two phases. |
| * |
| * Additionally a <code>haveAnimal == false</code> condition also represents scenarios where a given |
| * group has documents on some but not all shards in the collection. |
| */ |
| private void narrativeMergeTestImplementation( |
| boolean haveBlueWhale, |
| boolean haveRedAnt, |
| boolean haveBlueDragonfly, |
| boolean haveRedSquirrel) { |
| |
| final String blueGroupValue = "blue"; |
| final String redGroupValue = "red"; |
| |
| final Integer redAntSize = 1; |
| final Integer blueDragonflySize = 10; |
| final Integer redSquirrelSize = 100; |
| final Integer blueWhaleSize = 1000; |
| |
| final float redAntScore = redAntSize; |
| final float blueDragonflyScore = blueDragonflySize; |
| final float redSquirrelScore = redSquirrelSize; |
| final float blueWhaleScore = blueWhaleSize; |
| |
| final Sort sort = Sort.RELEVANCE; |
| |
| final TopGroups<String> shard1TopGroups; |
| { |
| final GroupDocs<String> group1 = haveBlueWhale |
| ? createSingletonGroupDocs(blueGroupValue, new Object[] { blueWhaleSize }, 1 /* docId */, blueWhaleScore, 0 /* shardIndex */) |
| : createEmptyGroupDocs(blueGroupValue, new Object[] { blueWhaleSize }); |
| |
| final GroupDocs<String> group2 = haveRedAnt |
| ? createSingletonGroupDocs(redGroupValue, new Object[] { redAntSize }, 2 /* docId */, redAntScore, 0 /* shardIndex */) |
| : createEmptyGroupDocs(redGroupValue, new Object[] { redAntSize }); |
| |
| shard1TopGroups = new TopGroups<String>( |
| sort.getSort() /* groupSort */, |
| sort.getSort() /* withinGroupSort */, |
| group1.scoreDocs.length + group2.scoreDocs.length /* totalHitCount */, |
| group1.scoreDocs.length + group2.scoreDocs.length /* totalGroupedHitCount */, |
| combineGroupDocs(group1, group2) /* groups */, |
| (haveBlueWhale ? blueWhaleScore : (haveRedAnt ? redAntScore : Float.NaN)) /* maxScore */); |
| } |
| |
| final TopGroups<String> shard2TopGroups; |
| { |
| final GroupDocs<String> group1 = haveBlueDragonfly |
| ? createSingletonGroupDocs(blueGroupValue, new Object[] { blueDragonflySize }, 3 /* docId */, blueDragonflyScore, 1 /* shardIndex */) |
| : createEmptyGroupDocs(blueGroupValue, new Object[] { blueDragonflySize }); |
| |
| final GroupDocs<String> group2 = haveRedSquirrel |
| ? createSingletonGroupDocs(redGroupValue, new Object[] { redSquirrelSize }, 4 /* docId */, redSquirrelScore, 1 /* shardIndex */) |
| : createEmptyGroupDocs(redGroupValue, new Object[] { redSquirrelSize }); |
| |
| shard2TopGroups = new TopGroups<String>( |
| sort.getSort() /* groupSort */, |
| sort.getSort() /* withinGroupSort */, |
| group1.scoreDocs.length + group2.scoreDocs.length /* totalHitCount */, |
| group1.scoreDocs.length + group2.scoreDocs.length /* totalGroupedHitCount */, |
| combineGroupDocs(group1, group2) /* groups */, |
| (haveRedSquirrel ? redSquirrelScore : (haveBlueDragonfly ? blueDragonflyScore : Float.NaN)) /* maxScore */); |
| } |
| |
| final TopGroups<String> mergedTopGroups = TopGroups.<String>merge( |
| combineTopGroups(shard1TopGroups, shard2TopGroups), |
| sort /* groupSort */, |
| sort /* docSort */, |
| 0 /* docOffset */, |
| 2 /* docTopN */, |
| TopGroups.ScoreMergeMode.None); |
| assertNotNull(mergedTopGroups); |
| |
| final int expectedCount = |
| (haveBlueWhale ? 1 : 0) + |
| (haveRedAnt ? 1 : 0) + |
| (haveBlueDragonfly ? 1 : 0) + |
| (haveRedSquirrel ? 1 : 0); |
| |
| assertEquals(expectedCount, mergedTopGroups.totalHitCount); |
| assertEquals(expectedCount, mergedTopGroups.totalGroupedHitCount); |
| |
| assertEquals(2, mergedTopGroups.groups.length); |
| { |
| assertEquals(blueGroupValue, mergedTopGroups.groups[0].groupValue); |
| final float expectedBlueMaxScore = |
| (haveBlueWhale ? blueWhaleScore : (haveBlueDragonfly ? blueDragonflyScore : Float.NaN)); |
| checkMaxScore(expectedBlueMaxScore, mergedTopGroups.groups[0].maxScore); |
| } |
| { |
| assertEquals(redGroupValue, mergedTopGroups.groups[1].groupValue); |
| final float expectedRedMaxScore = |
| (haveRedSquirrel ? redSquirrelScore : (haveRedAnt ? redAntScore : Float.NaN)); |
| checkMaxScore(expectedRedMaxScore, mergedTopGroups.groups[1].maxScore); |
| } |
| |
| final float expectedMaxScore = |
| (haveBlueWhale ? blueWhaleScore |
| : (haveRedSquirrel ? redSquirrelScore |
| : (haveBlueDragonfly ? blueDragonflyScore |
| : (haveRedAnt ? redAntScore |
| : Float.NaN)))); |
| checkMaxScore(expectedMaxScore, mergedTopGroups.maxScore); |
| } |
| |
| private static void checkMaxScore(float expected, float actual) { |
| if (Float.isNaN(expected)) { |
| assertTrue(Float.isNaN(actual)); |
| } else { |
| assertEquals(expected, actual, 0.0); |
| } |
| } |
| |
| // helper methods |
| |
| private static GroupDocs<String> createEmptyGroupDocs(String groupValue, Object[] groupSortValues) { |
| return new GroupDocs<String>( |
| Float.NaN /* score */, |
| Float.NaN /* maxScore */, |
| new TotalHits(0, TotalHits.Relation.EQUAL_TO), |
| new ScoreDoc[0], |
| groupValue, |
| groupSortValues); |
| } |
| |
| private static GroupDocs<String> createSingletonGroupDocs(String groupValue, Object[] groupSortValues, |
| int docId, float docScore, int shardIndex) { |
| return new GroupDocs<String>( |
| Float.NaN /* score */, |
| docScore /* maxScore */, |
| new TotalHits(1, TotalHits.Relation.EQUAL_TO), |
| new ScoreDoc[] { new ScoreDoc(docId, docScore, shardIndex) }, |
| groupValue, |
| groupSortValues); |
| } |
| |
| private static GroupDocs<String>[] combineGroupDocs(GroupDocs<String> group0, GroupDocs<String> group1) { |
| @SuppressWarnings({"unchecked","rawtypes"}) |
| final GroupDocs<String>[] groups = new GroupDocs[2]; |
| groups[0] = group0; |
| groups[1] = group1; |
| return groups; |
| } |
| |
| private static TopGroups<String>[] combineTopGroups(TopGroups<String> group0, TopGroups<String> group1) { |
| @SuppressWarnings({"unchecked","rawtypes"}) |
| final TopGroups<String>[] groups = new TopGroups[2]; |
| groups[0] = group0; |
| groups[1] = group1; |
| return groups; |
| } |
| |
| } |