blob: 3b4320e2744f0ee5f3383a740a0a913c32683bc0 [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.facet;
import java.lang.invoke.MethodHandles;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.apache.lucene.util.TestUtil;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.UpdateRequest;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.CoreAdminParams;
import org.apache.solr.common.params.FacetParams.FacetRangeOther;
import org.apache.solr.common.util.NamedList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.BeforeClass;
/**
* Builds a random index of a few simple fields, maintaining an in-memory model of the expected
* doc counts so that we can verify the results of range facets w/ nested field facets that need refinement.
*
* The focus here is on stressing the cases where the document values fall direct only on the
* range boundaries, and how the various "include" options affects refinement.
*/
public class RangeFacetCloudTest extends SolrCloudTestCase {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final String COLLECTION = MethodHandles.lookup().lookupClass().getName();
private static final String CONF = COLLECTION + "_configSet";
private static final String INT_FIELD = "range_i";
private static final String STR_FIELD = "facet_s";
private static final int NUM_RANGE_VALUES = 6;
private static final int TERM_VALUES_RANDOMIZER = 100;
private static final List<String> SORTS = Arrays.asList("count desc", "count asc", "index asc", "index desc");
private static final List<EnumSet<FacetRangeOther>> OTHERS = buildListOfFacetRangeOtherOptions();
private static final List<FacetRangeOther> BEFORE_AFTER_BETWEEN
= Arrays.asList(FacetRangeOther.BEFORE, FacetRangeOther.AFTER, FacetRangeOther.BETWEEN);
/**
* the array indexes represent values in our numeric field, while the array values
* track the number of docs that will have that value.
*/
private static final int[] RANGE_MODEL = new int[NUM_RANGE_VALUES];
/**
* the array indexes represent values in our numeric field, while the array values
* track the mapping from string field terms to facet counts for docs that have that numeric value
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private static final Map<String,Integer>[] TERM_MODEL = new Map[NUM_RANGE_VALUES];
@BeforeClass
public static void setupCluster() throws Exception {
final int numShards = TestUtil.nextInt(random(),1,5);
final int numReplicas = 1;
final int maxShardsPerNode = 1;
final int nodeCount = numShards * numReplicas;
configureCluster(nodeCount)
.addConfig(CONF, Paths.get(TEST_HOME(), "collection1", "conf"))
.configure();
assertEquals(0, (CollectionAdminRequest.createCollection(COLLECTION, CONF, numShards, numReplicas)
.setPerReplicaState(SolrCloudTestCase.USE_PER_REPLICA_STATE)
.setMaxShardsPerNode(maxShardsPerNode)
.setProperties(Collections.singletonMap(CoreAdminParams.CONFIG, "solrconfig-minimal.xml"))
.process(cluster.getSolrClient())).getStatus());
cluster.getSolrClient().setDefaultCollection(COLLECTION);
final int numDocs = atLeast(1000);
final int maxTermId = atLeast(TERM_VALUES_RANDOMIZER);
// clear the RANGE_MODEL
Arrays.fill(RANGE_MODEL, 0);
// seed the TERM_MODEL Maps so we don't have null check later
for (int i = 0; i < NUM_RANGE_VALUES; i++) {
TERM_MODEL[i] = new LinkedHashMap<>();
}
// build our index & our models
for (int id = 0; id < numDocs; id++) {
final int rangeVal = random().nextInt(NUM_RANGE_VALUES);
final String termVal = "x" + random().nextInt(maxTermId);
final SolrInputDocument doc = sdoc("id", ""+id,
INT_FIELD, ""+rangeVal,
STR_FIELD, termVal);
RANGE_MODEL[rangeVal]++;
TERM_MODEL[rangeVal].merge(termVal, 1, Integer::sum);
assertEquals(0, (new UpdateRequest().add(doc)).process(cluster.getSolrClient()).getStatus());
}
assertEquals(0, cluster.getSolrClient().commit().getStatus());
}
public void testInclude_Lower() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:lower", "")) { // same behavior
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:1, end:5, gap:1"+otherStr+include+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 4, buckets.size());
for (int i = 0; i < 4; i++) {
int expectedVal = i+1;
assertBucket("bucket#" + i, expectedVal, modelVals(expectedVal), subFacetLimit, buckets.get(i));
}
assertBeforeAfterBetween(other, modelVals(0), modelVals(5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
public void testInclude_Lower_Gap2() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:lower", "")) { // same behavior
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
"{ foo:{ type:range, field:"+INT_FIELD+" start:0, end:5, gap:2"+otherStr+include+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 3, buckets.size());
assertBucket("bucket#0", 0, modelVals(0,1), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 2, modelVals(2,3), subFacetLimit, buckets.get(1));
assertBucket("bucket#2", 4, modelVals(4,5), subFacetLimit, buckets.get(2));
assertBeforeAfterBetween(other, emptyVals(), emptyVals(), modelVals(0,5), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
public void testInclude_Lower_Gap2_hardend() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:lower", "")) { // same behavior
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
"{ foo:{ type:range, field:"+INT_FIELD+" start:0, end:5, gap:2, hardend:true"
+ otherStr+include+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 3, buckets.size());
assertBucket("bucket#0", 0, modelVals(0,1), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 2, modelVals(2,3), subFacetLimit, buckets.get(1));
assertBucket("bucket#2", 4, modelVals(4), subFacetLimit, buckets.get(2));
assertBeforeAfterBetween(other, emptyVals(), modelVals(5), modelVals(0,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
public void testStatsWithOmitHeader() throws Exception {
// SOLR-13509: no NPE should be thrown when only stats are specified with omitHeader=true
SolrQuery solrQuery = new SolrQuery("q", "*:*", "omitHeader", "true",
"json.facet", "{unique_foo:\"unique(" + STR_FIELD+ ")\"}");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
// response shouldn't contain header as omitHeader is set to true
assertNull(rsp.getResponseHeader());
}
public void testInclude_Upper() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:0, end:4, gap:1, include:upper"+otherStr+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 4, buckets.size());
for (int i = 0; i < 4; i++) {
assertBucket("bucket#" + i, i, modelVals(i+1), subFacetLimit, buckets.get(i));
}
assertBeforeAfterBetween(other, modelVals(0), modelVals(5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
public void testInclude_Upper_Gap2() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:0, end:4, gap:2, include:upper"+otherStr+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 2, buckets.size());
assertBucket("bucket#0", 0, modelVals(1,2), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 2, modelVals(3,4), subFacetLimit, buckets.get(1));
assertBeforeAfterBetween(other, modelVals(0), modelVals(5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
public void testInclude_Edge() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low/high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:1, end:4, gap:1, include:edge"+otherStr+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 3, buckets.size());
assertBucket("bucket#0", 1, modelVals(1), subFacetLimit, buckets.get(0));
// middle bucket doesn't include lower or upper so it's empty
assertBucket("bucket#1", 2, emptyVals(), subFacetLimit, buckets.get(1));
assertBucket("bucket#2", 3, modelVals(4), subFacetLimit, buckets.get(2));
assertBeforeAfterBetween(other, modelVals(0), modelVals(5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
public void testInclude_EdgeLower() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:'edge,lower'", ", include:[edge,lower]")) { // same
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low/high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:1, end:4, gap:1"+otherStr+include+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 3, buckets.size());
assertBucket("bucket#0", 1, modelVals(1), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 2, modelVals(2), subFacetLimit, buckets.get(1));
assertBucket("bucket#2", 3, modelVals(3,4), subFacetLimit, buckets.get(2));
assertBeforeAfterBetween(other, modelVals(0), modelVals(5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
public void testInclude_EdgeUpper() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:'edge,upper'", ", include:[edge,upper]")) { // same
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low/high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:1, end:4, gap:1"+otherStr+include+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 3, buckets.size());
assertBucket("bucket#0", 1, modelVals(1,2), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 2, modelVals(3), subFacetLimit, buckets.get(1));
assertBucket("bucket#2", 3, modelVals(4), subFacetLimit, buckets.get(2));
assertBeforeAfterBetween(other, modelVals(0), modelVals(5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
public void testInclude_EdgeLowerUpper() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:'edge,lower,upper'", ", include:[edge,lower,upper]")) { // same
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low/high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:1, end:4, gap:1"+otherStr+include+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 3, buckets.size());
assertBucket("bucket#0", 1, modelVals(1,2), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 2, modelVals(2,3), subFacetLimit, buckets.get(1));
assertBucket("bucket#2", 3, modelVals(3,4), subFacetLimit, buckets.get(2));
assertBeforeAfterBetween(other, modelVals(0), modelVals(5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
public void testInclude_All() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:'edge,lower,upper,outer'",
", include:[edge,lower,upper,outer]",
", include:all")) { // same
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low/high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:1, end:4, gap:1"+otherStr+include+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 3, buckets.size());
assertBucket("bucket#0", 1, modelVals(1,2), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 2, modelVals(2,3), subFacetLimit, buckets.get(1));
assertBucket("bucket#2", 3, modelVals(3,4), subFacetLimit, buckets.get(2));
assertBeforeAfterBetween(other, modelVals(0,1), modelVals(4,5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
/**
* This test will also sanity check that mincount is working properly
*/
public void testInclude_All_Gap2() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:'edge,lower,upper,outer'",
", include:[edge,lower,upper,outer]",
", include:all")) { // same
// we also want to sanity check that mincount doesn't bork anything,
// so we're going to do the query twice:
// 1) no mincount, keep track of which bucket has the highest count & what it was
// 2) use that value as the mincount, assert that the other bucket isn't returned
long mincount_to_use = -1;
Object expected_mincount_bucket_val = null; // HACK: use null to mean neither in case of tie
// initial query, no mincount...
SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low/high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+", start:1, end:4, gap:2"+otherStr+include+subFacet+" } }");
QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 2, buckets.size());
assertBucket("bucket#0", 1, modelVals(1,3), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 3, modelVals(3,5), subFacetLimit, buckets.get(1));
assertBeforeAfterBetween(other, modelVals(0,1), modelVals(5), modelVals(1,5), subFacetLimit, foo);
// if we've made it this far, then our buckets match the model
// now use our buckets to pick a mincount to use based on the MIN(+1) count seen
long count0 = ((Number)buckets.get(0).get("count")).longValue();
long count1 = ((Number)buckets.get(1).get("count")).longValue();
mincount_to_use = 1 + Math.min(count0, count1);
if (count0 > count1) {
expected_mincount_bucket_val = buckets.get(0).get("val");
} else if (count1 > count0) {
expected_mincount_bucket_val = buckets.get(1).get("val");
}
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
// second query, using mincount...
solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low/high value from our ranges,
"{ foo:{ type:range, field:"+INT_FIELD+", mincount:" + mincount_to_use +
", start:1, end:4, gap:2"+otherStr+include+subFacet+" } }");
rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
if (null == expected_mincount_bucket_val) {
assertEquals("num buckets", 0, buckets.size());
} else {
assertEquals("num buckets", 1, buckets.size());
final Object actualBucket = buckets.get(0);
if (expected_mincount_bucket_val.equals(1)) {
assertBucket("bucket#0(0)", 1, modelVals(1,3), subFacetLimit, actualBucket);
} else {
assertBucket("bucket#0(1)", 3, modelVals(3,5), subFacetLimit, actualBucket);
}
}
// regardless of mincount, the before/after/between special buckets should always be returned
assertBeforeAfterBetween(other, modelVals(0,1), modelVals(5), modelVals(1,5), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
public void testInclude_All_Gap2_hardend() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:'edge,lower,upper,outer'",
", include:[edge,lower,upper,outer]",
", include:all")) { // same
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low/high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:1, end:4, gap:2, hardend:true"
+ otherStr+include+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 2, buckets.size());
assertBucket("bucket#0", 1, modelVals(1,3), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 3, modelVals(3,4), subFacetLimit, buckets.get(1));
assertBeforeAfterBetween(other, modelVals(0,1), modelVals(4,5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
public void testRangeWithInterval() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (boolean incUpper : Arrays.asList(false, true)) {
String incUpperStr = ",inclusive_to:"+incUpper;
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
"{ foo:{ type:range, field:" + INT_FIELD + " ranges:[{from:1, to:2"+ incUpperStr+ "}," +
"{from:2, to:3"+ incUpperStr +"},{from:3, to:4"+ incUpperStr +"},{from:4, to:5"+ incUpperStr+"}]"
+ subFacet + " } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>) rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 4, buckets.size());
for (int i = 0; i < 4; i++) {
String expectedVal = "[" + (i + 1) + "," + (i + 2) + (incUpper? "]": ")");
ModelRange modelVals = incUpper? modelVals(i+1, i+2) : modelVals(i+1);
assertBucket("bucket#" + i, expectedVal, modelVals, subFacetLimit, buckets.get(i));
}
} catch (AssertionError | RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
public void testRangeWithOldIntervalFormat() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (boolean incUpper : Arrays.asList(false, true)) {
String incUpperStr = incUpper? "]\"":")\"";
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
"{ foo:{ type:range, field:" + INT_FIELD + " ranges:[{range:\"[1,2"+ incUpperStr+ "}," +
"{range:\"[2,3"+ incUpperStr +"},{range:\"[3,4"+ incUpperStr +"},{range:\"[4,5"+ incUpperStr+"}]"
+ subFacet + " } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>) rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 4, buckets.size());
for (int i = 0; i < 4; i++) {
String expectedVal = "[" + (i + 1) + "," + (i + 2) + (incUpper? "]": ")");
ModelRange modelVals = incUpper? modelVals(i+1, i+2) : modelVals(i+1);
assertBucket("bucket#" + i, expectedVal, modelVals, subFacetLimit, buckets.get(i));
}
} catch (AssertionError | RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
public void testIntervalWithMincount() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
long mincount_to_use = -1;
Object expected_mincount_bucket_val = null;
// without mincount
SolrQuery solrQuery = new SolrQuery(
"q", "*:*", "rows", "0", "json.facet",
"{ foo:{ type:range, field:" + INT_FIELD + " ranges:[{from:1, to:3},{from:3, to:5}]" +
subFacet + " } }"
);
QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 2, buckets.size());
// upper is not included
assertBucket("bucket#0", "[1,3)", modelVals(1,2), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", "[3,5)", modelVals(3,4), subFacetLimit, buckets.get(1));
// if we've made it this far, then our buckets match the model
// now use our buckets to pick a mincount to use based on the MIN(+1) count seen
long count0 = ((Number)buckets.get(0).get("count")).longValue();
long count1 = ((Number)buckets.get(1).get("count")).longValue();
mincount_to_use = 1 + Math.min(count0, count1);
if (count0 > count1) {
expected_mincount_bucket_val = buckets.get(0).get("val");
} else if (count1 > count0) {
expected_mincount_bucket_val = buckets.get(1).get("val");
}
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
// with mincount
solrQuery = new SolrQuery(
"q", "*:*", "rows", "0", "json.facet",
"{ foo:{ type:range, field:" + INT_FIELD + " ranges:[{from:1, to:3},{from:3, to:5}]" +
",mincount:" + mincount_to_use + subFacet + " } }"
);
rsp = cluster.getSolrClient().query(solrQuery);
try {
@SuppressWarnings({"unchecked"})
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
if (null == expected_mincount_bucket_val) {
assertEquals("num buckets", 0, buckets.size());
} else {
assertEquals("num buckets", 1, buckets.size());
final Object actualBucket = buckets.get(0);
if (expected_mincount_bucket_val.equals("[1,3)")) {
assertBucket("bucket#0(0)", "[1,3)", modelVals(1,2), subFacetLimit, actualBucket);
} else {
assertBucket("bucket#0(1)", "[3,5)", modelVals(3,4), subFacetLimit, actualBucket);
}
}
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
/**
* Helper method for validating a single 'bucket' from a Range facet.
*
* @param label to use in assertions
* @param expectedVal <code>"val"</code> to assert for this bucket, use <code>null</code> for special "buckets" like before, after, between.
* @param expectedRangeValues a range of the expected values in the numeric field whose cumulative counts should match this buckets <code>"count"</code>
* @param subFacetLimitUsed if null, then assert this bucket has no <code>"bar"</code> subfacet, otherwise assert expected term counts for each actual term, and sanity check the number terms returnd against the model and/or this limit.
* @param actualBucket the actual bucket returned from a query for all assertions to be conducted against.
*/
private static void assertBucket(final String label,
final Object expectedVal,
final ModelRange expectedRangeValues,
final Integer subFacetLimitUsed,
final Object actualBucket) {
try {
assertNotNull("null bucket", actualBucket);
assertNotNull("expectedRangeValues", expectedRangeValues);
assertTrue("bucket is not a NamedList", actualBucket instanceof NamedList);
@SuppressWarnings({"unchecked"})
final NamedList<Object> bucket = (NamedList<Object>) actualBucket;
if (null != expectedVal) {
assertEquals("val", expectedVal, bucket.get("val"));
}
// figure out the model from our range of values...
long expectedCount = 0;
List<Map<String,Integer>> toMerge = new ArrayList<>(NUM_RANGE_VALUES);
for (int i = expectedRangeValues.lower; i <= expectedRangeValues.upper; i++) {
expectedCount += RANGE_MODEL[i];
toMerge.add(TERM_MODEL[i]);
}
assertEqualsHACK("count", expectedCount, bucket.get("count"));
// merge the maps of our range values by summing the (int) values on key collisions
final Map<String,Long> expectedTermCounts = toMerge.stream()
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Entry::getKey, (e -> e.getValue().longValue()), Long::sum));
if (null == subFacetLimitUsed || 0 == expectedCount) {
assertNull("unexpected subfacets", bucket.get("bar"));
} else {
@SuppressWarnings({"unchecked"})
NamedList<Object> bar = ((NamedList<Object>)bucket.get("bar"));
assertNotNull("can't find subfacet 'bar'", bar);
final int numBucketsExpected = subFacetLimitUsed < 0
? expectedTermCounts.size() : Math.min(subFacetLimitUsed, expectedTermCounts.size());
@SuppressWarnings({"unchecked"})
final List<NamedList<Object>> subBuckets = (List<NamedList<Object>>) bar.get("buckets");
// we should either have filled out the expected limit, or
assertEquals("num subfacet buckets", numBucketsExpected, subBuckets.size());
// assert sub-facet term counts for the subBuckets that do exist
for (NamedList<Object> subBucket : subBuckets) {
final Object term = subBucket.get("val");
assertNotNull("subfacet bucket with null term: " + subBucket, term);
final Long expectedTermCount = expectedTermCounts.get(term.toString());
assertNotNull("unexpected subfacet bucket: " + subBucket, expectedTermCount);
assertEqualsHACK("subfacet count for term: " + term, expectedTermCount, subBucket.get("count"));
}
}
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(label + ": " + ae.getMessage(), ae);
}
}
/**
* A convenience method for calling {@link #assertBucket} on the before/after/between buckets
* of a facet result, based on the {@link FacetRangeOther} specified for this facet.
*
* @see #assertBucket
* @see #buildListOfFacetRangeOtherOptions
*/
private static void assertBeforeAfterBetween(final EnumSet<FacetRangeOther> other,
final ModelRange before,
final ModelRange after,
final ModelRange between,
final Integer subFacetLimitUsed,
final NamedList<Object> facet) {
//final String[] names = new String[] { "before", "after", "between" };
assertEquals(3, BEFORE_AFTER_BETWEEN.size());
final ModelRange[] expected = new ModelRange[] { before, after, between };
for (int i = 0; i < 3; i++) {
FacetRangeOther key = BEFORE_AFTER_BETWEEN.get(i);
String name = key.toString();
if (other.contains(key) || other.contains(FacetRangeOther.ALL)) {
assertBucket(name, null, expected[i], subFacetLimitUsed, facet.get(name));
} else {
assertNull("unexpected other=" + name, facet.get(name));
}
}
}
/**
* A little helper struct to make the method sig of {@link #assertBucket} more readable.
* If lower (or upper) is negative, then both must be negative and upper must be less then
* lower -- this indicate that the bucket should be empty.
* @see #modelVals
* @see #emptyVals
*/
private static final class ModelRange {
public final int lower;
public final int upper;
/** Don't use, use the convenience methods */
public ModelRange(int lower, int upper) {
if (lower < 0 || upper < 0) {
assert(lower < 0 && upper < lower);
} else {
assert(lower <= upper);
}
this.lower = lower;
this.upper = upper;
}
}
private static final ModelRange emptyVals() {
return new ModelRange(-1, -100);
}
private static final ModelRange modelVals(int value) {
return modelVals(value, value);
}
private static final ModelRange modelVals(int lower, int upper) {
assertTrue(upper + " < " + lower, lower <= upper);
assertTrue("negative lower", 0 <= lower);
assertTrue("negative upper", 0 <= upper);
return new ModelRange(lower, upper);
}
/** randomized helper */
private static final Integer pickSubFacetLimit(final boolean doSubFacet) {
if (! doSubFacet) { return null; }
int result = TestUtil.nextInt(random(), -10, atLeast(TERM_VALUES_RANDOMIZER));
return (result <= 0) ? -1 : result;
}
/** randomized helper */
private static final CharSequence makeSubFacet(final Integer subFacetLimit) {
if (null == subFacetLimit) {
return "";
}
final StringBuilder result = new StringBuilder(", facet:{ bar:{ type:terms, refine:true, field:"+STR_FIELD);
// constrain overrequesting to stress refiement, but still test those codepaths
final String overrequest = random().nextBoolean() ? "0" : "1";
result.append(", overrequest:").append(overrequest).append(", limit:").append(subFacetLimit);
// order should have no affect on our testing
if (random().nextBoolean()) {
result.append(", sort:'").append(SORTS.get(random().nextInt(SORTS.size()))).append("'");
}
result.append("} }");
return result;
}
/**
* Helper for seeding the re-used static struct, and asserting no one changes the Enum w/o updating this test
*
* @see #assertBeforeAfterBetween
* @see #formatFacetRangeOther
* @see #OTHERS
*/
private static final List<EnumSet<FacetRangeOther>> buildListOfFacetRangeOtherOptions() {
assertEquals("If someone adds to FacetRangeOther this method (and bulk of test) needs updated",
5, EnumSet.allOf(FacetRangeOther.class).size());
// we're not overly concerned about testing *EVERY* permutation,
// we just want to make sure we test multiple code paths (some, all, "ALL", none)
//
// NOTE: Don't mix "ALL" or "NONE" with other options so we don't have to make assertBeforeAfterBetween
// overly complicated
ArrayList<EnumSet<FacetRangeOther>> results = new ArrayList<>(5);
results.add(EnumSet.of(FacetRangeOther.ALL));
results.add(EnumSet.of(FacetRangeOther.BEFORE, FacetRangeOther.AFTER, FacetRangeOther.BETWEEN));
results.add(EnumSet.of(FacetRangeOther.BEFORE, FacetRangeOther.AFTER));
results.add(EnumSet.of(FacetRangeOther.BETWEEN));
results.add(EnumSet.of(FacetRangeOther.NONE));
return results;
}
/**
* @see #assertBeforeAfterBetween
* @see #buildListOfFacetRangeOtherOptions
*/
private static final String formatFacetRangeOther(EnumSet<FacetRangeOther> other) {
if (other.contains(FacetRangeOther.NONE) && random().nextBoolean()) {
return ""; // sometimes don't output a param at all when we're dealing with the default NONE
}
String val = other.toString();
if (random().nextBoolean()) {
// two valid syntaxes to randomize between:
// - a JSON list of items (conveniently the default toString of EnumSet),
// - a single quoted string containing the comma separated list
val = val.replaceAll("\\[|\\]","'");
}
return ", other:" + val;
}
/**
* HACK to work around SOLR-11775.
* Asserts that the 'actual' argument is a (non-null) Number, then compares it's 'longValue' to the 'expected' argument
*/
private static void assertEqualsHACK(String msg, long expected, Object actual) {
assertNotNull(msg, actual);
assertTrue(msg + " ... NOT A NUMBER: " + actual.getClass(), Number.class.isInstance(actual));
assertEquals(msg, expected, ((Number)actual).longValue());
}
}