SOLR-6168: Add a 'sort' local param to the collapse QParser to support using complex sort options to select the representitive doc for each collapsed group

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1714133 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Chris M. Hostetter 2015-11-12 22:52:06 +00:00
parent 7d1ccd8ddb
commit 918476e0ac
7 changed files with 1353 additions and 347 deletions

View File

@ -240,6 +240,9 @@ New Features
* SOLR-7569: A collection API to force elect a leader, called FORCELEADER, when all replicas in a shard are down
(Ishan Chattopadhyaya, Mark Miller, shalin, noble)
* SOLR-6168: Add a 'sort' local param to the collapse QParser to support using complex sort options
to select the representitive doc for each collapsed group. (Umesh Prasad, hossman)
Bug Fixes
----------------------

View File

@ -52,6 +52,7 @@ public class CollapseScoreFunction extends ValueSource {
public CollapseScoreFunctionValues(Map context) {
this.cscore = (CollapseScore) context.get("CSCORE");
assert null != this.cscore;
}
public int intVal(int doc) {
@ -70,4 +71,4 @@ public class CollapseScoreFunction extends ValueSource {
return 0.0D;
}
}
}
}

View File

@ -118,7 +118,7 @@ NOTE: Tests expect every field in this schema to be sortable.
<!-- ensure function sorts don't mistakenly get interpreted as field sorts
https://issues.apache.org/jira/browse/SOLR-5354?focusedCommentId=13835891&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-13835891
-->
<dynamicField name="*" type="str" multiValued="true" />
<dynamicField name="*" type="str" multiValued="false" />
</fields>
<copyField source="str" dest="str_last" />

View File

@ -236,7 +236,8 @@ public class QueryEqualityTest extends SolrTestCaseJ4 {
}
public void testQueryCollapse() throws Exception {
SolrQueryRequest req = req("myField","foo_s");
SolrQueryRequest req = req("myField","foo_s",
"g_sort","foo_s1 asc, foo_i desc");
try {
assertQueryEquals("collapse", req,
@ -246,7 +247,13 @@ public class QueryEqualityTest extends SolrTestCaseJ4 {
"{!collapse field=$myField max=a}");
assertQueryEquals("collapse", req,
"{!collapse field=$myField min=a}");
"{!collapse field=$myField min=a}",
"{!collapse field=$myField min=a nullPolicy=ignore}");
assertQueryEquals("collapse", req,
"{!collapse field=$myField sort=$g_sort}",
"{!collapse field=$myField sort='foo_s1 asc, foo_i desc'}",
"{!collapse field=$myField sort=$g_sort nullPolicy=ignore}");
assertQueryEquals("collapse", req,
"{!collapse field=$myField max=a nullPolicy=expand}");

View File

@ -29,6 +29,9 @@ import org.apache.lucene.util.LuceneTestCase.SuppressCodecs;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.search.CollapsingQParserPlugin.GroupHeadSelector;
import org.apache.solr.search.CollapsingQParserPlugin.GroupHeadSelectorType;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
@ -52,29 +55,175 @@ public class TestCollapseQParserPlugin extends SolrTestCaseJ4 {
assertU(commit());
}
@Test
public void testStringCollapse() throws Exception {
List<String> types = new ArrayList();
types.add("group_s");
types.add("group_s_dv");
Collections.shuffle(types, random());
String group = types.get(0);
String hint = (random().nextBoolean() ? " hint="+CollapsingQParserPlugin.HINT_TOP_FC : "");
testCollapseQueries(group, hint, false);
public void testMultiSort() throws Exception {
assertU(adoc("id", "1", "group_s", "group1", "test_ti", "5", "test_tl", "10"));
assertU(commit());
assertU(adoc("id", "2", "group_s", "group1", "test_ti", "5", "test_tl", "1000"));
assertU(adoc("id", "3", "group_s", "group1", "test_ti", "5", "test_tl", "1000"));
assertU(adoc("id", "4", "group_s", "group1", "test_ti", "10", "test_tl", "100"));
//
assertU(adoc("id", "5", "group_s", "group2", "test_ti", "5", "test_tl", "10", "term_s", "YYYY"));
assertU(commit());
assertU(adoc("id", "6", "group_s", "group2", "test_ti", "5", "test_tl","1000"));
assertU(adoc("id", "7", "group_s", "group2", "test_ti", "5", "test_tl","1000", "term_s", "XXXX"));
assertU(adoc("id", "8", "group_s", "group2", "test_ti", "10","test_tl", "100"));
assertU(commit());
ModifiableSolrParams params;
// group heads are selected using the same sort that is then applied to the final groups
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field=group_s sort=$sort}");
params.add("sort", "test_ti asc, test_tl desc, id desc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/float[@name='id'][.='7.0']"
,"//result/doc[2]/float[@name='id'][.='3.0']"
);
// group heads are selected using a complex sort, simpler sort used for final groups
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field=group_s sort='test_ti asc, test_tl desc, id desc'}");
params.add("sort", "id asc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/float[@name='id'][.='3.0']"
,"//result/doc[2]/float[@name='id'][.='7.0']"
);
// diff up the sort directions, only first clause matters with our data
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field=group_s sort='test_ti desc, test_tl asc, id asc'}");
params.add("sort", "id desc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/float[@name='id'][.='8.0']"
,"//result/doc[2]/float[@name='id'][.='4.0']"
);
// tie broken by index order
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field=group_s sort='test_tl desc'}");
params.add("sort", "id desc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/float[@name='id'][.='6.0']"
,"//result/doc[2]/float[@name='id'][.='2.0']"
);
// score, then tiebreakers; note top level sort by score ASCENDING (just for weirdness)
params = new ModifiableSolrParams();
params.add("q", "*:* term_s:YYYY");
params.add("fq", "{!collapse field=group_s sort='score desc, test_tl desc, test_ti asc, id asc'}");
params.add("sort", "score asc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/float[@name='id'][.='2.0']"
,"//result/doc[2]/float[@name='id'][.='5.0']"
);
// score, then tiebreakers; note no score in top level sort/fl to check needsScores logic
params = new ModifiableSolrParams();
params.add("q", "*:* term_s:YYYY");
params.add("fq", "{!collapse field=group_s sort='score desc, test_tl desc, test_ti asc, id asc'}");
params.add("sort", "id desc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/float[@name='id'][.='5.0']"
,"//result/doc[2]/float[@name='id'][.='2.0']"
);
// term_s desc -- term_s is missing from many docs, and uses sortMissingLast=true
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field=group_s sort='term_s desc, test_tl asc'}");
params.add("sort", "id asc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/float[@name='id'][.='1.0']"
,"//result/doc[2]/float[@name='id'][.='5.0']"
);
// term_s asc -- term_s is missing from many docs, and uses sortMissingLast=true
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field=group_s sort='term_s asc, test_tl asc'}");
params.add("sort", "id asc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/float[@name='id'][.='1.0']"
,"//result/doc[2]/float[@name='id'][.='7.0']"
);
// collapse on int field
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field=test_ti sort='term_s asc, group_s asc'}");
params.add("sort", "id asc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/float[@name='id'][.='4.0']"
,"//result/doc[2]/float[@name='id'][.='7.0']"
);
// collapse on term_s (very sparse) with nullPolicy=collapse
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field=term_s nullPolicy=collapse sort='test_ti asc, test_tl desc, id asc'}");
params.add("sort", "test_tl asc, id asc");
assertQ(req(params)
, "*[count(//doc)=3]"
,"//result/doc[1]/float[@name='id'][.='5.0']"
,"//result/doc[2]/float[@name='id'][.='2.0']"
,"//result/doc[3]/float[@name='id'][.='7.0']"
);
// sort local param + elevation
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field=group_s sort='term_s desc, test_tl asc'}");
params.add("sort", "test_tl asc");
params.add("qt", "/elevate");
params.add("forceElevation", "true");
params.add("elevateIds", "4.0");
assertQ(req(params),
"*[count(//doc)=2]",
"//result/doc[1]/float[@name='id'][.='4.0']",
"//result/doc[2]/float[@name='id'][.='5.0']");
//
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field=group_s sort='term_s desc, test_tl asc'}");
params.add("sort", "test_tl asc");
params.add("qt", "/elevate");
params.add("forceElevation", "true");
params.add("elevateIds", "7.0");
assertQ(req(params),
"*[count(//doc)=2]",
"//result/doc[1]/float[@name='id'][.='7.0']",
"//result/doc[2]/float[@name='id'][.='1.0']");
}
@Test
public void testStringCollapse() throws Exception {
for (final String hint : new String[] {"", " hint="+CollapsingQParserPlugin.HINT_TOP_FC}) {
testCollapseQueries("group_s", hint, false);
testCollapseQueries("group_s_dv", hint, false);
}
}
@Test
public void testNumericCollapse() throws Exception {
List<String> types = new ArrayList();
types.add("group_i");
types.add("group_ti_dv");
types.add("group_f");
types.add("group_tf_dv");
Collections.shuffle(types, random());
String group = types.get(0);
String hint = "";
testCollapseQueries(group, hint, true);
final String hint = "";
testCollapseQueries("group_i", hint, true);
testCollapseQueries("group_ti_dv", hint, true);
testCollapseQueries("group_f", hint, true);
testCollapseQueries("group_tf_dv", hint, true);
}
@Test
@ -210,9 +359,6 @@ public class TestCollapseQParserPlugin extends SolrTestCaseJ4 {
assertU(commit());
//Test collapse by score and following sort by score
ModifiableSolrParams params = new ModifiableSolrParams();
params.add("q", "*:*");
@ -261,6 +407,20 @@ public class TestCollapseQParserPlugin extends SolrTestCaseJ4 {
"//result/doc[2]/float[@name='id'][.='1.0']",
"//result/doc[3]/float[@name='id'][.='5.0']"
);
// Test value source collapse criteria with cscore function but no top level score sort
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" nullPolicy=collapse min=cscore()"+hint+"}");
params.add("defType", "edismax");
params.add("bf", "field(test_ti)");
params.add("fl", "id");
params.add("sort", "id desc");
assertQ(req(params), "*[count(//doc)=3]",
"//result/doc[1]/float[@name='id'][.='5.0']",
"//result/doc[2]/float[@name='id'][.='4.0']",
"//result/doc[3]/float[@name='id'][.='1.0']"
);
// Test value source collapse criteria with compound cscore function
params = new ModifiableSolrParams();
@ -290,34 +450,56 @@ public class TestCollapseQParserPlugin extends SolrTestCaseJ4 {
"//result/doc[4]/float[@name='id'][.='6.0']");
//Test SOLR-5773 with score collapse criteria
params = new ModifiableSolrParams();
params.add("q", "YYYY");
params.add("fq", "{!collapse field="+group+" nullPolicy=collapse"+hint+"}");
params.add("defType", "edismax");
params.add("bf", "field(test_ti)");
params.add("qf", "term_s");
params.add("qt", "/elevate");
params.add("elevateIds", "1,5");
assertQ(req(params), "*[count(//doc)=3]",
"//result/doc[1]/float[@name='id'][.='1.0']",
"//result/doc[2]/float[@name='id'][.='5.0']",
"//result/doc[3]/float[@name='id'][.='3.0']");
// try both default & sort localparams as alternate ways to ask for max score
for (String maxscore : new String[] {" ", " sort='score desc' "}) {
params = new ModifiableSolrParams();
params.add("q", "YYYY");
params.add("fq", "{!collapse field="+group + maxscore + " nullPolicy=collapse"+hint+"}");
params.add("defType", "edismax");
params.add("bf", "field(test_ti)");
params.add("qf", "term_s");
params.add("qt", "/elevate");
params.add("elevateIds", "1,5");
assertQ(req(params), "*[count(//doc)=3]",
"//result/doc[1]/float[@name='id'][.='1.0']",
"//result/doc[2]/float[@name='id'][.='5.0']",
"//result/doc[3]/float[@name='id'][.='3.0']");
}
//Test SOLR-5773 with max field collapse criteria
params = new ModifiableSolrParams();
params.add("q", "YYYY");
params.add("fq", "{!collapse field="+group+" min=test_ti nullPolicy=collapse"+hint+"}");
params.add("defType", "edismax");
params.add("bf", "field(test_ti)");
params.add("qf", "term_s");
params.add("qt", "/elevate");
params.add("elevateIds", "1,5");
assertQ(req(params), "*[count(//doc)=3]",
"//result/doc[1]/float[@name='id'][.='1.0']",
"//result/doc[2]/float[@name='id'][.='5.0']",
"//result/doc[3]/float[@name='id'][.='4.0']");
// try both max & sort localparams as alternate ways to ask for max group head
for (String max : new String[] {" max=test_ti ", " sort='test_ti desc' "}) {
params = new ModifiableSolrParams();
params.add("q", "YYYY");
params.add("fq", "{!collapse field=" + group + max + "nullPolicy=collapse"+hint+"}");
params.add("defType", "edismax");
params.add("bf", "field(test_ti)");
params.add("qf", "term_s");
params.add("qt", "/elevate");
params.add("elevateIds", "1,5");
assertQ(req(params), "*[count(//doc)=3]",
"//result/doc[1]/float[@name='id'][.='1.0']",
"//result/doc[2]/float[@name='id'][.='5.0']",
"//result/doc[3]/float[@name='id'][.='3.0']");
}
//Test SOLR-5773 with min field collapse criteria
// try both min & sort localparams as alternate ways to ask for min group head
for (String min : new String[] {" min=test_ti ", " sort='test_ti asc' "}) {
params = new ModifiableSolrParams();
params.add("q", "YYYY");
params.add("fq", "{!collapse field=" + group + min + "nullPolicy=collapse"+hint+"}");
params.add("defType", "edismax");
params.add("bf", "field(test_ti)");
params.add("qf", "term_s");
params.add("qt", "/elevate");
params.add("elevateIds", "1,5");
assertQ(req(params), "*[count(//doc)=3]",
"//result/doc[1]/float[@name='id'][.='1.0']",
"//result/doc[2]/float[@name='id'][.='5.0']",
"//result/doc[3]/float[@name='id'][.='4.0']");
}
//Test SOLR-5773 elevating documents with null group
params = new ModifiableSolrParams();
params.add("q", "YYYY");
@ -334,45 +516,72 @@ public class TestCollapseQParserPlugin extends SolrTestCaseJ4 {
"//result/doc[4]/float[@name='id'][.='6.0']");
//Test collapse by min int field and sort
// Non trivial sort local param for picking group head
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" min=test_ti"+hint+"}");
params.add("fq", "{!collapse field="+group+" nullPolicy=collapse sort='term_s asc, test_ti asc' "+hint+"}");
params.add("sort", "id desc");
assertQ(req(params), "*[count(//doc)=2]",
"//result/doc[1]/float[@name='id'][.='5.0']",
"//result/doc[2]/float[@name='id'][.='1.0']");
assertQ(req(params),
"*[count(//doc)=3]",
"//result/doc[1]/float[@name='id'][.='5.0']",
"//result/doc[2]/float[@name='id'][.='4.0']",
"//result/doc[3]/float[@name='id'][.='1.0']"
);
//
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" min=test_ti"+hint+"}");
params.add("sort", "id asc");
assertQ(req(params), "*[count(//doc)=2]",
"//result/doc[1]/float[@name='id'][.='1.0']",
"//result/doc[2]/float[@name='id'][.='5.0']");
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" min=test_ti"+hint+"}");
params.add("sort", "test_tl asc,id desc");
assertQ(req(params), "*[count(//doc)=2]",
"//result/doc[1]/float[@name='id'][.='5.0']",
"//result/doc[2]/float[@name='id'][.='1.0']");
params.add("fq", "{!collapse field="+group+" nullPolicy=collapse sort='term_s asc, test_ti desc' "+hint+"}");
params.add("sort", "id desc");
assertQ(req(params),
"*[count(//doc)=3]",
"//result/doc[1]/float[@name='id'][.='6.0']",
"//result/doc[2]/float[@name='id'][.='3.0']",
"//result/doc[3]/float[@name='id'][.='2.0']"
);
// Test collapse by min int field and top level sort
// try both min & sort localparams as alternate ways to ask for min group head
for (String min : new String[] {" min=test_ti ", " sort='test_ti asc' "}) {
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group + min + hint+"}");
params.add("sort", "id desc");
assertQ(req(params),
"*[count(//doc)=2]",
"//result/doc[1]/float[@name='id'][.='5.0']",
"//result/doc[2]/float[@name='id'][.='1.0']");
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" min=test_ti"+hint+"}");
params.add("sort", "score desc,id asc");
params.add("defType", "edismax");
params.add("bf", "field(id)");
assertQ(req(params), "*[count(//doc)=2]",
"//result/doc[1]/float[@name='id'][.='5.0']",
"//result/doc[2]/float[@name='id'][.='1.0']");
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group + min + hint+"}");
params.add("sort", "id asc");
assertQ(req(params),
"*[count(//doc)=2]",
"//result/doc[1]/float[@name='id'][.='1.0']",
"//result/doc[2]/float[@name='id'][.='5.0']");
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group + min + hint+"}");
params.add("sort", "test_tl asc,id desc");
assertQ(req(params),
"*[count(//doc)=2]",
"//result/doc[1]/float[@name='id'][.='5.0']",
"//result/doc[2]/float[@name='id'][.='1.0']");
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group + min + hint+"}");
params.add("sort", "score desc,id asc");
params.add("defType", "edismax");
params.add("bf", "field(id)");
assertQ(req(params),
"*[count(//doc)=2]",
"//result/doc[1]/float[@name='id'][.='5.0']",
"//result/doc[2]/float[@name='id'][.='1.0']");
}
//Test collapse by max int field
@ -420,9 +629,6 @@ public class TestCollapseQParserPlugin extends SolrTestCaseJ4 {
"//result/doc[1]/float[@name='id'][.='2.0']",
"//result/doc[2]/float[@name='id'][.='6.0']");
//Test collapse by min float field
params = new ModifiableSolrParams();
params.add("q", "*:*");
@ -446,7 +652,42 @@ public class TestCollapseQParserPlugin extends SolrTestCaseJ4 {
assertQ(req(params), "*[count(//doc)=2]",
"//result/doc[1]/float[@name='id'][.='5.0']",
"//result/doc[2]/float[@name='id'][.='1.0']");
// attempting to use cscore() in sort local param should fail
assertQEx("expected error trying to sort on a function that includes cscore()",
req(params("q", "{!func}sub(sub(test_tl,1000),id)",
"fq", "{!collapse field="+group+" sort='abs(cscore()) asc, id asc'}",
"sort", "score asc")),
SolrException.ErrorCode.BAD_REQUEST);
// multiple params for picking groupHead should all fail
for (String bad : new String[] {
"{!collapse field="+group+" min=test_tf max=test_tf}",
"{!collapse field="+group+" min=test_tf sort='test_tf asc'}",
"{!collapse field="+group+" max=test_tf sort='test_tf asc'}" }) {
assertQEx("Expected error: " + bad, req(params("q", "*:*", "fq", bad)),
SolrException.ErrorCode.BAD_REQUEST);
}
// multiple params for picking groupHead should work as long as only one is non-null
// sort used
for (SolrParams collapse : new SolrParams[] {
// these should all be equivilently valid
params("fq", "{!collapse field="+group+" nullPolicy=collapse sort='test_ti asc'"+hint+"}"),
params("fq", "{!collapse field="+group+" nullPolicy=collapse min='' sort='test_ti asc'"+hint+"}"),
params("fq", "{!collapse field="+group+" nullPolicy=collapse max='' sort='test_ti asc'"+hint+"}"),
params("fq", "{!collapse field="+group+" nullPolicy=collapse min=$x sort='test_ti asc'"+hint+"}"),
params("fq", "{!collapse field="+group+" nullPolicy=collapse min=$x sort='test_ti asc'"+hint+"}",
"x",""),
}) {
assertQ(req(collapse, "q", "*:*", "sort", "test_ti desc"),
"*[count(//doc)=3]",
"//result/doc[1]/float[@name='id'][.='4.0']",
"//result/doc[2]/float[@name='id'][.='1.0']",
"//result/doc[3]/float[@name='id'][.='5.0']");
}
//Test nullPolicy expand
params = new ModifiableSolrParams();
@ -460,7 +701,6 @@ public class TestCollapseQParserPlugin extends SolrTestCaseJ4 {
"//result/doc[4]/float[@name='id'][.='1.0']");
//Test nullPolicy collapse
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" max=test_tf nullPolicy=collapse"+hint+"}");
@ -533,5 +773,44 @@ public class TestCollapseQParserPlugin extends SolrTestCaseJ4 {
assertQ(req(params), "*[count(//doc)=0]");
}
public void testGroupHeadSelector() {
GroupHeadSelector s;
try {
s = GroupHeadSelector.build(params("sort", "foo_s asc", "min", "bar_s"));
fail("no exception with multi criteria");
} catch (SolrException e) {
// expected
}
s = GroupHeadSelector.build(params("min", "foo_s"));
assertEquals(GroupHeadSelectorType.MIN, s.type);
assertEquals("foo_s", s.selectorText);
s = GroupHeadSelector.build(params("max", "foo_s"));
assertEquals(GroupHeadSelectorType.MAX, s.type);
assertEquals("foo_s", s.selectorText);
assertFalse(s.equals(GroupHeadSelector.build(params("min", "foo_s", "other", "stuff"))));
s = GroupHeadSelector.build(params());
assertEquals(GroupHeadSelectorType.SCORE, s.type);
assertNotNull(s.selectorText);
assertEquals(GroupHeadSelector.build(params()), s);
assertFalse(s.equals(GroupHeadSelector.build(params("min", "BAR_s"))));
s = GroupHeadSelector.build(params("sort", "foo_s asc"));
assertEquals(GroupHeadSelectorType.SORT, s.type);
assertEquals("foo_s asc", s.selectorText);
assertEquals(GroupHeadSelector.build(params("sort", "foo_s asc")),
s);
assertFalse(s.equals(GroupHeadSelector.build(params("sort", "BAR_s asc"))));
assertFalse(s.equals(GroupHeadSelector.build(params("min", "BAR_s"))));
assertFalse(s.equals(GroupHeadSelector.build(params())));
assertEquals(GroupHeadSelector.build(params("sort", "foo_s asc")).hashCode(),
GroupHeadSelector.build(params("sort", "foo_s asc",
"other", "stuff")).hashCode());
}
}

View File

@ -0,0 +1,215 @@
/*
* 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.solr.search;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import org.apache.lucene.util.LuceneTestCase.SuppressCodecs;
import org.apache.lucene.util.TestUtil;
import org.apache.solr.CursorPagingTest;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import static org.apache.solr.search.CollapsingQParserPlugin.NULL_IGNORE;
import static org.apache.solr.search.CollapsingQParserPlugin.NULL_COLLAPSE;
import static org.apache.solr.search.CollapsingQParserPlugin.NULL_EXPAND;
import org.junit.AfterClass;
import org.junit.BeforeClass;
//We want codecs that support DocValues, and ones supporting blank/empty values.
@SuppressCodecs({"Appending","Lucene3x","Lucene40","Lucene41","Lucene42"})
public class TestRandomCollapseQParserPlugin extends SolrTestCaseJ4 {
/** Full SolrServer instance for arbitrary introspection of response data and adding fqs */
public static SolrClient SOLR;
public static List<String> ALL_SORT_FIELD_NAMES;
public static List<String> ALL_COLLAPSE_FIELD_NAMES;
private static String[] NULL_POLICIES
= new String[] {NULL_IGNORE, NULL_COLLAPSE, NULL_EXPAND};
@BeforeClass
public static void buildIndexAndClient() throws Exception {
initCore("solrconfig-minimal.xml", "schema-sorts.xml");
final int totalDocs = atLeast(500);
for (int i = 1; i <= totalDocs; i++) {
SolrInputDocument doc = CursorPagingTest.buildRandomDocument(i);
// every doc will be in the same group for this (string) field
doc.addField("same_for_all_docs", "xxx");
assertU(adoc(doc));
}
assertU(commit());
// Don't close this client, it would shutdown the CoreContainer
SOLR = new EmbeddedSolrServer(h.getCoreContainer(), h.coreName);
ALL_SORT_FIELD_NAMES = CursorPagingTest.pruneAndDeterministicallySort
(h.getCore().getLatestSchema().getFields().keySet());
ALL_COLLAPSE_FIELD_NAMES = new ArrayList<String>(ALL_SORT_FIELD_NAMES.size());
for (String candidate : ALL_SORT_FIELD_NAMES) {
if (candidate.startsWith("str")
|| candidate.startsWith("float")
|| candidate.startsWith("int") ) {
ALL_COLLAPSE_FIELD_NAMES.add(candidate);
}
}
}
@AfterClass
public static void cleanupStatics() throws Exception {
deleteCore();
SOLR = null;
ALL_SORT_FIELD_NAMES = ALL_COLLAPSE_FIELD_NAMES = null;
}
public void testEveryIsolatedSortFieldOnSingleGroup() throws Exception {
for (String sortField : ALL_SORT_FIELD_NAMES) {
for (String dir : Arrays.asList(" asc", " desc")) {
final String sort = sortField + dir + ", id" + dir; // need id for tie breaker
final String q = random().nextBoolean() ? "*:*" : CursorPagingTest.buildRandomQuery();
final SolrParams sortedP = params("q", q, "rows", "1",
"sort", sort);
final QueryResponse sortedRsp = SOLR.query(sortedP);
// random data -- might be no docs matching our query
if (0 != sortedRsp.getResults().getNumFound()) {
final SolrDocument firstDoc = sortedRsp.getResults().get(0);
// check forced array resizing starting from 1
for (String p : Arrays.asList("{!collapse field=", "{!collapse size='1' field=")) {
for (String fq : Arrays.asList
(p + "same_for_all_docs sort='"+sort+"'}",
// nullPolicy=expand shouldn't change anything since every doc has field
p + "same_for_all_docs sort='"+sort+"' nullPolicy=expand}",
// a field in no docs with nullPolicy=collapse should have same effect as
// collapsing on a field in every doc
p + "not_in_any_docs sort='"+sort+"' nullPolicy=collapse}")) {
final SolrParams collapseP = params("q", q, "rows", "1", "fq", fq);
// since every doc is in the same group, collapse query should return exactly one doc
final QueryResponse collapseRsp = SOLR.query(collapseP);
assertEquals("collapse should have produced exactly one doc: " + collapseP,
1, collapseRsp.getResults().getNumFound());
final SolrDocument groupHead = collapseRsp.getResults().get(0);
// the group head from the collapse query should match the first doc of a simple sort
assertEquals(sortedP + " => " + firstDoc + " :VS: " + collapseP + " => " + groupHead,
firstDoc.getFieldValue("id"), groupHead.getFieldValue("id"));
}
}
}
}
}
}
public void testRandomCollpaseWithSort() throws Exception {
final int numMainQueriesPerCollapseField = atLeast(5);
for (String collapseField : ALL_COLLAPSE_FIELD_NAMES) {
for (int i = 0; i < numMainQueriesPerCollapseField; i++) {
final String topSort = CursorPagingTest.buildRandomSort(ALL_SORT_FIELD_NAMES);
final String collapseSort = CursorPagingTest.buildRandomSort(ALL_SORT_FIELD_NAMES);
final String q = random().nextBoolean() ? "*:*" : CursorPagingTest.buildRandomQuery();
final SolrParams mainP = params("q", q, "fl", "id,"+collapseField);
final String csize = random().nextBoolean() ?
"" : " size=" + TestUtil.nextInt(random(),1,10000);
final String nullPolicy = randomNullPolicy();
final String nullPs = NULL_IGNORE.equals(nullPolicy)
// ignore is default, randomly be explicit about it
? (random().nextBoolean() ? "" : " nullPolicy=ignore")
: (" nullPolicy=" + nullPolicy);
final SolrParams collapseP
= params("sort", topSort,
"rows", "200",
"fq", ("{!collapse" + csize + nullPs +
" field="+collapseField+" sort='"+collapseSort+"'}"));
final QueryResponse mainRsp = SOLR.query(SolrParams.wrapDefaults(collapseP, mainP));
for (SolrDocument doc : mainRsp.getResults()) {
final Object groupHeadId = doc.getFieldValue("id");
final Object collapseVal = doc.getFieldValue(collapseField);
if (null == collapseVal) {
if (NULL_EXPAND.equals(nullPolicy)) {
// nothing to check for this doc, it's in it's own group
continue;
}
assertFalse(groupHeadId + " has null collapseVal but nullPolicy==ignore; " +
"mainP: " + mainP + ", collapseP: " + collapseP,
NULL_IGNORE.equals(nullPolicy));
}
// work arround for SOLR-8082...
//
// what's important is that we already did the collapsing on the *real* collapseField
// to verify the groupHead returned is really the best our verification filter
// on docs with that value in a differnet ifeld containing the exact same values
final String checkField = collapseField.replace("float_dv", "float");
final String checkFQ = ((null == collapseVal)
? ("-" + checkField + ":[* TO *]")
: ("{!field f="+checkField+"}" + collapseVal.toString()));
final SolrParams checkP = params("fq", checkFQ,
"rows", "1",
"sort", collapseSort);
final QueryResponse checkRsp = SOLR.query(SolrParams.wrapDefaults(checkP, mainP));
assertTrue("not even 1 match for sanity check query? expected: " + doc,
! checkRsp.getResults().isEmpty());
final SolrDocument firstMatch = checkRsp.getResults().get(0);
final Object firstMatchId = firstMatch.getFieldValue("id");
assertEquals("first match for filtered group '"+ collapseVal +
"' not matching expected group head ... " +
"mainP: " + mainP + ", collapseP: " + collapseP + ", checkP: " + checkP,
groupHeadId, firstMatchId);
}
}
}
}
private String randomNullPolicy() {
return NULL_POLICIES[ TestUtil.nextInt(random(), 0, NULL_POLICIES.length-1) ];
}
}