You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by be...@apache.org on 2025/02/11 19:20:03 UTC

(lucene) branch branch_10x updated: Bugfix/fix hnsw search termination check (#14215)

This is an automated email from the ASF dual-hosted git repository.

benwtrent pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/lucene.git


The following commit(s) were added to refs/heads/branch_10x by this push:
     new 430411a833b Bugfix/fix hnsw search termination check (#14215)
430411a833b is described below

commit 430411a833bb65620ce08f342af9b4d9835531c4
Author: Benjamin Trent <be...@gmail.com>
AuthorDate: Tue Feb 11 14:05:26 2025 -0500

    Bugfix/fix hnsw search termination check (#14215)
    
    previously related PR: https://github.com/apache/lucene/pull/12770
    
    While my original change to help move us towards a saner HNSW search behavior, it is will still actually explore a candidate if its score is `==` min accepted. This will devolve in the degenerate case where all vectors are the same.
    
    This change adjusts minimum required candidate score to match `Math.nextUp`, similar to TopScoreDocCollector
    related to (but doesn't fully solve): https://github.com/apache/lucene/issues/14214
---
 lucene/CHANGES.txt                                 |  2 ++
 .../apache/lucene/util/hnsw/HnswGraphSearcher.java |  4 ++--
 .../apache/lucene/document/TestManyKnnDocs.java    | 24 +++++++++++++++++++++-
 3 files changed, 27 insertions(+), 3 deletions(-)

diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index dd3dbac10fb..8ddf8b6e6a9 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -82,6 +82,8 @@ Bug Fixes
 * GITHUB#14126: Avoid overflow in index input slices invariant checks
   (Chris Hegarty)
 
+* GITHUB#14215: Fix degenerate case in HNSW where all vectors have the same score. (Ben Trent)
+
 Other
 ---------------------
 
diff --git a/lucene/core/src/java/org/apache/lucene/util/hnsw/HnswGraphSearcher.java b/lucene/core/src/java/org/apache/lucene/util/hnsw/HnswGraphSearcher.java
index a414bb9dfe0..9278c252541 100644
--- a/lucene/core/src/java/org/apache/lucene/util/hnsw/HnswGraphSearcher.java
+++ b/lucene/core/src/java/org/apache/lucene/util/hnsw/HnswGraphSearcher.java
@@ -212,7 +212,7 @@ public class HnswGraphSearcher extends AbstractHnswGraphSearcher {
 
     // A bound that holds the minimum similarity to the query vector that a candidate vector must
     // have to be considered.
-    float minAcceptedSimilarity = results.minCompetitiveSimilarity();
+    float minAcceptedSimilarity = Math.nextUp(results.minCompetitiveSimilarity());
     while (candidates.size() > 0 && results.earlyTerminated() == false) {
       // get the best candidate (closest or best scoring)
       float topCandidateSimilarity = candidates.topScore();
@@ -238,7 +238,7 @@ public class HnswGraphSearcher extends AbstractHnswGraphSearcher {
           candidates.add(friendOrd, friendSimilarity);
           if (acceptOrds == null || acceptOrds.get(friendOrd)) {
             if (results.collect(friendOrd, friendSimilarity)) {
-              minAcceptedSimilarity = results.minCompetitiveSimilarity();
+              minAcceptedSimilarity = Math.nextUp(results.minCompetitiveSimilarity());
             }
           }
         }
diff --git a/lucene/core/src/test/org/apache/lucene/document/TestManyKnnDocs.java b/lucene/core/src/test/org/apache/lucene/document/TestManyKnnDocs.java
index 2023ee73391..9130a2bf311 100644
--- a/lucene/core/src/test/org/apache/lucene/document/TestManyKnnDocs.java
+++ b/lucene/core/src/test/org/apache/lucene/document/TestManyKnnDocs.java
@@ -16,7 +16,11 @@
  */
 package org.apache.lucene.document;
 
+import static org.apache.lucene.index.VectorSimilarityFunction.DOT_PRODUCT;
+
 import com.carrotsearch.randomizedtesting.annotations.TimeoutSuite;
+import java.io.IOException;
+import java.util.Arrays;
 import org.apache.lucene.index.DirectoryReader;
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.index.IndexWriterConfig;
@@ -36,6 +40,24 @@ import org.apache.lucene.tests.util.LuceneTestCase.Monster;
 public class TestManyKnnDocs extends LuceneTestCase {
   // gradlew -p lucene/core test --tests TestManyKnnDocs -Ptests.heapsize=16g -Dtests.monster=true
 
+  public void testSameVectorIndexedMultipleTimes() throws IOException {
+    try (Directory d = newDirectory()) {
+      float[] vector = new float[16];
+      Arrays.fill(vector, 0.5f);
+      try (IndexWriter w = new IndexWriter(d, new IndexWriterConfig())) {
+        for (int j = 1; j <= 100_000; j++) {
+          Document doc = new Document();
+          doc.add(new KnnFloatVectorField("field", vector, DOT_PRODUCT));
+          w.addDocument(doc);
+          if (j % 1000 == 0) {
+            w.flush();
+          }
+        }
+        w.commit();
+      }
+    }
+  }
+
   public void testLargeSegment() throws Exception {
     IndexWriterConfig iwc = new IndexWriterConfig();
     iwc.setCodec(
@@ -47,7 +69,7 @@ public class TestManyKnnDocs extends LuceneTestCase {
     mp.setSegmentsPerTier(256); // only merge once at the end when we ask
     iwc.setMergePolicy(mp);
     String fieldName = "field";
-    VectorSimilarityFunction similarityFunction = VectorSimilarityFunction.DOT_PRODUCT;
+    VectorSimilarityFunction similarityFunction = DOT_PRODUCT;
 
     try (Directory dir = FSDirectory.open(createTempDir("ManyKnnVectorDocs"));
         IndexWriter iw = new IndexWriter(dir, iwc)) {