diff --git a/server/src/main/java/org/opensearch/index/fielddata/FieldData.java b/server/src/main/java/org/opensearch/index/fielddata/FieldData.java index 6db6bbccacae5..61641f65489ae 100644 --- a/server/src/main/java/org/opensearch/index/fielddata/FieldData.java +++ b/server/src/main/java/org/opensearch/index/fielddata/FieldData.java @@ -311,6 +311,18 @@ public static NumericDoubleValues unwrapSingleton(SortedNumericDoubleValues valu return null; } + /** + * Returns a single-valued view of the {@link SortedNumericDoubleValues}, + * if it was previously wrapped with {@link DocValues#singleton(NumericDocValues)}, + * or null. + */ + public static SortedNumericDocValues unwrapSingleton(SortedNumericUnsignedLongValues values) { + if (values instanceof SingletonSortedNumericUnsignedLongValues) { + return ((SingletonSortedNumericUnsignedLongValues) values).getNumericUnsignedLongValues(); + } + return null; + } + /** * Returns a multi-valued view over the provided {@link GeoPointValues}. */ diff --git a/server/src/main/java/org/opensearch/index/fielddata/SingletonSortedNumericUnsignedLongValues.java b/server/src/main/java/org/opensearch/index/fielddata/SingletonSortedNumericUnsignedLongValues.java new file mode 100644 index 0000000000000..05929d08a142c --- /dev/null +++ b/server/src/main/java/org/opensearch/index/fielddata/SingletonSortedNumericUnsignedLongValues.java @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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. + */ + +/* + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.index.fielddata; + +import org.apache.lucene.index.SortedNumericDocValues; + +import java.io.IOException; + +/** + * Clone of {@link SortedNumericDocValues} for double values. + * + * @opensearch.internal + */ +public final class SingletonSortedNumericUnsignedLongValues extends SortedNumericUnsignedLongValues { + private final SortedNumericDocValues values; + + public SingletonSortedNumericUnsignedLongValues(SortedNumericDocValues values) { + this.values = values; + } + + @Override + public boolean advanceExact(int target) throws IOException { + return values.advanceExact(target); + } + + @Override + public long nextValue() throws IOException { + return values.nextValue(); + } + + @Override + public int docValueCount() { + return values.docValueCount(); + } + + public int advance(int target) throws IOException { + return values.advance(target); + } + + public int docID() { + return values.docID(); + } + + /** Return the wrapped values. */ + public SortedNumericDocValues getNumericUnsignedLongValues() { + return values; + } +} diff --git a/server/src/main/java/org/opensearch/index/fielddata/SortedNumericUnsignedLongValues.java b/server/src/main/java/org/opensearch/index/fielddata/SortedNumericUnsignedLongValues.java new file mode 100644 index 0000000000000..b22e732280fa2 --- /dev/null +++ b/server/src/main/java/org/opensearch/index/fielddata/SortedNumericUnsignedLongValues.java @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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. + */ + +/* + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.index.fielddata; + +import org.apache.lucene.index.SortedNumericDocValues; +import org.opensearch.common.annotation.PublicApi; + +import java.io.IOException; + +/** + * Clone of {@link SortedNumericDocValues} for unsigned long values. + * + * @opensearch.api + */ +@PublicApi(since = "1.0.0") +public abstract class SortedNumericUnsignedLongValues { + + /** Sole constructor. (For invocation by subclass + * constructors, typically implicit.) */ + protected SortedNumericUnsignedLongValues() {} + + /** Advance the iterator to exactly {@code target} and return whether + * {@code target} has a value. + * {@code target} must be greater than or equal to the current + * doc ID and must be a valid doc ID, ie. ≥ 0 and + * < {@code maxDoc}.*/ + public abstract boolean advanceExact(int target) throws IOException; + + /** + * Iterates to the next value in the current document. Do not call this more than + * {@link #docValueCount} times for the document. + */ + public abstract long nextValue() throws IOException; + + /** + * Retrieves the number of values for the current document. This must always + * be greater than zero. + * It is illegal to call this method after {@link #advanceExact(int)} + * returned {@code false}. + */ + public abstract int docValueCount(); + + /** + * Advances to the first beyond the current whose document number is greater than or equal to + * target, and returns the document number itself. Exhausts the iterator and returns {@link + * org.apache.lucene.search.DocIdSetIterator#NO_MORE_DOCS} if target is greater than the highest document number in the set. + * + * This method is being used by {@link org.apache.lucene.search.comparators.NumericComparator.NumericLeafComparator} when point values optimization kicks + * in and is implemented by most numeric types. + */ + public int advance(int target) throws IOException { + throw new UnsupportedOperationException(); + } + + public abstract int docID(); +} diff --git a/server/src/main/java/org/opensearch/index/fielddata/fieldcomparator/UnsignedLongMultiValueMode.java b/server/src/main/java/org/opensearch/index/fielddata/fieldcomparator/UnsignedLongMultiValueMode.java deleted file mode 100644 index 4e20460f5c820..0000000000000 --- a/server/src/main/java/org/opensearch/index/fielddata/fieldcomparator/UnsignedLongMultiValueMode.java +++ /dev/null @@ -1,400 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.index.fielddata.fieldcomparator; - -import org.apache.lucene.index.DocValues; -import org.apache.lucene.index.NumericDocValues; -import org.apache.lucene.index.SortedNumericDocValues; -import org.apache.lucene.search.DocIdSetIterator; -import org.apache.lucene.util.BitSet; -import org.opensearch.common.Numbers; -import org.opensearch.index.fielddata.AbstractNumericDocValues; -import org.opensearch.index.fielddata.FieldData; -import org.opensearch.search.MultiValueMode; - -import java.io.IOException; -import java.util.Locale; - -/** - * Defines what values to pick in the case a document contains multiple values for an unsigned long field. - * - * @opensearch.internal - */ -enum UnsignedLongMultiValueMode { - /** - * Pick the sum of all the values. - */ - SUM { - @Override - protected long pick(SortedNumericDocValues values) throws IOException { - final int count = values.docValueCount(); - long total = 0; - for (int index = 0; index < count; ++index) { - total += values.nextValue(); - } - return total; - } - - @Override - protected long pick( - SortedNumericDocValues values, - long missingValue, - DocIdSetIterator docItr, - int startDoc, - int endDoc, - int maxChildren - ) throws IOException { - int totalCount = 0; - long totalValue = 0; - int count = 0; - for (int doc = startDoc; doc < endDoc; doc = docItr.nextDoc()) { - if (values.advanceExact(doc)) { - if (++count > maxChildren) { - break; - } - - final int docCount = values.docValueCount(); - for (int index = 0; index < docCount; ++index) { - totalValue += values.nextValue(); - } - totalCount += docCount; - } - } - return totalCount > 0 ? totalValue : missingValue; - } - }, - - /** - * Pick the average of all the values. - */ - AVG { - @Override - protected long pick(SortedNumericDocValues values) throws IOException { - final int count = values.docValueCount(); - long total = 0; - for (int index = 0; index < count; ++index) { - total += values.nextValue(); - } - return count > 1 ? divideUnsignedAndRoundUp(total, count) : total; - } - - @Override - protected long pick( - SortedNumericDocValues values, - long missingValue, - DocIdSetIterator docItr, - int startDoc, - int endDoc, - int maxChildren - ) throws IOException { - int totalCount = 0; - long totalValue = 0; - int count = 0; - for (int doc = startDoc; doc < endDoc; doc = docItr.nextDoc()) { - if (values.advanceExact(doc)) { - if (++count > maxChildren) { - break; - } - final int docCount = values.docValueCount(); - for (int index = 0; index < docCount; ++index) { - totalValue += values.nextValue(); - } - totalCount += docCount; - } - } - if (totalCount < 1) { - return missingValue; - } - return totalCount > 1 ? divideUnsignedAndRoundUp(totalValue, totalCount) : totalValue; - } - }, - - /** - * Pick the median of the values. - */ - MEDIAN { - @Override - protected long pick(SortedNumericDocValues values) throws IOException { - int count = values.docValueCount(); - long firstValue = values.nextValue(); - if (count == 1) { - return firstValue; - } else if (count == 2) { - long total = firstValue + values.nextValue(); - return (total >>> 1) + (total & 1); - } else if (firstValue >= 0) { - for (int i = 1; i < (count - 1) / 2; ++i) { - values.nextValue(); - } - if (count % 2 == 0) { - long total = values.nextValue() + values.nextValue(); - return (total >>> 1) + (total & 1); - } else { - return values.nextValue(); - } - } - - final long[] docValues = new long[count]; - docValues[0] = firstValue; - int firstPositiveIndex = 0; - for (int i = 1; i < count; ++i) { - docValues[i] = values.nextValue(); - if (docValues[i] >= 0 && firstPositiveIndex == 0) { - firstPositiveIndex = i; - } - } - final int mid = ((count - 1) / 2 + firstPositiveIndex) % count; - if (count % 2 == 0) { - long total = docValues[mid] + docValues[(mid + 1) % count]; - return (total >>> 1) + (total & 1); - } else { - return docValues[mid]; - } - } - }, - - /** - * Pick the lowest value. - */ - MIN { - @Override - protected long pick(SortedNumericDocValues values) throws IOException { - final int count = values.docValueCount(); - final long min = values.nextValue(); - if (count == 1 || min > 0) { - return min; - } - for (int i = 1; i < count; ++i) { - long val = values.nextValue(); - if (val >= 0) { - return val; - } - } - return min; - } - - @Override - protected long pick( - SortedNumericDocValues values, - long missingValue, - DocIdSetIterator docItr, - int startDoc, - int endDoc, - int maxChildren - ) throws IOException { - boolean hasValue = false; - long minValue = Numbers.MAX_UNSIGNED_LONG_VALUE_AS_LONG; - int count = 0; - for (int doc = startDoc; doc < endDoc; doc = docItr.nextDoc()) { - if (values.advanceExact(doc)) { - if (++count > maxChildren) { - break; - } - final long docMin = pick(values); - minValue = Long.compareUnsigned(docMin, minValue) < 0 ? docMin : minValue; - hasValue = true; - } - } - return hasValue ? minValue : missingValue; - } - }, - - /** - * Pick the highest value. - */ - MAX { - @Override - protected long pick(SortedNumericDocValues values) throws IOException { - final int count = values.docValueCount(); - long max = values.nextValue(); - long val; - for (int i = 1; i < count; ++i) { - val = values.nextValue(); - if (max < 0 && val >= 0) { - return max; - } - max = val; - } - return max; - } - - @Override - protected long pick( - SortedNumericDocValues values, - long missingValue, - DocIdSetIterator docItr, - int startDoc, - int endDoc, - int maxChildren - ) throws IOException { - boolean hasValue = false; - long maxValue = Numbers.MIN_UNSIGNED_LONG_VALUE_AS_LONG; - int count = 0; - for (int doc = startDoc; doc < endDoc; doc = docItr.nextDoc()) { - if (values.advanceExact(doc)) { - if (++count > maxChildren) { - break; - } - final long docMax = pick(values); - maxValue = Long.compareUnsigned(maxValue, docMax) < 0 ? docMax : maxValue; - hasValue = true; - } - } - return hasValue ? maxValue : missingValue; - } - }; - - /** - * A case insensitive version of {@link #valueOf(String)} - * - * @throws IllegalArgumentException if the given string doesn't match a sort mode or is null. - */ - private static UnsignedLongMultiValueMode fromString(String sortMode) { - try { - return valueOf(sortMode.toUpperCase(Locale.ROOT)); - } catch (Exception e) { - throw new IllegalArgumentException("Illegal sort mode: " + sortMode); - } - } - - /** - * Convert a {@link MultiValueMode} to a {@link UnsignedLongMultiValueMode}. - */ - public static UnsignedLongMultiValueMode toUnsignedSortMode(MultiValueMode sortMode) { - return fromString(sortMode.name()); - } - - /** - * Return a {@link NumericDocValues} instance that can be used to sort documents - * with this mode and the provided values. When a document has no value, - * missingValue is returned. - *

- * Allowed Modes: SUM, AVG, MEDIAN, MIN, MAX - */ - public NumericDocValues select(final SortedNumericDocValues values) { - final NumericDocValues singleton = DocValues.unwrapSingleton(values); - if (singleton != null) { - return singleton; - } else { - return new AbstractNumericDocValues() { - - private long value; - - @Override - public boolean advanceExact(int target) throws IOException { - if (values.advanceExact(target)) { - value = pick(values); - return true; - } - return false; - } - - @Override - public int docID() { - return values.docID(); - } - - @Override - public long longValue() throws IOException { - return value; - } - }; - } - } - - protected long pick(SortedNumericDocValues values) throws IOException { - throw new IllegalArgumentException("Unsupported sort mode: " + this); - } - - /** - * Return a {@link NumericDocValues} instance that can be used to sort root documents - * with this mode, the provided values and filters for root/inner documents. - *

- * For every root document, the values of its inner documents will be aggregated. - * If none of the inner documents has a value, then missingValue is returned. - *

- * Allowed Modes: SUM, AVG, MIN, MAX - *

- * NOTE: Calling the returned instance on docs that are not root docs is illegal - * The returned instance can only be evaluate the current and upcoming docs - */ - public NumericDocValues select( - final SortedNumericDocValues values, - final long missingValue, - final BitSet parentDocs, - final DocIdSetIterator childDocs, - int maxDoc, - int maxChildren - ) throws IOException { - if (parentDocs == null || childDocs == null) { - return FieldData.replaceMissing(DocValues.emptyNumeric(), missingValue); - } - - return new AbstractNumericDocValues() { - - int lastSeenParentDoc = -1; - long lastEmittedValue = missingValue; - - @Override - public boolean advanceExact(int parentDoc) throws IOException { - assert parentDoc >= lastSeenParentDoc : "can only evaluate current and upcoming parent docs"; - if (parentDoc == lastSeenParentDoc) { - return true; - } else if (parentDoc == 0) { - lastEmittedValue = missingValue; - return true; - } - final int prevParentDoc = parentDocs.prevSetBit(parentDoc - 1); - final int firstChildDoc; - if (childDocs.docID() > prevParentDoc) { - firstChildDoc = childDocs.docID(); - } else { - firstChildDoc = childDocs.advance(prevParentDoc + 1); - } - - lastSeenParentDoc = parentDoc; - lastEmittedValue = pick(values, missingValue, childDocs, firstChildDoc, parentDoc, maxChildren); - return true; - } - - @Override - public int docID() { - return lastSeenParentDoc; - } - - @Override - public long longValue() { - return lastEmittedValue; - } - }; - } - - protected long pick( - SortedNumericDocValues values, - long missingValue, - DocIdSetIterator docItr, - int startDoc, - int endDoc, - int maxChildren - ) throws IOException { - throw new IllegalArgumentException("Unsupported sort mode: " + this); - } - - /** - * Copied from {@link Long#divideUnsigned(long, long)} and {@link Long#remainderUnsigned(long, long)} - */ - private static long divideUnsignedAndRoundUp(long dividend, long divisor) { - assert divisor > 0; - final long q = (dividend >>> 1) / divisor << 1; - final long r = dividend - q * divisor; - final long quotient = q + ((r | ~(r - divisor)) >>> (Long.SIZE - 1)); - final long rem = r - ((~(r - divisor) >> (Long.SIZE - 1)) & divisor); - return quotient + Math.round((double) rem / divisor); - } -} diff --git a/server/src/main/java/org/opensearch/index/fielddata/fieldcomparator/UnsignedLongValuesComparatorSource.java b/server/src/main/java/org/opensearch/index/fielddata/fieldcomparator/UnsignedLongValuesComparatorSource.java index c7b9c9f445484..c9076e773ba29 100644 --- a/server/src/main/java/org/opensearch/index/fielddata/fieldcomparator/UnsignedLongValuesComparatorSource.java +++ b/server/src/main/java/org/opensearch/index/fielddata/fieldcomparator/UnsignedLongValuesComparatorSource.java @@ -24,6 +24,8 @@ import org.opensearch.index.fielddata.IndexFieldData; import org.opensearch.index.fielddata.IndexNumericFieldData; import org.opensearch.index.fielddata.LeafNumericFieldData; +import org.opensearch.index.fielddata.SingletonSortedNumericUnsignedLongValues; +import org.opensearch.index.fielddata.SortedNumericUnsignedLongValues; import org.opensearch.index.search.comparators.UnsignedLongComparator; import org.opensearch.search.DocValueFormat; import org.opensearch.search.MultiValueMode; @@ -41,7 +43,6 @@ public class UnsignedLongValuesComparatorSource extends IndexFieldData.XFieldComparatorSource { private final IndexNumericFieldData indexFieldData; - private final UnsignedLongMultiValueMode unsignedSortMode; public UnsignedLongValuesComparatorSource( IndexNumericFieldData indexFieldData, @@ -51,7 +52,6 @@ public UnsignedLongValuesComparatorSource( ) { super(missingValue, sortMode, nested); this.indexFieldData = indexFieldData; - this.unsignedSortMode = UnsignedLongMultiValueMode.toUnsignedSortMode(sortMode); } @Override @@ -59,21 +59,21 @@ public SortField.Type reducedType() { return SortField.Type.LONG; } - private SortedNumericDocValues loadDocValues(LeafReaderContext context) { + private SortedNumericUnsignedLongValues loadDocValues(LeafReaderContext context) { final LeafNumericFieldData data = indexFieldData.load(context); SortedNumericDocValues values = data.getLongValues(); - return values; + return new SingletonSortedNumericUnsignedLongValues(values); } private NumericDocValues getNumericDocValues(LeafReaderContext context, BigInteger missingValue) throws IOException { - final SortedNumericDocValues values = loadDocValues(context); + final SortedNumericUnsignedLongValues values = loadDocValues(context); if (nested == null) { - return FieldData.replaceMissing(unsignedSortMode.select(values), missingValue); + return FieldData.replaceMissing(sortMode.select(values), missingValue); } final BitSet rootDocs = nested.rootDocs(context); final DocIdSetIterator innerDocs = nested.innerDocs(context); final int maxChildren = nested.getNestedSort() != null ? nested.getNestedSort().getMaxChildren() : Integer.MAX_VALUE; - return unsignedSortMode.select(values, missingValue.longValue(), rootDocs, innerDocs, context.reader().maxDoc(), maxChildren); + return sortMode.select(values, missingValue.longValue(), rootDocs, innerDocs, context.reader().maxDoc(), maxChildren); } @Override diff --git a/server/src/main/java/org/opensearch/search/MultiValueMode.java b/server/src/main/java/org/opensearch/search/MultiValueMode.java index a99da674836f2..b9df357016788 100644 --- a/server/src/main/java/org/opensearch/search/MultiValueMode.java +++ b/server/src/main/java/org/opensearch/search/MultiValueMode.java @@ -42,6 +42,7 @@ import org.apache.lucene.util.BitSet; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.BytesRefBuilder; +import org.opensearch.common.Numbers; import org.opensearch.common.annotation.PublicApi; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; @@ -53,6 +54,7 @@ import org.opensearch.index.fielddata.NumericDoubleValues; import org.opensearch.index.fielddata.SortedBinaryDocValues; import org.opensearch.index.fielddata.SortedNumericDoubleValues; +import org.opensearch.index.fielddata.SortedNumericUnsignedLongValues; import java.io.IOException; import java.util.Locale; @@ -143,6 +145,44 @@ protected double pick( return totalCount > 0 ? totalValue : missingValue; } + + @Override + protected long pick(SortedNumericUnsignedLongValues values) throws IOException { + final int count = values.docValueCount(); + long total = 0; + for (int index = 0; index < count; ++index) { + total += values.nextValue(); + } + return total; + } + + @Override + protected long pick( + SortedNumericUnsignedLongValues values, + long missingValue, + DocIdSetIterator docItr, + int startDoc, + int endDoc, + int maxChildren + ) throws IOException { + int totalCount = 0; + long totalValue = 0; + int count = 0; + for (int doc = startDoc; doc < endDoc; doc = docItr.nextDoc()) { + if (values.advanceExact(doc)) { + if (++count > maxChildren) { + break; + } + + final int docCount = values.docValueCount(); + for (int index = 0; index < docCount; ++index) { + totalValue += values.nextValue(); + } + totalCount += docCount; + } + } + return totalCount > 0 ? totalValue : missingValue; + } }, /** @@ -228,6 +268,46 @@ protected double pick( } return totalValue / totalCount; } + + @Override + protected long pick(SortedNumericUnsignedLongValues values) throws IOException { + final int count = values.docValueCount(); + long total = 0; + for (int index = 0; index < count; ++index) { + total += values.nextValue(); + } + return count > 1 ? divideUnsignedAndRoundUp(total, count) : total; + } + + @Override + protected long pick( + SortedNumericUnsignedLongValues values, + long missingValue, + DocIdSetIterator docItr, + int startDoc, + int endDoc, + int maxChildren + ) throws IOException { + int totalCount = 0; + long totalValue = 0; + int count = 0; + for (int doc = startDoc; doc < endDoc; doc = docItr.nextDoc()) { + if (values.advanceExact(doc)) { + if (++count > maxChildren) { + break; + } + final int docCount = values.docValueCount(); + for (int index = 0; index < docCount; ++index) { + totalValue += values.nextValue(); + } + totalCount += docCount; + } + } + if (totalCount < 1) { + return missingValue; + } + return totalCount > 1 ? divideUnsignedAndRoundUp(totalValue, totalCount) : totalValue; + } }, /** @@ -259,6 +339,45 @@ protected double pick(SortedNumericDoubleValues values) throws IOException { return values.nextValue(); } } + + @Override + protected long pick(SortedNumericUnsignedLongValues values) throws IOException { + int count = values.docValueCount(); + long firstValue = values.nextValue(); + if (count == 1) { + return firstValue; + } else if (count == 2) { + long total = firstValue + values.nextValue(); + return (total >>> 1) + (total & 1); + } else if (firstValue >= 0) { + for (int i = 1; i < (count - 1) / 2; ++i) { + values.nextValue(); + } + if (count % 2 == 0) { + long total = values.nextValue() + values.nextValue(); + return (total >>> 1) + (total & 1); + } else { + return values.nextValue(); + } + } + + final long[] docValues = new long[count]; + docValues[0] = firstValue; + int firstPositiveIndex = 0; + for (int i = 1; i < count; ++i) { + docValues[i] = values.nextValue(); + if (docValues[i] >= 0 && firstPositiveIndex == 0) { + firstPositiveIndex = i; + } + } + final int mid = ((count - 1) / 2 + firstPositiveIndex) % count; + if (count % 2 == 0) { + long total = docValues[mid] + docValues[(mid + 1) % count]; + return (total >>> 1) + (total & 1); + } else { + return docValues[mid]; + } + } }, /** @@ -382,6 +501,47 @@ protected int pick(SortedDocValues values, DocIdSetIterator docItr, int startDoc return hasValue ? ord : -1; } + + @Override + protected long pick(SortedNumericUnsignedLongValues values) throws IOException { + final int count = values.docValueCount(); + final long min = values.nextValue(); + if (count == 1 || min > 0) { + return min; + } + for (int i = 1; i < count; ++i) { + long val = values.nextValue(); + if (val >= 0) { + return val; + } + } + return min; + } + + @Override + protected long pick( + SortedNumericUnsignedLongValues values, + long missingValue, + DocIdSetIterator docItr, + int startDoc, + int endDoc, + int maxChildren + ) throws IOException { + boolean hasValue = false; + long minValue = Numbers.MAX_UNSIGNED_LONG_VALUE_AS_LONG; + int count = 0; + for (int doc = startDoc; doc < endDoc; doc = docItr.nextDoc()) { + if (values.advanceExact(doc)) { + if (++count > maxChildren) { + break; + } + final long docMin = pick(values); + minValue = Long.compareUnsigned(docMin, minValue) < 0 ? docMin : minValue; + hasValue = true; + } + } + return hasValue ? minValue : missingValue; + } }, /** @@ -525,6 +685,46 @@ protected int pick(SortedDocValues values, DocIdSetIterator docItr, int startDoc } return ord; } + + @Override + protected long pick(SortedNumericUnsignedLongValues values) throws IOException { + final int count = values.docValueCount(); + long max = values.nextValue(); + long val; + for (int i = 1; i < count; ++i) { + val = values.nextValue(); + if (max < 0 && val >= 0) { + return max; + } + max = val; + } + return max; + } + + @Override + protected long pick( + SortedNumericUnsignedLongValues values, + long missingValue, + DocIdSetIterator docItr, + int startDoc, + int endDoc, + int maxChildren + ) throws IOException { + boolean hasValue = false; + long maxValue = Numbers.MIN_UNSIGNED_LONG_VALUE_AS_LONG; + int count = 0; + for (int doc = startDoc; doc < endDoc; doc = docItr.nextDoc()) { + if (values.advanceExact(doc)) { + if (++count > maxChildren) { + break; + } + final long docMax = pick(values); + maxValue = Long.compareUnsigned(maxValue, docMax) < 0 ? docMax : maxValue; + hasValue = true; + } + } + return hasValue ? maxValue : missingValue; + } }; /** @@ -1032,6 +1232,122 @@ protected int pick(SortedDocValues values, DocIdSetIterator docItr, int startDoc throw new IllegalArgumentException("Unsupported sort mode: " + this); } + /** + * Return a {@link NumericDoubleValues} instance that can be used to sort documents + * with this mode and the provided values. When a document has no value, + * missingValue is returned. + *

+ * Allowed Modes: SUM, AVG, MEDIAN, MIN, MAX + */ + public NumericDocValues select(final SortedNumericUnsignedLongValues values) { + final SortedNumericDocValues sortedNumericDocValues = FieldData.unwrapSingleton(values); + final NumericDocValues singleton = DocValues.unwrapSingleton(sortedNumericDocValues); + if (singleton != null) { + return singleton; + } else { + return new AbstractNumericDocValues() { + + private long value; + + @Override + public boolean advanceExact(int target) throws IOException { + if (values.advanceExact(target)) { + value = pick(values); + return true; + } + return false; + } + + @Override + public int docID() { + return values.docID(); + } + + @Override + public long longValue() throws IOException { + return value; + } + }; + } + } + + protected long pick(SortedNumericUnsignedLongValues values) throws IOException { + throw new IllegalArgumentException("Unsupported sort mode: " + this); + } + + /** + * Return a {@link SortedDocValues} instance that can be used to sort root documents + * with this mode, the provided values and filters for root/inner documents. + *

+ * For every root document, the values of its inner documents will be aggregated. + *

+ * Allowed Modes: MIN, MAX + *

+ * NOTE: Calling the returned instance on docs that are not root docs is illegal + * The returned instance can only be evaluate the current and upcoming docs + */ + public NumericDocValues select( + final SortedNumericUnsignedLongValues values, + final long missingValue, + final BitSet parentDocs, + final DocIdSetIterator childDocs, + int maxDoc, + int maxChildren + ) throws IOException { + if (parentDocs == null || childDocs == null) { + return FieldData.replaceMissing(DocValues.emptyNumeric(), missingValue); + } + + return new AbstractNumericDocValues() { + + int lastSeenParentDoc = -1; + long lastEmittedValue = missingValue; + + @Override + public boolean advanceExact(int parentDoc) throws IOException { + assert parentDoc >= lastSeenParentDoc : "can only evaluate current and upcoming parent docs"; + if (parentDoc == lastSeenParentDoc) { + return true; + } else if (parentDoc == 0) { + lastEmittedValue = missingValue; + return true; + } + final int prevParentDoc = parentDocs.prevSetBit(parentDoc - 1); + final int firstChildDoc; + if (childDocs.docID() > prevParentDoc) { + firstChildDoc = childDocs.docID(); + } else { + firstChildDoc = childDocs.advance(prevParentDoc + 1); + } + + lastSeenParentDoc = parentDoc; + lastEmittedValue = pick(values, missingValue, childDocs, firstChildDoc, parentDoc, maxChildren); + return true; + } + + @Override + public int docID() { + return lastSeenParentDoc; + } + + @Override + public long longValue() { + return lastEmittedValue; + } + }; + } + + protected long pick( + SortedNumericUnsignedLongValues values, + long missingValue, + DocIdSetIterator docItr, + int startDoc, + int endDoc, + int maxChildren + ) throws IOException { + throw new IllegalArgumentException("Unsupported sort mode: " + this); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeEnum(this); @@ -1040,4 +1356,16 @@ public void writeTo(StreamOutput out) throws IOException { public static MultiValueMode readMultiValueModeFrom(StreamInput in) throws IOException { return in.readEnum(MultiValueMode.class); } + + /** + * Copied from {@link Long#divideUnsigned(long, long)} and {@link Long#remainderUnsigned(long, long)} + */ + private static long divideUnsignedAndRoundUp(long dividend, long divisor) { + assert divisor > 0; + final long q = (dividend >>> 1) / divisor << 1; + final long r = dividend - q * divisor; + final long quotient = q + ((r | ~(r - divisor)) >>> (Long.SIZE - 1)); + final long rem = r - ((~(r - divisor) >> (Long.SIZE - 1)) & divisor); + return quotient + Math.round((double) rem / divisor); + } } diff --git a/server/src/test/java/org/opensearch/index/fielddata/fieldcomparator/UnsignedLongMultiValueModeTests.java b/server/src/test/java/org/opensearch/index/fielddata/fieldcomparator/UnsignedLongMultiValueModeTests.java deleted file mode 100644 index afa562862cfe9..0000000000000 --- a/server/src/test/java/org/opensearch/index/fielddata/fieldcomparator/UnsignedLongMultiValueModeTests.java +++ /dev/null @@ -1,271 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.index.fielddata.fieldcomparator; - -import org.apache.lucene.index.DocValues; -import org.apache.lucene.index.NumericDocValues; -import org.apache.lucene.index.SortedNumericDocValues; -import org.apache.lucene.util.BitSetIterator; -import org.apache.lucene.util.FixedBitSet; -import org.opensearch.common.Numbers; -import org.opensearch.index.fielddata.AbstractNumericDocValues; -import org.opensearch.index.fielddata.AbstractSortedNumericDocValues; -import org.opensearch.test.OpenSearchTestCase; - -import java.io.IOException; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.math.RoundingMode; -import java.util.Arrays; -import java.util.function.Supplier; - -public class UnsignedLongMultiValueModeTests extends OpenSearchTestCase { - private static FixedBitSet randomRootDocs(int maxDoc) { - FixedBitSet set = new FixedBitSet(maxDoc); - for (int i = 0; i < maxDoc; ++i) { - if (randomBoolean()) { - set.set(i); - } - } - // the last doc must be a root doc - set.set(maxDoc - 1); - return set; - } - - private static FixedBitSet randomInnerDocs(FixedBitSet rootDocs) { - FixedBitSet innerDocs = new FixedBitSet(rootDocs.length()); - for (int i = 0; i < innerDocs.length(); ++i) { - if (!rootDocs.get(i) && randomBoolean()) { - innerDocs.set(i); - } - } - return innerDocs; - } - - public void testSingleValuedLongs() throws Exception { - final int numDocs = scaledRandomIntBetween(1, 100); - final long[] array = new long[numDocs]; - final FixedBitSet docsWithValue = randomBoolean() ? null : new FixedBitSet(numDocs); - for (int i = 0; i < array.length; ++i) { - if (randomBoolean()) { - array[i] = randomUnsignedLong().longValue(); - if (docsWithValue != null) { - docsWithValue.set(i); - } - } else if (docsWithValue != null && randomBoolean()) { - docsWithValue.set(i); - } - } - - final Supplier multiValues = () -> DocValues.singleton(new AbstractNumericDocValues() { - int docId = -1; - - @Override - public boolean advanceExact(int target) throws IOException { - this.docId = target; - return docsWithValue == null || docsWithValue.get(docId); - } - - @Override - public int docID() { - return docId; - } - - @Override - public long longValue() { - return array[docId]; - } - }); - verifySortedNumeric(multiValues, numDocs); - final FixedBitSet rootDocs = randomRootDocs(numDocs); - final FixedBitSet innerDocs = randomInnerDocs(rootDocs); - verifySortedNumeric(multiValues, numDocs, rootDocs, innerDocs, Integer.MAX_VALUE); - verifySortedNumeric(multiValues, numDocs, rootDocs, innerDocs, randomIntBetween(1, numDocs)); - } - - public void testMultiValuedLongs() throws Exception { - final int numDocs = scaledRandomIntBetween(1, 100); - final long[][] array = new long[numDocs][]; - for (int i = 0; i < numDocs; ++i) { - final long[] values = new long[randomInt(4)]; - for (int j = 0; j < values.length; ++j) { - values[j] = randomUnsignedLong().longValue(); - } - Arrays.sort(values); - array[i] = values; - } - final Supplier multiValues = () -> new AbstractSortedNumericDocValues() { - int doc; - int i; - - @Override - public long nextValue() { - return array[doc][i++]; - } - - @Override - public boolean advanceExact(int doc) { - this.doc = doc; - i = 0; - return array[doc].length > 0; - } - - @Override - public int docValueCount() { - return array[doc].length; - } - }; - verifySortedNumeric(multiValues, numDocs); - final FixedBitSet rootDocs = randomRootDocs(numDocs); - final FixedBitSet innerDocs = randomInnerDocs(rootDocs); - verifySortedNumeric(multiValues, numDocs, rootDocs, innerDocs, Integer.MAX_VALUE); - verifySortedNumeric(multiValues, numDocs, rootDocs, innerDocs, randomIntBetween(1, numDocs)); - } - - private void verifySortedNumeric(Supplier supplier, int maxDoc) throws IOException { - for (UnsignedLongMultiValueMode mode : UnsignedLongMultiValueMode.values()) { - SortedNumericDocValues values = supplier.get(); - final NumericDocValues selected = mode.select(values); - for (int i = 0; i < maxDoc; ++i) { - Long actual = null; - if (selected.advanceExact(i)) { - actual = selected.longValue(); - verifyLongValueCanCalledMoreThanOnce(selected, actual); - } - - BigInteger expected = null; - if (values.advanceExact(i)) { - int numValues = values.docValueCount(); - if (mode == UnsignedLongMultiValueMode.MAX) { - expected = Numbers.MIN_UNSIGNED_LONG_VALUE; - } else if (mode == UnsignedLongMultiValueMode.MIN) { - expected = Numbers.MAX_UNSIGNED_LONG_VALUE; - } else { - expected = BigInteger.ZERO; - } - for (int j = 0; j < numValues; ++j) { - if (mode == UnsignedLongMultiValueMode.SUM || mode == UnsignedLongMultiValueMode.AVG) { - expected = expected.add(Numbers.toUnsignedBigInteger(values.nextValue())); - } else if (mode == UnsignedLongMultiValueMode.MIN) { - expected = expected.min(Numbers.toUnsignedBigInteger(values.nextValue())); - } else if (mode == UnsignedLongMultiValueMode.MAX) { - expected = expected.max(Numbers.toUnsignedBigInteger(values.nextValue())); - } - } - if (mode == UnsignedLongMultiValueMode.AVG) { - expected = Numbers.toUnsignedBigInteger(expected.longValue()); - expected = numValues > 1 - ? new BigDecimal(expected).divide(new BigDecimal(numValues), RoundingMode.HALF_UP).toBigInteger() - : expected; - } else if (mode == UnsignedLongMultiValueMode.MEDIAN) { - final Long[] docValues = new Long[numValues]; - for (int j = 0; j < numValues; ++j) { - docValues[j] = values.nextValue(); - } - Arrays.sort(docValues, Long::compareUnsigned); - int value = numValues / 2; - if (numValues % 2 == 0) { - expected = Numbers.toUnsignedBigInteger(docValues[value - 1]) - .add(Numbers.toUnsignedBigInteger(docValues[value])); - expected = Numbers.toUnsignedBigInteger(expected.longValue()); - expected = new BigDecimal(expected).divide(new BigDecimal(2), RoundingMode.HALF_UP).toBigInteger(); - } else { - expected = Numbers.toUnsignedBigInteger(docValues[value]); - } - } - } - - final Long expectedLong = expected == null ? null : expected.longValue(); - assertEquals(mode.toString() + " docId=" + i, expectedLong, actual); - } - } - } - - private void verifyLongValueCanCalledMoreThanOnce(NumericDocValues values, long expected) throws IOException { - for (int j = 0, numCall = randomIntBetween(1, 10); j < numCall; j++) { - assertEquals(expected, values.longValue()); - } - } - - private void verifySortedNumeric( - Supplier supplier, - int maxDoc, - FixedBitSet rootDocs, - FixedBitSet innerDocs, - int maxChildren - ) throws IOException { - for (long missingValue : new long[] { 0, randomUnsignedLong().longValue() }) { - for (UnsignedLongMultiValueMode mode : new UnsignedLongMultiValueMode[] { - UnsignedLongMultiValueMode.MIN, - UnsignedLongMultiValueMode.MAX, - UnsignedLongMultiValueMode.SUM, - UnsignedLongMultiValueMode.AVG }) { - SortedNumericDocValues values = supplier.get(); - final NumericDocValues selected = mode.select( - values, - missingValue, - rootDocs, - new BitSetIterator(innerDocs, 0L), - maxDoc, - maxChildren - ); - int prevRoot = -1; - for (int root = rootDocs.nextSetBit(0); root != -1; root = root + 1 < maxDoc ? rootDocs.nextSetBit(root + 1) : -1) { - assertTrue(selected.advanceExact(root)); - final long actual = selected.longValue(); - verifyLongValueCanCalledMoreThanOnce(selected, actual); - - BigInteger expected = BigInteger.ZERO; - if (mode == UnsignedLongMultiValueMode.MAX) { - expected = Numbers.MIN_UNSIGNED_LONG_VALUE; - } else if (mode == UnsignedLongMultiValueMode.MIN) { - expected = Numbers.MAX_UNSIGNED_LONG_VALUE; - } - int numValues = 0; - int count = 0; - for (int child = innerDocs.nextSetBit(prevRoot + 1); child != -1 && child < root; child = innerDocs.nextSetBit( - child + 1 - )) { - if (values.advanceExact(child)) { - if (++count > maxChildren) { - break; - } - for (int j = 0; j < values.docValueCount(); ++j) { - if (mode == UnsignedLongMultiValueMode.SUM || mode == UnsignedLongMultiValueMode.AVG) { - expected = expected.add(Numbers.toUnsignedBigInteger(values.nextValue())); - } else if (mode == UnsignedLongMultiValueMode.MIN) { - expected = expected.min(Numbers.toUnsignedBigInteger(values.nextValue())); - } else if (mode == UnsignedLongMultiValueMode.MAX) { - expected = expected.max(Numbers.toUnsignedBigInteger(values.nextValue())); - } - ++numValues; - } - } - } - final long expectedLong; - if (numValues == 0) { - expectedLong = missingValue; - } else if (mode == UnsignedLongMultiValueMode.AVG) { - expected = Numbers.toUnsignedBigInteger(expected.longValue()); - expected = numValues > 1 - ? new BigDecimal(expected).divide(new BigDecimal(numValues), RoundingMode.HALF_UP).toBigInteger() - : expected; - expectedLong = expected.longValue(); - } else { - expectedLong = expected.longValue(); - } - - assertEquals(mode.toString() + " docId=" + root, expectedLong, actual); - - prevRoot = root; - } - } - } - } -} diff --git a/server/src/test/java/org/opensearch/search/MultiValueModeTests.java b/server/src/test/java/org/opensearch/search/MultiValueModeTests.java index 948d2cffceabe..e011dd0bcf6c0 100644 --- a/server/src/test/java/org/opensearch/search/MultiValueModeTests.java +++ b/server/src/test/java/org/opensearch/search/MultiValueModeTests.java @@ -41,6 +41,7 @@ import org.apache.lucene.util.BitSetIterator; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.FixedBitSet; +import org.opensearch.common.Numbers; import org.opensearch.common.io.stream.BytesStreamOutput; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.index.fielddata.AbstractBinaryDocValues; @@ -52,9 +53,13 @@ import org.opensearch.index.fielddata.NumericDoubleValues; import org.opensearch.index.fielddata.SortedBinaryDocValues; import org.opensearch.index.fielddata.SortedNumericDoubleValues; +import org.opensearch.index.fielddata.SortedNumericUnsignedLongValues; import org.opensearch.test.OpenSearchTestCase; import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; import java.util.Arrays; import static org.hamcrest.Matchers.equalTo; @@ -776,6 +781,96 @@ public int docValueCount() { verifySortedSet(multiValues, numDocs, rootDocs, innerDocs, randomIntBetween(1, numDocs)); } + public void testSingleValuedUnsignedLongs() throws Exception { + final int numDocs = scaledRandomIntBetween(1, 100); + final long[] array = new long[numDocs]; + final FixedBitSet docsWithValue = randomBoolean() ? null : new FixedBitSet(numDocs); + for (int i = 0; i < array.length; ++i) { + if (randomBoolean()) { + array[i] = randomUnsignedLong().longValue(); + if (docsWithValue != null) { + docsWithValue.set(i); + } + } else if (docsWithValue != null && randomBoolean()) { + docsWithValue.set(i); + } + } + + final Supplier multiValues = () -> new SortedNumericUnsignedLongValues() { + int docId = -1; + + @Override + public boolean advanceExact(int target) throws IOException { + this.docId = target; + return docsWithValue == null || docsWithValue.get(docId); + } + + @Override + public int docID() { + return docId; + } + + @Override + public long nextValue() { + return array[docId]; + } + + @Override + public int docValueCount() { + return 1; + } + }; + verifySortedUnsignedLong(multiValues, numDocs); + final FixedBitSet rootDocs = randomRootDocs(numDocs); + final FixedBitSet innerDocs = randomInnerDocs(rootDocs); + verifySortedUnsignedLong(multiValues, numDocs, rootDocs, innerDocs, Integer.MAX_VALUE); + verifySortedUnsignedLong(multiValues, numDocs, rootDocs, innerDocs, randomIntBetween(1, numDocs)); + } + + public void testMultiValuedUnsignedLongs() throws Exception { + final int numDocs = scaledRandomIntBetween(1, 100); + final long[][] array = new long[numDocs][]; + for (int i = 0; i < numDocs; ++i) { + final long[] values = new long[randomInt(4)]; + for (int j = 0; j < values.length; ++j) { + values[j] = randomUnsignedLong().longValue(); + } + Arrays.sort(values); + array[i] = values; + } + final Supplier multiValues = () -> new SortedNumericUnsignedLongValues() { + int doc; + int i; + + @Override + public long nextValue() { + return array[doc][i++]; + } + + @Override + public boolean advanceExact(int doc) { + this.doc = doc; + i = 0; + return array[doc].length > 0; + } + + @Override + public int docValueCount() { + return array[doc].length; + } + + @Override + public int docID() { + return doc; + } + }; + verifySortedUnsignedLong(multiValues, numDocs); + final FixedBitSet rootDocs = randomRootDocs(numDocs); + final FixedBitSet innerDocs = randomInnerDocs(rootDocs); + verifySortedUnsignedLong(multiValues, numDocs, rootDocs, innerDocs, Integer.MAX_VALUE); + verifySortedUnsignedLong(multiValues, numDocs, rootDocs, innerDocs, randomIntBetween(1, numDocs)); + } + private void verifySortedSet(Supplier supplier, int maxDoc) throws IOException { for (MultiValueMode mode : new MultiValueMode[] { MultiValueMode.MIN, MultiValueMode.MAX }) { SortedSetDocValues values = supplier.get(); @@ -857,6 +952,141 @@ private void verifySortedSet( } } + private void verifySortedUnsignedLong(Supplier supplier, int maxDoc) throws IOException { + for (MultiValueMode mode : MultiValueMode.values()) { + SortedNumericUnsignedLongValues values = supplier.get(); + final NumericDocValues selected = mode.select(values); + for (int i = 0; i < maxDoc; ++i) { + Long actual = null; + if (selected.advanceExact(i)) { + actual = selected.longValue(); + verifyLongValueCanCalledMoreThanOnce(selected, actual); + } + + BigInteger expected = null; + if (values.advanceExact(i)) { + int numValues = values.docValueCount(); + if (mode == MultiValueMode.MAX) { + expected = Numbers.MIN_UNSIGNED_LONG_VALUE; + } else if (mode == MultiValueMode.MIN) { + expected = Numbers.MAX_UNSIGNED_LONG_VALUE; + } else { + expected = BigInteger.ZERO; + } + for (int j = 0; j < numValues; ++j) { + if (mode == MultiValueMode.SUM || mode == MultiValueMode.AVG) { + expected = expected.add(Numbers.toUnsignedBigInteger(values.nextValue())); + } else if (mode == MultiValueMode.MIN) { + expected = expected.min(Numbers.toUnsignedBigInteger(values.nextValue())); + } else if (mode == MultiValueMode.MAX) { + expected = expected.max(Numbers.toUnsignedBigInteger(values.nextValue())); + } + } + if (mode == MultiValueMode.AVG) { + expected = Numbers.toUnsignedBigInteger(expected.longValue()); + expected = numValues > 1 + ? new BigDecimal(expected).divide(new BigDecimal(numValues), RoundingMode.HALF_UP).toBigInteger() + : expected; + } else if (mode == MultiValueMode.MEDIAN) { + final Long[] docValues = new Long[numValues]; + for (int j = 0; j < numValues; ++j) { + docValues[j] = values.nextValue(); + } + Arrays.sort(docValues, Long::compareUnsigned); + int value = numValues / 2; + if (numValues % 2 == 0) { + expected = Numbers.toUnsignedBigInteger(docValues[value - 1]) + .add(Numbers.toUnsignedBigInteger(docValues[value])); + expected = Numbers.toUnsignedBigInteger(expected.longValue()); + expected = new BigDecimal(expected).divide(new BigDecimal(2), RoundingMode.HALF_UP).toBigInteger(); + } else { + expected = Numbers.toUnsignedBigInteger(docValues[value]); + } + } + } + + final Long expectedLong = expected == null ? null : expected.longValue(); + assertEquals(mode.toString() + " docId=" + i, expectedLong, actual); + } + } + } + + private void verifySortedUnsignedLong( + Supplier supplier, + int maxDoc, + FixedBitSet rootDocs, + FixedBitSet innerDocs, + int maxChildren + ) throws IOException { + for (long missingValue : new long[] { 0, randomUnsignedLong().longValue() }) { + for (MultiValueMode mode : new MultiValueMode[] { + MultiValueMode.MIN, + MultiValueMode.MAX, + MultiValueMode.SUM, + MultiValueMode.AVG }) { + SortedNumericUnsignedLongValues values = supplier.get(); + final NumericDocValues selected = mode.select( + values, + missingValue, + rootDocs, + new BitSetIterator(innerDocs, 0L), + maxDoc, + maxChildren + ); + int prevRoot = -1; + for (int root = rootDocs.nextSetBit(0); root != -1; root = root + 1 < maxDoc ? rootDocs.nextSetBit(root + 1) : -1) { + assertTrue(selected.advanceExact(root)); + final long actual = selected.longValue(); + verifyLongValueCanCalledMoreThanOnce(selected, actual); + + BigInteger expected = BigInteger.ZERO; + if (mode == MultiValueMode.MAX) { + expected = Numbers.MIN_UNSIGNED_LONG_VALUE; + } else if (mode == MultiValueMode.MIN) { + expected = Numbers.MAX_UNSIGNED_LONG_VALUE; + } + int numValues = 0; + int count = 0; + for (int child = innerDocs.nextSetBit(prevRoot + 1); child != -1 && child < root; child = innerDocs.nextSetBit( + child + 1 + )) { + if (values.advanceExact(child)) { + if (++count > maxChildren) { + break; + } + for (int j = 0; j < values.docValueCount(); ++j) { + if (mode == MultiValueMode.SUM || mode == MultiValueMode.AVG) { + expected = expected.add(Numbers.toUnsignedBigInteger(values.nextValue())); + } else if (mode == MultiValueMode.MIN) { + expected = expected.min(Numbers.toUnsignedBigInteger(values.nextValue())); + } else if (mode == MultiValueMode.MAX) { + expected = expected.max(Numbers.toUnsignedBigInteger(values.nextValue())); + } + ++numValues; + } + } + } + final long expectedLong; + if (numValues == 0) { + expectedLong = missingValue; + } else if (mode == MultiValueMode.AVG) { + expected = Numbers.toUnsignedBigInteger(expected.longValue()); + expected = numValues > 1 + ? new BigDecimal(expected).divide(new BigDecimal(numValues), RoundingMode.HALF_UP).toBigInteger() + : expected; + expectedLong = expected.longValue(); + } else { + expectedLong = expected.longValue(); + } + + assertEquals(mode.toString() + " docId=" + root, expectedLong, actual); + + prevRoot = root; + } + } + } + } + public void testValidOrdinals() { assertThat(MultiValueMode.SUM.ordinal(), equalTo(0)); assertThat(MultiValueMode.AVG.ordinal(), equalTo(1));