/* * 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.function; import org.apache.lucene.util.LuceneTestCase.SuppressCodecs; import org.apache.lucene.util.TestUtil; import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.schema.IndexSchema; import org.apache.solr.schema.SchemaField; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrInputDocument; import org.junit.Before; import org.junit.BeforeClass; @SuppressCodecs({"Memory", "SimpleText"}) // see TestSortedSetSelector public class TestMinMaxOnMultiValuedField extends SolrTestCaseJ4 { /** Initializes core and does some sanity checking of schema */ @BeforeClass public static void beforeClass() throws Exception { initCore("solrconfig-functionquery.xml","schema11.xml"); // sanity check the expected properties of our fields (ie: who broke the schema?) IndexSchema schema = h.getCore().getLatestSchema(); for (String type : new String[] {"i", "l", "f", "d"}) { for (String suffix : new String [] {"", "_dv", "_ni_dv"}) { String f = "val_t" + type + "s" + suffix; SchemaField sf = schema.getField(f); assertTrue(f + " is not multivalued", sf.multiValued()); assertEquals(f + " doesn't have expected docValues status", f.contains("dv"), sf.hasDocValues()); assertEquals(f + " doesn't have expected index status", ! f.contains("ni"), sf.indexed()); } } } /** Deletes all docs (which may be left over from a previous test */ @Before public void before() throws Exception { assertU(delQ("*:*")); assertU(commit()); } public void testBasics() throws Exception { assertU(adoc(sdoc("id", "1" // int ,"val_tis_dv", "42" ,"val_tis_dv", "9" ,"val_tis_dv", "-54" // long ,"val_tls_dv", "420" ,"val_tls_dv", "90" ,"val_tls_dv", "-540" // float ,"val_tfs_dv", "-42.5" ,"val_tfs_dv", "-4.5" ,"val_tfs_dv", "-13.5" // double ,"val_tds_dv", "-420.5" ,"val_tds_dv", "-40.5" ,"val_tds_dv", "-130.5" ))); assertU(commit()); assertQ(req("q","id:1" // int ,"fl","exists_min_i:exists(field(val_tis_dv,min))" ,"fl","exists_max_i:exists(field(val_tis_dv,max))" ,"fl","min_i:field(val_tis_dv,min)" ,"fl","max_i:field(val_tis_dv,max)" // long ,"fl","exists_min_l:exists(field(val_tls_dv,min))" ,"fl","exists_max_l:exists(field(val_tls_dv,max))" ,"fl","min_l:field(val_tls_dv,min)" ,"fl","max_l:field(val_tls_dv,max)" // float ,"fl","exists_min_f:exists(field(val_tfs_dv,min))" ,"fl","exists_max_f:exists(field(val_tfs_dv,max))" ,"fl","min_f:field(val_tfs_dv,min)" ,"fl","max_f:field(val_tfs_dv,max)" // double ,"fl","exists_min_d:exists(field(val_tds_dv,min))" ,"fl","exists_max_d:exists(field(val_tds_dv,max))" ,"fl","min_d:field(val_tds_dv,min)" ,"fl","max_d:field(val_tds_dv,max)" ) ,"//*[@numFound='1']" // int ,"//bool[@name='exists_min_i']='true'" ,"//bool[@name='exists_max_i']='true'" ,"//int[@name='min_i']='-54'" ,"//int[@name='max_i']='42'" // long ,"//bool[@name='exists_min_l']='true'" ,"//bool[@name='exists_max_l']='true'" ,"//long[@name='min_l']='-540'" ,"//long[@name='max_l']='420'" // float ,"//bool[@name='exists_min_f']='true'" ,"//bool[@name='exists_max_f']='true'" ,"//float[@name='min_f']='-42.5'" ,"//float[@name='max_f']='-4.5'" // double ,"//bool[@name='exists_min_d']='true'" ,"//bool[@name='exists_max_d']='true'" ,"//double[@name='min_d']='-420.5'" ,"//double[@name='max_d']='-40.5'" ); } @AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/LUCENE-6709") public void testIntFieldCache() { testSimpleInt("val_tis"); } public void testIntDocValues() { testSimpleInt("val_tis_dv"); testSimpleInt("val_tis_ni_dv"); } @AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/LUCENE-6709") public void testLongFieldCache() { testSimpleLong("val_tls"); } public void testLongDocValues() { testSimpleLong("val_tls_dv"); testSimpleLong("val_tls_ni_dv"); } @AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/LUCENE-6709") public void testFloatFieldCache() { testSimpleFloat("val_tfs"); } public void testFloatDocValues() { testSimpleFloat("val_tfs_dv"); testSimpleFloat("val_tfs_ni_dv"); } @AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/LUCENE-6709") public void testDoubleFieldCache() { testSimpleDouble("val_tds"); } public void testDoubleDocValues() { testSimpleDouble("val_tds_dv"); testSimpleDouble("val_tds_ni_dv"); } public void testBadRequests() { // useful error msg when bogus selector is requested (ie: not min or max) assertQEx("no error asking for bogus selector", "hoss", req("q","*:*", "fl", "field(val_tds_dv,'hoss')"), SolrException.ErrorCode.BAD_REQUEST); // useful error until/unless LUCENE-6709 assertQEx("no error asking for max on a non docVals field", "val_tds", req("q","*:*", "fl", "field(val_tds,'max')"), SolrException.ErrorCode.BAD_REQUEST); assertQEx("no error asking for max on a non docVals field", "max", req("q","*:*", "fl", "field(val_tds,'max')"), SolrException.ErrorCode.BAD_REQUEST); assertQEx("no error asking for max on a non docVals field", "docValues", req("q","*:*", "fl", "field(val_tds,'max')"), SolrException.ErrorCode.BAD_REQUEST); // useful error if min/max is unsupported for fieldtype assertQEx("no error asking for max on a str field", "cat_docValues", req("q","*:*", "fl", "field(cat_docValues,'max')"), SolrException.ErrorCode.BAD_REQUEST); assertQEx("no error asking for max on a str field", "string", req("q","*:*", "fl", "field(cat_docValues,'max')"), SolrException.ErrorCode.BAD_REQUEST); } public void testRandom() throws Exception { Comparable[] vals = new Comparable[TestUtil.nextInt(random(), 1, 17)]; // random ints for (int i = 0; i < vals.length; i++) { vals[i] = random().nextInt(); } testSimpleValues("val_tis_dv", int.class, vals); // random longs for (int i = 0; i < vals.length; i++) { vals[i] = random().nextLong(); } testSimpleValues("val_tls_dv", long.class, vals); // random floats for (int i = 0; i < vals.length; i++) { // Random.nextFloat is lame Float f = Float.NaN; while (f.isNaN()) { f = Float.intBitsToFloat(random().nextInt()); } vals[i] = f; } testSimpleValues("val_tfs_dv", float.class, vals); // random doubles for (int i = 0; i < vals.length; i++) { // Random.nextDouble is lame Double d = Double.NaN; while (d.isNaN()) { d = Double.longBitsToDouble(random().nextLong()); } vals[i] = d; } testSimpleValues("val_tds_dv", double.class, vals); } /** @see #testSimpleValues */ protected void testSimpleInt(final String fieldname) { // most basic case testSimpleValues(fieldname, int.class, 0); // order of values shouldn't matter testSimpleValues(fieldname, int.class, -42, 51, 3); testSimpleValues(fieldname, int.class, 51, 3, -42); // extreme's of the data type testSimpleValues(fieldname, int.class, Integer.MIN_VALUE, 42, -550); testSimpleValues(fieldname, int.class, Integer.MAX_VALUE, 0, Integer.MIN_VALUE); testSimpleSort(fieldname, -42, 666); } /** @see #testSimpleValues */ protected void testSimpleLong(final String fieldname) { // most basic case testSimpleValues(fieldname, long.class, 0); // order of values shouldn't matter testSimpleValues(fieldname, long.class, -42L, 51L, 3L); testSimpleValues(fieldname, long.class, 51L, 3L, -42L); // extreme's of the data type testSimpleValues(fieldname, long.class, Long.MIN_VALUE, 42L, -550L); testSimpleValues(fieldname, long.class, Long.MAX_VALUE, 0L, Long.MIN_VALUE); testSimpleSort(fieldname, -42, 666); } /** @see #testSimpleValues */ protected void testSimpleFloat(final String fieldname) { // most basic case testSimpleValues(fieldname, float.class, 0.0F); // order of values shouldn't matter testSimpleValues(fieldname, float.class, -42.5F, 51.3F, 3.1415F); testSimpleValues(fieldname, float.class, 51.3F, 3.1415F, -42.5F); // extreme's of the data type testSimpleValues(fieldname, float.class, Float.NEGATIVE_INFINITY, 42.5F, -550.4F); testSimpleValues(fieldname, float.class, Float.POSITIVE_INFINITY, 0.0F, Float.NEGATIVE_INFINITY); testSimpleSort(fieldname, -4.2, 6.66); } /** @see #testSimpleValues */ protected void testSimpleDouble(final String fieldname) { // most basic case testSimpleValues(fieldname, double.class, 0.0D); // order of values shouldn't matter testSimpleValues(fieldname, double.class, -42.5D, 51.3D, 3.1415D); testSimpleValues(fieldname, double.class, 51.3D, 3.1415D, -42.5D); // extreme's of the data type testSimpleValues(fieldname, double.class, Double.NEGATIVE_INFINITY, 42.5D, -550.4D); testSimpleValues(fieldname, double.class, Double.POSITIVE_INFINITY, 0.0D, Double.NEGATIVE_INFINITY); testSimpleSort(fieldname, -4.2, 6.66); } /** Tests a single doc with a few explicit values, as well as testing exists with and w/o values */ protected void testSimpleValues(final String fieldname, final Class clazz, final Comparable... vals) { clearIndex(); assert 0 < vals.length; Comparable min = vals[0]; Comparable max = vals[0]; final String type = clazz.getName(); final SolrInputDocument doc1 = sdoc("id", "1"); for (Comparable v : vals) { doc1.addField(fieldname, v); if (0 < min.compareTo(v)) { min = v; } if (0 > max.compareTo(v)) { max = v; } } assertU(adoc(doc1)); assertU(adoc(sdoc("id", "2"))); // fieldname doesn't exist assertU(commit()); // doc with values assertQ(fieldname, req("q","id:1", "fl","exists_val_min:exists(field("+fieldname+",min))", "fl","exists_val_max:exists(field("+fieldname+",max))", "fl","val_min:field("+fieldname+",min)", "fl","val_max:field("+fieldname+",max)") ,"//*[@numFound='1']" ,"//bool[@name='exists_val_min']='true'" ,"//bool[@name='exists_val_max']='true'" ,"//"+type+"[@name='val_min']='"+min+"'" ,"//"+type+"[@name='val_max']='"+max+"'" ); // doc w/o values assertQ(fieldname, req("q","id:2", "fl","exists_val_min:exists(field("+fieldname+",min))", "fl","exists_val_max:exists(field("+fieldname+",max))", "fl","val_min:field("+fieldname+",min)", "fl","val_max:field("+fieldname+",max)") ,"//*[@numFound='1']" ,"//bool[@name='exists_val_min']='false'" ,"//bool[@name='exists_val_max']='false'" ,"count(//"+type+"[@name='val_min'])=0" ,"count(//"+type+"[@name='val_max'])=0" ); // sanity check no sort error when there are missing values for (String dir : new String[] { "asc", "desc" }) { for (String mm : new String[] { "min", "max" }) { for (String func : new String[] { "field("+fieldname+","+mm+")", "def(field("+fieldname+","+mm+"),42)", "sum(32,field("+fieldname+","+mm+"))" }) { assertQ(fieldname, req("q","*:*", "fl", "id", "sort", func + " " + dir) ,"//*[@numFound='2']" // no assumptions about order for now, see bug: SOLR-8005 ,"//float[@name='id']='1.0'" ,"//float[@name='id']='2.0'" ); } } } } /** * Tests sort order of min/max realtive to other docs w/o any values. * @param fieldname The field to test * @param negative a "negative" value for this field (ie: in a function context, is less then the "0") * @param positive a "positive" value for this field (ie: in a function context, is more then the "0") */ protected void testSimpleSort(final String fieldname, final Comparable negative, final Comparable positive) { clearIndex(); int numDocsExpected = 1; for (int i = 1; i < 4; i++) { // pos docids if (random().nextBoolean()) { assertU(adoc(sdoc("id",i))); // fieldname doesn't exist numDocsExpected++; } } assertU(adoc(sdoc("id", "0", fieldname, negative, fieldname, positive))); for (int i = 1; i < 4; i++) { // neg docids if (random().nextBoolean()) { assertU(adoc(sdoc("id",-i))); // fieldname doesn't exist numDocsExpected++; } } assertU(commit()); // need to wrap with "def" until SOLR-8005 is resolved assertDocWithValsIsFirst(numDocsExpected, "def(field("+fieldname+",min),0) asc"); assertDocWithValsIsLast(numDocsExpected, "def(field("+fieldname+",min),0) desc"); assertDocWithValsIsFirst(numDocsExpected, "def(field("+fieldname+",max),0) desc"); assertDocWithValsIsLast(numDocsExpected, "def(field("+fieldname+",max),0) asc"); // def wrapper shouldn't be needed since it's already part of another function assertDocWithValsIsFirst(numDocsExpected, "sum(32,field("+fieldname+",max)) desc"); assertDocWithValsIsLast(numDocsExpected, "sum(32,field("+fieldname+",max)) asc"); assertDocWithValsIsFirst(numDocsExpected, "sum(32,field("+fieldname+",min)) asc"); assertDocWithValsIsLast(numDocsExpected, "sum(32,field("+fieldname+",min)) desc"); } /** helper for testSimpleSort */ private static void assertDocWithValsIsFirst(final int numDocs, final String sort) { assertQ(sort, req("q","*:*", "rows", ""+numDocs, "sort", sort) ,"//result[@numFound='"+numDocs+"']" ,"//result/doc[1]/float[@name='id']='0.0'" ); } /** helper for testSimpleSort */ private static void assertDocWithValsIsLast(final int numDocs, final String sort) { assertQ(sort, req("q","*:*", "rows", ""+numDocs, "sort", sort) ,"//result[@numFound='"+numDocs+"']" ,"//result/doc["+numDocs+"]/float[@name='id']='0.0'" ); } }