org.elasticsearch.search.MultiValueModeTests.java Source code

Java tutorial

Introduction

Here is the source code for org.elasticsearch.search.MultiValueModeTests.java

Source

/*
 * 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.
 */

package org.elasticsearch.search;

import com.carrotsearch.randomizedtesting.generators.RandomStrings;

import org.apache.lucene.index.*;
import org.apache.lucene.util.BitSetIterator;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.FixedBitSet;
import org.elasticsearch.index.fielddata.FieldData;
import org.elasticsearch.index.fielddata.NumericDoubleValues;
import org.elasticsearch.index.fielddata.SortedBinaryDocValues;
import org.elasticsearch.index.fielddata.SortedNumericDoubleValues;
import org.elasticsearch.test.ESTestCase;

import java.io.IOException;
import java.util.Arrays;

public class MultiValueModeTests extends ESTestCase {

    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] = randomLong();
                if (docsWithValue != null) {
                    docsWithValue.set(i);
                }
            } else if (docsWithValue != null && randomBoolean()) {
                docsWithValue.set(i);
            }
        }
        final NumericDocValues singleValues = new NumericDocValues() {
            @Override
            public long get(int docID) {
                return array[docID];
            }
        };
        final SortedNumericDocValues multiValues = DocValues.singleton(singleValues, docsWithValue);
        verify(multiValues, numDocs);
        final FixedBitSet rootDocs = randomRootDocs(numDocs);
        final FixedBitSet innerDocs = randomInnerDocs(rootDocs);
        verify(multiValues, numDocs, rootDocs, innerDocs);
    }

    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] = randomLong();
            }
            Arrays.sort(values);
            array[i] = values;
        }
        final SortedNumericDocValues multiValues = new SortedNumericDocValues() {
            int doc;

            @Override
            public long valueAt(int index) {
                return array[doc][index];
            }

            @Override
            public void setDocument(int doc) {
                this.doc = doc;
            }

            @Override
            public int count() {
                return array[doc].length;
            }
        };
        verify(multiValues, numDocs);
        final FixedBitSet rootDocs = randomRootDocs(numDocs);
        final FixedBitSet innerDocs = randomInnerDocs(rootDocs);
        verify(multiValues, numDocs, rootDocs, innerDocs);
    }

    private void verify(SortedNumericDocValues values, int maxDoc) {
        for (long missingValue : new long[] { 0, randomLong() }) {
            for (MultiValueMode mode : MultiValueMode.values()) {
                final NumericDocValues selected = mode.select(values, missingValue);
                for (int i = 0; i < maxDoc; ++i) {
                    final long actual = selected.get(i);
                    long expected = 0;
                    values.setDocument(i);
                    int numValues = values.count();
                    if (numValues == 0) {
                        expected = missingValue;
                    } else {
                        if (mode == MultiValueMode.MAX) {
                            expected = Long.MIN_VALUE;
                        } else if (mode == MultiValueMode.MIN) {
                            expected = Long.MAX_VALUE;
                        }
                        for (int j = 0; j < numValues; ++j) {
                            if (mode == MultiValueMode.SUM || mode == MultiValueMode.AVG) {
                                expected += values.valueAt(j);
                            } else if (mode == MultiValueMode.MIN) {
                                expected = Math.min(expected, values.valueAt(j));
                            } else if (mode == MultiValueMode.MAX) {
                                expected = Math.max(expected, values.valueAt(j));
                            }
                        }
                        if (mode == MultiValueMode.AVG) {
                            expected = numValues > 1 ? Math.round((double) expected / (double) numValues)
                                    : expected;
                        } else if (mode == MultiValueMode.MEDIAN) {
                            int value = numValues / 2;
                            if (numValues % 2 == 0) {
                                expected = Math.round((values.valueAt(value - 1) + values.valueAt(value)) / 2.0);
                            } else {
                                expected = values.valueAt(value);
                            }
                        }
                    }

                    assertEquals(mode.toString() + " docId=" + i, expected, actual);
                }
            }
        }
    }

    private void verify(SortedNumericDocValues values, int maxDoc, FixedBitSet rootDocs, FixedBitSet innerDocs)
            throws IOException {
        for (long missingValue : new long[] { 0, randomLong() }) {
            for (MultiValueMode mode : new MultiValueMode[] { MultiValueMode.MIN, MultiValueMode.MAX,
                    MultiValueMode.SUM, MultiValueMode.AVG }) {
                final NumericDocValues selected = mode.select(values, missingValue, rootDocs,
                        new BitSetIterator(innerDocs, 0L), maxDoc);
                int prevRoot = -1;
                for (int root = rootDocs.nextSetBit(0); root != -1; root = root + 1 < maxDoc
                        ? rootDocs.nextSetBit(root + 1)
                        : -1) {
                    final long actual = selected.get(root);
                    long expected = 0;
                    if (mode == MultiValueMode.MAX) {
                        expected = Long.MIN_VALUE;
                    } else if (mode == MultiValueMode.MIN) {
                        expected = Long.MAX_VALUE;
                    }
                    int numValues = 0;
                    for (int child = innerDocs.nextSetBit(prevRoot + 1); child != -1
                            && child < root; child = innerDocs.nextSetBit(child + 1)) {
                        values.setDocument(child);
                        for (int j = 0; j < values.count(); ++j) {
                            if (mode == MultiValueMode.SUM || mode == MultiValueMode.AVG) {
                                expected += values.valueAt(j);
                            } else if (mode == MultiValueMode.MIN) {
                                expected = Math.min(expected, values.valueAt(j));
                            } else if (mode == MultiValueMode.MAX) {
                                expected = Math.max(expected, values.valueAt(j));
                            }
                            ++numValues;
                        }
                    }
                    if (numValues == 0) {
                        expected = missingValue;
                    } else if (mode == MultiValueMode.AVG) {
                        expected = numValues > 1 ? Math.round((double) expected / (double) numValues) : expected;
                    }

                    assertEquals(mode.toString() + " docId=" + root, expected, actual);

                    prevRoot = root;
                }
            }
        }
    }

    public void testSingleValuedDoubles() throws Exception {
        final int numDocs = scaledRandomIntBetween(1, 100);
        final double[] array = new double[numDocs];
        final FixedBitSet docsWithValue = randomBoolean() ? null : new FixedBitSet(numDocs);
        for (int i = 0; i < array.length; ++i) {
            if (randomBoolean()) {
                array[i] = randomDouble();
                if (docsWithValue != null) {
                    docsWithValue.set(i);
                }
            } else if (docsWithValue != null && randomBoolean()) {
                docsWithValue.set(i);
            }
        }
        final NumericDoubleValues singleValues = new NumericDoubleValues() {
            @Override
            public double get(int docID) {
                return array[docID];
            }
        };
        final SortedNumericDoubleValues multiValues = FieldData.singleton(singleValues, docsWithValue);
        verify(multiValues, numDocs);
        final FixedBitSet rootDocs = randomRootDocs(numDocs);
        final FixedBitSet innerDocs = randomInnerDocs(rootDocs);
        verify(multiValues, numDocs, rootDocs, innerDocs);
    }

    public void testMultiValuedDoubles() throws Exception {
        final int numDocs = scaledRandomIntBetween(1, 100);
        final double[][] array = new double[numDocs][];
        for (int i = 0; i < numDocs; ++i) {
            final double[] values = new double[randomInt(4)];
            for (int j = 0; j < values.length; ++j) {
                values[j] = randomDouble();
            }
            Arrays.sort(values);
            array[i] = values;
        }
        final SortedNumericDoubleValues multiValues = new SortedNumericDoubleValues() {
            int doc;

            @Override
            public double valueAt(int index) {
                return array[doc][index];
            }

            @Override
            public void setDocument(int doc) {
                this.doc = doc;
            }

            @Override
            public int count() {
                return array[doc].length;
            }
        };
        verify(multiValues, numDocs);
        final FixedBitSet rootDocs = randomRootDocs(numDocs);
        final FixedBitSet innerDocs = randomInnerDocs(rootDocs);
        verify(multiValues, numDocs, rootDocs, innerDocs);
    }

    private void verify(SortedNumericDoubleValues values, int maxDoc) {
        for (long missingValue : new long[] { 0, randomLong() }) {
            for (MultiValueMode mode : MultiValueMode.values()) {
                if (MultiValueMode.MEDIAN.equals(mode)) {
                    continue;
                }
                final NumericDoubleValues selected = mode.select(values, missingValue);
                for (int i = 0; i < maxDoc; ++i) {
                    final double actual = selected.get(i);
                    double expected = 0.0;
                    values.setDocument(i);
                    int numValues = values.count();
                    if (numValues == 0) {
                        expected = missingValue;
                    } else {
                        if (mode == MultiValueMode.MAX) {
                            expected = Long.MIN_VALUE;
                        } else if (mode == MultiValueMode.MIN) {
                            expected = Long.MAX_VALUE;
                        }
                        for (int j = 0; j < numValues; ++j) {
                            if (mode == MultiValueMode.SUM || mode == MultiValueMode.AVG) {
                                expected += values.valueAt(j);
                            } else if (mode == MultiValueMode.MIN) {
                                expected = Math.min(expected, values.valueAt(j));
                            } else if (mode == MultiValueMode.MAX) {
                                expected = Math.max(expected, values.valueAt(j));
                            }
                        }
                        if (mode == MultiValueMode.AVG) {
                            expected = expected / numValues;
                        } else if (mode == MultiValueMode.MEDIAN) {
                            int value = numValues / 2;
                            if (numValues % 2 == 0) {
                                expected = (values.valueAt(value - 1) + values.valueAt(value)) / 2.0;
                            } else {
                                expected = values.valueAt(value);
                            }
                        }
                    }

                    assertEquals(mode.toString() + " docId=" + i, expected, actual, 0.1);
                }
            }
        }
    }

    private void verify(SortedNumericDoubleValues values, int maxDoc, FixedBitSet rootDocs, FixedBitSet innerDocs)
            throws IOException {
        for (long missingValue : new long[] { 0, randomLong() }) {
            for (MultiValueMode mode : new MultiValueMode[] { MultiValueMode.MIN, MultiValueMode.MAX,
                    MultiValueMode.SUM, MultiValueMode.AVG }) {
                final NumericDoubleValues selected = mode.select(values, missingValue, rootDocs,
                        new BitSetIterator(innerDocs, 0L), maxDoc);
                int prevRoot = -1;
                for (int root = rootDocs.nextSetBit(0); root != -1; root = root + 1 < maxDoc
                        ? rootDocs.nextSetBit(root + 1)
                        : -1) {
                    final double actual = selected.get(root);
                    double expected = 0.0;
                    if (mode == MultiValueMode.MAX) {
                        expected = Long.MIN_VALUE;
                    } else if (mode == MultiValueMode.MIN) {
                        expected = Long.MAX_VALUE;
                    }
                    int numValues = 0;
                    for (int child = innerDocs.nextSetBit(prevRoot + 1); child != -1
                            && child < root; child = innerDocs.nextSetBit(child + 1)) {
                        values.setDocument(child);
                        for (int j = 0; j < values.count(); ++j) {
                            if (mode == MultiValueMode.SUM || mode == MultiValueMode.AVG) {
                                expected += values.valueAt(j);
                            } else if (mode == MultiValueMode.MIN) {
                                expected = Math.min(expected, values.valueAt(j));
                            } else if (mode == MultiValueMode.MAX) {
                                expected = Math.max(expected, values.valueAt(j));
                            }
                            ++numValues;
                        }
                    }
                    if (numValues == 0) {
                        expected = missingValue;
                    } else if (mode == MultiValueMode.AVG) {
                        expected = expected / numValues;
                    }

                    assertEquals(mode.toString() + " docId=" + root, expected, actual, 0.1);

                    prevRoot = root;
                }
            }
        }
    }

    public void testSingleValuedStrings() throws Exception {
        final int numDocs = scaledRandomIntBetween(1, 100);
        final BytesRef[] array = new BytesRef[numDocs];
        final FixedBitSet docsWithValue = randomBoolean() ? null : new FixedBitSet(numDocs);
        for (int i = 0; i < array.length; ++i) {
            if (randomBoolean()) {
                array[i] = new BytesRef(RandomStrings.randomAsciiOfLength(getRandom(), 8));
                if (docsWithValue != null) {
                    docsWithValue.set(i);
                }
            } else {
                array[i] = new BytesRef();
                if (docsWithValue != null && randomBoolean()) {
                    docsWithValue.set(i);
                }
            }
        }
        final BinaryDocValues singleValues = new BinaryDocValues() {
            @Override
            public BytesRef get(int docID) {
                return BytesRef.deepCopyOf(array[docID]);
            }
        };
        final SortedBinaryDocValues multiValues = FieldData.singleton(singleValues, docsWithValue);
        verify(multiValues, numDocs);
        final FixedBitSet rootDocs = randomRootDocs(numDocs);
        final FixedBitSet innerDocs = randomInnerDocs(rootDocs);
        verify(multiValues, numDocs, rootDocs, innerDocs);
    }

    public void testMultiValuedStrings() throws Exception {
        final int numDocs = scaledRandomIntBetween(1, 100);
        final BytesRef[][] array = new BytesRef[numDocs][];
        for (int i = 0; i < numDocs; ++i) {
            final BytesRef[] values = new BytesRef[randomInt(4)];
            for (int j = 0; j < values.length; ++j) {
                values[j] = new BytesRef(RandomStrings.randomAsciiOfLength(getRandom(), 8));
            }
            Arrays.sort(values);
            array[i] = values;
        }
        final SortedBinaryDocValues multiValues = new SortedBinaryDocValues() {
            int doc;

            @Override
            public BytesRef valueAt(int index) {
                return BytesRef.deepCopyOf(array[doc][index]);
            }

            @Override
            public void setDocument(int doc) {
                this.doc = doc;
            }

            @Override
            public int count() {
                return array[doc].length;
            }
        };
        verify(multiValues, numDocs);
        final FixedBitSet rootDocs = randomRootDocs(numDocs);
        final FixedBitSet innerDocs = randomInnerDocs(rootDocs);
        verify(multiValues, numDocs, rootDocs, innerDocs);
    }

    private void verify(SortedBinaryDocValues values, int maxDoc) {
        for (BytesRef missingValue : new BytesRef[] { new BytesRef(),
                new BytesRef(RandomStrings.randomAsciiOfLength(getRandom(), 8)) }) {
            for (MultiValueMode mode : new MultiValueMode[] { MultiValueMode.MIN, MultiValueMode.MAX }) {
                final BinaryDocValues selected = mode.select(values, missingValue);
                for (int i = 0; i < maxDoc; ++i) {
                    final BytesRef actual = selected.get(i);
                    BytesRef expected = null;
                    values.setDocument(i);
                    int numValues = values.count();
                    if (numValues == 0) {
                        expected = missingValue;
                    } else {
                        for (int j = 0; j < numValues; ++j) {
                            if (expected == null) {
                                expected = BytesRef.deepCopyOf(values.valueAt(j));
                            } else {
                                if (mode == MultiValueMode.MIN) {
                                    expected = expected.compareTo(values.valueAt(j)) <= 0 ? expected
                                            : BytesRef.deepCopyOf(values.valueAt(j));
                                } else if (mode == MultiValueMode.MAX) {
                                    expected = expected.compareTo(values.valueAt(j)) > 0 ? expected
                                            : BytesRef.deepCopyOf(values.valueAt(j));
                                }
                            }
                        }
                        if (expected == null) {
                            expected = missingValue;
                        }
                    }

                    assertEquals(mode.toString() + " docId=" + i, expected, actual);
                }
            }
        }
    }

    private void verify(SortedBinaryDocValues values, int maxDoc, FixedBitSet rootDocs, FixedBitSet innerDocs)
            throws IOException {
        for (BytesRef missingValue : new BytesRef[] { new BytesRef(),
                new BytesRef(RandomStrings.randomAsciiOfLength(getRandom(), 8)) }) {
            for (MultiValueMode mode : new MultiValueMode[] { MultiValueMode.MIN, MultiValueMode.MAX }) {
                final BinaryDocValues selected = mode.select(values, missingValue, rootDocs,
                        new BitSetIterator(innerDocs, 0L), maxDoc);
                int prevRoot = -1;
                for (int root = rootDocs.nextSetBit(0); root != -1; root = root + 1 < maxDoc
                        ? rootDocs.nextSetBit(root + 1)
                        : -1) {
                    final BytesRef actual = selected.get(root);
                    BytesRef expected = null;
                    for (int child = innerDocs.nextSetBit(prevRoot + 1); child != -1
                            && child < root; child = innerDocs.nextSetBit(child + 1)) {
                        values.setDocument(child);
                        for (int j = 0; j < values.count(); ++j) {
                            if (expected == null) {
                                expected = BytesRef.deepCopyOf(values.valueAt(j));
                            } else {
                                if (mode == MultiValueMode.MIN) {
                                    expected = expected.compareTo(values.valueAt(j)) <= 0 ? expected
                                            : BytesRef.deepCopyOf(values.valueAt(j));
                                } else if (mode == MultiValueMode.MAX) {
                                    expected = expected.compareTo(values.valueAt(j)) > 0 ? expected
                                            : BytesRef.deepCopyOf(values.valueAt(j));
                                }
                            }
                        }
                    }
                    if (expected == null) {
                        expected = missingValue;
                    }

                    assertEquals(mode.toString() + " docId=" + root, expected, actual);

                    prevRoot = root;
                }
            }
        }
    }

    public void testSingleValuedOrds() throws Exception {
        final int numDocs = scaledRandomIntBetween(1, 100);
        final int[] array = new int[numDocs];
        for (int i = 0; i < array.length; ++i) {
            if (randomBoolean()) {
                array[i] = randomInt(1000);
            } else {
                array[i] = -1;
            }
        }
        final SortedDocValues singleValues = new SortedDocValues() {
            @Override
            public int getOrd(int docID) {
                return array[docID];
            }

            @Override
            public BytesRef lookupOrd(int ord) {
                throw new UnsupportedOperationException();
            }

            @Override
            public int getValueCount() {
                return 1 << 20;
            }
        };
        final RandomAccessOrds multiValues = (RandomAccessOrds) DocValues.singleton(singleValues);
        verify(multiValues, numDocs);
        final FixedBitSet rootDocs = randomRootDocs(numDocs);
        final FixedBitSet innerDocs = randomInnerDocs(rootDocs);
        verify(multiValues, numDocs, rootDocs, innerDocs);
    }

    public void testMultiValuedOrds() 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] = j == 0 ? randomInt(1000) : values[j - 1] + 1 + randomInt(1000);
            }
            array[i] = values;
        }
        final RandomAccessOrds multiValues = new RandomAccessOrds() {
            int doc;

            @Override
            public long ordAt(int index) {
                return array[doc][index];
            }

            @Override
            public int cardinality() {
                return array[doc].length;
            }

            @Override
            public long nextOrd() {
                throw new UnsupportedOperationException();
            }

            @Override
            public void setDocument(int docID) {
                this.doc = docID;
            }

            @Override
            public BytesRef lookupOrd(long ord) {
                throw new UnsupportedOperationException();
            }

            @Override
            public long getValueCount() {
                return 1 << 20;
            }
        };
        verify(multiValues, numDocs);
        final FixedBitSet rootDocs = randomRootDocs(numDocs);
        final FixedBitSet innerDocs = randomInnerDocs(rootDocs);
        verify(multiValues, numDocs, rootDocs, innerDocs);
    }

    private void verify(RandomAccessOrds values, int maxDoc) {
        for (MultiValueMode mode : new MultiValueMode[] { MultiValueMode.MIN, MultiValueMode.MAX }) {
            final SortedDocValues selected = mode.select(values);
            for (int i = 0; i < maxDoc; ++i) {
                final long actual = selected.getOrd(i);
                int expected = -1;
                values.setDocument(i);
                for (int j = 0; j < values.cardinality(); ++j) {
                    if (expected == -1) {
                        expected = (int) values.ordAt(j);
                    } else {
                        if (mode == MultiValueMode.MIN) {
                            expected = Math.min(expected, (int) values.ordAt(j));
                        } else if (mode == MultiValueMode.MAX) {
                            expected = Math.max(expected, (int) values.ordAt(j));
                        }
                    }
                }

                assertEquals(mode.toString() + " docId=" + i, expected, actual);
            }
        }
    }

    private void verify(RandomAccessOrds values, int maxDoc, FixedBitSet rootDocs, FixedBitSet innerDocs)
            throws IOException {
        for (MultiValueMode mode : new MultiValueMode[] { MultiValueMode.MIN, MultiValueMode.MAX }) {
            final SortedDocValues selected = mode.select(values, rootDocs, new BitSetIterator(innerDocs, 0L));
            int prevRoot = -1;
            for (int root = rootDocs.nextSetBit(0); root != -1; root = root + 1 < maxDoc
                    ? rootDocs.nextSetBit(root + 1)
                    : -1) {
                final int actual = selected.getOrd(root);
                int expected = -1;
                for (int child = innerDocs.nextSetBit(prevRoot + 1); child != -1
                        && child < root; child = innerDocs.nextSetBit(child + 1)) {
                    values.setDocument(child);
                    for (int j = 0; j < values.cardinality(); ++j) {
                        if (expected == -1) {
                            expected = (int) values.ordAt(j);
                        } else {
                            if (mode == MultiValueMode.MIN) {
                                expected = Math.min(expected, (int) values.ordAt(j));
                            } else if (mode == MultiValueMode.MAX) {
                                expected = Math.max(expected, (int) values.ordAt(j));
                            }
                        }
                    }
                }

                assertEquals(mode.toString() + " docId=" + root, expected, actual);

                prevRoot = root;
            }
        }
    }

    public void testUnsortedSingleValuedDoubles() throws Exception {
        final int numDocs = scaledRandomIntBetween(1, 100);
        final double[] array = new double[numDocs];
        final FixedBitSet docsWithValue = randomBoolean() ? null : new FixedBitSet(numDocs);
        for (int i = 0; i < array.length; ++i) {
            if (randomBoolean()) {
                array[i] = randomDouble();
                if (docsWithValue != null) {
                    docsWithValue.set(i);
                }
            } else if (docsWithValue != null && randomBoolean()) {
                docsWithValue.set(i);
            }
        }
        final NumericDoubleValues singleValues = new NumericDoubleValues() {
            @Override
            public double get(int docID) {
                return array[docID];
            }
        };
        final SortedNumericDoubleValues singletonValues = FieldData.singleton(singleValues, docsWithValue);
        final MultiValueMode.UnsortedNumericDoubleValues multiValues = new MultiValueMode.UnsortedNumericDoubleValues() {

            @Override
            public int count() {
                return singletonValues.count();
            }

            @Override
            public void setDocument(int doc) {
                singletonValues.setDocument(doc);
            }

            @Override
            public double valueAt(int index) {
                return Math.cos(singletonValues.valueAt(index));
            }
        };
        verify(multiValues, numDocs);
    }

    public void testUnsortedMultiValuedDoubles() throws Exception {
        final int numDocs = scaledRandomIntBetween(1, 100);
        final double[][] array = new double[numDocs][];
        for (int i = 0; i < numDocs; ++i) {
            final double[] values = new double[randomInt(4)];
            for (int j = 0; j < values.length; ++j) {
                values[j] = randomDouble();
            }
            Arrays.sort(values);
            array[i] = values;
        }
        final MultiValueMode.UnsortedNumericDoubleValues multiValues = new MultiValueMode.UnsortedNumericDoubleValues() {
            int doc;

            @Override
            public int count() {
                return array[doc].length;
            }

            @Override
            public void setDocument(int doc) {
                this.doc = doc;
            }

            @Override
            public double valueAt(int index) {
                return Math.sin(array[doc][index]);
            }
        };
        verify(multiValues, numDocs);
    }

    private void verify(MultiValueMode.UnsortedNumericDoubleValues values, int maxDoc) {
        for (double missingValue : new double[] { 0, randomDouble() }) {
            for (MultiValueMode mode : new MultiValueMode[] { MultiValueMode.MIN, MultiValueMode.MAX,
                    MultiValueMode.SUM, MultiValueMode.AVG }) {
                final NumericDoubleValues selected = mode.select(values, missingValue);
                for (int i = 0; i < maxDoc; ++i) {
                    final double actual = selected.get(i);
                    double expected = 0.0;
                    values.setDocument(i);
                    int numValues = values.count();
                    if (numValues == 0) {
                        expected = missingValue;
                    } else {
                        if (mode == MultiValueMode.MAX) {
                            expected = Long.MIN_VALUE;
                        } else if (mode == MultiValueMode.MIN) {
                            expected = Long.MAX_VALUE;
                        }
                        for (int j = 0; j < numValues; ++j) {
                            if (mode == MultiValueMode.SUM || mode == MultiValueMode.AVG) {
                                expected += values.valueAt(j);
                            } else if (mode == MultiValueMode.MIN) {
                                expected = Math.min(expected, values.valueAt(j));
                            } else if (mode == MultiValueMode.MAX) {
                                expected = Math.max(expected, values.valueAt(j));
                            }
                        }
                        if (mode == MultiValueMode.AVG) {
                            expected = expected / numValues;
                        }
                    }

                    assertEquals(mode.toString() + " docId=" + i, expected, actual, 0.1);
                }
            }
        }
    }
}