Some checks failed
Coverage / coverage (push) Failing after 6s
Tests / test (1.24.x, ubuntu-latest) (push) Failing after 42s
Tests / test (1.26.x, ubuntu-latest) (push) Failing after 38s
Tests / test (1.25.x, ubuntu-latest) (push) Failing after 35s
Tests / test (1.25.x, windows-latest) (push) Has been cancelled
Tests / test (1.26.x, macos-latest) (push) Has been cancelled
Tests / test (1.26.x, windows-latest) (push) Has been cancelled
Tests / test (1.24.x, macos-latest) (push) Has been cancelled
Tests / test (1.24.x, windows-latest) (push) Has been cancelled
Tests / test (1.25.x, macos-latest) (push) Has been cancelled
1330 lines
37 KiB
Go
1330 lines
37 KiB
Go
// Copyright (c) 2025 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.
|
|
|
|
package bleve
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/blevesearch/bleve/v2/search"
|
|
"github.com/blevesearch/bleve/v2/search/query"
|
|
)
|
|
|
|
func createFTSIndex(path string) (Index, error) {
|
|
// Index mapping for FTS-only testing
|
|
indexMapping := NewIndexMapping()
|
|
|
|
// Disable default mapping to match expected configuration
|
|
indexMapping.DefaultMapping.Enabled = false
|
|
indexMapping.DefaultMapping.Dynamic = false
|
|
|
|
// Create a specific document mapping type
|
|
docMapping := NewDocumentMapping()
|
|
docMapping.Enabled = true
|
|
docMapping.Dynamic = false
|
|
|
|
// Text field for color with specific properties
|
|
colorFieldMapping := NewTextFieldMapping()
|
|
colorFieldMapping.Analyzer = "en" // Use "en" analyzer as specified
|
|
colorFieldMapping.DocValues = true
|
|
colorFieldMapping.IncludeInAll = true
|
|
colorFieldMapping.Store = true
|
|
colorFieldMapping.Index = true
|
|
docMapping.AddFieldMappingsAt("color", colorFieldMapping)
|
|
|
|
// Text field for description with specific properties
|
|
descriptionFieldMapping := NewTextFieldMapping()
|
|
descriptionFieldMapping.Analyzer = "en"
|
|
descriptionFieldMapping.DocValues = true
|
|
descriptionFieldMapping.IncludeInAll = true
|
|
descriptionFieldMapping.Store = true
|
|
descriptionFieldMapping.Index = true
|
|
docMapping.AddFieldMappingsAt("description", descriptionFieldMapping)
|
|
|
|
// Text field for category with specific properties
|
|
categoryFieldMapping := NewTextFieldMapping()
|
|
categoryFieldMapping.Analyzer = "en"
|
|
categoryFieldMapping.DocValues = true
|
|
categoryFieldMapping.IncludeInAll = true
|
|
categoryFieldMapping.Store = true
|
|
categoryFieldMapping.Index = true
|
|
docMapping.AddFieldMappingsAt("category", categoryFieldMapping)
|
|
|
|
// Add the document mapping to the index
|
|
indexMapping.AddDocumentMapping("_default", docMapping)
|
|
|
|
// Create index
|
|
return New(path, indexMapping)
|
|
}
|
|
|
|
var benchmarkResult search.DocumentMatchCollection
|
|
|
|
type benchmarkConfig struct {
|
|
name string
|
|
ftsHits int
|
|
knnHits int
|
|
knnQueries int
|
|
}
|
|
|
|
func BenchmarkRescorerRRF(b *testing.B) {
|
|
runRescorerBenchmarks(b, ScoreRRF)
|
|
}
|
|
|
|
func BenchmarkRescorerRSF(b *testing.B) {
|
|
runRescorerBenchmarks(b, ScoreRSF)
|
|
}
|
|
|
|
func runRescorerBenchmarks(b *testing.B, scoreMode string) {
|
|
configs := []benchmarkConfig{
|
|
{name: "small", ftsHits: 256, knnHits: 192, knnQueries: 1},
|
|
{name: "medium", ftsHits: 1024, knnHits: 896, knnQueries: 2},
|
|
{name: "large", ftsHits: 4096, knnHits: 3584, knnQueries: 3},
|
|
}
|
|
|
|
for _, cfg := range configs {
|
|
b.Run(fmt.Sprintf("%s/%s", scoreMode, cfg.name), func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
|
|
rescorer, baseFTSHits, baseKNNHits := buildBenchmarkInputs(cfg, scoreMode)
|
|
|
|
var last search.DocumentMatchCollection
|
|
b.StopTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
ftsHits := cloneDocumentMatches(baseFTSHits)
|
|
knnHits := cloneDocumentMatches(baseKNNHits)
|
|
|
|
b.StartTimer()
|
|
hits, _, _ := rescorer.rescore(ftsHits, knnHits)
|
|
b.StopTimer()
|
|
|
|
last = hits
|
|
}
|
|
|
|
if len(last) == 0 {
|
|
b.Fatalf("rescorer returned no hits for config %q", cfg.name)
|
|
}
|
|
|
|
benchmarkResult = last
|
|
})
|
|
}
|
|
}
|
|
|
|
func buildBenchmarkInputs(cfg benchmarkConfig, scoreMode string) (*rescorer, search.DocumentMatchCollection, search.DocumentMatchCollection) {
|
|
windowSize := cfg.ftsHits
|
|
if cfg.knnHits > windowSize {
|
|
windowSize = cfg.knnHits
|
|
}
|
|
|
|
matchQuery := query.NewMatchQuery("rescorer benchmark payload")
|
|
matchQuery.SetBoost(1.0)
|
|
|
|
req := &SearchRequest{
|
|
Query: matchQuery,
|
|
Size: cfg.ftsHits,
|
|
From: 0,
|
|
Score: scoreMode,
|
|
Params: &RequestParams{ScoreRankConstant: DefaultScoreRankConstant, ScoreWindowSize: windowSize},
|
|
}
|
|
|
|
activeKNNQueries := cfg.knnQueries
|
|
if knnAdder, ok := interface{}(req).(interface {
|
|
AddKNN(field string, vector []float32, k int64, boost float64)
|
|
}); ok {
|
|
for i := 0; i < cfg.knnQueries; i++ {
|
|
knnAdder.AddKNN(fmt.Sprintf("vector_%d", i), []float32{1.0, 0.5, 0.25}, int64(cfg.knnHits), 1.0)
|
|
}
|
|
} else {
|
|
activeKNNQueries = 0
|
|
}
|
|
|
|
r := newRescorer(req)
|
|
r.origBoosts = make([]float64, activeKNNQueries+1)
|
|
for i := range r.origBoosts {
|
|
r.origBoosts[i] = 1.0
|
|
}
|
|
|
|
ftsHits, knnHits := buildBenchmarkHits(cfg, activeKNNQueries)
|
|
return r, ftsHits, knnHits
|
|
}
|
|
|
|
func buildBenchmarkHits(cfg benchmarkConfig, activeKNNQueries int) (search.DocumentMatchCollection, search.DocumentMatchCollection) {
|
|
ftsHits := make(search.DocumentMatchCollection, cfg.ftsHits)
|
|
for i := 0; i < cfg.ftsHits; i++ {
|
|
ftsHits[i] = &search.DocumentMatch{
|
|
ID: fmt.Sprintf("doc-%06d", i),
|
|
Score: float64(cfg.ftsHits - i),
|
|
HitNumber: uint64(i + 1),
|
|
}
|
|
}
|
|
|
|
knnHits := make(search.DocumentMatchCollection, cfg.knnHits)
|
|
for i := 0; i < cfg.knnHits; i++ {
|
|
id := fmt.Sprintf("doc-%06d", i)
|
|
if cfg.ftsHits > 0 {
|
|
id = fmt.Sprintf("doc-%06d", i%cfg.ftsHits)
|
|
}
|
|
if cfg.ftsHits == 0 || i%4 == 0 {
|
|
id = fmt.Sprintf("knn-only-%06d", i/4)
|
|
}
|
|
|
|
scoreBreakdown := make(map[int]float64, activeKNNQueries)
|
|
for q := 0; q < activeKNNQueries; q++ {
|
|
scoreBreakdown[q] = float64(cfg.knnHits - i + q + 1)
|
|
}
|
|
|
|
knnHits[i] = &search.DocumentMatch{
|
|
ID: id,
|
|
Score: float64(cfg.knnHits - i),
|
|
ScoreBreakdown: scoreBreakdown,
|
|
HitNumber: uint64(i + 1),
|
|
}
|
|
}
|
|
|
|
return ftsHits, knnHits
|
|
}
|
|
|
|
func cloneDocumentMatches(src search.DocumentMatchCollection) search.DocumentMatchCollection {
|
|
dst := make(search.DocumentMatchCollection, len(src))
|
|
for i, hit := range src {
|
|
if hit == nil {
|
|
continue
|
|
}
|
|
|
|
cloned := *hit
|
|
|
|
if hit.ScoreBreakdown != nil {
|
|
cloned.ScoreBreakdown = make(map[int]float64, len(hit.ScoreBreakdown))
|
|
for k, v := range hit.ScoreBreakdown {
|
|
cloned.ScoreBreakdown[k] = v
|
|
}
|
|
}
|
|
|
|
if len(hit.Sort) > 0 {
|
|
cloned.Sort = append([]string(nil), hit.Sort...)
|
|
}
|
|
if len(hit.DecodedSort) > 0 {
|
|
cloned.DecodedSort = append([]string(nil), hit.DecodedSort...)
|
|
}
|
|
if len(hit.IndexNames) > 0 {
|
|
cloned.IndexNames = append([]string(nil), hit.IndexNames...)
|
|
}
|
|
|
|
dst[i] = &cloned
|
|
}
|
|
return dst
|
|
}
|
|
|
|
func getFTSDocuments() []map[string]interface{} {
|
|
documents := []map[string]interface{}{
|
|
{
|
|
"color": "dark slate blue",
|
|
"description": "deep and rich color with dark undertones",
|
|
"category": "blue shades",
|
|
},
|
|
{
|
|
"color": "blue",
|
|
"description": "primary color that is bright and vibrant",
|
|
"category": "primary colors",
|
|
},
|
|
{
|
|
"color": "navy",
|
|
"description": "dark blue color often used in uniforms",
|
|
"category": "dark colors",
|
|
},
|
|
{
|
|
"color": "steel blue",
|
|
"description": "metallic blue with gray undertones",
|
|
"category": "metallic shades",
|
|
},
|
|
{
|
|
"color": "light blue",
|
|
"description": "pale and soft blue color with light appearance",
|
|
"category": "light colors",
|
|
},
|
|
{
|
|
"color": "deep sky blue",
|
|
"description": "bright blue reminiscent of clear skies",
|
|
"category": "sky colors",
|
|
},
|
|
{
|
|
"color": "royal blue",
|
|
"description": "rich and regal blue color fit for royalty",
|
|
"category": "rich colors",
|
|
},
|
|
{
|
|
"color": "powder blue",
|
|
"description": "very light blue with powder-like softness",
|
|
"category": "light colors",
|
|
},
|
|
{
|
|
"color": "corn flower blue",
|
|
"description": "medium blue color named after the flower",
|
|
"category": "floral colors",
|
|
},
|
|
{
|
|
"color": "alice blue",
|
|
"description": "very pale blue with light and airy quality",
|
|
"category": "light colors",
|
|
},
|
|
{
|
|
"color": "blue violet",
|
|
"description": "purple-blue color with violet undertones",
|
|
"category": "purple shades",
|
|
},
|
|
{
|
|
"color": "sky blue",
|
|
"description": "bright blue color of a clear day sky",
|
|
"category": "sky colors",
|
|
},
|
|
{
|
|
"color": "indigo",
|
|
"description": "deep purple-blue color with dark intensity",
|
|
"category": "dark colors",
|
|
},
|
|
{
|
|
"color": "midnight blue",
|
|
"description": "very dark blue like the night sky",
|
|
"category": "dark colors",
|
|
},
|
|
{
|
|
"color": "dark blue",
|
|
"description": "deep blue color with dark characteristics",
|
|
"category": "dark colors",
|
|
},
|
|
{
|
|
"color": "medium slate blue",
|
|
"description": "medium intensity blue with slate properties",
|
|
"category": "blue shades",
|
|
},
|
|
{
|
|
"color": "cadet blue",
|
|
"description": "grayish blue color often used in uniforms",
|
|
"category": "metallic shades",
|
|
},
|
|
{
|
|
"color": "light steel blue",
|
|
"description": "light metallic blue with steel-like appearance",
|
|
"category": "light colors",
|
|
},
|
|
{
|
|
"color": "dodger blue",
|
|
"description": "bright medium blue with vibrant intensity",
|
|
"category": "bright colors",
|
|
},
|
|
{
|
|
"color": "medium blue",
|
|
"description": "standard blue with medium intensity and saturation",
|
|
"category": "blue shades",
|
|
},
|
|
{
|
|
"color": "slate blue",
|
|
"description": "blue-gray color with slate-like properties",
|
|
"category": "blue shades",
|
|
},
|
|
{
|
|
"color": "light sky blue",
|
|
"description": "light version of sky blue with airy quality",
|
|
"category": "light colors",
|
|
},
|
|
}
|
|
|
|
return documents
|
|
}
|
|
|
|
func createFTSSearchRequest(scoreMethod string) *SearchRequest {
|
|
// Create multi-FTS search request (multiple FTS queries for fusion scoring)
|
|
// Query 1: Search for "dark" in color field
|
|
query1 := query.NewMatchPhraseQuery("dark")
|
|
query1.SetField("color")
|
|
|
|
// Query 2: Search for "light" in description field
|
|
query2 := query.NewMatchPhraseQuery("light")
|
|
query2.SetField("description")
|
|
|
|
// // Query 3: Search for "blue" in category field
|
|
query3 := query.NewMatchPhraseQuery("blue")
|
|
query3.SetField("category")
|
|
|
|
// Use the first query as the main query for the search request
|
|
searchRequest := NewSearchRequest(query1)
|
|
|
|
// Add additional queries for fusion scoring (this simulates multiple query sources)
|
|
// Since SearchRequest doesn't have a direct way to add multiple FTS queries,
|
|
// we'll use a disjunction query to combine them for fusion scoring simulation
|
|
queries := []query.Query{query1, query2, query3}
|
|
disjunctionQuery := query.NewDisjunctionQuery(queries)
|
|
searchRequest.Query = disjunctionQuery
|
|
|
|
params := RequestParams{1, 10}
|
|
searchRequest.AddParams(params)
|
|
|
|
searchRequest.Size = 10
|
|
searchRequest.Score = scoreMethod
|
|
|
|
searchRequest.Explain = false
|
|
return searchRequest
|
|
}
|
|
|
|
// verifyFTSRRFResults verifies that the search hits match the expected RRF ranking and scores
|
|
func verifyFTSRRFResults(t *testing.T, hits search.DocumentMatchCollection) {
|
|
// Manual RRF calculation for verification
|
|
// With k=1 (ScoreRankConstant), RRF formula: 1/(1+rank)
|
|
//
|
|
// For FTS-only with disjunction query, we need to consider how each document
|
|
// matches each of the three query components:
|
|
// 1. "dark" in color field
|
|
// 2. "light" in description field
|
|
// 3. "blue" in category field
|
|
//
|
|
// Documents that match multiple query components will rank higher
|
|
|
|
// Expected matches:
|
|
// Query 1 ("dark" in color): dark slate blue, dark blue, midnight blue (has "dark")
|
|
// Query 2 ("light" in description): light blue, powder blue, alice blue, light steel blue, light sky blue
|
|
// Query 3 ("blue" in category): dark slate blue, medium slate blue, medium blue, slate blue
|
|
|
|
expectedTopDocuments := []string{
|
|
"dark slate blue", // matches query 1 and 3
|
|
"light blue", // matches query 2
|
|
"dark blue", // matches query 1
|
|
"light steel blue", // matches query 2
|
|
"medium slate blue", // matches query 3
|
|
}
|
|
|
|
if len(hits) == 0 {
|
|
t.Fatal("Expected search results, got none")
|
|
}
|
|
|
|
// Verify we have results and they're ranked by score
|
|
for i := 0; i < len(hits)-1; i++ {
|
|
if hits[i].Score < hits[i+1].Score {
|
|
t.Errorf("Results not properly ranked by score: position %d (%.6f) < position %d (%.6f)",
|
|
i, hits[i].Score, i+1, hits[i+1].Score)
|
|
}
|
|
}
|
|
|
|
// Check that some expected top documents are present in results
|
|
foundExpected := 0
|
|
for _, hit := range hits {
|
|
for _, expected := range expectedTopDocuments {
|
|
if hit.ID == expected {
|
|
foundExpected++
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if foundExpected < 3 {
|
|
t.Errorf("Expected to find at least 3 of the top expected documents, found %d", foundExpected)
|
|
t.Logf("Actual results:")
|
|
for i, hit := range hits {
|
|
t.Logf(" %d: %s (score: %.6f)", i+1, hit.ID, hit.Score)
|
|
}
|
|
}
|
|
}
|
|
|
|
// setupFTSSingleIndex creates a single index with all FTS documents
|
|
func setupFTSSingleIndex(t *testing.T) (Index, func()) {
|
|
tmpIndexPath := createTmpIndexPath(t)
|
|
|
|
index, err := createFTSIndex(tmpIndexPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
documents := getFTSDocuments()
|
|
|
|
// Index documents
|
|
batch := index.NewBatch()
|
|
for _, doc := range documents {
|
|
colorName := doc["color"].(string)
|
|
err = batch.Index(colorName, doc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
err = index.Batch(batch)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cleanup := func() {
|
|
err := index.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cleanupTmpIndexPath(t, tmpIndexPath)
|
|
}
|
|
|
|
return index, cleanup
|
|
}
|
|
|
|
// setupFTSAliasWithSingleIndex creates an alias containing one index with all FTS documents
|
|
func setupFTSAliasWithSingleIndex(t *testing.T) (Index, func()) {
|
|
tmpIndexPath := createTmpIndexPath(t)
|
|
|
|
index, err := createFTSIndex(tmpIndexPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
documents := getFTSDocuments()
|
|
|
|
// Create alias and add the single index
|
|
alias := NewIndexAlias()
|
|
alias.Add(index)
|
|
|
|
// Index all documents
|
|
batch := alias.NewBatch()
|
|
for _, doc := range documents {
|
|
colorName := doc["color"].(string)
|
|
err = batch.Index(colorName, doc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
err = alias.Batch(batch)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cleanup := func() {
|
|
err := index.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cleanupTmpIndexPath(t, tmpIndexPath)
|
|
}
|
|
|
|
return alias, cleanup
|
|
}
|
|
|
|
// setupFTSAliasWithTwoIndexes creates an alias containing two indexes with FTS documents split between them
|
|
func setupFTSAliasWithTwoIndexes(t *testing.T) (Index, func()) {
|
|
documents := getFTSDocuments()
|
|
|
|
// Split documents into two groups
|
|
midpoint := len(documents) / 2
|
|
docs1 := documents[:midpoint]
|
|
docs2 := documents[midpoint:]
|
|
|
|
// Create first index
|
|
tmpIndexPath1 := createTmpIndexPath(t)
|
|
index1, err := createFTSIndex(tmpIndexPath1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Index first half of documents
|
|
batch1 := index1.NewBatch()
|
|
for _, doc := range docs1 {
|
|
colorName := doc["color"].(string)
|
|
err = batch1.Index(colorName, doc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
err = index1.Batch(batch1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create second index
|
|
tmpIndexPath2 := createTmpIndexPath(t)
|
|
index2, err := createFTSIndex(tmpIndexPath2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Index second half of documents
|
|
batch2 := index2.NewBatch()
|
|
for _, doc := range docs2 {
|
|
colorName := doc["color"].(string)
|
|
err = batch2.Index(colorName, doc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
err = index2.Batch(batch2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create alias and add both indexes
|
|
alias := NewIndexAlias()
|
|
alias.Add(index1, index2)
|
|
|
|
cleanup := func() {
|
|
err := index1.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = index2.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cleanupTmpIndexPath(t, tmpIndexPath1)
|
|
cleanupTmpIndexPath(t, tmpIndexPath2)
|
|
}
|
|
|
|
return alias, cleanup
|
|
}
|
|
|
|
// setupFTSNestedAliases creates nested aliases with three indexes spread across sub-aliases
|
|
func setupFTSNestedAliases(t *testing.T) (Index, func()) {
|
|
documents := getFTSDocuments()
|
|
|
|
// Split documents into three groups
|
|
thirdPoint1 := len(documents) / 3
|
|
thirdPoint2 := 2 * len(documents) / 3
|
|
docs1 := documents[:thirdPoint1]
|
|
docs2 := documents[thirdPoint1:thirdPoint2]
|
|
docs3 := documents[thirdPoint2:]
|
|
|
|
// Create first index
|
|
tmpIndexPath1 := createTmpIndexPath(t)
|
|
index1, err := createFTSIndex(tmpIndexPath1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Index first third of documents
|
|
batch1 := index1.NewBatch()
|
|
for _, doc := range docs1 {
|
|
colorName := doc["color"].(string)
|
|
err = batch1.Index(colorName, doc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
err = index1.Batch(batch1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create second index
|
|
tmpIndexPath2 := createTmpIndexPath(t)
|
|
index2, err := createFTSIndex(tmpIndexPath2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Index second third of documents
|
|
batch2 := index2.NewBatch()
|
|
for _, doc := range docs2 {
|
|
colorName := doc["color"].(string)
|
|
err = batch2.Index(colorName, doc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
err = index2.Batch(batch2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create third index
|
|
tmpIndexPath3 := createTmpIndexPath(t)
|
|
index3, err := createFTSIndex(tmpIndexPath3)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Index third third of documents
|
|
batch3 := index3.NewBatch()
|
|
for _, doc := range docs3 {
|
|
colorName := doc["color"].(string)
|
|
err = batch3.Index(colorName, doc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
err = index3.Batch(batch3)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create first sub-alias (contains 1 index)
|
|
subAlias1 := NewIndexAlias()
|
|
subAlias1.SetName("subAlias1")
|
|
subAlias1.Add(index1)
|
|
|
|
// Create second sub-alias (contains 2 indexes)
|
|
subAlias2 := NewIndexAlias()
|
|
subAlias2.SetName("subAlias2")
|
|
subAlias2.Add(index2, index3)
|
|
|
|
// Create master alias containing the two sub-aliases
|
|
masterAlias := NewIndexAlias()
|
|
masterAlias.SetName("masterAlias")
|
|
masterAlias.Add(subAlias1, subAlias2)
|
|
|
|
cleanup := func() {
|
|
err := index1.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = index2.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = index3.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cleanupTmpIndexPath(t, tmpIndexPath1)
|
|
cleanupTmpIndexPath(t, tmpIndexPath2)
|
|
cleanupTmpIndexPath(t, tmpIndexPath3)
|
|
}
|
|
|
|
return masterAlias, cleanup
|
|
}
|
|
|
|
func TestFTSRRFEndToEnd(t *testing.T) {
|
|
// Setup the index configuration
|
|
index, cleanup := setupFTSSingleIndex(t)
|
|
defer cleanup()
|
|
|
|
// Create the search request
|
|
searchRequest := createFTSSearchRequest(ScoreRRF)
|
|
|
|
// Execute search
|
|
result, err := index.Search(searchRequest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify FTS RRF results
|
|
verifyFTSRRFResults(t, result.Hits)
|
|
}
|
|
|
|
// TestFTSRRFAliasWithSingleIndex tests RRF with an alias containing one index
|
|
func TestFTSRRFAliasWithSingleIndex(t *testing.T) {
|
|
// Setup the alias configuration
|
|
alias, cleanup := setupFTSAliasWithSingleIndex(t)
|
|
defer cleanup()
|
|
|
|
// Create the search request
|
|
searchRequest := createFTSSearchRequest(ScoreRRF)
|
|
|
|
// Execute search through alias
|
|
result, err := alias.Search(searchRequest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify FTS RRF results - should be identical to direct index search
|
|
verifyFTSRRFResults(t, result.Hits)
|
|
}
|
|
|
|
// TestFTSRRFAliasWithTwoIndexes tests RRF with an alias containing two indexes
|
|
func TestFTSRRFAliasWithTwoIndexes(t *testing.T) {
|
|
// Setup the alias configuration
|
|
alias, cleanup := setupFTSAliasWithTwoIndexes(t)
|
|
defer cleanup()
|
|
|
|
// Create the search request
|
|
searchRequest := createFTSSearchRequest(ScoreRRF)
|
|
|
|
// Execute search through alias
|
|
result, err := alias.Search(searchRequest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify FTS RRF results - should be consistent across distributed indexes
|
|
verifyFTSRRFResults(t, result.Hits)
|
|
}
|
|
|
|
// TestFTSRRFNestedAliases tests RRF with an alias containing two index aliases
|
|
func TestFTSRRFNestedAliases(t *testing.T) {
|
|
// Setup the nested aliases configuration
|
|
masterAlias, cleanup := setupFTSNestedAliases(t)
|
|
defer cleanup()
|
|
|
|
// Create the search request
|
|
searchRequest := createFTSSearchRequest(ScoreRRF)
|
|
|
|
// Execute search through master alias
|
|
result, err := masterAlias.Search(searchRequest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify FTS RRF results - should be consistent across nested aliases
|
|
verifyFTSRRFResults(t, result.Hits)
|
|
}
|
|
|
|
// TestFTSRRFPagination tests FTS RRF with pagination across different index/alias configurations
|
|
func TestFTSRRFPagination(t *testing.T) {
|
|
scenarios := []struct {
|
|
name string
|
|
setup func(t *testing.T) (Index, func())
|
|
}{
|
|
{
|
|
name: "SingleIndex",
|
|
setup: setupFTSSingleIndex,
|
|
},
|
|
{
|
|
name: "AliasWithSingleIndex",
|
|
setup: setupFTSAliasWithSingleIndex,
|
|
},
|
|
{
|
|
name: "AliasWithTwoIndexes",
|
|
setup: setupFTSAliasWithTwoIndexes,
|
|
},
|
|
{
|
|
name: "NestedAliases",
|
|
setup: setupFTSNestedAliases,
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
t.Run(scenario.name, func(t *testing.T) {
|
|
// Setup the index/alias configuration
|
|
index, cleanup := scenario.setup(t)
|
|
defer cleanup()
|
|
|
|
// Create first page request (first 5 results)
|
|
firstPageRequest := createFTSSearchRequest(ScoreRRF)
|
|
firstPageRequest.From = 0
|
|
firstPageRequest.Size = 5
|
|
|
|
// Execute first page search
|
|
firstPageResult, err := index.Search(firstPageRequest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create second page request (next 5 results, starting from index 5)
|
|
secondPageRequest := createFTSSearchRequest(ScoreRRF)
|
|
secondPageRequest.From = 5
|
|
secondPageRequest.Size = 5
|
|
|
|
// Execute second page search
|
|
secondPageResult, err := index.Search(secondPageRequest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Combine results from both pages
|
|
combinedHits := make(search.DocumentMatchCollection, 0, len(firstPageResult.Hits)+len(secondPageResult.Hits))
|
|
combinedHits = append(combinedHits, firstPageResult.Hits...)
|
|
combinedHits = append(combinedHits, secondPageResult.Hits...)
|
|
|
|
// Verify we have results (FTS may have variable results based on matches)
|
|
if len(firstPageResult.Hits) == 0 {
|
|
t.Fatal("Expected at least some results in first page, got 0")
|
|
}
|
|
if len(firstPageResult.Hits) > 5 {
|
|
t.Errorf("Expected at most 5 results in first page, got %d", len(firstPageResult.Hits))
|
|
}
|
|
|
|
// Total hits should not exceed the number of documents that match our queries
|
|
totalHits := len(combinedHits)
|
|
if totalHits == 0 {
|
|
t.Fatal("Expected at least some combined results, got 0")
|
|
}
|
|
|
|
// Verify combined FTS RRF results
|
|
verifyFTSRRFResults(t, combinedHits)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFTSRRFFaceting tests that facet results are identical whether using RRF or default scoring
|
|
func TestFTSRRFFaceting(t *testing.T) {
|
|
scenarios := []struct {
|
|
name string
|
|
setup func(t *testing.T) (Index, func())
|
|
}{
|
|
{
|
|
name: "SingleIndex",
|
|
setup: setupFTSSingleIndex,
|
|
},
|
|
{
|
|
name: "AliasWithSingleIndex",
|
|
setup: setupFTSAliasWithSingleIndex,
|
|
},
|
|
{
|
|
name: "AliasWithTwoIndexes",
|
|
setup: setupFTSAliasWithTwoIndexes,
|
|
},
|
|
{
|
|
name: "NestedAliases",
|
|
setup: setupFTSNestedAliases,
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
t.Run(scenario.name, func(t *testing.T) {
|
|
// Setup the index/alias configuration
|
|
index, cleanup := scenario.setup(t)
|
|
defer cleanup()
|
|
|
|
// Create search request with default scoring and facets
|
|
defaultRequest := createFTSSearchRequest(ScoreRRF)
|
|
defaultRequest.Score = ScoreDefault // Use default scoring
|
|
defaultRequest.Size = 10
|
|
// Add facet for color field with size 10
|
|
colorFacet := NewFacetRequest("color", 10)
|
|
defaultRequest.AddFacet("color", colorFacet)
|
|
|
|
// Create search request with RRF scoring and identical facets
|
|
rrfRequest := createFTSSearchRequest(ScoreRRF)
|
|
rrfRequest.Size = 10
|
|
rrfRequest.Score = ScoreRRF
|
|
// Add identical facet for color field with size 10
|
|
colorFacetRRF := NewFacetRequest("color", 10)
|
|
rrfRequest.AddFacet("color", colorFacetRRF)
|
|
|
|
// Execute both searches
|
|
defaultResult, err := index.Search(defaultRequest)
|
|
if err != nil {
|
|
t.Fatalf("Default scoring search failed: %v", err)
|
|
}
|
|
|
|
rrfResult, err := index.Search(rrfRequest)
|
|
if err != nil {
|
|
t.Fatalf("RRF scoring search failed: %v", err)
|
|
}
|
|
|
|
// Verify both searches returned results
|
|
if len(defaultResult.Hits) == 0 {
|
|
t.Fatal("Expected search results with default scoring, got none")
|
|
}
|
|
if len(rrfResult.Hits) == 0 {
|
|
t.Fatal("Expected search results with RRF scoring, got none")
|
|
}
|
|
|
|
// Verify both searches returned facets
|
|
if defaultResult.Facets == nil {
|
|
t.Fatal("Expected facets with default scoring, got nil")
|
|
}
|
|
if rrfResult.Facets == nil {
|
|
t.Fatal("Expected facets with RRF scoring, got nil")
|
|
}
|
|
|
|
// Check that color facet exists in both results
|
|
defaultColorFacet, defaultExists := defaultResult.Facets["color"]
|
|
rrfColorFacet, rrfExists := rrfResult.Facets["color"]
|
|
|
|
if !defaultExists {
|
|
t.Fatal("Expected color facet in default scoring results")
|
|
}
|
|
if !rrfExists {
|
|
t.Fatal("Expected color facet in RRF scoring results")
|
|
}
|
|
|
|
// Compare the facet results - they should be identical
|
|
// Since facets are based on the document corpus and not scoring,
|
|
// they should not be affected by the scoring method
|
|
if defaultColorFacet.Total != rrfColorFacet.Total {
|
|
t.Errorf("Facet totals differ: default=%d, RRF=%d",
|
|
defaultColorFacet.Total, rrfColorFacet.Total)
|
|
}
|
|
|
|
if defaultColorFacet.Missing != rrfColorFacet.Missing {
|
|
t.Errorf("Facet missing counts differ: default=%d, RRF=%d",
|
|
defaultColorFacet.Missing, rrfColorFacet.Missing)
|
|
}
|
|
|
|
if defaultColorFacet.Other != rrfColorFacet.Other {
|
|
t.Errorf("Facet other counts differ: default=%d, RRF=%d",
|
|
defaultColorFacet.Other, rrfColorFacet.Other)
|
|
}
|
|
|
|
// Compare the facet terms
|
|
defaultTerms := defaultColorFacet.Terms.Terms()
|
|
rrfTerms := rrfColorFacet.Terms.Terms()
|
|
|
|
if len(defaultTerms) != len(rrfTerms) {
|
|
t.Errorf("Facet terms count differs: default=%d, RRF=%d",
|
|
len(defaultTerms), len(rrfTerms))
|
|
} else {
|
|
// Compare each term
|
|
for i, defaultTerm := range defaultTerms {
|
|
rrfTerm := rrfTerms[i]
|
|
if defaultTerm.Term != rrfTerm.Term {
|
|
t.Errorf("Facet term differs at position %d: default=%s, RRF=%s",
|
|
i, defaultTerm.Term, rrfTerm.Term)
|
|
}
|
|
if defaultTerm.Count != rrfTerm.Count {
|
|
t.Errorf("Facet term count differs for %s: default=%d, RRF=%d",
|
|
defaultTerm.Term, defaultTerm.Count, rrfTerm.Count)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// verifyFTSRSFResults verifies that the search hits match expected RSF ranking and scores for FTS-only search
|
|
func verifyFTSRSFResults(t *testing.T, hits search.DocumentMatchCollection) {
|
|
// For FTS RSF, we expect documents that match multiple query components to rank higher
|
|
// Query components: "dark" in color, "light" in description, "blue" in category
|
|
// RSF uses min-max normalization of scores within the window
|
|
|
|
// Verify we have reasonable number of results
|
|
if len(hits) == 0 {
|
|
t.Fatal("Expected non-empty search results for FTS RSF")
|
|
}
|
|
|
|
// Verify we have at least 5 results for meaningful comparison
|
|
if len(hits) < 5 {
|
|
t.Errorf("Expected at least 5 results for FTS RSF, got %d", len(hits))
|
|
}
|
|
|
|
// Documents that should appear in top results based on multi-query matching:
|
|
// - "dark slate blue": matches "dark" in color AND "blue" in category (2 matches)
|
|
// - "light blue": matches "light" in description (1 strong match)
|
|
// - "dark blue": matches "dark" in color (1 match)
|
|
// - Documents with "light" in description should rank well
|
|
topExpectedDocs := []string{"dark slate blue", "light blue", "dark blue", "medium slate blue", "light sky blue"}
|
|
|
|
// Create map of all hits for easier lookup
|
|
docMap := make(map[string]int) // doc -> position (0-based)
|
|
for i, hit := range hits {
|
|
docMap[hit.ID] = i
|
|
}
|
|
|
|
// Verify that "dark slate blue" appears in top 3 positions (matches 2 query components)
|
|
if pos, found := docMap["dark slate blue"]; !found {
|
|
t.Error("Expected 'dark slate blue' to appear in results but not found")
|
|
} else if pos >= 3 {
|
|
t.Errorf("Expected 'dark slate blue' in top 3 positions, found at position %d", pos+1)
|
|
}
|
|
|
|
// Verify that at least 3 of the top expected documents appear in top 5 results
|
|
topFoundCount := 0
|
|
for _, expectedDoc := range topExpectedDocs {
|
|
if pos, found := docMap[expectedDoc]; found && pos < 5 {
|
|
topFoundCount++
|
|
}
|
|
}
|
|
if topFoundCount < 3 {
|
|
t.Errorf("Expected at least 3 of top expected documents in top 5 results, found %d", topFoundCount)
|
|
}
|
|
|
|
// Verify scores are reasonable and within expected range
|
|
// RSF scores should be between 0 and sum of weights (3.0 with default weights)
|
|
for i, hit := range hits {
|
|
if hit.Score < 0 || hit.Score > 3.0 {
|
|
t.Errorf("Hit %d (%s) has unreasonable score: %.6f", i, hit.ID, hit.Score)
|
|
}
|
|
// First hit should have a substantial score (at least 0.1)
|
|
if i == 0 && hit.Score < 0.1 {
|
|
t.Errorf("Top hit (%s) has unexpectedly low score: %.6f", hit.ID, hit.Score)
|
|
}
|
|
}
|
|
|
|
// Verify hits are sorted by score descending with strict ordering
|
|
for i := 1; i < len(hits); i++ {
|
|
if hits[i-1].Score < hits[i].Score {
|
|
t.Errorf("Hits not sorted properly: hit %d (%s, score %.6f) < hit %d (%s, score %.6f)",
|
|
i, hits[i-1].ID, hits[i-1].Score, i+1, hits[i].ID, hits[i].Score)
|
|
}
|
|
}
|
|
|
|
// Verify score range is reasonable - top score should be significantly higher than 5th
|
|
if len(hits) >= 5 {
|
|
topScore := hits[0].Score
|
|
fifthScore := hits[4].Score
|
|
if topScore-fifthScore < 0.001 {
|
|
t.Errorf("Insufficient score differentiation: top score %.6f, 5th score %.6f (diff: %.6f)",
|
|
topScore, fifthScore, topScore-fifthScore)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFTSRSFEndToEnd tests RSF scoring with a single FTS index
|
|
func TestFTSRSFEndToEnd(t *testing.T) {
|
|
// Setup the index configuration
|
|
index, cleanup := setupFTSSingleIndex(t)
|
|
defer cleanup()
|
|
|
|
// Create the search request
|
|
searchRequest := createFTSSearchRequest(ScoreRSF)
|
|
|
|
// Execute search
|
|
result, err := index.Search(searchRequest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify FTS RSF results
|
|
verifyFTSRSFResults(t, result.Hits)
|
|
}
|
|
|
|
// TestFTSRSFAliasWithSingleIndex tests RSF with an alias containing one FTS index
|
|
func TestFTSRSFAliasWithSingleIndex(t *testing.T) {
|
|
// Setup the alias configuration
|
|
alias, cleanup := setupFTSAliasWithSingleIndex(t)
|
|
defer cleanup()
|
|
|
|
// Create the search request
|
|
searchRequest := createFTSSearchRequest(ScoreRSF)
|
|
|
|
// Execute search through alias
|
|
result, err := alias.Search(searchRequest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify FTS RSF results - should be identical to direct index search
|
|
verifyFTSRSFResults(t, result.Hits)
|
|
}
|
|
|
|
// TestFTSRSFAliasWithTwoIndexes tests RSF with an alias containing two FTS indexes
|
|
func TestFTSRSFAliasWithTwoIndexes(t *testing.T) {
|
|
// Setup the alias configuration
|
|
alias, cleanup := setupFTSAliasWithTwoIndexes(t)
|
|
defer cleanup()
|
|
|
|
// Create the search request
|
|
searchRequest := createFTSSearchRequest(ScoreRSF)
|
|
|
|
// Execute search through alias
|
|
result, err := alias.Search(searchRequest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify FTS RSF results - should be consistent across distributed indexes
|
|
verifyFTSRSFResults(t, result.Hits)
|
|
}
|
|
|
|
// TestFTSRSFNestedAliases tests RSF with an alias containing two index aliases
|
|
func TestFTSRSFNestedAliases(t *testing.T) {
|
|
// Setup the nested aliases configuration
|
|
masterAlias, cleanup := setupFTSNestedAliases(t)
|
|
defer cleanup()
|
|
|
|
// Create the search request
|
|
searchRequest := createFTSSearchRequest(ScoreRSF)
|
|
|
|
// Execute search through master alias
|
|
result, err := masterAlias.Search(searchRequest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify FTS RSF results - should be consistent across nested aliases
|
|
verifyFTSRSFResults(t, result.Hits)
|
|
}
|
|
|
|
// TestFTSRSFPagination tests FTS RSF with pagination across different index/alias configurations
|
|
func TestFTSRSFPagination(t *testing.T) {
|
|
scenarios := []struct {
|
|
name string
|
|
setup func(t *testing.T) (Index, func())
|
|
}{
|
|
{
|
|
name: "SingleIndex",
|
|
setup: setupFTSSingleIndex,
|
|
},
|
|
{
|
|
name: "AliasWithSingleIndex",
|
|
setup: setupFTSAliasWithSingleIndex,
|
|
},
|
|
{
|
|
name: "AliasWithTwoIndexes",
|
|
setup: setupFTSAliasWithTwoIndexes,
|
|
},
|
|
{
|
|
name: "NestedAliases",
|
|
setup: setupFTSNestedAliases,
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
t.Run(scenario.name, func(t *testing.T) {
|
|
// Setup the index/alias configuration
|
|
index, cleanup := scenario.setup(t)
|
|
defer cleanup()
|
|
|
|
// Create first page request (first 5 results)
|
|
firstPageRequest := createFTSSearchRequest(ScoreRSF)
|
|
firstPageRequest.From = 0
|
|
firstPageRequest.Size = 5
|
|
|
|
// Execute first page search
|
|
firstPageResult, err := index.Search(firstPageRequest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create second page request (next 5 results, starting from index 5)
|
|
secondPageRequest := createFTSSearchRequest(ScoreRSF)
|
|
secondPageRequest.From = 5
|
|
secondPageRequest.Size = 5
|
|
|
|
// Execute second page search
|
|
secondPageResult, err := index.Search(secondPageRequest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Combine results from both pages
|
|
combinedHits := make(search.DocumentMatchCollection, 0, len(firstPageResult.Hits)+len(secondPageResult.Hits))
|
|
combinedHits = append(combinedHits, firstPageResult.Hits...)
|
|
combinedHits = append(combinedHits, secondPageResult.Hits...)
|
|
|
|
// Verify we have results (FTS may have variable results based on matches)
|
|
if len(firstPageResult.Hits) == 0 {
|
|
t.Fatal("Expected at least some results in first page, got 0")
|
|
}
|
|
if len(firstPageResult.Hits) > 5 {
|
|
t.Errorf("Expected at most 5 results in first page, got %d", len(firstPageResult.Hits))
|
|
}
|
|
|
|
// Total hits should not exceed the number of documents that match our queries
|
|
totalHits := len(combinedHits)
|
|
if totalHits == 0 {
|
|
t.Fatal("Expected at least some combined results, got 0")
|
|
}
|
|
|
|
// Verify combined FTS RSF results
|
|
verifyFTSRSFResults(t, combinedHits)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFTSRSFFaceting tests that facet results are identical whether using RSF or default scoring
|
|
func TestFTSRSFFaceting(t *testing.T) {
|
|
scenarios := []struct {
|
|
name string
|
|
setup func(t *testing.T) (Index, func())
|
|
}{
|
|
{
|
|
name: "SingleIndex",
|
|
setup: setupFTSSingleIndex,
|
|
},
|
|
{
|
|
name: "AliasWithSingleIndex",
|
|
setup: setupFTSAliasWithSingleIndex,
|
|
},
|
|
{
|
|
name: "AliasWithTwoIndexes",
|
|
setup: setupFTSAliasWithTwoIndexes,
|
|
},
|
|
{
|
|
name: "NestedAliases",
|
|
setup: setupFTSNestedAliases,
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
t.Run(scenario.name, func(t *testing.T) {
|
|
// Setup the index/alias configuration
|
|
index, cleanup := scenario.setup(t)
|
|
defer cleanup()
|
|
|
|
// Create search request with default scoring and facets
|
|
defaultRequest := createFTSSearchRequest(ScoreRRF)
|
|
defaultRequest.Score = ScoreDefault // Use default scoring
|
|
defaultRequest.Size = 10
|
|
// Add facet for category field with size 10
|
|
categoryFacet := NewFacetRequest("category", 10)
|
|
defaultRequest.AddFacet("category", categoryFacet)
|
|
|
|
// Create search request with RSF scoring and identical facets
|
|
rsfRequest := createFTSSearchRequest(ScoreRSF)
|
|
rsfRequest.Size = 10
|
|
// Add identical facet for category field with size 10
|
|
categoryFacetRSF := NewFacetRequest("category", 10)
|
|
rsfRequest.AddFacet("category", categoryFacetRSF)
|
|
|
|
// Execute both searches
|
|
defaultResult, err := index.Search(defaultRequest)
|
|
if err != nil {
|
|
t.Fatalf("Default scoring search failed: %v", err)
|
|
}
|
|
|
|
rsfResult, err := index.Search(rsfRequest)
|
|
if err != nil {
|
|
t.Fatalf("RSF scoring search failed: %v", err)
|
|
}
|
|
|
|
// Verify both searches returned results
|
|
if len(defaultResult.Hits) == 0 {
|
|
t.Fatal("Expected search results with default scoring, got none")
|
|
}
|
|
if len(rsfResult.Hits) == 0 {
|
|
t.Fatal("Expected search results with RSF scoring, got none")
|
|
}
|
|
|
|
// Verify both searches returned facets
|
|
if defaultResult.Facets == nil {
|
|
t.Fatal("Expected facets with default scoring, got nil")
|
|
}
|
|
if rsfResult.Facets == nil {
|
|
t.Fatal("Expected facets with RSF scoring, got nil")
|
|
}
|
|
|
|
// Check that category facet exists in both results
|
|
defaultCategoryFacet, defaultExists := defaultResult.Facets["category"]
|
|
rsfCategoryFacet, rsfExists := rsfResult.Facets["category"]
|
|
|
|
if !defaultExists {
|
|
t.Fatal("Expected category facet in default scoring results")
|
|
}
|
|
if !rsfExists {
|
|
t.Fatal("Expected category facet in RSF scoring results")
|
|
}
|
|
|
|
// Compare the facet results - they should be identical
|
|
// Since facets are based on the document corpus and not scoring,
|
|
// they should not be affected by the scoring method
|
|
if defaultCategoryFacet.Total != rsfCategoryFacet.Total {
|
|
t.Errorf("Facet totals differ: default=%d, RSF=%d",
|
|
defaultCategoryFacet.Total, rsfCategoryFacet.Total)
|
|
}
|
|
|
|
if defaultCategoryFacet.Missing != rsfCategoryFacet.Missing {
|
|
t.Errorf("Facet missing counts differ: default=%d, RSF=%d",
|
|
defaultCategoryFacet.Missing, rsfCategoryFacet.Missing)
|
|
}
|
|
|
|
if defaultCategoryFacet.Other != rsfCategoryFacet.Other {
|
|
t.Errorf("Facet other counts differ: default=%d, RSF=%d",
|
|
defaultCategoryFacet.Other, rsfCategoryFacet.Other)
|
|
}
|
|
|
|
// Compare the facet terms
|
|
defaultTerms := defaultCategoryFacet.Terms.Terms()
|
|
rsfTerms := rsfCategoryFacet.Terms.Terms()
|
|
|
|
if len(defaultTerms) != len(rsfTerms) {
|
|
t.Errorf("Facet terms count differs: default=%d, RSF=%d",
|
|
len(defaultTerms), len(rsfTerms))
|
|
} else {
|
|
// Compare each term
|
|
for i, defaultTerm := range defaultTerms {
|
|
rsfTerm := rsfTerms[i]
|
|
if defaultTerm.Term != rsfTerm.Term {
|
|
t.Errorf("Facet term differs at position %d: default=%s, RSF=%s",
|
|
i, defaultTerm.Term, rsfTerm.Term)
|
|
}
|
|
if defaultTerm.Count != rsfTerm.Count {
|
|
t.Errorf("Facet term count differs for %s: default=%d, RSF=%d",
|
|
defaultTerm.Term, defaultTerm.Count, rsfTerm.Count)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|