From 5f5ab2a79fb643ee69b6a654d9664f9dd5898411 Mon Sep 17 00:00:00 2001 From: "Chris M. Hostetter" Date: Fri, 31 Jul 2015 16:21:44 +0000 Subject: [PATCH] SOLR-2522: new two argument option for the existing field() function; picks the min/max value of a docValues field to use as a ValueSource: "field(field_name,min)" and "field(field_name,max)" git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1693625 13f79535-47bb-0310-9956-ffa450edef68 --- .../lucene/search/SortedSetSelector.java | 2 +- solr/CHANGES.txt | 3 + .../org/apache/solr/schema/FieldType.java | 85 +++++ .../apache/solr/schema/TrieDoubleField.java | 62 +++ .../org/apache/solr/schema/TrieField.java | 44 +++ .../apache/solr/schema/TrieFloatField.java | 63 +++ .../org/apache/solr/schema/TrieIntField.java | 62 +++ .../org/apache/solr/schema/TrieLongField.java | 62 +++ .../apache/solr/search/ValueSourceParser.java | 12 + .../solr/collection1/conf/schema15.xml | 2 +- .../apache/solr/search/QueryEqualityTest.java | 22 ++ .../TestMinMaxOnMultiValuedField.java | 358 ++++++++++++++++++ 12 files changed, 775 insertions(+), 2 deletions(-) create mode 100644 solr/core/src/test/org/apache/solr/search/function/TestMinMaxOnMultiValuedField.java diff --git a/lucene/core/src/java/org/apache/lucene/search/SortedSetSelector.java b/lucene/core/src/java/org/apache/lucene/search/SortedSetSelector.java index a382e823e46..0e356d41b08 100644 --- a/lucene/core/src/java/org/apache/lucene/search/SortedSetSelector.java +++ b/lucene/core/src/java/org/apache/lucene/search/SortedSetSelector.java @@ -77,7 +77,7 @@ public class SortedSetSelector { return new MinValue(sortedSet); } else { if (sortedSet instanceof RandomAccessOrds == false) { - throw new UnsupportedOperationException("codec does not support random access ordinals, cannot use selector: " + selector); + throw new UnsupportedOperationException("codec does not support random access ordinals, cannot use selector: " + selector + " docValsImpl: " + sortedSet.toString()); } RandomAccessOrds randomOrds = (RandomAccessOrds) sortedSet; switch(selector) { diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index 70bfaa4ed51..1660e2f8603 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -166,6 +166,9 @@ New Features * SOLR-7742: Support for Immutable ConfigSets (Gregory Chanan) +* SOLR-2522: new two argument option for the existing field() function; picks the min/max value of a + docValues field to use as a ValueSource: "field(field_name,min)" and "field(field_name,max)" (hossman) + Bug Fixes ---------------------- diff --git a/solr/core/src/java/org/apache/solr/schema/FieldType.java b/solr/core/src/java/org/apache/solr/schema/FieldType.java index 41b89889e3b..3bc4ea40ed8 100644 --- a/solr/core/src/java/org/apache/solr/schema/FieldType.java +++ b/solr/core/src/java/org/apache/solr/schema/FieldType.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -44,6 +45,8 @@ import org.apache.lucene.search.MultiTermQuery; import org.apache.lucene.search.PrefixQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.SortField; +import org.apache.lucene.search.SortedSetSelector; +import org.apache.lucene.search.SortedNumericSelector; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TermRangeQuery; import org.apache.lucene.search.similarities.Similarity; @@ -669,6 +672,27 @@ public abstract class FieldType extends FieldProperties { return new StrFieldSource(field.name); } + /** + * Method for dynamically building a ValueSource based on a single value of a multivalued field. + * + * The default implementation throws an error except in the trivial case where this method is used on + * a {@link SchemaField} that is in fact not-multivalued, in which case it delegates to + * {@link #getValueSource} + * + * @see MultiValueSelector + */ + public ValueSource getSingleValueSource(MultiValueSelector choice, SchemaField field, QParser parser) { + // trivial base case + if (!field.multiValued()) { + // single value matches any selector + return getValueSource(field, parser); + } + + throw new SolrException(ErrorCode.BAD_REQUEST, "Selecting a single value from a multivalued field is not supported for this field: " + field.getName() + " (type: " + this.getTypeName() + ")"); + } + + + /** * Returns a Query instance for doing range searches on this field type. {@link org.apache.solr.search.SolrQueryParser} * currently passes part1 and part2 as null if they are '*' respectively. minInclusive and maxInclusive are both true @@ -1028,4 +1052,65 @@ public abstract class FieldType extends FieldProperties { final byte[] bytes = Base64.base64ToByteArray(val); return new BytesRef(bytes); } + + /** + * An enumeration representing various options that may exist for selecting a single value from a + * multivalued field. This class is designed to be an abstract representation, agnostic of some of + * the underlying specifics. Not all enum value are garunteeded work in all contexts -- null checks + * must be dont by the caller for the specific methods needed. + * + * @see FieldType#getSingleValueSource + */ + public enum MultiValueSelector { + // trying to be agnostic about SortedSetSelector.Type vs SortedNumericSelector.Type + MIN(SortedSetSelector.Type.MIN, SortedNumericSelector.Type.MIN), + MAX(SortedSetSelector.Type.MAX, SortedNumericSelector.Type.MAX); + + @Override + public String toString() { return super.toString().toLowerCase(Locale.ROOT); } + + /** + * The appropriate SortedSetSelector.Type option for this MultiValueSelector, + * may be null if there is no equivilent + */ + public SortedSetSelector.Type getSortedSetSelectorType() { + return sType; + } + + /** + * The appropriate SortedNumericSelector.Type option for this MultiValueSelector, + * may be null if there is no equivilent + */ + public SortedNumericSelector.Type getSortedNumericSelectorType() { + return nType; + } + + private final SortedSetSelector.Type sType; + private final SortedNumericSelector.Type nType; + + private MultiValueSelector(SortedSetSelector.Type sType, SortedNumericSelector.Type nType) { + this.sType = sType; + this.nType = nType; + } + + /** + * Returns a MultiValueSelector matching the specified (case insensitive) label, or null if + * no corrisponding MultiValueSelector exists. + * + * @param label a non null label to be checked for a corrisponding MultiValueSelector + * @return a MultiValueSelector or null if no MultiValueSelector matches the specified label + */ + public static MultiValueSelector lookup(String label) { + if (null == label) { + throw new NullPointerException("label must not be null when calling MultiValueSelector.lookup"); + } + try { + return valueOf(label.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return null; + } + } + + } + } diff --git a/solr/core/src/java/org/apache/solr/schema/TrieDoubleField.java b/solr/core/src/java/org/apache/solr/schema/TrieDoubleField.java index 1f0da8aaaa8..848104ef3a7 100644 --- a/solr/core/src/java/org/apache/solr/schema/TrieDoubleField.java +++ b/solr/core/src/java/org/apache/solr/schema/TrieDoubleField.java @@ -17,6 +17,23 @@ package org.apache.solr.schema; +import java.io.IOException; +import java.util.Map; + +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.DocValues; +import org.apache.lucene.index.SortedDocValues; +import org.apache.lucene.index.SortedSetDocValues; +import org.apache.lucene.queries.function.ValueSource; +import org.apache.lucene.queries.function.FunctionValues; +import org.apache.lucene.queries.function.docvalues.DoubleDocValues; +import org.apache.lucene.queries.function.valuesource.SortedSetFieldSource; +import org.apache.lucene.search.SortedSetSelector; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.NumericUtils; +import org.apache.lucene.util.mutable.MutableValue; +import org.apache.lucene.util.mutable.MutableValueDouble; + /** * A numeric field that can contain double-precision 64-bit IEEE 754 floating * point values. @@ -37,4 +54,49 @@ public class TrieDoubleField extends TrieField implements DoubleValueFieldType { { type=TrieTypes.DOUBLE; } + + @Override + protected ValueSource getSingleValueSource(SortedSetSelector.Type choice, SchemaField f) { + + return new SortedSetFieldSource(f.getName(), choice) { + @Override + public FunctionValues getValues(Map context, LeafReaderContext readerContext) throws IOException { + SortedSetFieldSource thisAsSortedSetFieldSource = this; // needed for nested anon class ref + + SortedSetDocValues sortedSet = DocValues.getSortedSet(readerContext.reader(), field); + SortedDocValues view = SortedSetSelector.wrap(sortedSet, selector); + + return new DoubleDocValues(thisAsSortedSetFieldSource) { + @Override + public double doubleVal(int doc) { + BytesRef bytes = view.get(doc); + return NumericUtils.sortableLongToDouble(NumericUtils.prefixCodedToLong(bytes)); + } + + @Override + public boolean exists(int doc) { + return -1 != view.getOrd(doc); + } + + @Override + public ValueFiller getValueFiller() { + return new ValueFiller() { + private final MutableValueDouble mval = new MutableValueDouble(); + + @Override + public MutableValue getValue() { + return mval; + } + + @Override + public void fillValue(int doc) { + mval.exists = exists(doc); + mval.value = mval.exists ? doubleVal(doc) : 0.0D; + } + }; + } + }; + } + }; + } } diff --git a/solr/core/src/java/org/apache/solr/schema/TrieField.java b/solr/core/src/java/org/apache/solr/schema/TrieField.java index d3b9c2900c0..56197cd3677 100644 --- a/solr/core/src/java/org/apache/solr/schema/TrieField.java +++ b/solr/core/src/java/org/apache/solr/schema/TrieField.java @@ -42,6 +42,7 @@ import org.apache.lucene.queries.function.valuesource.LongFieldSource; import org.apache.lucene.search.DocValuesRangeQuery; import org.apache.lucene.search.NumericRangeQuery; import org.apache.lucene.search.Query; +import org.apache.lucene.search.SortedSetSelector; import org.apache.lucene.search.SortField; import org.apache.lucene.uninverting.UninvertingReader.Type; import org.apache.lucene.util.BytesRef; @@ -244,7 +245,50 @@ public class TrieField extends PrimitiveFieldType { } } + @Override + public final ValueSource getSingleValueSource(MultiValueSelector choice, SchemaField field, QParser parser) { + // trivial base case + if (!field.multiValued()) { + // single value matches any selector + return getValueSource(field, parser); + } + // See LUCENE-6709 + if (! field.hasDocValues()) { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, + "docValues='true' is required to select '" + choice.toString() + + "' value from multivalued field ("+ field.getName() +") at query time"); + } + + // multivalued Trie fields all use SortedSetDocValues, so we give a clean error if that's + // not supported by the specified choice, else we delegate to a helper + SortedSetSelector.Type selectorType = choice.getSortedSetSelectorType(); + if (null == selectorType) { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, + choice.toString() + " is not a supported option for picking a single value" + + " from the multivalued field: " + field.getName() + + " (type: " + this.getTypeName() + ")"); + } + + return getSingleValueSource(selectorType, field); + } + + /** + * Helper method that will only be called for multivalued Trie fields that have doc values. + * Default impl throws an error indicating that selecting a single value from this multivalued + * field is not supported for this field type + * + * @param choice the selector Type to use, will never be null + * @param field the field to use, garunteed to be multivalued. + * @see #getSingleValueSource(MultiValueSelector,SchemaField,QParser) + */ + protected ValueSource getSingleValueSource(SortedSetSelector.Type choice, SchemaField field) { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, + "Can not select a single value for multivalued field: " + field.getName() + + " (single valued field selection not supported for type: " + this.getTypeName() + + ")"); + } + @Override public void write(TextResponseWriter writer, String name, StorableField f) throws IOException { writer.writeVal(name, toObject(f)); diff --git a/solr/core/src/java/org/apache/solr/schema/TrieFloatField.java b/solr/core/src/java/org/apache/solr/schema/TrieFloatField.java index 3e129c318d3..e2d4ef449cd 100644 --- a/solr/core/src/java/org/apache/solr/schema/TrieFloatField.java +++ b/solr/core/src/java/org/apache/solr/schema/TrieFloatField.java @@ -17,6 +17,23 @@ package org.apache.solr.schema; +import java.io.IOException; +import java.util.Map; + +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.DocValues; +import org.apache.lucene.index.SortedDocValues; +import org.apache.lucene.index.SortedSetDocValues; +import org.apache.lucene.queries.function.ValueSource; +import org.apache.lucene.queries.function.FunctionValues; +import org.apache.lucene.queries.function.docvalues.FloatDocValues; +import org.apache.lucene.queries.function.valuesource.SortedSetFieldSource; +import org.apache.lucene.search.SortedSetSelector; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.NumericUtils; +import org.apache.lucene.util.mutable.MutableValue; +import org.apache.lucene.util.mutable.MutableValueFloat; + /** * A numeric field that can contain single-precision 32-bit IEEE 754 * floating point values. @@ -45,4 +62,50 @@ public class TrieFloatField extends TrieField implements FloatValueFieldType { if (val instanceof String) return Float.parseFloat((String) val); return super.toNativeType(val); } + + @Override + protected ValueSource getSingleValueSource(SortedSetSelector.Type choice, SchemaField f) { + + return new SortedSetFieldSource(f.getName(), choice) { + @Override + public FunctionValues getValues(Map context, LeafReaderContext readerContext) throws IOException { + SortedSetFieldSource thisAsSortedSetFieldSource = this; // needed for nested anon class ref + + SortedSetDocValues sortedSet = DocValues.getSortedSet(readerContext.reader(), field); + SortedDocValues view = SortedSetSelector.wrap(sortedSet, selector); + + return new FloatDocValues(thisAsSortedSetFieldSource) { + @Override + public float floatVal(int doc) { + BytesRef bytes = view.get(doc); + return NumericUtils.sortableIntToFloat(NumericUtils.prefixCodedToInt(bytes)); + } + + @Override + public boolean exists(int doc) { + return -1 != view.getOrd(doc); + } + + @Override + public ValueFiller getValueFiller() { + return new ValueFiller() { + private final MutableValueFloat mval = new MutableValueFloat(); + + @Override + public MutableValue getValue() { + return mval; + } + + @Override + public void fillValue(int doc) { + mval.exists = exists(doc); + mval.value = mval.exists ? floatVal(doc) : 0.0F; + } + }; + } + }; + } + }; + } + } diff --git a/solr/core/src/java/org/apache/solr/schema/TrieIntField.java b/solr/core/src/java/org/apache/solr/schema/TrieIntField.java index ffa33815ef5..47ec74248de 100644 --- a/solr/core/src/java/org/apache/solr/schema/TrieIntField.java +++ b/solr/core/src/java/org/apache/solr/schema/TrieIntField.java @@ -17,6 +17,23 @@ package org.apache.solr.schema; +import java.io.IOException; +import java.util.Map; + +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.DocValues; +import org.apache.lucene.index.SortedDocValues; +import org.apache.lucene.index.SortedSetDocValues; +import org.apache.lucene.queries.function.ValueSource; +import org.apache.lucene.queries.function.FunctionValues; +import org.apache.lucene.queries.function.docvalues.IntDocValues; +import org.apache.lucene.queries.function.valuesource.SortedSetFieldSource; +import org.apache.lucene.search.SortedSetSelector; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.NumericUtils; +import org.apache.lucene.util.mutable.MutableValue; +import org.apache.lucene.util.mutable.MutableValueInt; + /** * A numeric field that can contain 32-bit signed two's complement integer values. * @@ -44,4 +61,49 @@ public class TrieIntField extends TrieField implements IntValueFieldType { } return super.toNativeType(val); } + + @Override + protected ValueSource getSingleValueSource(SortedSetSelector.Type choice, SchemaField f) { + + return new SortedSetFieldSource(f.getName(), choice) { + @Override + public FunctionValues getValues(Map context, LeafReaderContext readerContext) throws IOException { + SortedSetFieldSource thisAsSortedSetFieldSource = this; // needed for nested anon class ref + + SortedSetDocValues sortedSet = DocValues.getSortedSet(readerContext.reader(), field); + SortedDocValues view = SortedSetSelector.wrap(sortedSet, selector); + + return new IntDocValues(thisAsSortedSetFieldSource) { + @Override + public int intVal(int doc) { + BytesRef bytes = view.get(doc); + return NumericUtils.prefixCodedToInt(bytes); + } + + @Override + public boolean exists(int doc) { + return -1 != view.getOrd(doc); + } + + @Override + public ValueFiller getValueFiller() { + return new ValueFiller() { + private final MutableValueInt mval = new MutableValueInt(); + + @Override + public MutableValue getValue() { + return mval; + } + + @Override + public void fillValue(int doc) { + mval.exists = exists(doc); + mval.value = mval.exists ? intVal(doc) : 0; + } + }; + } + }; + } + }; + } } diff --git a/solr/core/src/java/org/apache/solr/schema/TrieLongField.java b/solr/core/src/java/org/apache/solr/schema/TrieLongField.java index 052e4a72c86..13e99eb4b15 100644 --- a/solr/core/src/java/org/apache/solr/schema/TrieLongField.java +++ b/solr/core/src/java/org/apache/solr/schema/TrieLongField.java @@ -17,6 +17,23 @@ package org.apache.solr.schema; +import java.io.IOException; +import java.util.Map; + +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.DocValues; +import org.apache.lucene.index.SortedDocValues; +import org.apache.lucene.index.SortedSetDocValues; +import org.apache.lucene.queries.function.ValueSource; +import org.apache.lucene.queries.function.FunctionValues; +import org.apache.lucene.queries.function.docvalues.LongDocValues; +import org.apache.lucene.queries.function.valuesource.SortedSetFieldSource; +import org.apache.lucene.search.SortedSetSelector; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.NumericUtils; +import org.apache.lucene.util.mutable.MutableValue; +import org.apache.lucene.util.mutable.MutableValueLong; + /** * A numeric field that can contain 64-bit signed two's complement integer values. * @@ -31,4 +48,49 @@ public class TrieLongField extends TrieField implements LongValueFieldType { { type=TrieTypes.LONG; } + + @Override + protected ValueSource getSingleValueSource(SortedSetSelector.Type choice, SchemaField f) { + + return new SortedSetFieldSource(f.getName(), choice) { + @Override + public FunctionValues getValues(Map context, LeafReaderContext readerContext) throws IOException { + SortedSetFieldSource thisAsSortedSetFieldSource = this; // needed for nested anon class ref + + SortedSetDocValues sortedSet = DocValues.getSortedSet(readerContext.reader(), field); + SortedDocValues view = SortedSetSelector.wrap(sortedSet, selector); + + return new LongDocValues(thisAsSortedSetFieldSource) { + @Override + public long longVal(int doc) { + BytesRef bytes = view.get(doc); + return NumericUtils.prefixCodedToLong(bytes); + } + + @Override + public boolean exists(int doc) { + return -1 != view.getOrd(doc); + } + + @Override + public ValueFiller getValueFiller() { + return new ValueFiller() { + private final MutableValueLong mval = new MutableValueLong(); + + @Override + public MutableValue getValue() { + return mval; + } + + @Override + public void fillValue(int doc) { + mval.exists = exists(doc); + mval.value = mval.exists ? longVal(doc) : 0; + } + }; + } + }; + } + }; + } } diff --git a/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java b/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java index 19936a209ec..2c05f33c3e8 100644 --- a/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java +++ b/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java @@ -398,6 +398,17 @@ public abstract class ValueSourceParser implements NamedListInitializedPlugin { String fieldName = fp.parseArg(); SchemaField f = fp.getReq().getSchema().getField(fieldName); + if (fp.hasMoreArguments()) { + // multivalued selector option + String s = fp.parseArg(); + FieldType.MultiValueSelector selector = FieldType.MultiValueSelector.lookup(s); + if (null == selector) { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, + "Multi-Valued field selector '"+s+"' not spported"); + } + return f.getType().getSingleValueSource(selector, f, fp); + } + // simple field ValueSource return f.getType().getValueSource(f, fp); } }); @@ -563,6 +574,7 @@ public abstract class ValueSourceParser implements NamedListInitializedPlugin { return new MinFloatFunction(sources.toArray(new ValueSource[sources.size()])); } }); + addParser("sqedist", new ValueSourceParser() { @Override public ValueSource parse(FunctionQParser fp) throws SyntaxError { diff --git a/solr/core/src/test-files/solr/collection1/conf/schema15.xml b/solr/core/src/test-files/solr/collection1/conf/schema15.xml index 39c2ed333cf..37bc618a2ca 100644 --- a/solr/core/src/test-files/solr/collection1/conf/schema15.xml +++ b/solr/core/src/test-files/solr/collection1/conf/schema15.xml @@ -506,7 +506,7 @@ - + diff --git a/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java b/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java index 537b8a6e01b..f01435a7c9b 100644 --- a/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java +++ b/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java @@ -817,6 +817,28 @@ public class QueryEqualityTest extends SolrTestCaseJ4 { assertFuncEquals("field(\"foo_i\")", "field('foo_i\')", "foo_i"); + + // simple VS of single valued field should be same as asking for min/max on that field + assertFuncEquals("field(\"foo_i\")", + "field('foo_i',min)", + "field(foo_i,'min')", + "field('foo_i',max)", + "field(foo_i,'max')", + "foo_i"); + + // multivalued field with selector + String multif = "multi_int_with_docvals"; + SolrQueryRequest req = req("my_field", multif); + // this test is only viable if it's a multivalued field, sanity check the schema + assertTrue(multif + " is no longer multivalued, who broke this schema?", + req.getSchema().getField(multif).multiValued()); + assertFuncEquals(req, + "field($my_field,'MIN')", + "field('"+multif+"',min)"); + assertFuncEquals(req, + "field($my_field,'max')", + "field('"+multif+"',Max)"); + } public void testFuncCurrency() throws Exception { assertFuncEquals("currency(\"amount\")", diff --git a/solr/core/src/test/org/apache/solr/search/function/TestMinMaxOnMultiValuedField.java b/solr/core/src/test/org/apache/solr/search/function/TestMinMaxOnMultiValuedField.java new file mode 100644 index 00000000000..9627449e00e --- /dev/null +++ b/solr/core/src/test/org/apache/solr/search/function/TestMinMaxOnMultiValuedField.java @@ -0,0 +1,358 @@ +/* + * 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.apache.solr.common.params.SolrParams; +import org.apache.solr.common.util.NamedList; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; + +@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); + } + + /** @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); + } + + /** @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); + } + + /** @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); + } + + /** 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) { + + 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()); + + 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+"'" + ); + + 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" + ); + } + +}