// Copyright (c) 2023 Couchbase, Inc. // // Licensed 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. //go:build vectors // +build vectors package scorer import ( "reflect" "testing" "github.com/blevesearch/bleve/v2/search" index "github.com/blevesearch/bleve_index_api" ) func TestKNNScorerExplanation(t *testing.T) { var queryVector []float32 // arbitrary vector of dims: 64 for i := 0; i < 64; i++ { queryVector = append(queryVector, float32(i)) } var resVector []float32 // arbitrary res vector. for i := 0; i < 64; i++ { resVector = append(resVector, float32(i)) } tests := []struct { vectorMatch *index.VectorDoc scorer *KNNQueryScorer norm float64 result *search.DocumentMatch }{ { vectorMatch: &index.VectorDoc{ ID: index.IndexInternalID("one"), Score: 0.5, Vector: resVector, }, norm: 1.0, scorer: NewKNNQueryScorer(queryVector, "desc", 1.0, search.SearcherOptions{Explain: true}, index.EuclideanDistance), // Specifically testing EuclideanDistance since that involves score inversion. result: &search.DocumentMatch{ IndexInternalID: index.IndexInternalID("one"), Score: 0.5, Expl: &search.Explanation{ Value: 1 / 0.5, Message: "fieldWeight(desc in doc one), score of:", Children: []*search.Explanation{ { Value: 1 / 0.5, Message: "vector(field(desc:one) with similarity_metric(l2_norm)=2.000000e+00", }, }, }, }, }, { vectorMatch: &index.VectorDoc{ ID: index.IndexInternalID("one"), Score: 0.0, // Result vector is an exact match of an existing vector. Vector: queryVector, }, norm: 1.0, scorer: NewKNNQueryScorer(queryVector, "desc", 1.0, search.SearcherOptions{Explain: true}, index.EuclideanDistance), // Specifically testing EuclideanDistance with 0 score. result: &search.DocumentMatch{ IndexInternalID: index.IndexInternalID("one"), Score: 0.0, Expl: &search.Explanation{ Value: maxKNNScore, Message: "fieldWeight(desc in doc one), score of:", Children: []*search.Explanation{ { Value: maxKNNScore, Message: "vector(field(desc:one) with similarity_metric(l2_norm)=3.402823e+38", }, }, }, }, }, { vectorMatch: &index.VectorDoc{ ID: index.IndexInternalID("one"), Score: 0.5, Vector: resVector, }, norm: 1.0, scorer: NewKNNQueryScorer(queryVector, "desc", 1.0, search.SearcherOptions{Explain: true}, index.InnerProduct), result: &search.DocumentMatch{ IndexInternalID: index.IndexInternalID("one"), Score: 0.5, Expl: &search.Explanation{ Value: 0.5, Message: "fieldWeight(desc in doc one), score of:", Children: []*search.Explanation{ { Value: 0.5, Message: "vector(field(desc:one) with similarity_metric(dot_product)=5.000000e-01", }, }, }, }, }, { vectorMatch: &index.VectorDoc{ ID: index.IndexInternalID("one"), Score: 0.25, Vector: resVector, }, norm: 0.5, scorer: NewKNNQueryScorer(queryVector, "desc", 1.0, search.SearcherOptions{Explain: true}, index.InnerProduct), result: &search.DocumentMatch{ IndexInternalID: index.IndexInternalID("one"), Score: 0.25, Expl: &search.Explanation{ Value: 0.125, Message: "weight(desc:query Vector^1.000000 in one), product of:", Children: []*search.Explanation{ { Value: 0.5, Message: "queryWeight(desc:query Vector^1.000000), product of:", Children: []*search.Explanation{ { Value: 1, Message: "boost", }, { Value: 0.5, Message: "queryNorm", }, }, }, { Value: 0.25, Message: "fieldWeight(desc in doc one), score of:", Children: []*search.Explanation{ { Value: 0.25, Message: "vector(field(desc:one) with similarity_metric(dot_product)=2.500000e-01", }, }, }, }, }, }, }, } for _, test := range tests { ctx := &search.SearchContext{ DocumentMatchPool: search.NewDocumentMatchPool(1, 0), } test.scorer.SetQueryNorm(test.norm) actual := test.scorer.Score(ctx, test.vectorMatch) actual.Complete(nil) if !reflect.DeepEqual(actual.Expl, test.result.Expl) { t.Errorf("expected %#v got %#v for %#v", test.result.Expl, actual.Expl, test.vectorMatch) } } }