Repository: alexklibisz/elastiknn (language: python)

Issue:
Query explanation is not implemented
https://github.com/alexklibisz/elastiknn/blob/af673dad0a392f74695535f7f817fa23852a362e/elastiknn-lucene/src/main/java/org/apache/lucene/search/MatchHashesAndScoreQuery.java#L158

I didn't really know what the explanation should return, so I just put a dummy value and message to satisfy the API. If anyone knows what this should return, please let me know. I've personally never had any need to use this part of the ES API.


Discussion / hints:

Fix patch:
diff --git a/changelog.md b/changelog.md
index 135639f55..378e76908 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,5 @@
+- elastiknn-lucene: implemented proper explanation for the MatchHashesAndScoreQuery. 
+---
 - Plugin: when the `index.elastiknn` setting is `true`, Elastiknn will now use the `Lucene87Codec` with the `BEST_SPEED` setting for storing doc values. 
   It was previously using the `Lucene70DocValuesFormat`, which did not compress doc values as aggressively. 
   Since Lucene 8.8 added configurable compression, we can use Lucene87Codec and just provide the BEST_SPEED setting.
diff --git a/elastiknn-benchmarks/src/main/scala/com/klibisz/elastiknn/benchmarks/LocalBenchmark.scala b/elastiknn-benchmarks/src/main/scala/com/klibisz/elastiknn/benchmarks/LocalBenchmark.scala
index b252f8d2e..2602387e6 100644
--- a/elastiknn-benchmarks/src/main/scala/com/klibisz/elastiknn/benchmarks/LocalBenchmark.scala
+++ b/elastiknn-benchmarks/src/main/scala/com/klibisz/elastiknn/benchmarks/LocalBenchmark.scala
@@ -51,7 +51,8 @@ object LocalBenchmark extends App {
             resultsPrefix = "results",
             bucket = bucket,
             s3Url = Some(s3Url)
-          ))
+          )
+        )
       } yield ()
     }
     val pipeline = for {
@@ -64,7 +65,8 @@ object LocalBenchmark extends App {
           "results/aggregate.csv",
           bucket,
           Some(s3Url)
-        ))
+        )
+      )
     } yield ()
     pipeline.exitCode
   }
diff --git a/elastiknn-lucene/src/main/java/org/apache/lucene/search/MatchHashesAndScoreQuery.java b/elastiknn-lucene/src/main/java/org/apache/lucene/search/MatchHashesAndScoreQuery.java
index c5c58141d..4f1cb8d8d 100644
--- a/elastiknn-lucene/src/main/java/org/apache/lucene/search/MatchHashesAndScoreQuery.java
+++ b/elastiknn-lucene/src/main/java/org/apache/lucene/search/MatchHashesAndScoreQuery.java
@@ -154,8 +154,15 @@ public long cost() {
             public void extractTerms(Set<Term> terms) { }
 
             @Override
-            public Explanation explain(LeafReaderContext context, int doc) {
-                return Explanation.match( 0, "If someone knows what this should return, please submit a PR. :)");
+            public Explanation explain(LeafReaderContext context, int doc) throws IOException {
+                HitCounter counter = countHits(context.reader());
+                if (counter.get(doc) > 0) {
+                    ScoreFunction scoreFunction = scoreFunctionBuilder.apply(context);
+                    double score = scoreFunction.score(doc, counter.get(doc));
+                    return Explanation.match(score, String.format("Document [%d] and the query vector share [%d] of [%d] hashes. Their exact similarity score is [%f].", doc, counter.get(doc), hashAndFrequencies.length, score));
+                } else {
+                    return Explanation.noMatch(String.format("Document [%d] and the query vector share no common hashes.", doc));
+                }
             }
 
             @Override
diff --git a/version b/version
index 4c1618202..5acb61cb7 100644
--- a/version
+++ b/version
@@ -1,1 +1,1 @@
-7.12.0.2
+7.12.0.3


Test patch:
diff --git a/elastiknn-testing/src/test/scala/com/klibisz/elastiknn/query/MatchHashesAndScoreQuerySuite.scala b/elastiknn-testing/src/test/scala/com/klibisz/elastiknn/query/MatchHashesAndScoreQuerySuite.scala
index b4af85be4..19a71ae32 100644
--- a/elastiknn-testing/src/test/scala/com/klibisz/elastiknn/query/MatchHashesAndScoreQuerySuite.scala
+++ b/elastiknn-testing/src/test/scala/com/klibisz/elastiknn/query/MatchHashesAndScoreQuerySuite.scala
@@ -18,11 +18,7 @@ class MatchHashesAndScoreQuerySuite extends AnyFunSuite with Matchers with Lucen
   val ft: FieldType = VectorMapper.denseFloatVector.luceneFieldType
 
   test("empty harness") {
-    indexAndSearch() { (_: IndexWriter) =>
-      Assertions.succeed
-    } { (_: IndexReader, _: IndexSearcher) =>
-      Assertions.succeed
-    }
+    indexAndSearch() { (_: IndexWriter) => Assertions.succeed } { (_: IndexReader, _: IndexSearcher) => Assertions.succeed }
   }
 
   test("minimal id example") {
@@ -60,7 +56,7 @@ class MatchHashesAndScoreQuerySuite extends AnyFunSuite with Matchers with Lucen
               docId shouldBe 0
               numMatchingHashes shouldBe 2
               99d
-          }
+            }
         )
         val dd = s.search(q, 10)
         dd.scoreDocs should have length 1
@@ -90,6 +86,14 @@ class MatchHashesAndScoreQuerySuite extends AnyFunSuite with Matchers with Lucen
         val dd = s.search(q, 10)
         dd.scoreDocs should have length 2
         dd.scoreDocs.map(_.score) shouldBe Array(3f, 1f)
+        val ex0 = s.explain(q, 0)
+        ex0.isMatch shouldBe true
+        ex0.getValue.doubleValue() shouldBe 3d
+        ex0.getDescription shouldBe "Document [0] and the query vector share [3] of [6] hashes. Their exact similarity score is [3.000000]."
+        val ex1 = s.explain(q, 1)
+        ex1.isMatch shouldBe true
+        ex1.getValue.doubleValue() shouldBe 1d
+        ex1.getDescription shouldBe "Document [1] and the query vector share [1] of [6] hashes. Their exact similarity score is [1.000000]."
     }
   }
 
@@ -106,6 +110,9 @@ class MatchHashesAndScoreQuerySuite extends AnyFunSuite with Matchers with Lucen
         val q = new MatchHashesAndScoreQuery("vec", hashes, 5, r, (_: LeafReaderContext) => (_: Int, m: Int) => m * 1f)
         val dd = s.search(q, 10)
         dd.scoreDocs shouldBe empty
+        val ex = s.explain(q, 0)
+        ex.isMatch shouldBe false
+        ex.getDescription shouldBe "Document [0] and the query vector share no common hashes."
     }
   }
 
@@ -134,15 +141,17 @@ class MatchHashesAndScoreQuerySuite extends AnyFunSuite with Matchers with Lucen
     } {
       case (r, s) =>
         val counts = ArrayBuffer.empty[Int]
-        val q = new MatchHashesAndScoreQuery("vec",
-                                             query,
-                                             candidates,
-                                             r,
-                                             (_: LeafReaderContext) =>
-                                               (_: Int, c: Int) => {
-                                                 counts.append(c)
-                                                 c.toFloat
-                                             })
+        val q = new MatchHashesAndScoreQuery(
+          "vec",
+          query,
+          candidates,
+          r,
+          (_: LeafReaderContext) =>
+            (_: Int, c: Int) => {
+              counts.append(c)
+              c.toFloat
+            }
+        )
         val dd = s.search(q, 10)
         dd.scoreDocs.length shouldBe 5
         counts.toVector.sorted shouldBe Vector(1, 1, 2, 2, 2)