/* * 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.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.SolrInputDocument; import org.apache.solr.common.params.SolrParams; 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; 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+"'}")); 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 (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)); } // 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); } } } } private String randomNullPolicy() { return NULL_POLICIES[ TestUtil.nextInt(random(), 0, NULL_POLICIES.length-1) ]; } }