blob: 3d2e89d6744e3774e5b85904fc7abe9ae7c69923 [file] [log] [blame]
/*
* 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.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import java.util.stream.Collectors;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrInputDocument;
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;
import static org.hamcrest.core.StringContains.containsString;
public class TestCollapseQParserPlugin extends SolrTestCaseJ4 {
@BeforeClass
public static void beforeClass() throws Exception {
// we need DVs on point fields to compute stats & facets
if (Boolean.getBoolean(NUMERIC_POINTS_SYSPROP)) System.setProperty(NUMERIC_DOCVALUES_SYSPROP,"true");
initCore("solrconfig-collapseqparser.xml", "schema11.xml");
}
@Override
@Before
public void setUp() throws Exception {
// if you override setUp or tearDown, you better call
// the super classes version
super.setUp();
clearIndex();
assertU(commit());
}
public void testMultiSort() throws Exception {
assertU(adoc("id", "1", "group_s", "group1", "test_i", "5", "test_l", "10"));
assertU(commit());
assertU(adoc("id", "2", "group_s", "group1", "test_i", "5", "test_l", "1000"));
assertU(adoc("id", "3", "group_s", "group1", "test_i", "5", "test_l", "1000"));
assertU(adoc("id", "4", "group_s", "group1", "test_i", "10", "test_l", "100"));
//
assertU(adoc("id", "5", "group_s", "group2", "test_i", "5", "test_l", "10", "term_s", "YYYY"));
assertU(commit());
assertU(adoc("id", "6", "group_s", "group2", "test_i", "5", "test_l","1000"));
assertU(adoc("id", "7", "group_s", "group2", "test_i", "5", "test_l","1000", "term_s", "XXXX"));
assertU(adoc("id", "8", "group_s", "group2", "test_i", "10","test_l", "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_i asc, test_l desc, id_i desc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/str[@name='id'][.='7']"
,"//result/doc[2]/str[@name='id'][.='3']"
);
// 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_i asc, test_l desc, id_i desc'}");
params.add("sort", "id_i asc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/str[@name='id'][.='3']"
,"//result/doc[2]/str[@name='id'][.='7']"
);
// 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_i desc, test_l asc, id_i asc'}");
params.add("sort", "id_i desc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/str[@name='id'][.='8']"
,"//result/doc[2]/str[@name='id'][.='4']"
);
// tie broken by index order
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field=group_s sort='test_l desc'}");
params.add("sort", "id_i desc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/str[@name='id'][.='6']"
,"//result/doc[2]/str[@name='id'][.='2']"
);
// 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_l desc, test_i asc, id_i asc'}");
params.add("sort", "score asc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/str[@name='id'][.='2']"
,"//result/doc[2]/str[@name='id'][.='5']"
);
// 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_l desc, test_i asc, id_i asc'}");
params.add("sort", "id_i desc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/str[@name='id'][.='5']"
,"//result/doc[2]/str[@name='id'][.='2']"
);
// 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_l asc'}");
params.add("sort", "id_i asc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/str[@name='id'][.='1']"
,"//result/doc[2]/str[@name='id'][.='5']"
);
// 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_l asc'}");
params.add("sort", "id_i asc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/str[@name='id'][.='1']"
,"//result/doc[2]/str[@name='id'][.='7']"
);
// collapse on int field
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field=test_i sort='term_s asc, group_s asc'}");
params.add("sort", "id_i asc");
assertQ(req(params)
, "*[count(//doc)=2]"
,"//result/doc[1]/str[@name='id'][.='4']"
,"//result/doc[2]/str[@name='id'][.='7']"
);
// 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_i asc, test_l desc, id_i asc'}");
params.add("sort", "test_l asc, id_i asc");
assertQ(req(params)
, "*[count(//doc)=3]"
,"//result/doc[1]/str[@name='id'][.='5']"
,"//result/doc[2]/str[@name='id'][.='2']"
,"//result/doc[3]/str[@name='id'][.='7']"
);
// sort local param + elevation
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field=group_s sort='term_s desc, test_l asc'}");
params.add("sort", "test_l asc");
params.add("qt", "/elevate");
params.add("forceElevation", "true");
params.add("elevateIds", "4");
assertQ(req(params),
"*[count(//doc)=2]",
"//result/doc[1]/str[@name='id'][.='4']",
"//result/doc[2]/str[@name='id'][.='5']");
//
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field=group_s sort='term_s desc, test_l asc'}");
params.add("sort", "test_l asc");
params.add("qt", "/elevate");
params.add("forceElevation", "true");
params.add("elevateIds", "7");
assertQ(req(params),
"*[count(//doc)=2]",
"//result/doc[1]/str[@name='id'][.='7']",
"//result/doc[2]/str[@name='id'][.='1']");
}
@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 {
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
public void testFieldValueCollapseWithNegativeMinMax() throws Exception {
String[] doc = {"id","1", "group_i", "-1000", "test_i", "5", "test_l", "-10", "test_f", "2000.32"};
assertU(adoc(doc));
assertU(commit());
String[] doc1 = {"id","2", "group_i", "-1000", "test_i", "50", "test_l", "-100", "test_f", "2000.33"};
assertU(adoc(doc1));
String[] doc2 = {"id","3", "group_i", "-1000", "test_l", "100", "test_f", "200"};
assertU(adoc(doc2));
assertU(commit());
String[] doc3 = {"id","4", "test_i", "500", "test_l", "1000", "test_f", "2000"};
assertU(adoc(doc3));
String[] doc4 = {"id","5", "group_i", "-1000", "test_i", "4", "test_l", "10", "test_f", "2000.31"};
assertU(adoc(doc4));
assertU(commit());
String[] doc5 = {"id","6", "group_i", "-1000", "test_i", "10", "test_l", "100", "test_f", "-2000.12"};
assertU(adoc(doc5));
assertU(commit());
String[] doc6 = {"id","7", "group_i", "-1000", "test_i", "8", "test_l", "-50", "test_f", "-100.2"};
assertU(adoc(doc6));
assertU(commit());
ModifiableSolrParams params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field=group_i min=test_f}");
assertQ(req(params), "*[count(//doc)=1]",
"//result/doc[1]/str[@name='id'][.='6']");
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field=group_i max=test_f}");
assertQ(req(params), "*[count(//doc)=1]",
"//result/doc[1]/str[@name='id'][.='2']");
}
@Test // https://issues.apache.org/jira/browse/SOLR-9494
public void testNeedsScoreBugFixed() throws Exception {
String[] doc = {"id","1", "group_s", "xyz", "text_ws", "hello xxx world"};
assertU(adoc(doc));
assertU(commit());
ModifiableSolrParams params = params(
"q", "{!surround df=text_ws} 2W(hello, world)", // a SpanQuery that matches
"fq", "{!collapse field=group_s}", // collapse on some field
// note: rows= whatever; doesn't matter
"facet", "true", // facet on something
"facet.field", "group_s"
);
assertQ(req(params));
assertQ(req(params)); // fails *second* time!
}
@Test
public void testMergeBoost() throws Exception {
Set<Integer> boosted = new HashSet<>();
Set<Integer> results = new HashSet<>();
for(int i=0; i<200; i++) {
boosted.add(random().nextInt(1000));
}
for(int i=0; i<200; i++) {
results.add(random().nextInt(1000));
}
int[] boostedArray = new int[boosted.size()];
int[] resultsArray = new int[results.size()];
Iterator<Integer> boostIt = boosted.iterator();
int index = 0;
while(boostIt.hasNext()) {
boostedArray[index++] = boostIt.next();
}
Iterator<Integer> resultsIt = results.iterator();
index = 0;
while(resultsIt.hasNext()) {
resultsArray[index++] = resultsIt.next();
}
Arrays.sort(boostedArray);
Arrays.sort(resultsArray);
CollapsingQParserPlugin.MergeBoost mergeBoost = new CollapsingQParserPlugin.MergeBoost(boostedArray);
List<Integer> boostedResults = new ArrayList<>();
for(int i=0; i<resultsArray.length; i++) {
int result = resultsArray[i];
if(mergeBoost.boost(result)) {
boostedResults.add(result);
}
}
List<Integer> controlResults = new ArrayList<>();
for(int i=0; i<resultsArray.length; i++) {
int result = resultsArray[i];
if(Arrays.binarySearch(boostedArray, result) > -1) {
controlResults.add(result);
}
}
if(boostedResults.size() == controlResults.size()) {
for(int i=0; i<boostedResults.size(); i++) {
if(!boostedResults.get(i).equals(controlResults.get(i))) {
throw new Exception("boosted results do not match control results, boostedResults size:"+boostedResults.toString()+", controlResults size:"+controlResults.toString());
}
}
} else {
throw new Exception("boosted results do not match control results, boostedResults size:"+boostedResults.toString()+", controlResults size:"+controlResults.toString());
}
}
@Test
public void testDoubleCollapse() {
testDoubleCollapse("group_s", "");
testDoubleCollapse("group_i", "");
}
/*
* SOLR-14073
* The double collapse causes a look ahead in the second collapse to a segment that was not visited by
* the by finally method of the first collapse. This specific test is meant to confirm that any feature
* that causes searches to not visit each segment (such as early query termination) doesn't break collapse.
*/
private void testDoubleCollapse(String group, String hint) {
String[] doc = {"id","1", "term_s", "YYYY", group, "1", "test_i", "5", "test_l", "10", "test_f", "2000"};
assertU(adoc(doc));
assertU(commit());
String[] doc1 = {"id","2", "term_s","YYYY", group, "2", "test_i", "50", "test_l", "100", "test_f", "200"};
assertU(adoc(doc1));
String[] doc2 = {"id","3", "term_s", "YYYY", "test_i", "5000", "test_l", "100", "test_f", "200"};
assertU(adoc(doc2));
assertU(commit());
String[] doc3 = {"id","4", "term_s", "YYYY", "test_i", "500", "test_l", "1000", "test_f", "2000"};
assertU(adoc(doc3));
String[] doc4 = {"id","5", "term_s", "YYYN", group, "2", "test_i", "4", "test_l", "10", "test_f", "2000"};
assertU(adoc(doc4));
assertU(commit());
String[] doc5 = {"id","6", "term_s","YYYY", group, "2", "test_i", "10", "test_l", "100", "test_f", "200"};
assertU(adoc(doc5));
assertU(commit());
String[] doc6 = {"id","7", "term_s", "YYYY", group, "1", "test_i", "8", "test_l", "50", "test_f", "300"};
assertU(adoc(doc6));
assertU(commit());
ModifiableSolrParams params = new ModifiableSolrParams();
params.add("q", "id:(1 2 5)");
params.add("fq", "{!collapse cost=200 field=term_s "+hint+"}");
params.add("fq", "{!collapse cost=400 field="+group+""+hint+"}");
params.add("defType", "edismax");
params.add("bf", "field(test_i)");
assertQ(req(params, "indent", "on"), "*[count(//doc)=1]",
"//result/doc[1]/str[@name='id'][.='2']"
);
params = new ModifiableSolrParams();
params.add("q", "id:(1 2 5)");
params.add("fq", "{!collapse cost=200 max=test_i field=term_s "+hint+"}");
params.add("fq", "{!collapse cost=400 max=test_i field="+group+""+hint+"}");
params.add("defType", "edismax");
params.add("bf", "field(test_i)");
assertQ(req(params, "indent", "on"), "*[count(//doc)=1]",
"//result/doc[1]/str[@name='id'][.='2']"
);
}
private void testCollapseQueries(String group, String hint, boolean numeric) throws Exception {
String[] doc = {"id","1", "term_s", "YYYY", group, "1", "test_i", "5", "test_l", "10", "test_f", "2000"};
assertU(adoc(doc));
assertU(commit());
String[] doc1 = {"id","2", "term_s","YYYY", group, "1", "test_i", "50", "test_l", "100", "test_f", "200"};
assertU(adoc(doc1));
String[] doc2 = {"id","3", "term_s", "YYYY", "test_i", "5000", "test_l", "100", "test_f", "200"};
assertU(adoc(doc2));
assertU(commit());
String[] doc3 = {"id","4", "term_s", "YYYY", "test_i", "500", "test_l", "1000", "test_f", "2000"};
assertU(adoc(doc3));
String[] doc4 = {"id","5", "term_s", "YYYY", group, "2", "test_i", "4", "test_l", "10", "test_f", "2000"};
assertU(adoc(doc4));
assertU(commit());
String[] doc5 = {"id","6", "term_s","YYYY", group, "2", "test_i", "10", "test_l", "100", "test_f", "200"};
assertU(adoc(doc5));
assertU(commit());
String[] doc6 = {"id","7", "term_s", "YYYY", group, "1", "test_i", "8", "test_l", "50", "test_f", "300"};
assertU(adoc(doc6));
assertU(commit());
//Test collapse by score and following sort by score
ModifiableSolrParams params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+""+hint+"}");
params.add("defType", "edismax");
params.add("bf", "field(test_i)");
assertQ(req(params, "indent", "on"), "*[count(//doc)=2]",
"//result/doc[1]/str[@name='id'][.='2']",
"//result/doc[2]/str[@name='id'][.='6']"
);
// SOLR-5544 test ordering with empty sort param
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" nullPolicy=expand min=test_f"+hint+"}");
params.add("defType", "edismax");
params.add("bf", "field(test_i)");
params.add("sort","");
assertQ(req(params), "*[count(//doc)=4]",
"//result/doc[1]/str[@name='id'][.='3']",
"//result/doc[2]/str[@name='id'][.='4']",
"//result/doc[3]/str[@name='id'][.='2']",
"//result/doc[4]/str[@name='id'][.='6']"
);
// Test value source collapse criteria
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" nullPolicy=collapse min=field(test_i)"+hint+"}");
params.add("sort", "test_i desc");
assertQ(req(params), "*[count(//doc)=3]",
"//result/doc[1]/str[@name='id'][.='4']",
"//result/doc[2]/str[@name='id'][.='1']",
"//result/doc[3]/str[@name='id'][.='5']"
);
// Test value source collapse criteria with cscore function
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_i)");
assertQ(req(params), "*[count(//doc)=3]",
"//result/doc[1]/str[@name='id'][.='4']",
"//result/doc[2]/str[@name='id'][.='1']",
"//result/doc[3]/str[@name='id'][.='5']"
);
// 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_i)");
params.add("fl", "id");
params.add("sort", "id_i desc");
assertQ(req(params), "*[count(//doc)=3]",
"//result/doc[1]/str[@name='id'][.='5']",
"//result/doc[2]/str[@name='id'][.='4']",
"//result/doc[3]/str[@name='id'][.='1']"
);
// Test value source collapse criteria with compound cscore function
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" nullPolicy=collapse min=sum(cscore(),field(test_i))"+hint+"}");
params.add("defType", "edismax");
params.add("bf", "field(test_i)");
assertQ(req(params), "*[count(//doc)=3]",
"//result/doc[1]/str[@name='id'][.='4']",
"//result/doc[2]/str[@name='id'][.='1']",
"//result/doc[3]/str[@name='id'][.='5']"
);
//Test collapse by score with elevation
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_i)");
params.add("qf", "term_s");
params.add("qt", "/elevate");
assertQ(req(params), "*[count(//doc)=4]",
"//result/doc[1]/str[@name='id'][.='1']",
"//result/doc[2]/str[@name='id'][.='2']",
"//result/doc[3]/str[@name='id'][.='3']",
"//result/doc[4]/str[@name='id'][.='6']");
//Test SOLR-5773 with score collapse criteria
// 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_i)");
params.add("qf", "term_s");
params.add("qt", "/elevate");
params.add("elevateIds", "1,5");
assertQ(req(params), "*[count(//doc)=3]",
"//result/doc[1]/str[@name='id'][.='1']",
"//result/doc[2]/str[@name='id'][.='5']",
"//result/doc[3]/str[@name='id'][.='3']");
}
//Test SOLR-5773 with max field collapse criteria
// try both max & sort localparams as alternate ways to ask for max group head
for (String max : new String[] {" max=test_i ", " sort='test_i 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_i)");
params.add("qf", "term_s");
params.add("qt", "/elevate");
params.add("elevateIds", "1,5");
assertQ(req(params), "*[count(//doc)=3]",
"//result/doc[1]/str[@name='id'][.='1']",
"//result/doc[2]/str[@name='id'][.='5']",
"//result/doc[3]/str[@name='id'][.='3']");
}
//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_i ", " sort='test_i 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_i)");
params.add("qf", "term_s");
params.add("qt", "/elevate");
params.add("elevateIds", "1,5");
assertQ(req(params), "*[count(//doc)=3]",
"//result/doc[1]/str[@name='id'][.='1']",
"//result/doc[2]/str[@name='id'][.='5']",
"//result/doc[3]/str[@name='id'][.='4']");
}
//Test SOLR-5773 elevating documents with null group
params = new ModifiableSolrParams();
params.add("q", "YYYY");
params.add("fq", "{!collapse field="+group+""+hint+"}");
params.add("defType", "edismax");
params.add("bf", "field(test_i)");
params.add("qf", "term_s");
params.add("qt", "/elevate");
params.add("elevateIds", "3,4");
assertQ(req(params), "*[count(//doc)=4]",
"//result/doc[1]/str[@name='id'][.='3']",
"//result/doc[2]/str[@name='id'][.='4']",
"//result/doc[3]/str[@name='id'][.='2']",
"//result/doc[4]/str[@name='id'][.='6']");
// Non trivial sort local param for picking group head
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" nullPolicy=collapse sort='term_s asc, test_i asc' "+hint+"}");
params.add("sort", "id_i desc");
assertQ(req(params),
"*[count(//doc)=3]",
"//result/doc[1]/str[@name='id'][.='5']",
"//result/doc[2]/str[@name='id'][.='4']",
"//result/doc[3]/str[@name='id'][.='1']"
);
//
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" nullPolicy=collapse sort='term_s asc, test_i desc' "+hint+"}");
params.add("sort", "id_i desc");
assertQ(req(params),
"*[count(//doc)=3]",
"//result/doc[1]/str[@name='id'][.='6']",
"//result/doc[2]/str[@name='id'][.='3']",
"//result/doc[3]/str[@name='id'][.='2']"
);
// 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_i ", " sort='test_i asc' "}) {
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group + min + hint+"}");
params.add("sort", "id_i desc");
assertQ(req(params),
"*[count(//doc)=2]",
"//result/doc[1]/str[@name='id'][.='5']",
"//result/doc[2]/str[@name='id'][.='1']");
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group + min + hint+"}");
params.add("sort", "id_i asc");
assertQ(req(params),
"*[count(//doc)=2]",
"//result/doc[1]/str[@name='id'][.='1']",
"//result/doc[2]/str[@name='id'][.='5']");
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group + min + hint+"}");
params.add("sort", "test_l asc,id_i desc");
assertQ(req(params),
"*[count(//doc)=2]",
"//result/doc[1]/str[@name='id'][.='5']",
"//result/doc[2]/str[@name='id'][.='1']");
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group + min + hint+"}");
params.add("sort", "score desc,id_i asc");
params.add("defType", "edismax");
params.add("bf", "field(id_i)");
assertQ(req(params),
"*[count(//doc)=2]",
"//result/doc[1]/str[@name='id'][.='5']",
"//result/doc[2]/str[@name='id'][.='1']");
}
//Test collapse by max int field
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" max=test_i"+hint+"}");
params.add("sort", "test_i asc");
assertQ(req(params), "*[count(//doc)=2]",
"//result/doc[1]/str[@name='id'][.='6']",
"//result/doc[2]/str[@name='id'][.='2']"
);
try {
//Test collapse by min long field
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" min=test_l"+hint+"}");
params.add("sort", "test_i desc");
assertQ(req(params), "*[count(//doc)=2]",
"//result/doc[1]/str[@name='id'][.='1']",
"//result/doc[2]/str[@name='id'][.='5']");
//Test collapse by max long field
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" max=test_l"+hint+"}");
params.add("sort", "test_i desc");
assertQ(req(params), "*[count(//doc)=2]",
"//result/doc[1]/str[@name='id'][.='2']",
"//result/doc[2]/str[@name='id'][.='6']");
} catch (Exception e) {
if(!numeric) {
throw e;
}
}
//Test collapse by min float field
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" min=test_f"+hint+"}");
params.add("sort", "test_i desc");
assertQ(req(params), "*[count(//doc)=2]",
"//result/doc[1]/str[@name='id'][.='2']",
"//result/doc[2]/str[@name='id'][.='6']");
//Test collapse by min float field
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" max=test_f"+hint+"}");
params.add("sort", "test_i asc");
assertQ(req(params), "*[count(//doc)=2]",
"//result/doc[1]/str[@name='id'][.='5']",
"//result/doc[2]/str[@name='id'][.='1']");
//Test collapse by min float field sort by score
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" max=test_f"+hint+"}");
params.add("defType", "edismax");
params.add("bf", "field(id_i)");
params.add("fl", "score, id");
params.add("facet","true");
params.add("fq", "{!tag=test}term_s:YYYY");
params.add("facet.field", "{!ex=test}term_s");
assertQ(req(params), "*[count(//doc)=2]",
"//result/doc[1]/str[@name='id'][.='5']",
"//result/doc[2]/str[@name='id'][.='1']");
// Test collapse using selector field in no docs
// tie selector in all of these cases
for (String selector : new String[] {
" min=bogus_i ", " sort='bogus_i asc' ",
" max=bogus_i ", " sort='bogus_i desc' ",
" min=bogus_tf ", " sort='bogus_tf asc' ",
" max=bogus_tf ", " sort='bogus_tf desc' ",
" sort='bogus_td asc' ", " sort='bogus_td desc' ",
" sort='bogus_s asc' ", " sort='bogus_s desc' ",
}) {
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group + selector + hint+"}");
params.add("sort", group + " asc");
assertQ(req(params),
"*[count(//doc)=2]",
// since selector is bogus, group head is undefined
// (should be index order, but don't make absolute assumptions: segments may be re-ordered)
// key assertion is that there is one doc from each group & groups are in order
"//result/doc[1]/*[@name='"+group+"'][starts-with(.,'1')]",
"//result/doc[2]/*[@name='"+group+"'][starts-with(.,'2')]");
}
// 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_l,1000),id_i)",
"fq", "{!collapse field="+group+" sort='abs(cscore()) asc, id_i 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_f max=test_f}",
"{!collapse field="+group+" min=test_f sort='test_f asc'}",
"{!collapse field="+group+" max=test_f sort='test_f 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 equally valid
params("fq", "{!collapse field="+group+" nullPolicy=collapse sort='test_i asc'"+hint+"}"),
params("fq", "{!collapse field="+group+" nullPolicy=collapse min='' sort='test_i asc'"+hint+"}"),
params("fq", "{!collapse field="+group+" nullPolicy=collapse max='' sort='test_i asc'"+hint+"}"),
params("fq", "{!collapse field="+group+" nullPolicy=collapse min=$x sort='test_i asc'"+hint+"}"),
params("fq", "{!collapse field="+group+" nullPolicy=collapse min=$x sort='test_i asc'"+hint+"}",
"x",""),
}) {
assertQ(req(collapse, "q", "*:*", "sort", "test_i desc"),
"*[count(//doc)=3]",
"//result/doc[1]/str[@name='id'][.='4']",
"//result/doc[2]/str[@name='id'][.='1']",
"//result/doc[3]/str[@name='id'][.='5']");
}
//Test nullPolicy expand
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" max=test_f nullPolicy=expand"+hint+"}");
params.add("sort", "id_i desc");
assertQ(req(params), "*[count(//doc)=4]",
"//result/doc[1]/str[@name='id'][.='5']",
"//result/doc[2]/str[@name='id'][.='4']",
"//result/doc[3]/str[@name='id'][.='3']",
"//result/doc[4]/str[@name='id'][.='1']");
//Test nullPolicy collapse
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" max=test_f nullPolicy=collapse"+hint+"}");
params.add("sort", "id_i desc");
assertQ(req(params), "*[count(//doc)=3]",
"//result/doc[1]/str[@name='id'][.='5']",
"//result/doc[2]/str[@name='id'][.='4']",
"//result/doc[3]/str[@name='id'][.='1']");
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+hint+"}");
params.add("defType", "edismax");
params.add("bf", "field(test_i)");
params.add("fq","{!tag=test_i}id:5");
params.add("facet","true");
params.add("facet.field","{!ex=test_i}test_i");
params.add("facet.mincount", "1");
assertQ(req(params), "*[count(//doc)=1]", "*[count(//lst[@name='facet_fields']/lst[@name='test_i']/int)=2]");
// SOLR-13970
SolrException ex = expectThrows(SolrException.class, () -> {
h.query(req(params("q", "*:*", "fq", "{!collapse field="+group+hint+"}", "group", "true", "group.field", "id")));
});
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
assertThat(ex.getMessage(), containsString("Can not use collapse with Grouping enabled"));
// delete the elevated docs, confirm collapsing still works
assertU(delI("1"));
assertU(delI("2"));
assertU(commit());
params = new ModifiableSolrParams();
params.add("q", "YYYY");
params.add("fq", "{!collapse field="+group+hint+" nullPolicy=collapse}");
params.add("defType", "edismax");
params.add("bf", "field(test_i)");
params.add("qf", "term_s");
params.add("qt", "/elevate");
assertQ(req(params), "*[count(//doc)=3]",
"//result/doc[1]/str[@name='id'][.='3']",
"//result/doc[2]/str[@name='id'][.='6']",
"//result/doc[3]/str[@name='id'][.='7']");
}
@Test
public void testMissingFieldParam() throws Exception {
ModifiableSolrParams params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse}");
assertQEx("It should respond with a bad request when the 'field' param is missing", req(params),
SolrException.ErrorCode.BAD_REQUEST);
}
@Test
public void testEmptyCollection() throws Exception {
// group_s is docValues=false and group_dv_s is docValues=true
String group = (random().nextBoolean() ? "group_s" : "group_s_dv");
// min-or-max is for CollapsingScoreCollector vs. CollapsingFieldValueCollector
String optional_min_or_max = (random().nextBoolean() ? "" : (random().nextBoolean() ? "min=field(test_i)" : "max=field(test_i)"));
ModifiableSolrParams params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("fq", "{!collapse field="+group+" "+optional_min_or_max+"}");
assertQ(req(params), "*[count(//doc)=0]");
// if a field is uninvertible=false, it should behave the same as a field that is indexed=false
// this is currently ok on fields that don't exist on any docs in the index
for (String f : Arrays.asList("not_indexed_sS", "indexed_s_not_uninvert")) {
for (String hint : Arrays.asList("", " hint=top_fc")) {
SolrException e = expectThrows(SolrException.class,
() -> h.query(req("q", "*:*", "fq", "{!collapse field="+f + hint +"}")));
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
assertTrue("unexpected Message: " + e.getMessage(),
e.getMessage().contains("Collapsing field '" + f + "' " +
"should be either docValues enabled or indexed with uninvertible enabled"));
}
}
}
public void testNoDocsHaveGroupField() throws Exception {
// as unlikely as this test seems, it's important for the possibility that a segment exists w/o
// any live docs that have DocValues for the group field -- ie: every doc in segment is in null group.
assertU(adoc("id", "1", "group_s", "group1", "test_i", "5", "test_l", "10"));
assertU(commit());
assertU(adoc("id", "2", "group_s", "group1", "test_i", "5", "test_l", "1000"));
assertU(adoc("id", "3", "group_s", "group1", "test_i", "5", "test_l", "1000"));
assertU(adoc("id", "4", "group_s", "group1", "test_i", "10", "test_l", "100"));
//
assertU(adoc("id", "5", "group_s", "group2", "test_i", "5", "test_l", "10", "term_s", "YYYY"));
assertU(commit());
assertU(adoc("id", "6", "group_s", "group2", "test_i", "5", "test_l","1000"));
assertU(adoc("id", "7", "group_s", "group2", "test_i", "5", "test_l","1000", "term_s", "XXXX"));
assertU(adoc("id", "8", "group_s", "group2", "test_i", "10","test_l", "100"));
assertU(commit());
// none of these grouping fields are in any doc
for (String group : new String[] {
"field=bogus_s", "field=bogus_s_dv",
"field=bogus_s hint=top_fc", // alternative docvalues codepath w/ hint
"field=bogus_s_dv hint=top_fc", // alternative docvalues codepath w/ hint
"field=bogus_i", "field=bogus_tf" }) {
// for any of these selectors, behavior of these checks should be consistent
for (String selector : new String[] {
"", " sort='score desc' ",
" min=test_i ", " max=test_i ", " sort='test_i asc' ", " sort='test_i desc' ",
" min=test_f ", " max=test_f ", " sort='test_f asc' ", " sort='test_f desc' ",
" sort='group_s asc' ", " sort='group_s desc' ",
// fields that don't exist
" min=bogus_sort_i ", " max=bogus_sort_i ",
" sort='bogus_sort_i asc' ", " sort='bogus_sort_i desc' ",
" sort='bogus_sort_s asc' ", " sort='bogus_sort_s desc' ",
}) {
ModifiableSolrParams params = null;
// w/default nullPolicy, no groups found
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("sort", "id_i desc");
params.add("fq", "{!collapse "+group+" "+selector+"}");
assertQ(req(params), "*[count(//doc)=0]");
// w/nullPolicy=expand, every doc found
params = new ModifiableSolrParams();
params.add("q", "*:*");
params.add("sort", "id_i desc");
params.add("fq", "{!collapse field="+group+" nullPolicy=expand "+selector+"}");
assertQ(req(params)
, "*[count(//doc)=8]"
,"//result/doc[1]/str[@name='id'][.='8']"
,"//result/doc[2]/str[@name='id'][.='7']"
,"//result/doc[3]/str[@name='id'][.='6']"
,"//result/doc[4]/str[@name='id'][.='5']"
,"//result/doc[5]/str[@name='id'][.='4']"
,"//result/doc[6]/str[@name='id'][.='3']"
,"//result/doc[7]/str[@name='id'][.='2']"
,"//result/doc[8]/str[@name='id'][.='1']"
);
}
}
}
public void testGroupHeadSelector() {
GroupHeadSelector s;
expectThrows(SolrException.class, "no exception with multi criteria",
() -> GroupHeadSelector.build(params("sort", "foo_s asc", "min", "bar_s"))
);
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());
}
@Test
public void testForNotSupportedCases() {
String[] doc = {"id","3", "term_s", "YYYY", "test_ii", "5000", "test_l", "100", "test_f", "200",
"not_indexed_sS", "zzz", "indexed_s_not_uninvert", "zzz"};
assertU(adoc(doc));
assertU(commit());
// collapsing on multivalued field
assertQEx("Should Fail with Bad Request", "Collapsing not supported on multivalued fields",
req("q","*:*", "fq","{!collapse field=test_ii}"), SolrException.ErrorCode.BAD_REQUEST);
// collapsing on unknown field
assertQEx("Should Fail with Bad Request", "org.apache.solr.search.SyntaxError: undefined field: \"bleh\"",
req("q","*:*", "fq","{!collapse field=bleh}"), SolrException.ErrorCode.BAD_REQUEST);
// if a field is uninvertible=false, it should behave the same as a field that is indexed=false ...
// this also tests docValues=false along with indexed=false or univertible=false
for (String f : Arrays.asList("not_indexed_sS", "indexed_s_not_uninvert")) {
{
SolrException e = expectThrows(SolrException.class,
() -> h.query(req(params("q", "*:*",
"fq", "{!collapse field="+f+"}"))));
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
assertTrue("unexpected Message: " + e.getMessage(),
e.getMessage().contains("Collapsing field '" + f + "' " +
"should be either docValues enabled or indexed with uninvertible enabled"));
}
{
SolrException e = expectThrows(SolrException.class,
() -> h.query(req("q", "*:*", "fq", "{!collapse field="+f+" hint=top_fc}")));
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
assertTrue("unexpected Message: " + e.getMessage(),
e.getMessage().contains("Collapsing field '" + f + "' " +
"should be either docValues enabled or indexed with uninvertible enabled"));
}
}
}
@Test
public void test64BitCollapseFieldException() {
assertQEx("Should Fail For collapsing on Long fields", "Collapsing field should be of either String, Int or Float type",
req("q", "*:*", "fq", "{!collapse field=group_l}"), SolrException.ErrorCode.BAD_REQUEST);
assertQEx("Should Fail For collapsing on Double fields", "Collapsing field should be of either String, Int or Float type",
req("q", "*:*", "fq", "{!collapse field=group_d}"), SolrException.ErrorCode.BAD_REQUEST);
assertQEx("Should Fail For collapsing on Date fields", "Collapsing field should be of either String, Int or Float type",
req("q", "*:*", "fq", "{!collapse field=group_dt}"), SolrException.ErrorCode.BAD_REQUEST);
}
@Test
public void testMinExactCountDisabledByCollapse() throws Exception {
int numDocs = 10;
String collapseFieldInt = "field_ti_dv";
String collapseFieldFloat = "field_tf_dv";
String collapseFieldString = "field_s_dv";
for (int i = 0 ; i < numDocs ; i ++) {
assertU(adoc(
"id", String.valueOf(i),
"field_s", String.valueOf(i % 2),
collapseFieldInt, String.valueOf(i),
collapseFieldFloat, String.valueOf(i),
collapseFieldString, String.valueOf(i)));
assertU(commit());
}
for (String collapseField : Arrays.asList(collapseFieldInt, collapseFieldFloat, collapseFieldString)) {
// all of our docs have a value in the collapse field(s) so the policy shouldn't matter...
for (String policy : Arrays.asList("", " nullPolicy=ignore", " nullPolicy=expand", " nullPolicy=collapse")) {
assertQ(req("q", "{!cache=false}field_s:1",
"rows", "1",
"minExactCount", "1", // collapse should force this to be ignored
// this collapse will end up creating a group for each matched doc
"fq", "{!collapse field=" + collapseField + policy + "}"
)
, "//*[@numFoundExact='true']"
, "//*[@numFound='" + (numDocs/2) + "']"
);
}
}
}
public void testNullGroupNumericVsStringCollapse() throws Exception {
// NOTE: group_i and group_s will contain identical content so these need to be "numbers"...
// The specific numbers shouldn't matter (and we explicitly test '0' to confirm legacy bug/behavior
// of treating 0 as null is no longer a problem) ...
final String A = "-1";
final String B = "0";
final String C = "1";
// Stub out our documents. From now on assume highest "id" of each group should be group head...
final List<SolrInputDocument> docs = sdocs
(sdoc("id", "0"), // null group
sdoc("id", "1", "group_i", A, "group_s", A),
sdoc("id", "2", "group_i", B, "group_s", B),
sdoc("id", "3", "group_i", B, "group_s", B), // B head
sdoc("id", "4"), // null group
sdoc("id", "5", "group_i", A, "group_s", A),
sdoc("id", "6", "group_i", C, "group_s", C),
sdoc("id", "7"), // null group // null head
sdoc("id", "8", "group_i", A, "group_s", A), // A head
sdoc("id", "9", "group_i", C, "group_s", C)); // C head
final List<String> SELECTOR_FIELD_SUFFIXES = Arrays.asList("_i", "_l", "_f");
// add all the fields we'll be using as group head selectors...
int asc = 0;
int desc = 0;
for (SolrInputDocument doc : docs) {
for (String type : SELECTOR_FIELD_SUFFIXES) {
doc.setField("asc" + type, asc);
doc.setField("desc" + type, desc);
}
asc++;
desc--;
}
// convert our docs to update commands, along with some commits, in a shuffled order and process all of them...
final List<String> updates = Stream.concat(Stream.of(commit(), commit()),
docs.stream().map(doc -> adoc(doc))).collect(Collectors.toList());
Collections.shuffle(updates, random());
for (String u : updates) {
assertU(u);
}
assertU(commit());
// function based query for deterministic scores
final String q = "{!func}sum(asc_i,42)";
// results should be the same regardless of wether we collapse on a string field or numeric field
// (docs have identicle group identifiers in both fields)
for (String f : Arrays.asList("group_i",
"group_s")) {
// these group head selectors should all result in identical group heads for our query...
for (String suffix : SELECTOR_FIELD_SUFFIXES) {
for (String selector : Arrays.asList("",
"max=asc" + suffix,
"min=desc" + suffix,
"sort='asc" + suffix + " desc'",
"sort='desc" +suffix + " asc'",
"max=sum(42,asc" + suffix + ")",
"min=sum(42,desc" + suffix + ")",
"max=sub(0,desc" + suffix + ")",
"min=sub(0,asc" + suffix + ")")) {
if (selector.endsWith("_l") && f.endsWith("_i")) {
assertQEx("expected known limitation of using long for min/max selector when doing numeric collapse",
"min/max must be Int or Float",
req("q", q,
"fq", "{!collapse field=" + f + " nullPolicy=ignore " + selector + "}"),
SolrException.ErrorCode.BAD_REQUEST);
continue;
}
// ignore nulls
assertQ(req(params("q", q,
"fq", "{!collapse field=" + f + " nullPolicy=ignore " + selector + "}"))
, "*[count(//doc)=3]"
,"//result/doc[1]/str[@name='id'][.='9']" // group C
,"//result/doc[2]/str[@name='id'][.='8']" // group A
,"//result/doc[3]/str[@name='id'][.='3']" // group B
);
assertQ(req(params("qt", "/elevate", "elevateIds", "1,5",
"q", q,
"fq", "{!collapse field=" + f + " nullPolicy=ignore " + selector + "}"))
, "*[count(//doc)=4]"
,"//result/doc[1]/str[@name='id'][.='1']" // elevated, prevents group A
,"//result/doc[2]/str[@name='id'][.='5']" // elevated, (also) prevents group A
,"//result/doc[3]/str[@name='id'][.='9']" // group C
,"//result/doc[4]/str[@name='id'][.='3']" // group B
);
assertQ(req(params("qt", "/elevate", "elevateIds", "0,7",
"q", q,
"fq", "{!collapse field=" + f + " nullPolicy=ignore " + selector + "}"))
, "*[count(//doc)=5]"
,"//result/doc[1]/str[@name='id'][.='0']" // elevated (null)
,"//result/doc[2]/str[@name='id'][.='7']" // elevated (null)
,"//result/doc[3]/str[@name='id'][.='9']" // group C
,"//result/doc[4]/str[@name='id'][.='8']" // group A
,"//result/doc[5]/str[@name='id'][.='3']" // group B
);
assertQ(req(params("qt", "/elevate", "elevateIds", "6,0",
"q", q,
"fq", "{!collapse field=" + f + " nullPolicy=ignore " + selector + "}"))
, "*[count(//doc)=4]"
,"//result/doc[1]/str[@name='id'][.='6']" // elevated, prevents group C
,"//result/doc[2]/str[@name='id'][.='0']" // elevated (null)
,"//result/doc[3]/str[@name='id'][.='8']" // group A
,"//result/doc[4]/str[@name='id'][.='3']" // group B
);
// collapse nulls
assertQ(req(params("q", q,
"fq", "{!collapse field=" + f + " nullPolicy=collapse " + selector + "}"))
, "*[count(//doc)=4]"
,"//result/doc[1]/str[@name='id'][.='9']" // group C
,"//result/doc[2]/str[@name='id'][.='8']" // group A
,"//result/doc[3]/str[@name='id'][.='7']" // group null
,"//result/doc[4]/str[@name='id'][.='3']" // group B
);
assertQ(req(params("qt", "/elevate", "elevateIds", "1,5",
"q", q,
"fq", "{!collapse field=" + f + " nullPolicy=collapse " + selector + "}"))
, "*[count(//doc)=5]"
,"//result/doc[1]/str[@name='id'][.='1']" // elevated, prevents group A
,"//result/doc[2]/str[@name='id'][.='5']" // elevated, (also) prevents group A
,"//result/doc[3]/str[@name='id'][.='9']" // group C
,"//result/doc[4]/str[@name='id'][.='7']" // group null
,"//result/doc[5]/str[@name='id'][.='3']" // group B
);
assertQ(req(params("qt", "/elevate", "elevateIds", "0,7",
"q", q,
"fq", "{!collapse field=" + f + " nullPolicy=collapse " + selector + "}"))
, "*[count(//doc)=5]"
,"//result/doc[1]/str[@name='id'][.='0']" // elevated (null)
,"//result/doc[2]/str[@name='id'][.='7']" // elevated (null)
,"//result/doc[3]/str[@name='id'][.='9']" // group C
,"//result/doc[4]/str[@name='id'][.='8']" // group A
,"//result/doc[5]/str[@name='id'][.='3']" // group B
);
assertQ(req(params("qt", "/elevate", "elevateIds", "6,0",
"q", q,
"fq", "{!collapse field=" + f + " nullPolicy=collapse " + selector + "}"))
, "*[count(//doc)=4]"
,"//result/doc[1]/str[@name='id'][.='6']" // elevated, prevents group C
,"//result/doc[2]/str[@name='id'][.='0']" // elevated (null)
,"//result/doc[3]/str[@name='id'][.='8']" // group A
,"//result/doc[4]/str[@name='id'][.='3']" // group B
);
// expand nulls
assertQ(req(params("q", q,
"fq", "{!collapse field=" + f + " nullPolicy=expand " + selector + "}"))
, "*[count(//doc)=6]"
,"//result/doc[1]/str[@name='id'][.='9']" // group C
,"//result/doc[2]/str[@name='id'][.='8']" // group A
,"//result/doc[3]/str[@name='id'][.='7']" // null
,"//result/doc[4]/str[@name='id'][.='4']" // null
,"//result/doc[5]/str[@name='id'][.='3']" // group B
,"//result/doc[6]/str[@name='id'][.='0']" // null
);
assertQ(req(params("qt", "/elevate", "elevateIds", "1,5",
"q", q,
"fq", "{!collapse field=" + f + " nullPolicy=expand " + selector + "}"))
, "*[count(//doc)=7]"
,"//result/doc[1]/str[@name='id'][.='1']" // elevated, prevents group A
,"//result/doc[2]/str[@name='id'][.='5']" // elevated, (also) prevents group A
,"//result/doc[3]/str[@name='id'][.='9']" // group C
,"//result/doc[4]/str[@name='id'][.='7']" // null
,"//result/doc[5]/str[@name='id'][.='4']" // null
,"//result/doc[6]/str[@name='id'][.='3']" // group B
,"//result/doc[7]/str[@name='id'][.='0']" // null
);
assertQ(req(params("qt", "/elevate", "elevateIds", "0,7",
"q", q,
"fq", "{!collapse field=" + f + " nullPolicy=expand " + selector + "}"))
, "*[count(//doc)=6]"
,"//result/doc[1]/str[@name='id'][.='0']" // elevated (null)
,"//result/doc[2]/str[@name='id'][.='7']" // elevated (null)
,"//result/doc[3]/str[@name='id'][.='9']" // group C
,"//result/doc[4]/str[@name='id'][.='8']" // group A
,"//result/doc[5]/str[@name='id'][.='4']" // null
,"//result/doc[6]/str[@name='id'][.='3']" // group B
);
assertQ(req(params("qt", "/elevate", "elevateIds", "6,0",
"q", q,
"fq", "{!collapse field=" + f + " nullPolicy=expand " + selector + "}"))
, "*[count(//doc)=6]"
,"//result/doc[1]/str[@name='id'][.='6']" // elevated, prevents group C
,"//result/doc[2]/str[@name='id'][.='0']" // elevated (null)
,"//result/doc[3]/str[@name='id'][.='8']" // group A
,"//result/doc[4]/str[@name='id'][.='7']" // null
,"//result/doc[5]/str[@name='id'][.='4']" // null
,"//result/doc[6]/str[@name='id'][.='3']" // group B
);
}
}
}
}
}