/* * 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.common.lucene; import org.apache.lucene.document.LatLonPoint; import org.apache.lucene.index.IndexCommit; import org.apache.lucene.index.IndexFormatTooOldException; import org.apache.lucene.index.StandardDirectoryReader; import org.apache.lucene.index.StoredFields; import org.apache.lucene.tests.analysis.MockAnalyzer; import org.apache.lucene.analysis.core.KeywordAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.Field.Store; import org.apache.lucene.document.LatLonDocValuesField; import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.document.StringField; import org.apache.lucene.document.TextField; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.NoDeletionPolicy; import org.apache.lucene.index.NoMergePolicy; import org.apache.lucene.tests.index.RandomIndexWriter; import org.apache.lucene.index.SegmentInfos; import org.apache.lucene.index.SoftDeletesRetentionMergePolicy; import org.apache.lucene.index.Term; import org.apache.lucene.search.Explanation; import org.apache.lucene.search.IndexOrDocValuesQuery; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.QueryVisitor; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.ScoreMode; import org.apache.lucene.search.Scorer; import org.apache.lucene.search.ScorerSupplier; import org.apache.lucene.search.SortField; import org.apache.lucene.search.SortedNumericSortField; import org.apache.lucene.search.SortedSetSelector; import org.apache.lucene.search.SortedSetSortField; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.Weight; import org.apache.lucene.store.Directory; import org.apache.lucene.store.MMapDirectory; import org.apache.lucene.tests.store.MockDirectoryWrapper; import org.apache.lucene.tests.util.TestUtil; import org.apache.lucene.util.Bits; import org.apache.lucene.util.BytesRef; import org.opensearch.LegacyESVersion; import org.opensearch.Version; import org.opensearch.common.collect.Tuple; import org.opensearch.core.common.io.stream.NamedWriteableRegistry; import org.opensearch.common.util.io.IOUtils; import org.opensearch.index.fielddata.IndexFieldData; import org.opensearch.index.fielddata.fieldcomparator.BytesRefFieldComparatorSource; import org.opensearch.index.fielddata.fieldcomparator.DoubleValuesComparatorSource; import org.opensearch.index.fielddata.fieldcomparator.FloatValuesComparatorSource; import org.opensearch.index.fielddata.fieldcomparator.IntValuesComparatorSource; import org.opensearch.index.fielddata.fieldcomparator.LongValuesComparatorSource; import org.opensearch.search.MultiValueMode; import org.opensearch.test.OpenSearchTestCase; import org.opensearch.test.VersionUtils; import java.io.IOException; import java.io.StringReader; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import static org.hamcrest.Matchers.equalTo; public class LuceneTests extends OpenSearchTestCase { private static final NamedWriteableRegistry EMPTY_REGISTRY = new NamedWriteableRegistry(Collections.emptyList()); public void testCleanIndex() throws IOException { MockDirectoryWrapper dir = newMockDirectory(); IndexWriterConfig iwc = newIndexWriterConfig(); iwc.setIndexDeletionPolicy(NoDeletionPolicy.INSTANCE); iwc.setMergePolicy(NoMergePolicy.INSTANCE); iwc.setMaxBufferedDocs(2); IndexWriter writer = new IndexWriter(dir, iwc); Document doc = new Document(); doc.add(new TextField("id", "1", random().nextBoolean() ? Field.Store.YES : Field.Store.NO)); writer.addDocument(doc); writer.commit(); doc = new Document(); doc.add(new TextField("id", "2", random().nextBoolean() ? Field.Store.YES : Field.Store.NO)); writer.addDocument(doc); doc = new Document(); doc.add(new TextField("id", "3", random().nextBoolean() ? Field.Store.YES : Field.Store.NO)); writer.addDocument(doc); writer.commit(); doc = new Document(); doc.add(new TextField("id", "4", random().nextBoolean() ? Field.Store.YES : Field.Store.NO)); writer.addDocument(doc); writer.deleteDocuments(new Term("id", "2")); writer.commit(); try (DirectoryReader open = DirectoryReader.open(writer)) { assertEquals(3, open.numDocs()); assertEquals(1, open.numDeletedDocs()); assertEquals(4, open.maxDoc()); } writer.close(); if (random().nextBoolean()) { for (String file : dir.listAll()) { if (file.startsWith("_1")) { // delete a random file dir.deleteFile(file); break; } } } Lucene.cleanLuceneIndex(dir); if (dir.listAll().length > 0) { for (String file : dir.listAll()) { if (file.startsWith("extra") == false) { assertEquals(file, "write.lock"); } } } dir.close(); } public void testPruneUnreferencedFiles() throws IOException { MockDirectoryWrapper dir = newMockDirectory(); IndexWriterConfig iwc = newIndexWriterConfig(); iwc.setIndexDeletionPolicy(NoDeletionPolicy.INSTANCE); iwc.setMergePolicy(NoMergePolicy.INSTANCE); iwc.setMaxBufferedDocs(2); IndexWriter writer = new IndexWriter(dir, iwc); Document doc = new Document(); doc.add(new TextField("id", "1", random().nextBoolean() ? Field.Store.YES : Field.Store.NO)); writer.addDocument(doc); writer.commit(); doc = new Document(); doc.add(new TextField("id", "2", random().nextBoolean() ? Field.Store.YES : Field.Store.NO)); writer.addDocument(doc); doc = new Document(); doc.add(new TextField("id", "3", random().nextBoolean() ? Field.Store.YES : Field.Store.NO)); writer.addDocument(doc); writer.commit(); SegmentInfos segmentCommitInfos = Lucene.readSegmentInfos(dir); doc = new Document(); doc.add(new TextField("id", "4", random().nextBoolean() ? Field.Store.YES : Field.Store.NO)); writer.addDocument(doc); writer.deleteDocuments(new Term("id", "2")); writer.commit(); DirectoryReader open = DirectoryReader.open(writer); assertEquals(3, open.numDocs()); assertEquals(1, open.numDeletedDocs()); assertEquals(4, open.maxDoc()); open.close(); writer.close(); SegmentInfos si = Lucene.pruneUnreferencedFiles(segmentCommitInfos.getSegmentsFileName(), dir); assertEquals(si.getSegmentsFileName(), segmentCommitInfos.getSegmentsFileName()); open = DirectoryReader.open(dir); assertEquals(3, open.numDocs()); assertEquals(0, open.numDeletedDocs()); assertEquals(3, open.maxDoc()); IndexSearcher s = new IndexSearcher(open); assertEquals(s.search(new TermQuery(new Term("id", "1")), 1).totalHits.value, 1); assertEquals(s.search(new TermQuery(new Term("id", "2")), 1).totalHits.value, 1); assertEquals(s.search(new TermQuery(new Term("id", "3")), 1).totalHits.value, 1); assertEquals(s.search(new TermQuery(new Term("id", "4")), 1).totalHits.value, 0); for (String file : dir.listAll()) { assertFalse("unexpected file: " + file, file.equals("segments_3") || file.startsWith("_2")); } open.close(); dir.close(); } public void testFiles() throws IOException { MockDirectoryWrapper dir = newMockDirectory(); IndexWriterConfig iwc = newIndexWriterConfig(new MockAnalyzer(random())); iwc.setMergePolicy(NoMergePolicy.INSTANCE); iwc.setMaxBufferedDocs(2); iwc.setUseCompoundFile(true); IndexWriter writer = new IndexWriter(dir, iwc); Document doc = new Document(); doc.add(new TextField("id", "1", random().nextBoolean() ? Field.Store.YES : Field.Store.NO)); writer.addDocument(doc); writer.commit(); Set<String> files = new HashSet<>(); for (String f : Lucene.files(Lucene.readSegmentInfos(dir))) { files.add(f); } final boolean simpleTextCFS = files.contains("_0.scf"); assertTrue(files.toString(), files.contains("segments_1")); if (simpleTextCFS) { assertFalse(files.toString(), files.contains("_0.cfs")); assertFalse(files.toString(), files.contains("_0.cfe")); } else { assertTrue(files.toString(), files.contains("_0.cfs")); assertTrue(files.toString(), files.contains("_0.cfe")); } assertTrue(files.toString(), files.contains("_0.si")); doc = new Document(); doc.add(new TextField("id", "2", random().nextBoolean() ? Field.Store.YES : Field.Store.NO)); writer.addDocument(doc); doc = new Document(); doc.add(new TextField("id", "3", random().nextBoolean() ? Field.Store.YES : Field.Store.NO)); writer.addDocument(doc); writer.commit(); files.clear(); for (String f : Lucene.files(Lucene.readSegmentInfos(dir))) { files.add(f); } assertFalse(files.toString(), files.contains("segments_1")); assertTrue(files.toString(), files.contains("segments_2")); if (simpleTextCFS) { assertFalse(files.toString(), files.contains("_0.cfs")); assertFalse(files.toString(), files.contains("_0.cfe")); } else { assertTrue(files.toString(), files.contains("_0.cfs")); assertTrue(files.toString(), files.contains("_0.cfe")); } assertTrue(files.toString(), files.contains("_0.si")); if (simpleTextCFS) { assertFalse(files.toString(), files.contains("_1.cfs")); assertFalse(files.toString(), files.contains("_1.cfe")); } else { assertTrue(files.toString(), files.contains("_1.cfs")); assertTrue(files.toString(), files.contains("_1.cfe")); } assertTrue(files.toString(), files.contains("_1.si")); writer.close(); dir.close(); } public void testNumDocs() throws IOException { MockDirectoryWrapper dir = newMockDirectory(); IndexWriterConfig iwc = newIndexWriterConfig(); IndexWriter writer = new IndexWriter(dir, iwc); Document doc = new Document(); doc.add(new TextField("id", "1", random().nextBoolean() ? Field.Store.YES : Field.Store.NO)); writer.addDocument(doc); writer.commit(); SegmentInfos segmentCommitInfos = Lucene.readSegmentInfos(dir); assertEquals(1, Lucene.getNumDocs(segmentCommitInfos)); doc = new Document(); doc.add(new TextField("id", "2", random().nextBoolean() ? Field.Store.YES : Field.Store.NO)); writer.addDocument(doc); doc = new Document(); doc.add(new TextField("id", "3", random().nextBoolean() ? Field.Store.YES : Field.Store.NO)); writer.addDocument(doc); segmentCommitInfos = Lucene.readSegmentInfos(dir); assertEquals(1, Lucene.getNumDocs(segmentCommitInfos)); writer.commit(); segmentCommitInfos = Lucene.readSegmentInfos(dir); assertEquals(3, Lucene.getNumDocs(segmentCommitInfos)); writer.deleteDocuments(new Term("id", "2")); writer.commit(); segmentCommitInfos = Lucene.readSegmentInfos(dir); assertEquals(2, Lucene.getNumDocs(segmentCommitInfos)); int numDocsToIndex = randomIntBetween(10, 50); List<Term> deleteTerms = new ArrayList<>(); for (int i = 0; i < numDocsToIndex; i++) { doc = new Document(); doc.add(new TextField("id", "extra_" + i, random().nextBoolean() ? Field.Store.YES : Field.Store.NO)); deleteTerms.add(new Term("id", "extra_" + i)); writer.addDocument(doc); } int numDocsToDelete = randomIntBetween(0, numDocsToIndex); Collections.shuffle(deleteTerms, random()); for (int i = 0; i < numDocsToDelete; i++) { Term remove = deleteTerms.remove(0); writer.deleteDocuments(remove); } writer.commit(); segmentCommitInfos = Lucene.readSegmentInfos(dir); assertEquals(2 + deleteTerms.size(), Lucene.getNumDocs(segmentCommitInfos)); writer.close(); dir.close(); } /** * Tests whether old segments are readable and queryable based on the data documented * in the README <a href="file:../../../../../resources/indices/bwc/es-6.3.0/README.md">here</a>. */ public void testReadSegmentInfosExtendedCompatibility() throws IOException { final String pathToTestIndex = "/indices/bwc/es-6.3.0/testIndex-es-6.3.0.zip"; final Version minVersion = LegacyESVersion.V_6_0_0; Path tmp = createTempDir(); TestUtil.unzip(getClass().getResourceAsStream(pathToTestIndex), tmp); try (MockDirectoryWrapper dir = newMockFSDirectory(tmp)) { // The standard API will throw an exception expectThrows(IndexFormatTooOldException.class, () -> Lucene.readSegmentInfos(dir)); SegmentInfos si = Lucene.readSegmentInfos(dir, minVersion); assertEquals(1, Lucene.getNumDocs(si)); IndexCommit indexCommit = Lucene.getIndexCommit(si, dir); // uses the "expert" Lucene API try ( StandardDirectoryReader reader = (StandardDirectoryReader) DirectoryReader.open( indexCommit, minVersion.minimumIndexCompatibilityVersion().luceneVersion.major, null ) ) { IndexSearcher searcher = newSearcher(reader); // radius too small, should get no results assertFalse(Lucene.exists(searcher, LatLonPoint.newDistanceQuery("testLocation", 48.57532, -112.87695, 2))); assertTrue(Lucene.exists(searcher, LatLonPoint.newDistanceQuery("testLocation", 48.57532, -112.87695, 20000))); } } } public void testCount() throws Exception { Directory dir = newDirectory(); RandomIndexWriter w = new RandomIndexWriter(random(), dir); try (DirectoryReader reader = w.getReader()) { // match_all does not match anything on an empty index IndexSearcher searcher = newSearcher(reader); assertFalse(Lucene.exists(searcher, new MatchAllDocsQuery())); } Document doc = new Document(); w.addDocument(doc); doc.add(new StringField("foo", "bar", Store.NO)); w.addDocument(doc); try (DirectoryReader reader = w.getReader()) { IndexSearcher searcher = newSearcher(reader); assertTrue(Lucene.exists(searcher, new MatchAllDocsQuery())); assertFalse(Lucene.exists(searcher, new TermQuery(new Term("baz", "bar")))); assertTrue(Lucene.exists(searcher, new TermQuery(new Term("foo", "bar")))); } w.deleteDocuments(new Term("foo", "bar")); try (DirectoryReader reader = w.getReader()) { IndexSearcher searcher = newSearcher(reader); assertFalse(Lucene.exists(searcher, new TermQuery(new Term("foo", "bar")))); } w.close(); dir.close(); } public void testAsSequentialAccessBits() throws Exception { Directory dir = newDirectory(); IndexWriter w = new IndexWriter(dir, new IndexWriterConfig(new KeywordAnalyzer())); Document doc = new Document(); doc.add(new StringField("foo", "bar", Store.NO)); w.addDocument(doc); doc = new Document(); w.addDocument(doc); doc = new Document(); doc.add(new StringField("foo", "bar", Store.NO)); w.addDocument(doc); try (DirectoryReader reader = DirectoryReader.open(w)) { IndexSearcher searcher = newSearcher(reader); Weight termWeight = new TermQuery(new Term("foo", "bar")).createWeight(searcher, ScoreMode.COMPLETE_NO_SCORES, 1f); assertEquals(1, reader.leaves().size()); LeafReaderContext leafReaderContext = searcher.getIndexReader().leaves().get(0); Bits bits = Lucene.asSequentialAccessBits(leafReaderContext.reader().maxDoc(), termWeight.scorerSupplier(leafReaderContext)); expectThrows(IndexOutOfBoundsException.class, () -> bits.get(-1)); expectThrows(IndexOutOfBoundsException.class, () -> bits.get(leafReaderContext.reader().maxDoc())); assertTrue(bits.get(0)); assertTrue(bits.get(0)); assertFalse(bits.get(1)); assertFalse(bits.get(1)); expectThrows(IllegalArgumentException.class, () -> bits.get(0)); assertTrue(bits.get(2)); assertTrue(bits.get(2)); expectThrows(IllegalArgumentException.class, () -> bits.get(1)); } w.close(); dir.close(); } private static class UnsupportedQuery extends Query { @Override public void visit(QueryVisitor visitor) { visitor.visitLeaf(this); } @Override public String toString(String field) { return "Unsupported"; } @Override public boolean equals(Object obj) { return obj instanceof UnsupportedQuery; } @Override public int hashCode() { return 42; } @Override public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException { return new Weight(this) { @Override public boolean isCacheable(LeafReaderContext ctx) { return true; } @Override public Explanation explain(LeafReaderContext context, int doc) throws IOException { throw new UnsupportedOperationException(); } @Override public Scorer scorer(LeafReaderContext context) throws IOException { throw new UnsupportedOperationException(); } @Override public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException { return new ScorerSupplier() { @Override public Scorer get(long leadCost) throws IOException { throw new UnsupportedOperationException(); } @Override public long cost() { return context.reader().maxDoc(); } }; } }; } } public void testAsSequentialBitsUsesRandomAccess() throws IOException { try (Directory dir = newDirectory()) { try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig(new KeywordAnalyzer()))) { Document doc = new Document(); doc.add(new NumericDocValuesField("foo", 5L)); // we need more than 8 documents because doc values are artificially penalized by IndexOrDocValuesQuery for (int i = 0; i < 10; ++i) { w.addDocument(doc); } w.forceMerge(1); try (IndexReader reader = DirectoryReader.open(w)) { IndexSearcher searcher = newSearcher(reader); searcher.setQueryCache(null); Query query = new IndexOrDocValuesQuery(new UnsupportedQuery(), NumericDocValuesField.newSlowRangeQuery("foo", 3L, 5L)); Weight weight = searcher.createWeight(query, ScoreMode.COMPLETE_NO_SCORES, 1f); // Random access by default ScorerSupplier scorerSupplier = weight.scorerSupplier(reader.leaves().get(0)); Bits bits = Lucene.asSequentialAccessBits(reader.maxDoc(), scorerSupplier); assertNotNull(bits); assertTrue(bits.get(0)); // Moves to sequential access if Bits#get is called more than the number of matches ScorerSupplier scorerSupplier2 = weight.scorerSupplier(reader.leaves().get(0)); expectThrows( UnsupportedOperationException.class, () -> Lucene.asSequentialAccessBits(reader.maxDoc(), scorerSupplier2, reader.maxDoc()) ); } } } } /** * Test that the "unmap hack" is detected as supported by lucene. * This works around the following bug: https://bugs.openjdk.java.net/browse/JDK-4724038 * <p> * While not guaranteed, current status is "Critical Internal API": http://openjdk.java.net/jeps/260 * Additionally this checks we did not screw up the security logic around the hack. */ public void testMMapHackSupported() throws Exception { // add assume's here if needed for certain platforms, but we should know if it does not work. assertTrue("MMapDirectory does not support unmapping: " + MMapDirectory.UNMAP_NOT_SUPPORTED_REASON, MMapDirectory.UNMAP_SUPPORTED); } public void testWrapAllDocsLive() throws Exception { Directory dir = newDirectory(); IndexWriterConfig config = newIndexWriterConfig().setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setMergePolicy(new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, MatchAllDocsQuery::new, newMergePolicy())); IndexWriter writer = new IndexWriter(dir, config); int numDocs = between(1, 10); Set<String> liveDocs = new HashSet<>(); for (int i = 0; i < numDocs; i++) { String id = Integer.toString(i); Document doc = new Document(); doc.add(new StringField("id", id, Store.YES)); writer.addDocument(doc); liveDocs.add(id); } for (int i = 0; i < numDocs; i++) { if (randomBoolean()) { String id = Integer.toString(i); Document doc = new Document(); doc.add(new StringField("id", "v2-" + id, Store.YES)); if (randomBoolean()) { doc.add(Lucene.newSoftDeletesField()); } writer.softUpdateDocument(new Term("id", id), doc, Lucene.newSoftDeletesField()); liveDocs.add("v2-" + id); } } try (DirectoryReader unwrapped = DirectoryReader.open(writer)) { DirectoryReader reader = Lucene.wrapAllDocsLive(unwrapped); StoredFields storedFields = reader.storedFields(); assertThat(reader.numDocs(), equalTo(liveDocs.size())); IndexSearcher searcher = new IndexSearcher(reader); Set<String> actualDocs = new HashSet<>(); TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), Integer.MAX_VALUE); for (ScoreDoc scoreDoc : topDocs.scoreDocs) { actualDocs.add(storedFields.document(scoreDoc.doc).get("id")); } assertThat(actualDocs, equalTo(liveDocs)); } IOUtils.close(writer, dir); } public void testWrapLiveDocsNotExposeAbortedDocuments() throws Exception { Directory dir = newDirectory(); IndexWriterConfig config = newIndexWriterConfig().setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setMergePolicy(new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, MatchAllDocsQuery::new, newMergePolicy())) .setMaxFullFlushMergeWaitMillis(0); IndexWriter writer = new IndexWriter(dir, config); int numDocs = between(1, 10); List<String> liveDocs = new ArrayList<>(); for (int i = 0; i < numDocs; i++) { String id = Integer.toString(i); Document doc = new Document(); doc.add(new StringField("id", id, Store.YES)); if (randomBoolean()) { doc.add(Lucene.newSoftDeletesField()); } writer.addDocument(doc); liveDocs.add(id); } int abortedDocs = between(1, 10); for (int i = 0; i < abortedDocs; i++) { try { Document doc = new Document(); doc.add(new StringField("id", "aborted-" + i, Store.YES)); StringReader reader = new StringReader(""); doc.add(new TextField("other", reader)); reader.close(); // mark the indexing hit non-aborting error writer.addDocument(doc); fail("index should have failed"); } catch (Exception ignored) {} } try (DirectoryReader unwrapped = DirectoryReader.open(writer)) { DirectoryReader reader = Lucene.wrapAllDocsLive(unwrapped); StoredFields storedFields = reader.storedFields(); assertThat(reader.maxDoc(), equalTo(numDocs + abortedDocs)); assertThat(reader.numDocs(), equalTo(liveDocs.size())); IndexSearcher searcher = new IndexSearcher(reader); List<String> actualDocs = new ArrayList<>(); TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), Integer.MAX_VALUE); for (ScoreDoc scoreDoc : topDocs.scoreDocs) { actualDocs.add(storedFields.document(scoreDoc.doc).get("id")); } assertThat(actualDocs, equalTo(liveDocs)); } IOUtils.close(writer, dir); } public void testSortFieldSerialization() throws IOException { Tuple<SortField, SortField> sortFieldTuple = randomSortField(); SortField deserialized = copyInstance( sortFieldTuple.v1(), EMPTY_REGISTRY, Lucene::writeSortField, Lucene::readSortField, VersionUtils.randomVersion(random()) ); assertEquals(sortFieldTuple.v2(), deserialized); } public void testSortValueSerialization() throws IOException { Object sortValue = randomSortValue(); Object deserialized = copyInstance( sortValue, EMPTY_REGISTRY, Lucene::writeSortValue, Lucene::readSortValue, VersionUtils.randomVersion(random()) ); assertEquals(sortValue, deserialized); } public static Object randomSortValue() { switch (randomIntBetween(0, 9)) { case 0: return null; case 1: return randomAlphaOfLengthBetween(3, 10); case 2: return randomInt(); case 3: return randomLong(); case 4: return randomFloat(); case 5: return randomDouble(); case 6: return randomByte(); case 7: return randomShort(); case 8: return randomBoolean(); case 9: return new BytesRef(randomAlphaOfLengthBetween(3, 10)); default: throw new UnsupportedOperationException(); } } public static Tuple<SortField, SortField> randomSortField() { switch (randomIntBetween(0, 2)) { case 0: return randomSortFieldCustomComparatorSource(); case 1: return randomCustomSortField(); case 2: String field = randomAlphaOfLengthBetween(3, 10); SortField.Type type = randomFrom(SortField.Type.values()); if ((type == SortField.Type.SCORE || type == SortField.Type.DOC) && randomBoolean()) { field = null; } SortField sortField = new SortField(field, type, randomBoolean()); Object missingValue = randomMissingValue(sortField.getType()); if (missingValue != null) { sortField.setMissingValue(missingValue); } return Tuple.tuple(sortField, sortField); default: throw new UnsupportedOperationException(); } } private static Tuple<SortField, SortField> randomSortFieldCustomComparatorSource() { String field = randomAlphaOfLengthBetween(3, 10); IndexFieldData.XFieldComparatorSource comparatorSource; boolean reverse = randomBoolean(); Object missingValue = null; switch (randomIntBetween(0, 4)) { case 0: comparatorSource = new LongValuesComparatorSource( null, randomBoolean() ? randomLong() : null, randomFrom(MultiValueMode.values()), null ); break; case 1: comparatorSource = new DoubleValuesComparatorSource( null, randomBoolean() ? randomDouble() : null, randomFrom(MultiValueMode.values()), null ); break; case 2: comparatorSource = new FloatValuesComparatorSource( null, randomBoolean() ? randomFloat() : null, randomFrom(MultiValueMode.values()), null ); break; case 3: comparatorSource = new BytesRefFieldComparatorSource( null, randomBoolean() ? "_first" : "_last", randomFrom(MultiValueMode.values()), null ); missingValue = comparatorSource.missingValue(reverse); break; case 4: comparatorSource = new IntValuesComparatorSource( null, randomBoolean() ? randomInt() : null, randomFrom(MultiValueMode.values()), null ); missingValue = comparatorSource.missingValue(reverse); break; default: throw new UnsupportedOperationException(); } SortField sortField = new SortField(field, comparatorSource, reverse); SortField expected = new SortField(field, comparatorSource.reducedType(), reverse); expected.setMissingValue(missingValue); return Tuple.tuple(sortField, expected); } private static Tuple<SortField, SortField> randomCustomSortField() { String field = randomAlphaOfLengthBetween(3, 10); switch (randomIntBetween(0, 2)) { case 0: { SortField sortField = LatLonDocValuesField.newDistanceSort(field, 0, 0); SortField expected = new SortField(field, SortField.Type.DOUBLE); expected.setMissingValue(Double.POSITIVE_INFINITY); return Tuple.tuple(sortField, expected); } case 1: { SortedSetSortField sortField = new SortedSetSortField(field, randomBoolean(), randomFrom(SortedSetSelector.Type.values())); SortField expected = new SortField(sortField.getField(), SortField.Type.STRING, sortField.getReverse()); Object missingValue = randomMissingValue(SortField.Type.STRING); sortField.setMissingValue(missingValue); expected.setMissingValue(missingValue); return Tuple.tuple(sortField, expected); } case 2: { SortField.Type type = randomFrom(SortField.Type.DOUBLE, SortField.Type.INT, SortField.Type.FLOAT, SortField.Type.LONG); SortedNumericSortField sortField = new SortedNumericSortField(field, type, randomBoolean()); SortField expected = new SortField(sortField.getField(), sortField.getNumericType(), sortField.getReverse()); Object missingValue = randomMissingValue(type); if (missingValue != null) { sortField.setMissingValue(missingValue); expected.setMissingValue(missingValue); } return Tuple.tuple(sortField, expected); } default: throw new UnsupportedOperationException(); } } private static Object randomMissingValue(SortField.Type type) { switch (type) { case INT: return randomInt(); case FLOAT: return randomFloat(); case DOUBLE: return randomDouble(); case LONG: return randomLong(); case STRING: return randomBoolean() ? SortField.STRING_FIRST : SortField.STRING_LAST; default: return null; } } }