blob: e384d84a278a60c205805510f61c803ac8dceac8 [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.List;
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.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.SolrParams;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import static org.hamcrest.core.StringContains.containsString;
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[] {CollapsingQParserPlugin.NullPolicy.IGNORE.getName(),
CollapsingQParserPlugin.NullPolicy.COLLAPSE.getName(),
CollapsingQParserPlugin.NullPolicy.EXPAND.getName()};
@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 = nullPolicy.equals(CollapsingQParserPlugin.NullPolicy.IGNORE.getName())
// 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+"'}"));
try {
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 (nullPolicy.equals(CollapsingQParserPlugin.NullPolicy.EXPAND.getName())) {
// nothing to check for this doc, it's in its own group
continue;
}
assertFalse(groupHeadId + " has null collapseVal but nullPolicy==ignore; " +
"mainP: " + mainP + ", collapseP: " + collapseP,
nullPolicy.equals(CollapsingQParserPlugin.NullPolicy.IGNORE.getName()));
}
// workaround 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 different field 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);
}
} catch (Exception e) {
throw new RuntimeException("BUG using params: " + collapseP + " + " + mainP, e);
}
}
}
}
public void testParsedFilterQueryResponse() throws Exception {
String nullPolicy = randomNullPolicy();
String groupHeadSort = "'_version_ asc'";
String collapseSize = "5000";
String collapseHint = "top_fc";
String filterQuery = "{!collapse field=id sort=" + groupHeadSort + " nullPolicy=" + nullPolicy + " size=" +
collapseSize + " hint=" + collapseHint + "}";
SolrParams solrParams = params("q", "*:*", "rows", "0", "debug", "true", "fq", filterQuery);
QueryResponse response = SOLR.query(solrParams);
// Query name is occurring twice, this should be handled in QueryParsing.toString
String expectedParsedFilterString = "CollapsingPostFilter(CollapsingPostFilter(field=id, " +
"nullPolicy=" + nullPolicy + ", GroupHeadSelector(selectorText=" + groupHeadSort.substring(1,
groupHeadSort.length() - 1) + ", type=SORT" +
"), hint=" + collapseHint + ", size=" + collapseSize + "))";
List<String> expectedParsedFilterQuery = Collections.singletonList(expectedParsedFilterString);
assertEquals(expectedParsedFilterQuery, response.getDebugMap().get("parsed_filter_queries"));
assertEquals(Collections.singletonList(filterQuery), response.getDebugMap().get("filter_queries"));
}
public void testNullPolicy() {
String nullPolicy = "xyz";
String groupHeadSort = "'_version_ asc'";
String filterQuery = "{!collapse field=id sort=" + groupHeadSort + " nullPolicy=" + nullPolicy + "}";
SolrParams solrParams = params("q", "*:*", "fq", filterQuery);
SolrException e = expectThrows(SolrException.class, () -> SOLR.query(solrParams));
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
assertThat(e.getMessage(), containsString("Invalid nullPolicy: " + nullPolicy));
// valid nullPolicy
assertQ(req("q", "*:*", "fq", "{!collapse field=id nullPolicy=" + randomNullPolicy() + "}"));
}
private String randomNullPolicy() {
return NULL_POLICIES[ TestUtil.nextInt(random(), 0, NULL_POLICIES.length-1) ];
}
}