You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ho...@apache.org on 2015/11/12 23:52:06 UTC

svn commit: r1714133 [2/2] - in /lucene/dev/trunk/solr: ./ core/src/java/org/apache/solr/search/ core/src/java/org/apache/solr/search/function/ core/src/test-files/solr/collection1/conf/ core/src/test/org/apache/solr/search/

Modified: lucene/dev/trunk/solr/core/src/test/org/apache/solr/search/TestCollapseQParserPlugin.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/search/TestCollapseQParserPlugin.java?rev=1714133&r1=1714132&r2=1714133&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/search/TestCollapseQParserPlugin.java (original)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/search/TestCollapseQParserPlugin.java Thu Nov 12 22:52:06 2015
@@ -29,6 +29,9 @@ import org.apache.lucene.util.LuceneTest
 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 e
     assertU(commit());
   }
 
+  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 {
-    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);
+    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 e
     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 e
         "//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 e
                          "//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 e
         "//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']");
-
-    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']");
-
+    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", "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']"
+    );
+    
 
 
-    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']");
+    // 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 + 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 e
                          "//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 e
     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 e
         "//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 e
     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());
+    
+  }
 
 }

Added: lucene/dev/trunk/solr/core/src/test/org/apache/solr/search/TestRandomCollapseQParserPlugin.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/search/TestRandomCollapseQParserPlugin.java?rev=1714133&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/search/TestRandomCollapseQParserPlugin.java (added)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/search/TestRandomCollapseQParserPlugin.java Thu Nov 12 22:52:06 2015
@@ -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) ];
+  }
+  
+}