diff --git a/document/field_vector.go b/document/field_vector.go index 4c20013c7..136b79bbc 100644 --- a/document/field_vector.go +++ b/document/field_vector.go @@ -114,6 +114,13 @@ func NewVectorFieldWithIndexingOptions(name string, arrayPositions []uint64, // skip freq/norms for vector field options |= index.SkipFreqNorm + // bivf-sq8 indexes only supports hamming distance for the primary + // binary index. Similarity here is used for the backing flat index, + // which is set to cosine similarity for recall reasons + if index.OptimizationRequiresBinaryIndex(vectorIndexOptimizedFor) { + similarity = index.CosineSimilarity + } + return &VectorField{ name: name, dims: dims, diff --git a/go.mod b/go.mod index c6c65b557..eb1381fb1 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,9 @@ go 1.24.0 require ( github.com/RoaringBitmap/roaring/v2 v2.14.4 github.com/bits-and-blooms/bitset v1.24.2 - github.com/blevesearch/bleve_index_api v1.3.2 + github.com/blevesearch/bleve_index_api v1.3.3-0.20260311083042-62a5343ee2d1 github.com/blevesearch/geo v0.2.5 - github.com/blevesearch/go-faiss v1.0.27 + github.com/blevesearch/go-faiss v1.0.28-0.20260311083224-baa72a4cd87f github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475 github.com/blevesearch/go-porterstemmer v1.0.3 github.com/blevesearch/goleveldb v1.0.1 @@ -25,7 +25,7 @@ require ( github.com/blevesearch/zapx/v14 v14.4.3 github.com/blevesearch/zapx/v15 v15.4.3 github.com/blevesearch/zapx/v16 v16.3.1 - github.com/blevesearch/zapx/v17 v17.0.2 + github.com/blevesearch/zapx/v17 v17.0.3-0.20260311100439-7e7434b4f844 github.com/couchbase/moss v0.2.0 github.com/spf13/cobra v1.10.2 go.etcd.io/bbolt v1.4.0 diff --git a/go.sum b/go.sum index b7040773a..81332e5a3 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,12 @@ github.com/RoaringBitmap/roaring/v2 v2.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1zn github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY= github.com/bits-and-blooms/bitset v1.24.2 h1:M7/NzVbsytmtfHbumG+K2bremQPMJuqv1JD3vOaFxp0= github.com/bits-and-blooms/bitset v1.24.2/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/blevesearch/bleve_index_api v1.3.2 h1:y4VLXBF7nQR01CvF+QzmCJKMpVPCLp1CJ5FsRSZXzRE= -github.com/blevesearch/bleve_index_api v1.3.2/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= +github.com/blevesearch/bleve_index_api v1.3.3-0.20260311083042-62a5343ee2d1 h1:k2Q/e830m+fHx/4djya4UFef/jCV53sr0s97n9ergEc= +github.com/blevesearch/bleve_index_api v1.3.3-0.20260311083042-62a5343ee2d1/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= github.com/blevesearch/geo v0.2.5 h1:yJg9FX1oRwLnjXSXF+ECHfXFTF4diF02Ca/qUGVjJhE= github.com/blevesearch/geo v0.2.5/go.mod h1:Jhq7WE2K6mJTx1xS44M2pUO6Io+wjCSHh1+co3YOgH4= -github.com/blevesearch/go-faiss v1.0.27 h1:7cBImYDDQ82WJd5RUZ1ie6zXztCsC73W94ZzwOjkatk= -github.com/blevesearch/go-faiss v1.0.27/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= +github.com/blevesearch/go-faiss v1.0.28-0.20260311083224-baa72a4cd87f h1:BPOdM0yGnr74P3NelllUa8BbvKxqY5UfCUunggoGwoM= +github.com/blevesearch/go-faiss v1.0.28-0.20260311083224-baa72a4cd87f/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:kDy+zgJFJJoJYBvdfBSiZYBbdsUL0XcjHYWezpQBGPA= github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= @@ -45,8 +45,8 @@ github.com/blevesearch/zapx/v15 v15.4.3 h1:iJiMJOHrz216jyO6lS0m9RTCEkprUnzvqAI2l github.com/blevesearch/zapx/v15 v15.4.3/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw= github.com/blevesearch/zapx/v16 v16.3.1 h1:ERxZUSC9UcuKggCQ6b3y4sTkyL4WnGOWuopzglR874g= github.com/blevesearch/zapx/v16 v16.3.1/go.mod h1:zCFjv7McXWm1C8rROL+3mUoD5WYe2RKsZP3ufqcYpLY= -github.com/blevesearch/zapx/v17 v17.0.2 h1:u0VpuBIKv5bDvtXMOEOwxsqZe30VFrDaE/QKG831CLQ= -github.com/blevesearch/zapx/v17 v17.0.2/go.mod h1:70Z2efizAb00jpeLJA7r8DuqWzEtUdcLcWqaHIaOols= +github.com/blevesearch/zapx/v17 v17.0.3-0.20260311100439-7e7434b4f844 h1:IMv2az9s66lo/7+RtsJek+Lanu9cnwU6a+YDwoh9m+s= +github.com/blevesearch/zapx/v17 v17.0.3-0.20260311100439-7e7434b4f844/go.mod h1:/TdOJs9QM/V/j6OSr14BHdAGO5cnLpa9qhX6Np9A/Ts= github.com/couchbase/ghistogram v0.1.0 h1:b95QcQTCzjTUocDXp/uMgSNQi8oj1tGwnJ4bODWZnps= github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k= github.com/couchbase/moss v0.2.0 h1:VCYrMzFwEryyhRSeI+/b3tRBSeTpi/8gn5Kf6dxqn+o= diff --git a/index_update_test.go b/index_update_test.go index 9ae9df83a..b504acaf8 100644 --- a/index_update_test.go +++ b/index_update_test.go @@ -3212,3 +3212,344 @@ func TestIndexUpdateNestedMapping(t *testing.T) { } } } + +func TestTemp(t *testing.T) { + + oriJSON := ` + { + "default_analyzer": "standard", + "default_datetime_parser": "dateTimeOptional", + "default_field": "_all", + "default_mapping": { + "dynamic": true, + "enabled": false + }, + "default_type": "_default", + "docvalues_dynamic": false, + "index_dynamic": true, + "scoring_model": "tf-idf", + "store_dynamic": false, + "type_field": "_type", + "types": { + "inventory.hotel": { + "dynamic": false, + "enabled": true, + "properties": { + "city": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "analyzer": "keyword", + "docvalues": true, + "index": true, + "name": "city", + "store": true, + "type": "text" + } + ] + }, + "country": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "analyzer": "keyword", + "docvalues": true, + "index": true, + "name": "country", + "store": true, + "type": "text" + } + ] + }, + "description": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "analyzer": "en", + "docvalues": true, + "index": true, + "name": "description", + "store": true, + "type": "text" + } + ] + }, + "free_breakfast": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "docvalues": true, + "index": true, + "name": "free_breakfast", + "store": true, + "type": "boolean" + } + ] + }, + "geo": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "index": true, + "name": "geo", + "store": true, + "type": "geopoint" + } + ] + }, + "id": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "docvalues": true, + "index": true, + "name": "id", + "store": true, + "type": "number" + } + ] + }, + "name": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "analyzer": "en", + "docvalues": true, + "index": true, + "name": "name", + "type": "text", + "store": true + } + ] + }, + "phone": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "analyzer": "keyword", + "docvalues": true, + "index": true, + "name": "phone", + "type": "text" + } + ] + }, + "title": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "analyzer": "en", + "docvalues": true, + "index": true, + "name": "title", + "store": true, + "type": "text" + } + ] + }, + "vacancy": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "docvalues": true, + "index": true, + "name": "vacancy", + "store": true, + "type": "boolean" + } + ] + } + } + } + } + }` + + updJSON := ` + { + "default_analyzer": "standard", + "default_datetime_parser": "dateTimeOptional", + "default_field": "_all", + "default_mapping": { + "dynamic": true, + "enabled": false + }, + "default_type": "_default", + "docvalues_dynamic": false, + "index_dynamic": true, + "scoring_model": "tf-idf", + "store_dynamic": false, + "type_field": "_type", + "types": { + "inventory.hotel": { + "dynamic": false, + "enabled": true, + "properties": { + "city": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "analyzer": "keyword", + "docvalues": true, + "index": true, + "name": "city", + "store": true, + "type": "text" + } + ] + }, + "country": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "analyzer": "keyword", + "docvalues": true, + "index": true, + "name": "country", + "store": true, + "type": "text" + } + ] + }, + "description": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "analyzer": "en", + "docvalues": true, + "index": true, + "name": "description", + "store": true, + "type": "text" + } + ] + }, + "free_breakfast": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "docvalues": true, + "index": true, + "name": "free_breakfast", + "store": true, + "type": "boolean" + } + ] + }, + "geo": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "index": true, + "name": "geo", + "store": true, + "type": "geopoint" + } + ] + }, + "id": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "docvalues": true, + "index": true, + "name": "id", + "store": true, + "type": "number" + } + ] + }, + "name": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "analyzer": "en", + "docvalues": true, + "index": true, + "name": "name", + "type": "text", + "store": false + } + ] + }, + "phone": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "analyzer": "keyword", + "docvalues": true, + "index": true, + "name": "phone", + "type": "text" + } + ] + }, + "title": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "analyzer": "en", + "docvalues": true, + "index": true, + "name": "title", + "store": true, + "type": "text" + } + ] + }, + "vacancy": { + "enabled": true, + "dynamic": false, + "fields": [ + { + "docvalues": true, + "index": true, + "name": "vacancy", + "store": true, + "type": "boolean" + } + ] + } + } + } + } + }` + + var originalMapping *mapping.IndexMappingImpl + err := json.Unmarshal([]byte(oriJSON), &originalMapping) + if err != nil { + t.Fatalf("Error unmarshalling mapping JSON: %v", err) + } + + var updatedMapping *mapping.IndexMappingImpl + err = json.Unmarshal([]byte(updJSON), &updatedMapping) + if err != nil { + t.Fatalf("Error unmarshalling mapping JSON: %v", err) + } + + deletedFields, err := DeletedFields(originalMapping, updatedMapping) + if err != nil { + t.Fatalf("Error comparing mappings: %v", err) + } + + fmt.Printf("Deleted fields: %v\n", deletedFields) + +} diff --git a/mapping/mapping_vectors.go b/mapping/mapping_vectors.go index 7c7ff1b98..5bddabdd9 100644 --- a/mapping/mapping_vectors.go +++ b/mapping/mapping_vectors.go @@ -151,6 +151,12 @@ func (fm *FieldMapping) processVector(propertyMightBeVector interface{}, if vectorIndexOptimizedFor == "" { vectorIndexOptimizedFor = index.DefaultIndexOptimization } + // bivf indexes only supports hamming distance for the primary + // binary index. Similarity here is used for the backing flat index, + // which is set to cosine similarity for recall reasons + if index.OptimizationRequiresBinaryIndex(vectorIndexOptimizedFor) { + similarity = index.CosineSimilarity + } // normalize raw vector if similarity is cosine // Since the vector can be multi-vector (flattened array of multiple vectors), // we use NormalizeMultiVector to normalize each sub-vector independently. @@ -185,6 +191,12 @@ func (fm *FieldMapping) processVectorBase64(propertyMightBeVectorBase64 interfac if vectorIndexOptimizedFor == "" { vectorIndexOptimizedFor = index.DefaultIndexOptimization } + // bivf indexes only supports hamming distance for the primary + // binary index. Similarity here is used for the backing flat index, + // which is set to cosine similarity for recall reasons + if index.OptimizationRequiresBinaryIndex(vectorIndexOptimizedFor) { + similarity = index.CosineSimilarity + } decodedVector, err := document.DecodeVector(encodedString) if err != nil || len(decodedVector) != fm.Dims { return @@ -289,6 +301,11 @@ func validateVectorFieldAlias(field *FieldMapping, path []string, effectiveOptimizedFor, reflect.ValueOf(index.SupportedVectorIndexOptimizations).MapKeys()) } + // bivf indexes requires vector dimensionality to be a multiple of 8 + if index.OptimizationRequiresBinaryIndex(effectiveOptimizedFor) && field.Dims%8 != 0 { + return fmt.Errorf("field: '%s', incompatible vector dimensionality for BIVF: %d,"+ + " dimension should be a multiple of 8", effectiveFieldName, field.Dims) + } if fieldAliasCtx != nil { // writing to a nil map is unsafe fieldAliasCtx[effectiveFieldName] = field diff --git a/search/query/knn.go b/search/query/knn.go index ea8780a41..5282a740f 100644 --- a/search/query/knn.go +++ b/search/query/knn.go @@ -84,6 +84,12 @@ func (q *KNNQuery) Searcher(ctx context.Context, i index.IndexReader, if q.K <= 0 || len(q.Vector) == 0 { return nil, fmt.Errorf("k must be greater than 0 and vector must be non-empty") } + // bivf-sq8 indexes only supports hamming distance for the primary + // binary index. Similarity here is used for the backing flat index, + // which is set to cosine similarity for recall reasons + if index.OptimizationRequiresBinaryIndex(fieldMapping.VectorIndexOptimizedFor) { + similarityMetric = index.CosineSimilarity + } if similarityMetric == index.CosineSimilarity { // normalize the vector q.Vector = mapping.NormalizeVector(q.Vector) diff --git a/search_knn_test.go b/search_knn_test.go index d053705ca..00d5cbb70 100644 --- a/search_knn_test.go +++ b/search_knn_test.go @@ -608,6 +608,282 @@ func TestVectorBase64Index(t *testing.T) { } } +// Test to verify that the BIVF-Flat index with vector base64 field mapping returns the +// same results as the non-optimized vector field mapping for L2, Dot Product and Cosine similarities. +// Also test to see no differences in results for any distance metric +func TestVectorBivfIndexes(t *testing.T) { + optimizations := []string{index.IndexOptimizedWithBivfForDisk, index.IndexOptimizedWithBivfForLatency} + for _, optimization := range optimizations { + testVectorBivfIndex(t, optimization) + } +} + +func testVectorBivfIndex(t *testing.T, optimization string) { + + dataset, searchRequests, err := readDatasetAndQueries(testInputCompressedFile) + if err != nil { + t.Fatal(err) + } + documents := makeDatasetIntoDocuments(dataset) + + _, searchRequestsCopy, err := readDatasetAndQueries(testInputCompressedFile) + if err != nil { + t.Fatal(err) + } + + for _, doc := range documents { + vec, ok := doc["vector"].([]float32) + if !ok { + t.Fatal("Typecasting vector to float array failed") + } + + buf := new(bytes.Buffer) + for _, v := range vec { + err := binary.Write(buf, binary.LittleEndian, v) + if err != nil { + t.Fatal(err) + } + } + + doc["vectorEncoded"] = base64.StdEncoding.EncodeToString(buf.Bytes()) + } + + for _, sr := range searchRequestsCopy { + for _, kr := range sr.KNN { + kr.Field = "vectorEncoded" + } + } + + contentFM := NewTextFieldMapping() + contentFM.Analyzer = en.AnalyzerName + + vecFML2 := mapping.NewVectorFieldMapping() + vecFML2.Dims = testDatasetDims + vecFML2.Similarity = index.EuclideanDistance + vecFML2.VectorIndexOptimizedFor = optimization + + vecBFML2 := mapping.NewVectorBase64FieldMapping() + vecBFML2.Dims = testDatasetDims + vecBFML2.Similarity = index.EuclideanDistance + vecBFML2.VectorIndexOptimizedFor = optimization + + vecFMDot := mapping.NewVectorFieldMapping() + vecFMDot.Dims = testDatasetDims + vecFMDot.Similarity = index.InnerProduct + vecFMDot.VectorIndexOptimizedFor = optimization + + vecBFMDot := mapping.NewVectorBase64FieldMapping() + vecBFMDot.Dims = testDatasetDims + vecBFMDot.Similarity = index.InnerProduct + vecBFMDot.VectorIndexOptimizedFor = optimization + + vecFMCosine := mapping.NewVectorFieldMapping() + vecFMCosine.Dims = testDatasetDims + vecFMCosine.Similarity = index.CosineSimilarity + + vecBFMCosine := mapping.NewVectorBase64FieldMapping() + vecBFMCosine.Dims = testDatasetDims + vecBFMCosine.Similarity = index.CosineSimilarity + vecBFMCosine.VectorIndexOptimizedFor = optimization + + indexMappingL2 := NewIndexMapping() + indexMappingL2.DefaultMapping.AddFieldMappingsAt("content", contentFM) + indexMappingL2.DefaultMapping.AddFieldMappingsAt("vector", vecFML2) + indexMappingL2.DefaultMapping.AddFieldMappingsAt("vectorEncoded", vecBFML2) + + indexMappingDot := NewIndexMapping() + indexMappingDot.DefaultMapping.AddFieldMappingsAt("content", contentFM) + indexMappingDot.DefaultMapping.AddFieldMappingsAt("vector", vecFMDot) + indexMappingDot.DefaultMapping.AddFieldMappingsAt("vectorEncoded", vecBFMDot) + + indexMappingCosine := NewIndexMapping() + indexMappingCosine.DefaultMapping.AddFieldMappingsAt("content", contentFM) + indexMappingCosine.DefaultMapping.AddFieldMappingsAt("vector", vecFMCosine) + indexMappingCosine.DefaultMapping.AddFieldMappingsAt("vectorEncoded", vecBFMCosine) + + tmpIndexPathL2 := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPathL2) + + tmpIndexPathDot := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPathDot) + + tmpIndexPathCosine := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPathCosine) + + indexL2, err := New(tmpIndexPathL2, indexMappingL2) + if err != nil { + t.Fatal(err) + } + defer func() { + err := indexL2.Close() + if err != nil { + t.Fatal(err) + } + }() + + indexDot, err := New(tmpIndexPathDot, indexMappingDot) + if err != nil { + t.Fatal(err) + } + defer func() { + err := indexDot.Close() + if err != nil { + t.Fatal(err) + } + }() + + indexCosine, err := New(tmpIndexPathCosine, indexMappingCosine) + if err != nil { + t.Fatal(err) + } + defer func() { + err := indexCosine.Close() + if err != nil { + t.Fatal(err) + } + }() + + batchL2 := indexL2.NewBatch() + batchDot := indexDot.NewBatch() + batchCosine := indexCosine.NewBatch() + + for _, doc := range documents { + err = batchL2.Index(doc["id"].(string), doc) + if err != nil { + t.Fatal(err) + } + err = batchDot.Index(doc["id"].(string), doc) + if err != nil { + t.Fatal(err) + } + err = batchCosine.Index(doc["id"].(string), doc) + if err != nil { + t.Fatal(err) + } + } + + err = indexL2.Batch(batchL2) + if err != nil { + t.Fatal(err) + } + + err = indexDot.Batch(batchDot) + if err != nil { + t.Fatal(err) + } + + err = indexCosine.Batch(batchCosine) + if err != nil { + t.Fatal(err) + } + + for i := range searchRequests { + for _, operator := range knnOperators { + normQuery := searchRequests[i] + base64Query := searchRequestsCopy[i] + + normQuery.AddKNNOperator(operator) + base64Query.AddKNNOperator(operator) + + normResultL2, err := indexL2.Search(normQuery) + if err != nil { + t.Fatal(err) + } + base64ResultL2, err := indexL2.Search(base64Query) + if err != nil { + t.Fatal(err) + } + + if normResultL2 != nil && base64ResultL2 != nil { + if len(normResultL2.Hits) == len(base64ResultL2.Hits) { + for j := range normResultL2.Hits { + if normResultL2.Hits[j].ID != base64ResultL2.Hits[j].ID { + t.Fatalf("testcase %d failed: expected hit id %s, got hit id %s", i, normResultL2.Hits[j].ID, base64ResultL2.Hits[j].ID) + } + } + } + } else if (normResultL2 == nil && base64ResultL2 != nil) || + (normResultL2 != nil && base64ResultL2 == nil) { + t.Fatalf("testcase %d failed: expected result %s, got result %s", i, normResultL2, base64ResultL2) + } + + normResultDot, err := indexDot.Search(normQuery) + if err != nil { + t.Fatal(err) + } + base64ResultDot, err := indexDot.Search(base64Query) + if err != nil { + t.Fatal(err) + } + + if normResultDot != nil && base64ResultDot != nil { + if len(normResultDot.Hits) == len(base64ResultDot.Hits) { + for j := range normResultDot.Hits { + if normResultDot.Hits[j].ID != base64ResultDot.Hits[j].ID { + t.Fatalf("testcase %d failed: expected hit id %s, got hit id %s", i, normResultDot.Hits[j].ID, base64ResultDot.Hits[j].ID) + } + } + } + } else if (normResultDot == nil && base64ResultDot != nil) || + (normResultDot != nil && base64ResultDot == nil) { + t.Fatalf("testcase %d failed: expected result %s, got result %s", i, normResultDot, base64ResultDot) + } + + normResultCosine, err := indexCosine.Search(normQuery) + if err != nil { + t.Fatal(err) + } + base64ResultCosine, err := indexCosine.Search(base64Query) + if err != nil { + t.Fatal(err) + } + + if normResultCosine != nil && base64ResultCosine != nil { + if len(normResultCosine.Hits) == len(base64ResultCosine.Hits) { + for j := range normResultCosine.Hits { + if normResultCosine.Hits[j].ID != base64ResultCosine.Hits[j].ID { + t.Fatalf("testcase %d failed: expected hit id %s, got hit id %s", i, normResultCosine.Hits[j].ID, base64ResultCosine.Hits[j].ID) + } + } + } + } else if (normResultCosine == nil && base64ResultCosine != nil) || + (normResultCosine != nil && base64ResultCosine == nil) { + t.Fatalf("testcase %d failed: expected result %s, got result %s", i, normResultCosine, base64ResultCosine) + } + + if normResultCosine != nil && normResultL2 != nil { + if len(normResultCosine.Hits) == len(normResultL2.Hits) { + for j := range normResultCosine.Hits { + if normResultCosine.Hits[j].ID != normResultL2.Hits[j].ID { + if normResultCosine.Hits[j].Score != normResultL2.Hits[j].Score { + t.Fatalf("testcase %d failed: expected hit id %s, got hit id %s", i, normResultCosine.Hits[j].ID, normResultL2.Hits[j].ID) + } + } + } + } + } else if (normResultCosine == nil && normResultL2 != nil) || + (normResultCosine != nil && normResultL2 == nil) { + t.Fatalf("testcase %d failed: expected result %s, got result %s", i, normResultCosine, normResultL2) + } + + if normResultCosine != nil && normResultDot != nil { + if len(normResultCosine.Hits) == len(normResultDot.Hits) { + for j := range normResultCosine.Hits { + if normResultCosine.Hits[j].ID != normResultDot.Hits[j].ID { + if normResultCosine.Hits[j].Score != normResultDot.Hits[j].Score { + t.Fatalf("testcase %d failed: expected hit id %s, got hit id %s", i, normResultCosine.Hits[j].ID, normResultDot.Hits[j].ID) + } + } + } + } + } else if (normResultCosine == nil && normResultDot != nil) || + (normResultCosine != nil && normResultDot == nil) { + t.Fatalf("testcase %d failed: expected result %s, got result %s", i, normResultCosine, normResultDot) + } + } + } +} + type testDocument struct { ID string `json:"id"` Content string `json:"content"`