diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 5ab649e..7a566ba 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -34,6 +34,10 @@ New features http://blog.mikemccandless.com/2012/04/lucenes-tokenstreams-are-actually.html for details. (Mike McCandless) +* LUCENE-5325: Added LongValuesSource and DoubleValuesSource, intended as + type-safe replacements for ValueSource in the queries module. These + expose per-segment LongValues or DoubleValues iterators. (Alan Woodward, Adrien Grand) + Bug Fixes * LUCENE-7547: JapaneseTokenizerFactory was failing to close the diff --git a/lucene/core/src/java/org/apache/lucene/search/DoubleValues.java b/lucene/core/src/java/org/apache/lucene/search/DoubleValues.java new file mode 100644 index 0000000..4f12390 --- /dev/null +++ b/lucene/core/src/java/org/apache/lucene/search/DoubleValues.java @@ -0,0 +1,38 @@ +/* + * 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.lucene.search; + +import java.io.IOException; + +/** + * Per-segment, per-document double values, which can be calculated at search-time + */ +public abstract class DoubleValues { + + /** + * Get the double value for the current document + */ + public abstract double doubleValue() throws IOException; + + /** + * Advance this instance to the given document id + * @return true if there is a value for this document + */ + public abstract boolean advanceExact(int doc) throws IOException; + +} diff --git a/lucene/core/src/java/org/apache/lucene/search/DoubleValuesSource.java b/lucene/core/src/java/org/apache/lucene/search/DoubleValuesSource.java new file mode 100644 index 0000000..4361243 --- /dev/null +++ b/lucene/core/src/java/org/apache/lucene/search/DoubleValuesSource.java @@ -0,0 +1,301 @@ +/* + * 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.lucene.search; + +import java.io.IOException; +import java.util.Objects; +import java.util.function.DoubleToLongFunction; +import java.util.function.LongToDoubleFunction; + +import org.apache.lucene.index.DocValues; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.NumericDocValues; + +/** + * Base class for producing {@link DoubleValues} + * + * To obtain a {@link DoubleValues} object for a leaf reader, clients should + * call {@link #getValues(LeafReaderContext, DoubleValues)}. + * + * DoubleValuesSource objects for NumericDocValues fields can be obtained by calling + * {@link #fromDoubleField(String)}, {@link #fromFloatField(String)}, {@link #fromIntField(String)} + * or {@link #fromLongField(String)}, or from {@link #fromField(String, LongToDoubleFunction)} if + * special long-to-double encoding is required. + * + * Scores may be used as a source for value calculations by wrapping a {@link Scorer} using + * {@link #fromScorer(Scorer)} and passing the resulting DoubleValues to {@link #getValues(LeafReaderContext, DoubleValues)}. + * The scores can then be accessed using the {@link #SCORES} DoubleValuesSource. + */ +public abstract class DoubleValuesSource { + + /** + * Returns a {@link DoubleValues} instance for the passed-in LeafReaderContext and scores + * + * If scores are not needed to calculate the values (ie {@link #needsScores() returns false}, callers + * may safely pass {@code null} for the {@code scores} parameter. + */ + public abstract DoubleValues getValues(LeafReaderContext ctx, DoubleValues scores) throws IOException; + + /** + * Return true if document scores are needed to calculate values + */ + public abstract boolean needsScores(); + + /** + * Create a sort field based on the value of this producer + * @param reverse true if the sort should be decreasing + */ + public SortField getSortField(boolean reverse) { + return new DoubleValuesSortField(this, reverse); + } + + /** + * Convert to a LongValuesSource by casting the double values to longs + */ + public final LongValuesSource toLongValuesSource() { + return new LongValuesSource() { + @Override + public LongValues getValues(LeafReaderContext ctx, DoubleValues scores) throws IOException { + DoubleValues in = DoubleValuesSource.this.getValues(ctx, scores); + return new LongValues() { + @Override + public long longValue() throws IOException { + return (long) in.doubleValue(); + } + + @Override + public boolean advanceExact(int doc) throws IOException { + return in.advanceExact(doc); + } + }; + } + + @Override + public boolean needsScores() { + return DoubleValuesSource.this.needsScores(); + } + }; + } + + /** + * Creates a DoubleValuesSource that wraps a generic NumericDocValues field + * + * @param field the field to wrap, must have NumericDocValues + * @param decoder a function to convert the long-valued doc values to doubles + */ + public static DoubleValuesSource fromField(String field, LongToDoubleFunction decoder) { + return new FieldValuesSource(field, decoder); + } + + /** + * Creates a DoubleValuesSource that wraps a double-valued field + */ + public static DoubleValuesSource fromDoubleField(String field) { + return fromField(field, Double::longBitsToDouble); + } + + /** + * Creates a DoubleValuesSource that wraps a float-valued field + */ + public static DoubleValuesSource fromFloatField(String field) { + return fromField(field, (v) -> (double)Float.intBitsToFloat((int)v)); + } + + /** + * Creates a DoubleValuesSource that wraps a long-valued field + */ + public static DoubleValuesSource fromLongField(String field) { + return fromField(field, (v) -> (double) v); + } + + /** + * Creates a DoubleValuesSource that wraps an int-valued field + */ + public static DoubleValuesSource fromIntField(String field) { + return fromLongField(field); + } + + /** + * A DoubleValuesSource that exposes a document's score + * + * If this source is used as part of a values calculation, then callers must not + * pass {@code null} as the {@link DoubleValues} parameter on {@link #getValues(LeafReaderContext, DoubleValues)} + */ + public static final DoubleValuesSource SCORES = new DoubleValuesSource() { + @Override + public DoubleValues getValues(LeafReaderContext ctx, DoubleValues scores) throws IOException { + assert scores != null; + return scores; + } + + @Override + public boolean needsScores() { + return true; + } + }; + + /** + * Returns a DoubleValues instance that wraps scores returned by a Scorer + */ + public static DoubleValues fromScorer(Scorer scorer) { + return new DoubleValues() { + @Override + public double doubleValue() throws IOException { + return scorer.score(); + } + + @Override + public boolean advanceExact(int doc) throws IOException { + assert scorer.docID() == doc; + return true; + } + }; + } + + private static class FieldValuesSource extends DoubleValuesSource { + + final String field; + final LongToDoubleFunction decoder; + + private FieldValuesSource(String field, LongToDoubleFunction decoder) { + this.field = field; + this.decoder = decoder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FieldValuesSource that = (FieldValuesSource) o; + return Objects.equals(field, that.field) && + Objects.equals(decoder, that.decoder); + } + + @Override + public int hashCode() { + return Objects.hash(field, decoder); + } + + @Override + public DoubleValues getValues(LeafReaderContext ctx, DoubleValues scores) throws IOException { + final NumericDocValues values = DocValues.getNumeric(ctx.reader(), field); + return toDoubleValues(values, decoder::applyAsDouble); + } + + @Override + public boolean needsScores() { + return false; + } + } + + private static class DoubleValuesSortField extends SortField { + + final DoubleValuesSource producer; + + public DoubleValuesSortField(DoubleValuesSource producer, boolean reverse) { + super(producer.toString(), new DoubleValuesComparatorSource(producer), reverse); + this.producer = producer; + } + + @Override + public boolean needsScores() { + return producer.needsScores(); + } + + @Override + public String toString() { + StringBuilder buffer = new StringBuilder("<"); + buffer.append(getField()).append(">"); + if (reverse) + buffer.append("!"); + return buffer.toString(); + } + + } + + private static class DoubleValuesHolder { + DoubleValues values; + } + + private static class DoubleValuesComparatorSource extends FieldComparatorSource { + private final DoubleValuesSource producer; + + public DoubleValuesComparatorSource(DoubleValuesSource producer) { + this.producer = producer; + } + + @Override + public FieldComparator newComparator(String fieldname, int numHits, + int sortPos, boolean reversed) throws IOException { + return new FieldComparator.DoubleComparator(numHits, fieldname, 0.0){ + + LeafReaderContext ctx; + DoubleValuesHolder holder = new DoubleValuesHolder(); + + @Override + protected NumericDocValues getNumericDocValues(LeafReaderContext context, String field) throws IOException { + ctx = context; + return asNumericDocValues(holder, missingValue, Double::doubleToLongBits); + } + + @Override + public void setScorer(Scorer scorer) throws IOException { + holder.values = producer.getValues(ctx, fromScorer(scorer)); + } + }; + } + } + + private static DoubleValues toDoubleValues(NumericDocValues in, LongToDoubleFunction map) { + return new DoubleValues() { + + int current = -1; + + @Override + public double doubleValue() throws IOException { + return map.applyAsDouble(in.get(current)); + } + + @Override + public boolean advanceExact(int target) throws IOException { + current = target; + return true; + } + + }; + } + + private static NumericDocValues asNumericDocValues(DoubleValuesHolder in, Double missingValue, DoubleToLongFunction converter) { + long missing = converter.applyAsLong(missingValue); + return new NumericDocValues() { + @Override + public long get(int docID) { + try { + if (in.values.advanceExact(docID)) + return converter.applyAsLong(in.values.doubleValue()); + } + catch (IOException e) { + throw new RuntimeException(e); + } + return missing; + } + + }; + } + +} diff --git a/lucene/core/src/java/org/apache/lucene/search/LongValues.java b/lucene/core/src/java/org/apache/lucene/search/LongValues.java new file mode 100644 index 0000000..54d3189 --- /dev/null +++ b/lucene/core/src/java/org/apache/lucene/search/LongValues.java @@ -0,0 +1,38 @@ +/* + * 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.lucene.search; + +import java.io.IOException; + +/** + * Per-segment, per-document long values, which can be calculated at search-time + */ +public abstract class LongValues { + + /** + * Get the long value for the current document + */ + public abstract long longValue() throws IOException; + + /** + * Advance this instance to the given document id + * @return true if there is a value for this document + */ + public abstract boolean advanceExact(int doc) throws IOException; + +} diff --git a/lucene/core/src/java/org/apache/lucene/search/LongValuesSource.java b/lucene/core/src/java/org/apache/lucene/search/LongValuesSource.java new file mode 100644 index 0000000..4165e90 --- /dev/null +++ b/lucene/core/src/java/org/apache/lucene/search/LongValuesSource.java @@ -0,0 +1,204 @@ +/* + * 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.lucene.search; + +import java.io.IOException; +import java.util.Objects; + +import org.apache.lucene.index.DocValues; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.NumericDocValues; + +/** + * Base class for producing {@link LongValues} + * + * To obtain a {@link LongValues} object for a leaf reader, clients should + * call {@link #getValues(LeafReaderContext, DoubleValues)}. + * + * LongValuesSource objects for long and int-valued NumericDocValues fields can + * be obtained by calling {@link #fromLongField(String)} and {@link #fromIntField(String)}. + * + * To obtain a LongValuesSource from a float or double-valued NumericDocValues field, + * use {@link DoubleValuesSource#fromFloatField(String)} or {@link DoubleValuesSource#fromDoubleField(String)} + * and then call {@link DoubleValuesSource#toLongValuesSource()}. + */ +public abstract class LongValuesSource { + + /** + * Returns a {@link LongValues} instance for the passed-in LeafReaderContext and scores + * + * If scores are not needed to calculate the values (ie {@link #needsScores() returns false}, callers + * may safely pass {@code null} for the {@code scores} parameter. + */ + public abstract LongValues getValues(LeafReaderContext ctx, DoubleValues scores) throws IOException; + + /** + * Return true if document scores are needed to calculate values + */ + public abstract boolean needsScores(); + + /** + * Create a sort field based on the value of this producer + * @param reverse true if the sort should be decreasing + */ + public SortField getSortField(boolean reverse) { + return new LongValuesSortField(this, reverse); + } + + /** + * Creates a LongValuesSource that wraps a long-valued field + */ + public static LongValuesSource fromLongField(String field) { + return new FieldValuesSource(field); + } + + /** + * Creates a LongValuesSource that wraps an int-valued field + */ + public static LongValuesSource fromIntField(String field) { + return fromLongField(field); + } + + private static class FieldValuesSource extends LongValuesSource { + + final String field; + + private FieldValuesSource(String field) { + this.field = field; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FieldValuesSource that = (FieldValuesSource) o; + return Objects.equals(field, that.field); + } + + @Override + public int hashCode() { + return Objects.hash(field); + } + + @Override + public LongValues getValues(LeafReaderContext ctx, DoubleValues scores) throws IOException { + final NumericDocValues values = DocValues.getNumeric(ctx.reader(), field); + return toLongValues(values); + } + + @Override + public boolean needsScores() { + return false; + } + } + + private static class LongValuesSortField extends SortField { + + final LongValuesSource producer; + + public LongValuesSortField(LongValuesSource producer, boolean reverse) { + super(producer.toString(), new LongValuesComparatorSource(producer), reverse); + this.producer = producer; + } + + @Override + public boolean needsScores() { + return producer.needsScores(); + } + + @Override + public String toString() { + StringBuilder buffer = new StringBuilder("<"); + buffer.append(getField()).append(">"); + if (reverse) + buffer.append("!"); + return buffer.toString(); + } + + } + + private static class LongValuesHolder { + LongValues values; + } + + private static class LongValuesComparatorSource extends FieldComparatorSource { + private final LongValuesSource producer; + + public LongValuesComparatorSource(LongValuesSource producer) { + this.producer = producer; + } + + @Override + public FieldComparator newComparator(String fieldname, int numHits, + int sortPos, boolean reversed) throws IOException { + return new FieldComparator.LongComparator(numHits, fieldname, 0L){ + + LeafReaderContext ctx; + LongValuesHolder holder = new LongValuesHolder(); + + @Override + protected NumericDocValues getNumericDocValues(LeafReaderContext context, String field) throws IOException { + ctx = context; + return asNumericDocValues(holder, missingValue); + } + + @Override + public void setScorer(Scorer scorer) throws IOException { + holder.values = producer.getValues(ctx, DoubleValuesSource.fromScorer(scorer)); + } + }; + } + } + + private static LongValues toLongValues(NumericDocValues in) { + return new LongValues() { + + int current = -1; + + @Override + public long longValue() throws IOException { + return in.get(current); + } + + @Override + public boolean advanceExact(int target) throws IOException { + current = target; + return true; + } + + }; + } + + private static NumericDocValues asNumericDocValues(LongValuesHolder in, Long missingValue) { + return new NumericDocValues() { + @Override + public long get(int docID) { + try { + if (in.values.advanceExact(docID)) + return in.values.longValue(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + return missingValue; + } + + }; + } + +} diff --git a/lucene/core/src/test/org/apache/lucene/search/TestDoubleValuesSource.java b/lucene/core/src/test/org/apache/lucene/search/TestDoubleValuesSource.java new file mode 100644 index 0000000..3860963 --- /dev/null +++ b/lucene/core/src/test/org/apache/lucene/search/TestDoubleValuesSource.java @@ -0,0 +1,158 @@ +/* + * 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.lucene.search; + +import java.util.Arrays; +import java.util.Collections; + +import org.apache.lucene.document.Document; +import org.apache.lucene.document.DoubleDocValuesField; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.FloatDocValuesField; +import org.apache.lucene.document.NumericDocValuesField; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.RandomIndexWriter; +import org.apache.lucene.index.Term; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.English; +import org.apache.lucene.util.LuceneTestCase; +import org.apache.lucene.util.TestUtil; + +public class TestDoubleValuesSource extends LuceneTestCase { + + private Directory dir; + private IndexReader reader; + private IndexSearcher searcher; + + @Override + public void setUp() throws Exception { + super.setUp(); + dir = newDirectory(); + RandomIndexWriter iw = new RandomIndexWriter(random(), dir); + int numDocs = TestUtil.nextInt(random(), 2049, 4000); + for (int i = 0; i < numDocs; i++) { + Document document = new Document(); + document.add(newTextField("english", English.intToEnglish(i), Field.Store.NO)); + document.add(newTextField("oddeven", (i % 2 == 0) ? "even" : "odd", Field.Store.NO)); + document.add(new NumericDocValuesField("int", random().nextInt())); + document.add(new NumericDocValuesField("long", random().nextLong())); + document.add(new FloatDocValuesField("float", random().nextFloat())); + document.add(new DoubleDocValuesField("double", random().nextDouble())); + iw.addDocument(document); + } + reader = iw.getReader(); + iw.close(); + searcher = newSearcher(reader); + } + + @Override + public void tearDown() throws Exception { + reader.close(); + dir.close(); + super.tearDown(); + } + + public void testSimpleFieldEquivalences() throws Exception { + checkSorts(new MatchAllDocsQuery(), new Sort(new SortField("int", SortField.Type.INT, random().nextBoolean()))); + checkSorts(new MatchAllDocsQuery(), new Sort(new SortField("long", SortField.Type.LONG, random().nextBoolean()))); + checkSorts(new MatchAllDocsQuery(), new Sort(new SortField("float", SortField.Type.FLOAT, random().nextBoolean()))); + checkSorts(new MatchAllDocsQuery(), new Sort(new SortField("double", SortField.Type.DOUBLE, random().nextBoolean()))); + } + + public void testHashCodeAndEquals() { + DoubleValuesSource vs1 = DoubleValuesSource.fromDoubleField("double"); + DoubleValuesSource vs2 = DoubleValuesSource.fromDoubleField("double"); + assertEquals(vs1, vs2); + assertEquals(vs1.hashCode(), vs2.hashCode()); + DoubleValuesSource v3 = DoubleValuesSource.fromLongField("long"); + assertFalse(vs1.equals(v3)); + } + + public void testSimpleFieldSortables() throws Exception { + int n = atLeast(4); + for (int i = 0; i < n; i++) { + Sort sort = randomSort(); + checkSorts(new MatchAllDocsQuery(), sort); + checkSorts(new TermQuery(new Term("english", "one")), sort); + } + } + + Sort randomSort() throws Exception { + boolean reversed = random().nextBoolean(); + SortField fields[] = new SortField[] { + new SortField("int", SortField.Type.INT, reversed), + new SortField("long", SortField.Type.LONG, reversed), + new SortField("float", SortField.Type.FLOAT, reversed), + new SortField("double", SortField.Type.DOUBLE, reversed), + new SortField("score", SortField.Type.SCORE) + }; + Collections.shuffle(Arrays.asList(fields), random()); + int numSorts = TestUtil.nextInt(random(), 1, fields.length); + return new Sort(Arrays.copyOfRange(fields, 0, numSorts)); + } + + // Take a Sort, and replace any field sorts with Sortables + Sort convertSortToSortable(Sort sort) { + SortField original[] = sort.getSort(); + SortField mutated[] = new SortField[original.length]; + for (int i = 0; i < mutated.length; i++) { + if (random().nextInt(3) > 0) { + SortField s = original[i]; + boolean reverse = s.getType() == SortField.Type.SCORE || s.getReverse(); + switch (s.getType()) { + case INT: + mutated[i] = DoubleValuesSource.fromIntField(s.getField()).getSortField(reverse); + break; + case LONG: + mutated[i] = DoubleValuesSource.fromLongField(s.getField()).getSortField(reverse); + break; + case FLOAT: + mutated[i] = DoubleValuesSource.fromFloatField(s.getField()).getSortField(reverse); + break; + case DOUBLE: + mutated[i] = DoubleValuesSource.fromDoubleField(s.getField()).getSortField(reverse); + break; + case SCORE: + mutated[i] = DoubleValuesSource.SCORES.getSortField(reverse); + break; + default: + mutated[i] = original[i]; + } + } else { + mutated[i] = original[i]; + } + } + + return new Sort(mutated); + } + + void checkSorts(Query query, Sort sort) throws Exception { + int size = TestUtil.nextInt(random(), 1, searcher.getIndexReader().maxDoc() / 5); + TopDocs expected = searcher.search(query, size, sort, random().nextBoolean(), random().nextBoolean()); + Sort mutatedSort = convertSortToSortable(sort); + TopDocs actual = searcher.search(query, size, mutatedSort, random().nextBoolean(), random().nextBoolean()); + + CheckHits.checkEqual(query, expected.scoreDocs, actual.scoreDocs); + + if (size < actual.totalHits) { + expected = searcher.searchAfter(expected.scoreDocs[size-1], query, size, sort); + actual = searcher.searchAfter(actual.scoreDocs[size-1], query, size, mutatedSort); + CheckHits.checkEqual(query, expected.scoreDocs, actual.scoreDocs); + } + } +} diff --git a/lucene/core/src/test/org/apache/lucene/search/TestLongValuesSource.java b/lucene/core/src/test/org/apache/lucene/search/TestLongValuesSource.java new file mode 100644 index 0000000..9148ad5 --- /dev/null +++ b/lucene/core/src/test/org/apache/lucene/search/TestLongValuesSource.java @@ -0,0 +1,140 @@ +/* + * 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.lucene.search; + +import java.util.Arrays; +import java.util.Collections; + +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.NumericDocValuesField; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.RandomIndexWriter; +import org.apache.lucene.index.Term; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.English; +import org.apache.lucene.util.LuceneTestCase; +import org.apache.lucene.util.TestUtil; + +public class TestLongValuesSource extends LuceneTestCase { + + private Directory dir; + private IndexReader reader; + private IndexSearcher searcher; + + @Override + public void setUp() throws Exception { + super.setUp(); + dir = newDirectory(); + RandomIndexWriter iw = new RandomIndexWriter(random(), dir); + int numDocs = TestUtil.nextInt(random(), 2049, 4000); + for (int i = 0; i < numDocs; i++) { + Document document = new Document(); + document.add(newTextField("english", English.intToEnglish(i), Field.Store.NO)); + document.add(newTextField("oddeven", (i % 2 == 0) ? "even" : "odd", Field.Store.NO)); + document.add(new NumericDocValuesField("int", random().nextInt())); + document.add(new NumericDocValuesField("long", random().nextLong())); + iw.addDocument(document); + } + reader = iw.getReader(); + iw.close(); + searcher = newSearcher(reader); + } + + @Override + public void tearDown() throws Exception { + reader.close(); + dir.close(); + super.tearDown(); + } + + public void testSimpleFieldEquivalences() throws Exception { + checkSorts(new MatchAllDocsQuery(), new Sort(new SortField("int", SortField.Type.INT, random().nextBoolean()))); + checkSorts(new MatchAllDocsQuery(), new Sort(new SortField("long", SortField.Type.LONG, random().nextBoolean()))); + } + + public void testHashCodeAndEquals() { + LongValuesSource vs1 = LongValuesSource.fromLongField("long"); + LongValuesSource vs2 = LongValuesSource.fromLongField("long"); + assertEquals(vs1, vs2); + assertEquals(vs1.hashCode(), vs2.hashCode()); + LongValuesSource v3 = LongValuesSource.fromLongField("int"); + assertFalse(vs1.equals(v3)); + } + + public void testSimpleFieldSortables() throws Exception { + int n = atLeast(4); + for (int i = 0; i < n; i++) { + Sort sort = randomSort(); + checkSorts(new MatchAllDocsQuery(), sort); + checkSorts(new TermQuery(new Term("english", "one")), sort); + } + } + + Sort randomSort() throws Exception { + boolean reversed = random().nextBoolean(); + SortField fields[] = new SortField[] { + new SortField("int", SortField.Type.INT, reversed), + new SortField("long", SortField.Type.LONG, reversed) + }; + Collections.shuffle(Arrays.asList(fields), random()); + int numSorts = TestUtil.nextInt(random(), 1, fields.length); + return new Sort(Arrays.copyOfRange(fields, 0, numSorts)); + } + + // Take a Sort, and replace any field sorts with Sortables + Sort convertSortToSortable(Sort sort) { + SortField original[] = sort.getSort(); + SortField mutated[] = new SortField[original.length]; + for (int i = 0; i < mutated.length; i++) { + if (random().nextInt(3) > 0) { + SortField s = original[i]; + boolean reverse = s.getType() == SortField.Type.SCORE || s.getReverse(); + switch (s.getType()) { + case INT: + mutated[i] = LongValuesSource.fromIntField(s.getField()).getSortField(reverse); + break; + case LONG: + mutated[i] = LongValuesSource.fromLongField(s.getField()).getSortField(reverse); + break; + default: + mutated[i] = original[i]; + } + } else { + mutated[i] = original[i]; + } + } + + return new Sort(mutated); + } + + void checkSorts(Query query, Sort sort) throws Exception { + int size = TestUtil.nextInt(random(), 1, searcher.getIndexReader().maxDoc() / 5); + Sort mutatedSort = convertSortToSortable(sort); + TopDocs actual = searcher.search(query, size, mutatedSort, random().nextBoolean(), random().nextBoolean()); + TopDocs expected = searcher.search(query, size, sort, random().nextBoolean(), random().nextBoolean()); + + CheckHits.checkEqual(query, expected.scoreDocs, actual.scoreDocs); + + if (size < actual.totalHits) { + expected = searcher.searchAfter(expected.scoreDocs[size-1], query, size, sort); + actual = searcher.searchAfter(actual.scoreDocs[size-1], query, size, mutatedSort); + CheckHits.checkEqual(query, expected.scoreDocs, actual.scoreDocs); + } + } +}