You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by at...@apache.org on 2025/04/18 10:09:16 UTC
(lucene) 02/09: More stuff without wiring
This is an automated email from the ASF dual-hosted git repository.
atri pushed a commit to branch index_binning_changes
in repository https://gitbox.apache.org/repos/asf/lucene.git
commit 47237db09a3b23e6ab0062d22a4df9cdf31f53c8
Author: Atri Sharma <at...@192.168.1.25>
AuthorDate: Fri Apr 18 01:10:56 2025 +0530
More stuff without wiring
---
.../lucene/codecs/TestBinMapReaderWriter.java | 144 ++++++++++++
.../lucene/codecs/TestBinQualityMetrics.java | 51 +++++
.../lucene/codecs/TestDocBinningGraphBuilder.java | 103 +++++++++
.../apache/lucene/codecs/TestDocGraphBuilder.java | 94 ++++++++
.../lucene103/TestLucene103PostingsFormat.java | 247 +++++++++++++++++++++
.../org/apache/lucene/index/TestBinScoreUtil.java | 123 ++++++++++
.../lucene/search/TestAnytimeRankingNDCG.java | 116 ++++++++++
.../search/TestAnytimeRankingRelevanceMatch.java | 121 ++++++++++
.../search/TestAnytimeRankingRelevanceSearch.java | 214 ++++++++++++++++++
.../lucene/search/TestAnytimeRankingSearch.java | 216 ++++++++++++++++++
.../lucene/search/TestBinningRelevanceBoost.java | 136 ++++++++++++
.../lucene/util/TestHeuristicSLAEstimator.java | 130 +++++++++++
12 files changed, 1695 insertions(+)
diff --git a/lucene/core/src/test/org/apache/lucene/codecs/TestBinMapReaderWriter.java b/lucene/core/src/test/org/apache/lucene/codecs/TestBinMapReaderWriter.java
new file mode 100644
index 00000000000..15d208d884f
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/codecs/TestBinMapReaderWriter.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.codecs;
+
+import org.apache.lucene.index.SegmentInfo;
+import org.apache.lucene.index.SegmentReadState;
+import org.apache.lucene.index.SegmentWriteState;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.apache.lucene.store.IOContext;
+import org.apache.lucene.tests.util.LuceneTestCase;
+import org.apache.lucene.util.StringHelper;
+import org.apache.lucene.util.Version;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collections;
+
+/**
+ * Unit tests for {@link BinMapWriter} and {@link BinMapReader} using {@link
+ * DocBinningGraphBuilder}.
+ */
+public class TestBinMapReaderWriter extends LuceneTestCase {
+
+ private Directory dir;
+ private SegmentWriteState writeState;
+ private SegmentReadState readState;
+
+ @Before
+ public void setUpTest() throws Exception {
+
+ super.setUp();
+ Path tempDir = createTempDir("binmap-test");
+ dir = FSDirectory.open(tempDir);
+ SegmentInfo info =
+ new SegmentInfo(
+ dir,
+ Version.LATEST,
+ Version.LATEST,
+ "_1",
+ 16,
+ false,
+ false,
+ Codec.getDefault(),
+ Collections.emptyMap(),
+ StringHelper.randomId(),
+ Collections.emptyMap(),
+ null);
+ readState = new SegmentReadState(dir, info, null, newIOContext(random()));
+ writeState = new SegmentWriteState(null, dir, info, null, null, IOContext.DEFAULT);
+ }
+
+ @After
+ public void tearDownTest() throws Exception {
+ super.tearDown();
+ dir.close();
+ }
+
+ @Test
+ public void testBinMapWriteAndRead() throws IOException {
+ final int maxDoc = writeState.segmentInfo.maxDoc();
+ final int numBins = 4;
+
+ InMemorySparseEdgeGraph graph = new InMemorySparseEdgeGraph();
+ for (int i = 0; i < maxDoc; i++) {
+ if (i < maxDoc - 1) {
+ graph.addEdge(i, i + 1, 1.0f);
+ }
+ // ensure all nodes are initialized in the graph
+ graph.ensureVertex(i);
+ }
+
+ int[] docToBin = DocBinningGraphBuilder.computeBins(graph, maxDoc, numBins);
+ BinMapWriter writer = new BinMapWriter(writeState.directory, writeState, docToBin, numBins);
+
+ writer.close();
+
+ try (BinMapReader reader = new BinMapReader(dir, readState)) {
+ assertEquals("bin count mismatch", numBins, reader.getBinCount());
+ for (int i = 0; i < maxDoc; i++) {
+ assertEquals("bin mismatch at docID " + i, docToBin[i], reader.getBin(i));
+ }
+ }
+ }
+
+ @Test
+ public void testEmptySegment() throws IOException {
+ Path emptyDir = createTempDir("binmap-empty");
+ Directory directory = FSDirectory.open(emptyDir);
+
+ SegmentInfo emptyInfo =
+ new SegmentInfo(
+ directory,
+ Version.LATEST,
+ Version.LATEST,
+ "_empty",
+ 0,
+ false,
+ false,
+ Codec.getDefault(),
+ Collections.emptyMap(),
+ StringHelper.randomId(),
+ Collections.emptyMap(),
+ null);
+
+ SegmentWriteState emptyWriteState =
+ new SegmentWriteState(null, directory, emptyInfo, null, null, IOContext.DEFAULT);
+ SegmentReadState emptyReadState =
+ new SegmentReadState(directory, emptyInfo, null, IOContext.DEFAULT);
+
+ int[] emptyBinArray = {
+ /*nothing in here = array with no slots*/
+ };
+
+ // Nothing to write since maxDoc is 0, but simulate writing header
+ BinMapWriter writer =
+ new BinMapWriter(emptyWriteState.directory, emptyWriteState, emptyBinArray, 0);
+ writer.close();
+
+ // Reader should read back bin count as 0
+ try (BinMapReader reader = new BinMapReader(directory, emptyReadState)) {
+ assertEquals(0, reader.getBinCount());
+ }
+
+ directory.close();
+ }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/codecs/TestBinQualityMetrics.java b/lucene/core/src/test/org/apache/lucene/codecs/TestBinQualityMetrics.java
new file mode 100644
index 00000000000..f9715e0a018
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/codecs/TestBinQualityMetrics.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.codecs;
+
+import org.apache.lucene.tests.util.LuceneTestCase;
+
+import java.util.Random;
+
+/** Unit tests for verifying bin balance and quality. */
+public class TestBinQualityMetrics extends LuceneTestCase {
+
+ public void testStandardDeviationAndGini() {
+ int[] counts = new int[] {100, 90, 110, 95, 105}; // reasonably balanced
+ double stddev = BinQualityMetrics.computeStdDev(counts);
+ double gini = BinQualityMetrics.computeGiniIndex(counts);
+ assertTrue("Expected low stddev", stddev < 10.0);
+ assertTrue("Expected low Gini", gini < 0.05);
+ }
+
+ public void testUnbalancedBins() {
+ int[] counts = new int[] {300, 0, 0, 0, 0}; // extremely skewed
+ double stddev = BinQualityMetrics.computeStdDev(counts);
+ double gini = BinQualityMetrics.computeGiniIndex(counts);
+ assertTrue("Expected high stddev", stddev > 100.0);
+ assertTrue("Expected high Gini", gini >= 0.8);
+ }
+
+ public void testRandomBins() {
+ Random random = new Random(42);
+ int[] counts = new int[16];
+ for (int i = 0; i < counts.length; i++) {
+ counts[i] = random.nextInt(100);
+ }
+ double gini = BinQualityMetrics.computeGiniIndex(counts);
+ assertTrue("Gini index must be in range [0,1]", gini >= 0.0 && gini <= 1.0);
+ }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/codecs/TestDocBinningGraphBuilder.java b/lucene/core/src/test/org/apache/lucene/codecs/TestDocBinningGraphBuilder.java
new file mode 100644
index 00000000000..39902bb8682
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/codecs/TestDocBinningGraphBuilder.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.codecs;
+
+import org.apache.lucene.tests.util.LuceneTestCase;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+public class TestDocBinningGraphBuilder extends LuceneTestCase {
+
+ public void testSimpleLinearGraph() throws IOException {
+ final int maxDoc = 8;
+ final int numBins = 4;
+
+ SparseEdgeGraph graph = new InMemorySparseEdgeGraph();
+ for (int i = 0; i < maxDoc - 1; i++) {
+ graph.addEdge(i, i + 1, 1.0f);
+ }
+
+ int[] docToBin = DocBinningGraphBuilder.computeBins(graph, maxDoc, numBins);
+ assertEquals(maxDoc, docToBin.length);
+
+ Set<Integer> seenBins = new HashSet<>();
+ for (int bin : docToBin) {
+ assertTrue("Bin must be non-negative", bin >= 0);
+ assertTrue("Bin must be less than numBins", bin < numBins);
+ seenBins.add(bin);
+ }
+
+ assertTrue("Expected to use multiple bins", seenBins.size() > 1);
+ }
+
+ public void testSingleBinAssignment() throws IOException {
+ final int maxDoc = 5;
+ final int numBins = 1;
+
+ SparseEdgeGraph graph = new InMemorySparseEdgeGraph();
+ for (int i = 0; i < maxDoc - 1; i++) {
+ graph.addEdge(i, i + 1, 1.0f);
+ }
+
+ int[] docToBin = DocBinningGraphBuilder.computeBins(graph, maxDoc, numBins);
+ for (int bin : docToBin) {
+ assertEquals(0, bin);
+ }
+ }
+
+ public void testDisconnectedGraph() throws IOException {
+ final int maxDoc = 6;
+ final int numBins = 2;
+
+ SparseEdgeGraph graph = new InMemorySparseEdgeGraph(); // no edges
+ for (int i = 0; i < maxDoc; i++) {
+ graph.ensureVertex(i);
+ }
+
+ int[] docToBin = DocBinningGraphBuilder.computeBins(graph, maxDoc, numBins);
+ assertEquals(maxDoc, docToBin.length);
+ for (int bin : docToBin) {
+ assertTrue(bin >= 0 && bin < numBins);
+ }
+ }
+
+ public void testMinimalDocsSplit() throws IOException {
+ final int maxDoc = 2;
+ final int numBins = 2;
+
+ SparseEdgeGraph graph = new InMemorySparseEdgeGraph();
+ graph.addEdge(0, 1, 1.0f);
+
+ int[] docToBin = DocBinningGraphBuilder.computeBins(graph, maxDoc, numBins);
+ assertEquals(2, docToBin.length);
+ assertNotEquals(docToBin[0], docToBin[1]);
+ }
+
+ public void testInvalidBinCount() {
+ SparseEdgeGraph graph = new InMemorySparseEdgeGraph();
+ expectThrows(
+ IllegalArgumentException.class, () -> DocBinningGraphBuilder.computeBins(graph, 10, 3));
+ }
+
+ public void testZeroDocsFails() {
+ SparseEdgeGraph graph = new InMemorySparseEdgeGraph();
+ expectThrows(
+ IllegalArgumentException.class, () -> DocBinningGraphBuilder.computeBins(graph, 0, 2));
+ }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/codecs/TestDocGraphBuilder.java b/lucene/core/src/test/org/apache/lucene/codecs/TestDocGraphBuilder.java
new file mode 100644
index 00000000000..69c665b64bd
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/codecs/TestDocGraphBuilder.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.codecs;
+
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.FieldType;
+import org.apache.lucene.document.TextField;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexOptions;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.LeafReader;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.apache.lucene.tests.analysis.MockAnalyzer;
+import org.apache.lucene.tests.analysis.MockTokenizer;
+import org.apache.lucene.tests.util.LuceneTestCase;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+/** Unit test for {@link DocGraphBuilder} without external analyzer dependencies. */
+public class TestDocGraphBuilder extends LuceneTestCase {
+
+ @Test
+ public void testSparseEdgeGraphConstruction() throws Exception {
+ Path indexPath = createTempDir("doc-graph-builder");
+ try (Directory dir = FSDirectory.open(indexPath)) {
+ IndexWriterConfig iwc =
+ new IndexWriterConfig(new MockAnalyzer(random(), MockTokenizer.SIMPLE, true));
+ iwc.setUseCompoundFile(false);
+ iwc.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
+
+ try (IndexWriter writer = new IndexWriter(dir, iwc)) {
+ addDoc(writer, "alpha beta gamma");
+ addDoc(writer, "beta gamma delta");
+ addDoc(writer, "alpha delta epsilon");
+ addDoc(writer, "zeta eta theta");
+ writer.commit();
+ }
+
+ try (IndexReader reader = DirectoryReader.open(dir)) {
+ LeafReader leaf = reader.leaves().get(0).reader();
+
+ DocGraphBuilder builder = new DocGraphBuilder("content", 2);
+ SparseEdgeGraph graph = builder.build(leaf);
+
+ assertEquals("Graph should contain one node per doc", leaf.maxDoc(), graph.size());
+
+ for (int docID = 0; docID < leaf.maxDoc(); docID++) {
+ int[] neighbors = graph.getNeighbors(docID);
+ float[] weights = graph.getWeights(docID);
+
+ assertEquals(neighbors.length, weights.length);
+ for (int i = 0; i < neighbors.length; i++) {
+ assertNotEquals("Self-loops are not expected", docID, neighbors[i]);
+ assertTrue("Edge weight should be positive", weights[i] > 0);
+ }
+ }
+ }
+ }
+ }
+
+ private void addDoc(IndexWriter writer, String content) throws IOException {
+ Document doc = new Document();
+
+ FieldType tvType = new FieldType(TextField.TYPE_STORED);
+ tvType.setStoreTermVectors(true);
+ tvType.setStoreTermVectorPositions(true);
+ tvType.setStoreTermVectorOffsets(true);
+ tvType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS);
+
+ Field field = new Field("content", content, tvType);
+ doc.add(field);
+ writer.addDocument(doc);
+ }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/codecs/lucene103/TestLucene103PostingsFormat.java b/lucene/core/src/test/org/apache/lucene/codecs/lucene103/TestLucene103PostingsFormat.java
index 210fde9fa23..bca6e565bf7 100644
--- a/lucene/core/src/test/org/apache/lucene/codecs/lucene103/TestLucene103PostingsFormat.java
+++ b/lucene/core/src/test/org/apache/lucene/codecs/lucene103/TestLucene103PostingsFormat.java
@@ -20,6 +20,8 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+
+import org.apache.lucene.codecs.BinMapReader;
import org.apache.lucene.codecs.Codec;
import org.apache.lucene.codecs.CompetitiveImpactAccumulator;
import org.apache.lucene.codecs.lucene103.Lucene103PostingsReader.MutableImpactList;
@@ -27,10 +29,18 @@ import org.apache.lucene.codecs.lucene103.blocktree.FieldReader;
import org.apache.lucene.codecs.lucene103.blocktree.Stats;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
+import org.apache.lucene.document.FieldType;
+import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.FilterLeafReader;
import org.apache.lucene.index.Impact;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.LeafReader;
+import org.apache.lucene.index.SegmentCommitInfo;
+import org.apache.lucene.index.SegmentInfos;
+import org.apache.lucene.index.SegmentReadState;
+import org.apache.lucene.index.SegmentReader;
import org.apache.lucene.store.ByteArrayDataInput;
import org.apache.lucene.store.ByteArrayDataOutput;
import org.apache.lucene.store.Directory;
@@ -134,6 +144,243 @@ public class TestLucene103PostingsFormat extends BasePostingsFormatTestCase {
new Impact(1005, -3L)));
}
+ public void testBinMapIsWrittenOnFlush() throws Exception {
+ try (Directory dir = newDirectory()) {
+ IndexWriterConfig iwc = new IndexWriterConfig(new MockAnalyzer(random()));
+ iwc.setCodec(getCodec());
+ iwc.setUseCompoundFile(false);
+
+ try (IndexWriter writer = new IndexWriter(dir, iwc)) {
+ for (int i = 0; i < 3; i++) {
+ Document doc = new Document();
+ FieldType ft = new FieldType(TextField.TYPE_STORED);
+ ft.setStoreTermVectors(true);
+ ft.putAttribute("doBinning", "true");
+ Field field = new Field("field", "value" + i, ft);
+ doc.add(field);
+ writer.addDocument(doc);
+ }
+ writer.commit();
+ }
+
+ SegmentInfos infos = SegmentInfos.readLatestCommit(dir);
+ SegmentCommitInfo info = infos.info(0);
+ String segmentName = info.info.name;
+ String segmentSuffix = null;
+ String binmapFile = null;
+
+ for (String file : dir.listAll()) {
+ if (file.startsWith(segmentName + "_") && file.endsWith(".binmap")) {
+ binmapFile = file;
+ segmentSuffix =
+ file.substring(segmentName.length() + 1, file.length() - ".binmap".length());
+ break;
+ }
+ }
+
+ assertNotNull("binmap file should exist", binmapFile);
+
+ SegmentReadState readState =
+ new SegmentReadState(dir, info.info, null, newIOContext(random()), segmentSuffix);
+
+ try (BinMapReader binMap = new BinMapReader(dir, readState)) {
+ assertEquals(3, binMap.getBinArrayCopy().length);
+ for (int i = 0; i < 3; i++) {
+ int bin = binMap.getBin(i);
+ assertTrue("Bin should be non-negative", bin >= 0);
+ }
+ }
+ }
+ }
+
+ public void testBinMapWithCompoundFileFormat() throws Exception {
+ try (Directory dir = newDirectory()) {
+ IndexWriterConfig iwc = new IndexWriterConfig(new MockAnalyzer(random()));
+ iwc.setCodec(getCodec());
+ iwc.setUseCompoundFile(true);
+
+ try (IndexWriter writer = new IndexWriter(dir, iwc)) {
+ for (int i = 0; i < 4; i++) {
+ Document doc = new Document();
+ FieldType ft = new FieldType(TextField.TYPE_STORED);
+ ft.setStoreTermVectors(true);
+ ft.putAttribute("doBinning", "true");
+ Field field = new Field("field", "term" + i, ft);
+ doc.add(field);
+ writer.addDocument(doc);
+ }
+ writer.commit();
+ }
+
+ SegmentInfos infos = SegmentInfos.readLatestCommit(dir);
+ SegmentCommitInfo info = infos.info(0);
+ String segmentName = info.info.name;
+
+ try (Directory cfsDir =
+ info.info.getCodec().compoundFormat().getCompoundReader(dir, info.info)) {
+ String binmapFile = null;
+ String segmentSuffix = null;
+ for (String file : cfsDir.listAll()) {
+ if (file.startsWith(segmentName + "_") && file.endsWith(".binmap")) {
+ binmapFile = file;
+ segmentSuffix =
+ file.substring(segmentName.length() + 1, file.length() - ".binmap".length());
+ break;
+ }
+ }
+
+ assertNotNull("compound file should contain binmap", binmapFile);
+
+ SegmentReadState readState =
+ new SegmentReadState(cfsDir, info.info, null, newIOContext(random()), segmentSuffix);
+
+ try (BinMapReader binMap = new BinMapReader(cfsDir, readState)) {
+ assertEquals(4, binMap.getBinArrayCopy().length);
+ for (int i = 0; i < 4; i++) {
+ int bin = binMap.getBin(i);
+ assertTrue("Bin should be non-negative", bin >= 0);
+ }
+ }
+ }
+ }
+ }
+
+ public void testBinMapBinningDisabled() throws Exception {
+ try (Directory dir = newDirectory()) {
+ IndexWriterConfig iwc = new IndexWriterConfig(new MockAnalyzer(random()));
+ iwc.setCodec(getCodec());
+ iwc.setUseCompoundFile(false); // standalone files
+
+ try (IndexWriter writer = new IndexWriter(dir, iwc)) {
+ Document doc = new Document();
+ doc.add(newTextField("field", "term value relevance", Field.Store.NO)); // no binning
+ writer.addDocument(doc);
+ writer.commit();
+ }
+
+ for (String file : dir.listAll()) {
+ assertFalse(
+ "binmap should not exist when binning is not enabled", file.endsWith(".binmap"));
+ }
+ }
+ }
+
+ public void testExplicitBinCountIsHonored() throws Exception {
+ try (Directory dir = newDirectory()) {
+ IndexWriterConfig iwc = new IndexWriterConfig(new MockAnalyzer(random()));
+ iwc.setCodec(getCodec());
+ iwc.setUseCompoundFile(false);
+
+ try (IndexWriter writer = new IndexWriter(dir, iwc)) {
+ for (int i = 0; i < 12; i++) {
+ Document doc = new Document();
+ FieldType ft = new FieldType(TextField.TYPE_STORED);
+ ft.setStoreTermVectors(true);
+ ft.putAttribute("doBinning", "true");
+ ft.putAttribute("bin.count", "4");
+ Field field = new Field("field", "lucene term doc", ft);
+ doc.add(field);
+ writer.addDocument(doc);
+ }
+ writer.commit();
+ }
+
+ SegmentInfos infos = SegmentInfos.readLatestCommit(dir);
+ SegmentCommitInfo info = infos.info(0);
+
+ String segmentName = info.info.name;
+ String segmentSuffix = null;
+ String binmapFile = null;
+
+ for (String file : dir.listAll()) {
+ if (file.startsWith(segmentName + "_") && file.endsWith(".binmap")) {
+ binmapFile = file;
+ segmentSuffix =
+ file.substring(segmentName.length() + 1, file.length() - ".binmap".length());
+ break;
+ }
+ }
+
+ assertNotNull("binmap file should exist", binmapFile);
+
+ SegmentReadState readState =
+ new SegmentReadState(dir, info.info, null, newIOContext(random()), segmentSuffix);
+
+ try (BinMapReader binMap = new BinMapReader(dir, readState)) {
+ assertEquals("Expected 4 bins", 4, binMap.getBinCount());
+ int[] bins = binMap.getBinArrayCopy();
+ assertEquals(12, bins.length);
+ for (int bin : bins) {
+ assertTrue("bin id should be in [0, 3]", bin >= 0 && bin < 4);
+ }
+ }
+ }
+ }
+
+ public void testBinMapIsPreservedAndUsableAfterMerge() throws Exception {
+ try (Directory dir = newDirectory()) {
+ IndexWriterConfig iwc = new IndexWriterConfig(new MockAnalyzer(random()));
+ iwc.setCodec(getCodec());
+ iwc.setUseCompoundFile(random().nextBoolean());
+ iwc.setMaxBufferedDocs(2);
+
+ FieldType ft = new FieldType(TextField.TYPE_NOT_STORED);
+ ft.setStoreTermVectors(false);
+ ft.putAttribute("postingsFormat", "Lucene103");
+ ft.putAttribute("doBinning", "true");
+ ft.putAttribute("bin.count", "4");
+ ft.freeze();
+
+ try (IndexWriter writer = new IndexWriter(dir, iwc)) {
+ for (int i = 0; i < 24; i++) {
+ Document doc = new Document();
+ doc.add(new Field("field", "lucene merge binning test", ft));
+ writer.addDocument(doc);
+ }
+ writer.commit();
+ writer.forceMerge(1);
+ }
+
+ // Confirm single merged segment exists
+ try (DirectoryReader reader = DirectoryReader.open(dir)) {
+ assertEquals("Expected single merged segment", 1, reader.leaves().size());
+
+ LeafReader leaf = reader.leaves().get(0).reader();
+ SegmentReader segmentReader = (SegmentReader) FilterLeafReader.unwrap(leaf);
+ SegmentCommitInfo info = segmentReader.getSegmentInfo();
+ String segmentName = info.info.name;
+
+ // Extract actual binmap file from Directory
+ String binmapFile = null;
+ for (String file : dir.listAll()) {
+ if (file.startsWith(segmentName + "_") && file.endsWith(".binmap")) {
+ binmapFile = file;
+ break;
+ }
+ }
+
+ assertNotNull("Expected .binmap file after merge", binmapFile);
+
+ String suffix =
+ binmapFile.substring(
+ segmentName.length() + 1, binmapFile.length() - ".binmap".length());
+
+ SegmentReadState readState =
+ new SegmentReadState(
+ dir, info.info, segmentReader.getFieldInfos(), newIOContext(random()), suffix);
+
+ try (BinMapReader binMap = new BinMapReader(dir, readState)) {
+ assertEquals("Expected 4 bins", 4, binMap.getBinCount());
+ int[] bins = binMap.getBinArrayCopy();
+ assertEquals("Unexpected number of docs", 24, bins.length);
+ for (int bin : bins) {
+ assertTrue("Invalid bin ID", bin >= 0 && bin < 4);
+ }
+ }
+ }
+ }
+ }
+
private void doTestImpactSerialization(List<Impact> impacts) throws IOException {
CompetitiveImpactAccumulator acc = new CompetitiveImpactAccumulator();
for (Impact impact : impacts) {
diff --git a/lucene/core/src/test/org/apache/lucene/index/TestBinScoreUtil.java b/lucene/core/src/test/org/apache/lucene/index/TestBinScoreUtil.java
new file mode 100644
index 00000000000..4b5d7c1b1b1
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/index/TestBinScoreUtil.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.index;
+
+import org.apache.lucene.codecs.lucene103.Lucene103PostingsFormat;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.FieldType;
+import org.apache.lucene.document.TextField;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.MMapDirectory;
+import org.apache.lucene.tests.analysis.MockAnalyzer;
+import org.apache.lucene.tests.util.LuceneTestCase;
+import org.apache.lucene.tests.util.TestUtil;
+
+import java.nio.file.Path;
+
+public class TestBinScoreUtil extends LuceneTestCase {
+
+ private static final String FIELD = "field";
+
+ private Directory dir;
+ private IndexWriter writer;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ Path path = createTempDir("binutil");
+ dir = new MMapDirectory(path);
+ IndexWriterConfig iwc = new IndexWriterConfig(new MockAnalyzer(random()));
+ iwc.setCodec(TestUtil.alwaysPostingsFormat(new Lucene103PostingsFormat()));
+ iwc.setUseCompoundFile(false);
+ writer = new IndexWriter(dir, iwc);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ writer.close();
+ dir.close();
+ }
+
+ public void testSingleSegmentWrapping() throws Exception {
+ for (int i = 0; i < 5; i++) {
+ Document doc = new Document();
+ doc.add(newBinningField(FIELD, "term " + i));
+ writer.addDocument(doc);
+ }
+ writer.commit();
+
+ try (DirectoryReader reader = DirectoryReader.open(dir)) {
+ IndexReader wrapped = BinScoreUtil.wrap(reader);
+ for (LeafReaderContext ctx : wrapped.leaves()) {
+ LeafReader leaf = ctx.reader();
+ assertTrue(leaf instanceof BinScoreLeafReader);
+ BinScoreReader bin = ((BinScoreLeafReader) leaf).getBinScoreReader();
+ assertTrue(bin.getBinCount() > 0);
+ for (int docID = 0; docID < leaf.maxDoc(); docID++) {
+ int b = bin.getBinForDoc(docID);
+ assertTrue("bin must be non-negative", b >= 0);
+ }
+ }
+ }
+ }
+
+ public void testMultiSegmentWrapping() throws Exception {
+ for (int i = 0; i < 3; i++) {
+ Document doc = new Document();
+ doc.add(newBinningField(FIELD, "segment " + i));
+ writer.addDocument(doc);
+ writer.commit();
+ }
+
+ try (DirectoryReader reader = DirectoryReader.open(dir)) {
+ IndexReader wrapped = BinScoreUtil.wrap(reader);
+ for (LeafReaderContext ctx : wrapped.leaves()) {
+ LeafReader leaf = ctx.reader();
+ assertTrue(leaf instanceof BinScoreLeafReader);
+ }
+ }
+ }
+
+ public void testCompoundFileWrapping() throws Exception {
+ writer.getConfig().setUseCompoundFile(true);
+ for (int i = 0; i < 4; i++) {
+ Document doc = new Document();
+ doc.add(newBinningField(FIELD, "compound " + i));
+ writer.addDocument(doc);
+ }
+ writer.commit();
+
+ try (DirectoryReader reader = DirectoryReader.open(dir)) {
+ IndexReader wrapped = BinScoreUtil.wrap(reader);
+ for (LeafReaderContext ctx : wrapped.leaves()) {
+ LeafReader leaf = ctx.reader();
+ assertTrue(leaf instanceof BinScoreLeafReader);
+ }
+ }
+ }
+
+ private static Field newBinningField(String name, String value) {
+ FieldType type = new FieldType(TextField.TYPE_NOT_STORED);
+ type.setStoreTermVectors(true);
+ type.putAttribute("doBinning", "true");
+ type.freeze();
+ return new Field(name, value, type);
+ }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestAnytimeRankingNDCG.java b/lucene/core/src/test/org/apache/lucene/search/TestAnytimeRankingNDCG.java
new file mode 100644
index 00000000000..d7198c1f58d
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/search/TestAnytimeRankingNDCG.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.lucene.search;
+
+import org.apache.lucene.codecs.lucene101.Lucene101Codec;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.FieldType;
+import org.apache.lucene.document.TextField;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexOptions;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.similarities.BM25Similarity;
+import org.apache.lucene.store.ByteBuffersDirectory;
+import org.apache.lucene.tests.analysis.MockAnalyzer;
+import org.apache.lucene.tests.store.MockDirectoryWrapper;
+import org.apache.lucene.tests.util.LuceneTestCase;
+import org.apache.lucene.tests.util.TestUtil;
+
+import java.util.Arrays;
+
+public class TestAnytimeRankingNDCG extends LuceneTestCase {
+
+ public void testNDCGDoesNotDiverge() throws Exception {
+ MockDirectoryWrapper dir = new MockDirectoryWrapper(random(), new ByteBuffersDirectory());
+ IndexWriterConfig iwc = new IndexWriterConfig(new MockAnalyzer(random()));
+ iwc.setCodec(new Lucene101Codec());
+
+ FieldType ft = new FieldType(TextField.TYPE_NOT_STORED);
+ ft.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS);
+ ft.setTokenized(true);
+ ft.putAttribute("postingsFormat", "Lucene101");
+ ft.putAttribute("doBinning", "true");
+ ft.putAttribute("bin.count", "4");
+ ft.freeze();
+
+ try (IndexWriter writer = new IndexWriter(dir, iwc)) {
+ for (int i = 0; i < 500; i++) {
+ Document doc = new Document();
+ StringBuilder content = new StringBuilder();
+
+ if (i % 50 == 0) {
+ content.append("lucene high quality scoring search");
+ } else {
+ content.append("random filler ").append(TestUtil.randomSimpleString(random()));
+ }
+
+ doc.add(new Field("field", content.toString(), ft));
+ writer.addDocument(doc);
+ }
+ writer.commit();
+ }
+
+ DirectoryReader reader = DirectoryReader.open(dir);
+ IndexSearcher baseline = new IndexSearcher(reader);
+ baseline.setSimilarity(new BM25Similarity());
+
+ AnytimeRankingSearcher anytime = new AnytimeRankingSearcher(baseline, 10, 5, "field");
+
+ TopDocs baselineDocs = baseline.search(new TermQuery(new Term("field", "lucene")), 10);
+ TopDocs anytimeDocs = anytime.search(new TermQuery(new Term("field", "lucene")));
+
+ double baselineNDCG = computeNDCG(baselineDocs);
+ double anytimeNDCG = computeNDCG(anytimeDocs);
+
+ // Ensure that NDCG does not diverge by more than 0.1
+ assertEquals("NDCG values should be close", baselineNDCG, anytimeNDCG, 0.1);
+
+ reader.close();
+ dir.close();
+ }
+
+ private double computeNDCG(TopDocs topDocs) {
+ int[] gains = new int[topDocs.scoreDocs.length];
+ for (int i = 0; i < topDocs.scoreDocs.length; i++) {
+ gains[i] = (topDocs.scoreDocs[i].doc % 50 == 0) ? 3 : 0;
+ }
+
+ double dcg = 0.0;
+ for (int i = 0; i < gains.length; i++) {
+ int rel = gains[i];
+ if (rel > 0) {
+ dcg += (Math.pow(2, rel) - 1) / (Math.log(i + 2) / Math.log(2));
+ }
+ }
+
+ Arrays.sort(gains);
+ double idcg = 0.0;
+ for (int i = gains.length - 1; i >= 0; i--) {
+ int rel = gains[i];
+ if (rel > 0) {
+ int rank = gains.length - i;
+ idcg += (Math.pow(2, rel) - 1) / (Math.log(rank + 1) / Math.log(2));
+ }
+ }
+
+ return idcg == 0.0 ? 0.0 : dcg / idcg;
+ }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestAnytimeRankingRelevanceMatch.java b/lucene/core/src/test/org/apache/lucene/search/TestAnytimeRankingRelevanceMatch.java
new file mode 100644
index 00000000000..80d17887f3d
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/search/TestAnytimeRankingRelevanceMatch.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ */
+package org.apache.lucene.search;
+
+import org.apache.lucene.codecs.lucene101.Lucene101Codec;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.FieldType;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexOptions;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.similarities.BM25Similarity;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.MMapDirectory;
+import org.apache.lucene.tests.analysis.MockAnalyzer;
+import org.apache.lucene.tests.util.LuceneTestCase;
+import org.junit.Test;
+
+public class TestAnytimeRankingRelevanceMatch extends LuceneTestCase {
+
+ private static final int DOC_COUNT = 20000;
+ private static final int RELEVANT_FREQ = 200;
+
+ @Test
+ public void testRelevanceMetricsComparison() throws Exception {
+ Directory dir = new MMapDirectory(createTempDir("relevance-test"));
+ IndexWriterConfig config = new IndexWriterConfig(new MockAnalyzer(random()));
+ config.setCodec(new Lucene101Codec());
+ config.setUseCompoundFile(false);
+ config.setMaxBufferedDocs(100);
+
+ FieldType ft = new FieldType();
+ ft.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS);
+ ft.setTokenized(true);
+ ft.setStored(false);
+ ft.putAttribute("postingsFormat", "Lucene101");
+ ft.putAttribute("doBinning", "true");
+ ft.putAttribute("bin.count", "4");
+ ft.freeze();
+
+ try (IndexWriter writer = new IndexWriter(dir, config)) {
+ for (int i = 0; i < DOC_COUNT; i++) {
+ Document doc = new Document();
+ String content =
+ (i % RELEVANT_FREQ == 0) ? "lucene precision recall document" : "random filler content";
+ doc.add(new Field("field", content, ft));
+ writer.addDocument(doc);
+ }
+ writer.commit();
+ }
+
+ IndexReader reader = DirectoryReader.open(dir);
+ IndexSearcher baselineSearcher = new IndexSearcher(reader);
+ baselineSearcher.setSimilarity(new BM25Similarity());
+
+ AnytimeRankingSearcher anytime = new AnytimeRankingSearcher(baselineSearcher, 10, 3, "field");
+
+ TermQuery query = new TermQuery(new Term("field", "lucene"));
+ TopDocs baseline = baselineSearcher.search(query, 10);
+ TopDocs anytimeResults = anytime.search(query);
+
+ float baselineRecall = countRelevant(baseline) / 10.0f;
+ float anytimeRecall = countRelevant(anytimeResults) / 10.0f;
+
+ float baselineMRR = computeMRR(baseline);
+ float anytimeMRR = computeMRR(anytimeResults);
+
+ float baselineNDCG = computeNDCG(baseline);
+ float anytimeNDCG = computeNDCG(anytimeResults);
+
+ assertTrue("Anytime recall should be >= baseline", anytimeRecall >= baselineRecall);
+ assertTrue("Anytime MRR should be >= baseline", anytimeMRR >= baselineMRR);
+ assertTrue("Anytime NDCG should be >= baseline", anytimeNDCG >= baselineNDCG);
+
+ reader.close();
+ dir.close();
+ }
+
+ private int countRelevant(TopDocs topDocs) {
+ int relevant = 0;
+ for (ScoreDoc doc : topDocs.scoreDocs) {
+ if (doc.doc % RELEVANT_FREQ == 0) {
+ relevant++;
+ }
+ }
+ return relevant;
+ }
+
+ private float computeMRR(TopDocs topDocs) {
+ for (int i = 0; i < topDocs.scoreDocs.length; i++) {
+ if (topDocs.scoreDocs[i].doc % RELEVANT_FREQ == 0) {
+ return 1.0f / (i + 1);
+ }
+ }
+ return 0f;
+ }
+
+ private float computeNDCG(TopDocs topDocs) {
+ float dcg = 0f;
+ float idcg = 0f;
+ int count = 0;
+ for (int i = 0; i < topDocs.scoreDocs.length; i++) {
+ int rank = i + 1;
+ if (topDocs.scoreDocs[i].doc % RELEVANT_FREQ == 0) {
+ dcg += (float) (1.0 / (Math.log(rank + 1) / Math.log(2)));
+ count++;
+ }
+ }
+ for (int j = 1; j <= count; j++) {
+ idcg += (float) (1.0 / (Math.log(j + 1) / Math.log(2)));
+ }
+ return idcg > 0 ? dcg / idcg : 0f;
+ }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestAnytimeRankingRelevanceSearch.java b/lucene/core/src/test/org/apache/lucene/search/TestAnytimeRankingRelevanceSearch.java
new file mode 100644
index 00000000000..024e97eb2f2
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/search/TestAnytimeRankingRelevanceSearch.java
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.search;
+
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.FieldType;
+import org.apache.lucene.document.IntPoint;
+import org.apache.lucene.document.StoredField;
+import org.apache.lucene.document.TextField;
+import org.apache.lucene.index.BinScoreUtil;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexOptions;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.similarities.BM25Similarity;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.apache.lucene.tests.util.LuceneTestCase;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+public class TestAnytimeRankingRelevanceSearch extends LuceneTestCase {
+
+ private String indexPath;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ indexPath = Files.createTempDirectory("testindex_relevance").toAbsolutePath().toString();
+
+ try (Directory directory = FSDirectory.open(Paths.get(indexPath));
+ IndexWriter writer =
+ new IndexWriter(directory, new IndexWriterConfig(new StandardAnalyzer()))) {
+
+ for (int i = 1; i <= 50; i++) {
+ addDocument(
+ writer,
+ "Lucene test document "
+ + i
+ + " containing ranking relevance efficiency performance scoring",
+ i);
+ if (i % 5 == 0) {
+ addDocument(writer, "Lucene boosted search efficiency query expansion", i + 50);
+ }
+ if (i % 3 == 0) {
+ addDocument(writer, "Lucene document test ranking optimization BM25 tuning", i + 100);
+ }
+ if (i % 7 == 0) {
+ addDocument(writer, "Lucene scalable indexing techniques for optimal retrieval", i + 150);
+ }
+ }
+
+ writer.commit();
+ }
+ }
+
+ @Test
+ public void testRelevanceAcrossDiverseDocuments() throws Exception {
+ try (IndexReader reader =
+ BinScoreUtil.wrap(DirectoryReader.open(FSDirectory.open(Paths.get(indexPath))))) {
+ IndexSearcher searcher = newSearcher(reader);
+ searcher.setSimilarity(new BM25Similarity());
+
+ Query query = new TermQuery(new Term("content", "lucene"));
+ AnytimeRankingSearcher anytimeSearcher =
+ new AnytimeRankingSearcher(searcher, 10, 20, "content");
+ TopDocs results = anytimeSearcher.search(query);
+
+ assertNotNull("Results should not be null", results);
+ assertTrue("Results should be greater than zero", results.scoreDocs.length > 0);
+ }
+ }
+
+ public void testPerformanceQueries() throws Exception {
+ try (IndexReader reader =
+ BinScoreUtil.wrap(DirectoryReader.open(FSDirectory.open(Paths.get(indexPath))))) {
+ IndexSearcher searcher = newSearcher(reader);
+ searcher.setSimilarity(new BM25Similarity());
+
+ AnytimeRankingSearcher anytimeSearcher =
+ new AnytimeRankingSearcher(searcher, 15, 30, "content");
+ Query query = new TermQuery(new Term("content", "performance"));
+ TopDocs results = anytimeSearcher.search(query);
+
+ assertNotNull("Results should not be null", results);
+ assertTrue("Results should be greater than zero", results.scoreDocs.length > 0);
+ }
+ }
+
+ @Test
+ public void testBM25ScoringDiversity() throws Exception {
+ try (IndexReader reader =
+ BinScoreUtil.wrap(DirectoryReader.open(FSDirectory.open(Paths.get(indexPath))))) {
+ IndexSearcher searcher = newSearcher(reader);
+ searcher.setSimilarity(new BM25Similarity());
+
+ AnytimeRankingSearcher anytimeSearcher =
+ new AnytimeRankingSearcher(searcher, 10, 50, "content");
+ Query query = new TermQuery(new Term("content", "scoring"));
+ TopDocs results = anytimeSearcher.search(query);
+
+ assertNotNull("Results should not be null", results);
+ assertTrue("Results should be greater than zero", results.scoreDocs.length > 0);
+ }
+ }
+
+ @Test
+ public void testEmptyQuery() throws Exception {
+ try (IndexReader reader =
+ BinScoreUtil.wrap(DirectoryReader.open(FSDirectory.open(Paths.get(indexPath))))) {
+ IndexSearcher searcher = newSearcher(reader);
+ searcher.setSimilarity(new BM25Similarity());
+
+ AnytimeRankingSearcher anytimeSearcher =
+ new AnytimeRankingSearcher(searcher, 5, 20, "content");
+ Query query = new TermQuery(new Term("content", " "));
+ TopDocs results = anytimeSearcher.search(query);
+
+ assertNotNull("Empty queries should return zero results", results);
+ assertEquals("Results should be zero for an empty query", 0, results.scoreDocs.length);
+ }
+ }
+
+ @Test
+ public void testBinAwareScoringImpact() throws Exception {
+ String path = Files.createTempDirectory("test-bin-aware").toString();
+ try (Directory dir = FSDirectory.open(Paths.get(path));
+ IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig(new StandardAnalyzer()))) {
+ for (int i = 0; i < 100; i++) {
+ Document doc = new Document();
+ doc.add(new TextField("content", "lucene bin test", Field.Store.NO));
+ doc.add(new IntPoint("docID", i));
+ writer.addDocument(doc);
+ }
+ writer.commit();
+ }
+
+ try (IndexReader reader =
+ BinScoreUtil.wrap(DirectoryReader.open(FSDirectory.open(Paths.get(indexPath))))) {
+ IndexSearcher searcher = newSearcher(reader);
+ AnytimeRankingSearcher ars = new AnytimeRankingSearcher(searcher, 5, 20, "content");
+ TopDocs results = ars.search(new TermQuery(new Term("content", "lucene")));
+ assertTrue(results.scoreDocs.length > 0);
+ }
+ }
+
+ @Test
+ public void testMultiSegmentIndexScoring() throws Exception {
+ Directory dir = newDirectory();
+ IndexWriterConfig config = new IndexWriterConfig(new StandardAnalyzer());
+ config.setMaxBufferedDocs(10); // force small segments
+ try (IndexWriter writer = new IndexWriter(dir, config)) {
+ for (int i = 0; i < 100; i++) {
+ Document doc = new Document();
+ doc.add(new TextField("content", "lucene segment scoring", Field.Store.NO));
+ doc.add(new IntPoint("docID", i));
+ writer.addDocument(doc);
+ }
+ writer.commit(); // multiple segments
+ }
+
+ try (IndexReader reader = DirectoryReader.open(dir)) {
+ assertTrue("Should have multiple segments", reader.leaves().size() > 1);
+
+ IndexSearcher searcher = newSearcher(reader);
+ AnytimeRankingSearcher anytimeSearcher =
+ new AnytimeRankingSearcher(searcher, 10, 50, "content");
+ TopDocs topDocs = anytimeSearcher.search(new TermQuery(new Term("content", "lucene")));
+
+ assertNotNull("TopDocs should not be null", topDocs);
+ assertTrue("Should return some results", topDocs.scoreDocs.length > 0);
+ }
+
+ dir.close();
+ }
+
+ private void addDocument(IndexWriter writer, String content, int docID) throws IOException {
+ FieldType type = new FieldType();
+ type.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS);
+ type.setTokenized(true);
+ type.setStored(true);
+ type.setStoreTermVectors(true);
+ type.putAttribute("doBinning", "true");
+ type.freeze();
+
+ Document doc = new Document();
+ doc.add(new IntPoint("docID", docID));
+ doc.add(new StoredField("docID", docID));
+ doc.add(new Field("content", content, type));
+
+ writer.addDocument(doc);
+ }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestAnytimeRankingSearch.java b/lucene/core/src/test/org/apache/lucene/search/TestAnytimeRankingSearch.java
new file mode 100644
index 00000000000..13aaba0a593
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/search/TestAnytimeRankingSearch.java
@@ -0,0 +1,216 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.lucene.search;
+
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.codecs.Codec;
+import org.apache.lucene.codecs.lucene101.Lucene101Codec;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.FieldType;
+import org.apache.lucene.document.IntPoint;
+import org.apache.lucene.document.StoredField;
+import org.apache.lucene.index.BinScoreReader;
+import org.apache.lucene.index.BinScoreUtil;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexOptions;
+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.NoMergePolicy;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.similarities.BM25Similarity;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.apache.lucene.tests.util.LuceneTestCase;
+import org.apache.lucene.tests.util.TestUtil;
+import org.apache.lucene.util.IOUtils;
+import org.apache.lucene.util.NamedThreadFactory;
+import org.junit.Test;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+public class TestAnytimeRankingSearch extends LuceneTestCase {
+
+ private FieldType newBinningFieldType() {
+ FieldType ft = new FieldType();
+ ft.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS);
+ ft.setTokenized(true);
+ ft.setStored(false);
+ ft.setStoreTermVectors(true);
+ ft.setStoreTermVectorPositions(true);
+ ft.setStoreTermVectorOffsets(true);
+ ft.setStoreTermVectorPayloads(true);
+ ft.putAttribute("postingsFormat", "Lucene101");
+ ft.putAttribute("doBinning", "true");
+ ft.putAttribute("bin.count", "4");
+ ft.freeze();
+ return ft;
+ }
+
+ private Document newDoc(int docID, String content, FieldType ft) {
+ Document doc = new Document();
+ doc.add(new IntPoint("docID", docID));
+ doc.add(new StoredField("docID", docID));
+ doc.add(new Field("content", content, ft));
+ return doc;
+ }
+
+ @Test
+ public void testSparseBinDistribution() throws Exception {
+ try (Directory dir = newDirectory()) {
+ IndexWriterConfig iwc = new IndexWriterConfig(new StandardAnalyzer());
+ iwc.setMaxBufferedDocs(5);
+ iwc.setMergePolicy(NoMergePolicy.INSTANCE);
+
+ FieldType ft = newBinningFieldType();
+
+ try (IndexWriter writer = new IndexWriter(dir, iwc)) {
+ for (int i = 0; i < 64; i++) {
+ String content = (i % 8 == 0) ? "lucene boost" : "noise filler content";
+ writer.addDocument(newDoc(i, content, ft));
+ }
+ writer.commit();
+ }
+
+ IndexReader base = DirectoryReader.open(dir);
+ IndexReader wrapped = BinScoreUtil.wrap(base);
+
+ try {
+ IndexSearcher searcher = newSearcher(wrapped);
+ searcher.setSimilarity(new BM25Similarity());
+
+ AnytimeRankingSearcher rankingSearcher =
+ new AnytimeRankingSearcher(searcher, 10, 100, "content");
+
+ TopDocs topDocs = rankingSearcher.search(new TermQuery(new Term("content", "lucene")));
+
+ assertNotNull(topDocs);
+ assertTrue("Expected some results", topDocs.scoreDocs.length > 0);
+
+ int bin0Hits = 0;
+ int total = 0;
+
+ for (ScoreDoc sd : topDocs.scoreDocs) {
+ int docID = sd.doc;
+ for (LeafReaderContext ctx : wrapped.leaves()) {
+ if (docID >= ctx.docBase && docID < ctx.docBase + ctx.reader().maxDoc()) {
+ int segDoc = docID - ctx.docBase;
+ BinScoreReader binReader = BinScoreUtil.getBinScoreReader(ctx.reader());
+ if (binReader != null) {
+ int bin = binReader.getBinForDoc(segDoc);
+ System.out.println("doc" + segDoc + " bin " + bin);
+ if (bin == 0) {
+ bin0Hits++;
+ }
+ total++;
+ }
+ break;
+ }
+ }
+ }
+
+ assertTrue(
+ "Bin 0 should dominate in sparse distribution, got " + bin0Hits + " of " + total,
+ total > 0 && bin0Hits >= total / 2);
+ } finally {
+ IOUtils.close(wrapped, base);
+ }
+ }
+ }
+
+ @Test
+ public void testSlaCutoffTriggersEarlyTermination() throws Exception {
+ Codec.setDefault(new Lucene101Codec());
+
+ Path indexPath = Files.createTempDirectory("testindex_sla_cutoff");
+ try (Directory dir = FSDirectory.open(indexPath);
+ IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig(new StandardAnalyzer()))) {
+
+ FieldType ft = newBinningFieldType();
+ for (int i = 0; i < 500; i++) {
+ writer.addDocument(newDoc(i, "lucene boost fast search scoring ranking", ft));
+ }
+ writer.commit();
+
+ IndexReader base = DirectoryReader.open(dir);
+ IndexReader wrapped = BinScoreUtil.wrap(base);
+ try {
+ IndexSearcher searcher = newSearcher(wrapped);
+ searcher.setSimilarity(new BM25Similarity());
+ AnytimeRankingSearcher s = new AnytimeRankingSearcher(searcher, 10, 1, "content");
+
+ TopDocs docs = s.search(new TermQuery(new Term("content", "lucene")));
+ assertNotNull(docs);
+ assertTrue("Expect partial results under tight SLA", docs.scoreDocs.length > 0);
+ } finally {
+ IOUtils.close(wrapped, base);
+ }
+ }
+ }
+
+ @Test
+ public void testMultiThreadedQueries() throws Exception {
+ Path path = Files.createTempDirectory("multiThreadedQuery");
+ try (Directory dir = FSDirectory.open(path);
+ IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig(new StandardAnalyzer()))) {
+
+ FieldType ft = newBinningFieldType();
+ for (int i = 0; i < 1000; i++) {
+ String content =
+ "lucene relevance scoring performance content " + TestUtil.randomSimpleString(random());
+ writer.addDocument(newDoc(i, content, ft));
+ }
+ writer.commit();
+
+ IndexReader base = DirectoryReader.open(dir);
+ IndexReader wrapped = BinScoreUtil.wrap(base);
+ try {
+ IndexSearcher searcher = newSearcher(wrapped);
+ searcher.setSimilarity(new BM25Similarity(2.0f, 0.2f));
+ AnytimeRankingSearcher anytimeSearcher =
+ new AnytimeRankingSearcher(searcher, 10, 100, "content");
+
+ ExecutorService exec =
+ Executors.newFixedThreadPool(4, new NamedThreadFactory("test-search"));
+ List<Future<TopDocs>> futures = new ArrayList<>();
+
+ Query query = new TermQuery(new Term("content", "lucene"));
+ for (int i = 0; i < 4; i++) {
+ futures.add(exec.submit(() -> anytimeSearcher.search(query)));
+ }
+
+ for (Future<TopDocs> f : futures) {
+ TopDocs d = f.get();
+ assertNotNull(d);
+ assertTrue(d.scoreDocs.length > 0);
+ }
+ exec.shutdown();
+ } finally {
+ IOUtils.close(wrapped, base);
+ }
+ }
+ }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestBinningRelevanceBoost.java b/lucene/core/src/test/org/apache/lucene/search/TestBinningRelevanceBoost.java
new file mode 100644
index 00000000000..801e6715fbc
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/search/TestBinningRelevanceBoost.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.search;
+
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.FieldType;
+import org.apache.lucene.document.StoredField;
+import org.apache.lucene.document.TextField;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexOptions;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.similarities.BM25Similarity;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.tests.index.RandomIndexWriter;
+import org.apache.lucene.tests.util.LuceneTestCase;
+import org.apache.lucene.tests.util.TestUtil;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class TestBinningRelevanceBoost extends LuceneTestCase {
+
+ public void testBinningImprovesRecallUnderTruncation() throws Exception {
+ final int totalDocs = 5000;
+ final int relevantEvery = 200;
+ final int truncationLimit = 20;
+
+ Directory dir = newDirectory();
+ IndexWriterConfig iwc = newIndexWriterConfig();
+ iwc.setCodec(TestUtil.getDefaultCodec());
+ iwc.setMaxBufferedDocs(100);
+ iwc.setUseCompoundFile(false);
+
+ FieldType ft = new FieldType(TextField.TYPE_NOT_STORED);
+ ft.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS);
+ ft.setTokenized(true);
+ ft.putAttribute("postingsFormat", "Lucene101");
+ ft.putAttribute("doBinning", "true");
+ ft.putAttribute("bin.count", "4");
+ ft.freeze();
+
+ try (RandomIndexWriter writer = new RandomIndexWriter(random(), dir, iwc)) {
+ for (int i = 0; i < totalDocs; i++) {
+ Document doc = new Document();
+ String content =
+ (i % relevantEvery == 0) ? "lucene highly relevant text" : "noise filler irrelevant";
+ doc.add(new StoredField("docID", i));
+ doc.add(new Field("content", content, ft));
+ writer.addDocument(doc);
+ }
+ writer.commit();
+ }
+
+ IndexReader reader = DirectoryReader.open(dir);
+ IndexSearcher baseline = newSearcher(reader);
+ baseline.setSimilarity(new BM25Similarity());
+
+ AnytimeRankingSearcher anytimeRankingSearcher =
+ new AnytimeRankingSearcher(baseline, 10, truncationLimit, "content");
+
+ TermQuery query = new TermQuery(new Term("content", "lucene"));
+
+ AtomicInteger baselineHits = new AtomicInteger();
+ baseline.search(
+ query,
+ new CollectorManager<SimpleCollector, Void>() {
+ @Override
+ public SimpleCollector newCollector() {
+ return new SimpleCollector() {
+ private int seen = 0;
+
+ @Override
+ public void collect(int doc) throws IOException {
+ if (doc % relevantEvery == 0) {
+ baselineHits.incrementAndGet();
+ }
+ if (++seen >= truncationLimit) {
+ throw new CollectionTerminatedException();
+ }
+ }
+
+ @Override
+ public void doSetNextReader(LeafReaderContext context) {}
+
+ @Override
+ public ScoreMode scoreMode() {
+ return ScoreMode.COMPLETE_NO_SCORES;
+ }
+ };
+ }
+
+ @Override
+ public Void reduce(Collection<SimpleCollector> collectors) {
+ return null;
+ }
+ });
+
+ TopDocs anytimeRankingResults = anytimeRankingSearcher.search(query);
+ int anytimeRankingHits = 0;
+ for (ScoreDoc sd : anytimeRankingResults.scoreDocs) {
+ if (sd.doc % relevantEvery == 0) {
+ anytimeRankingHits++;
+ }
+ }
+
+ assertTrue(
+ "AnytimeRankingSearch should retrieve more relevant results under truncation. "
+ + "Baseline hits="
+ + baselineHits.get()
+ + ", AnytimeRankingSearch hits="
+ + anytimeRankingHits,
+ anytimeRankingHits > baselineHits.get());
+
+ reader.close();
+ dir.close();
+ }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/util/TestHeuristicSLAEstimator.java b/lucene/core/src/test/org/apache/lucene/util/TestHeuristicSLAEstimator.java
new file mode 100644
index 00000000000..5251d4e7ea0
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/util/TestHeuristicSLAEstimator.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.util;
+
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+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.Term;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.DisjunctionMaxQuery;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.PhraseQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.apache.lucene.tests.util.LuceneTestCase;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+public class TestHeuristicSLAEstimator extends LuceneTestCase {
+
+ private static final String FIELD = "content";
+ private IndexReader reader;
+ private HeuristicSLAEstimator estimator;
+
+ @Before
+ public void setup() throws Exception {
+ String indexPath = Files.createTempDirectory("testindex1").toAbsolutePath().toString();
+ Directory dir = FSDirectory.open(Paths.get(indexPath));
+ IndexWriterConfig config = new IndexWriterConfig(new StandardAnalyzer());
+ IndexWriter writer = new IndexWriter(dir, config);
+
+ for (int i = 0; i < 100; i++) {
+ Document doc = new Document();
+ doc.add(
+ new TextField(
+ FIELD, "lucene search relevance ranking performance document", Field.Store.NO));
+ writer.addDocument(doc);
+ }
+
+ writer.commit();
+ writer.close();
+
+ reader = DirectoryReader.open(dir);
+ estimator = new HeuristicSLAEstimator(FIELD);
+ }
+
+ @Test
+ public void testTermQueryEstimate() throws IOException {
+ Query query = new TermQuery(new Term(FIELD, "lucene"));
+ double estimate = estimator.estimate(query, reader, 100);
+ assertTrue("Estimate should be >= base SLA", estimate >= 100);
+ }
+
+ @Test
+ public void testPhraseQueryEstimate() throws IOException {
+ PhraseQuery query = new PhraseQuery(FIELD, "lucene", "search", "ranking");
+ double estimate = estimator.estimate(query, reader, 100);
+ assertTrue("Phrase query should increase SLA", estimate > 100);
+ }
+
+ @Test
+ public void testBooleanQueryEstimate() throws IOException {
+ BooleanQuery.Builder builder = new BooleanQuery.Builder();
+ builder.add(new TermQuery(new Term(FIELD, "lucene")), BooleanClause.Occur.MUST);
+ builder.add(new TermQuery(new Term(FIELD, "search")), BooleanClause.Occur.SHOULD);
+ Query query = builder.build();
+ double estimate = estimator.estimate(query, reader, 100);
+ assertTrue("Boolean query should increase SLA", estimate > 100);
+ }
+
+ @Test
+ public void testDisjunctionMaxQueryEstimate() throws IOException {
+ DisjunctionMaxQuery query =
+ new DisjunctionMaxQuery(
+ java.util.Arrays.asList(
+ new TermQuery(new Term(FIELD, "lucene")),
+ new TermQuery(new Term(FIELD, "ranking"))),
+ 0.1f);
+ double estimate = estimator.estimate(query, reader, 100);
+ assertTrue("DisjunctionMaxQuery should increase SLA", estimate > 100);
+ }
+
+ @Test
+ public void testNestedBooleanQueryEstimate() throws IOException {
+ BooleanQuery.Builder inner = new BooleanQuery.Builder();
+ inner.add(new TermQuery(new Term(FIELD, "lucene")), BooleanClause.Occur.MUST);
+ inner.add(new TermQuery(new Term(FIELD, "search")), BooleanClause.Occur.MUST);
+
+ BooleanQuery.Builder outer = new BooleanQuery.Builder();
+ outer.add(inner.build(), BooleanClause.Occur.MUST);
+ outer.add(new TermQuery(new Term(FIELD, "ranking")), BooleanClause.Occur.SHOULD);
+
+ Query query = outer.build();
+ double estimate = estimator.estimate(query, reader, 100);
+ assertTrue("Nested Boolean query should increase SLA", estimate > 100);
+ }
+
+ @Test
+ public void testMatchAllDocsQueryEstimate() throws IOException {
+ Query query = new MatchAllDocsQuery();
+ double estimate = estimator.estimate(query, reader, 100);
+ assertTrue("MatchAllDocsQuery should be close to base SLA", estimate >= 100);
+ }
+}