Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

188 lines
6.7 KiB
Go

package search
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// fixedEmbedder is a deterministic test embedding provider: every query
// embeds to the same constant vector, so HybridBackend.Search exercises
// the vector channel without any model. The vector backend is what
// decides ranking via the indexed vectors.
type fixedEmbedder struct{ dims int }
func (f fixedEmbedder) Embed(_ context.Context, _ string) ([]float32, error) {
v := make([]float32, f.dims)
if f.dims > 0 {
v[0] = 1
}
return v, nil
}
func (f fixedEmbedder) EmbedBatch(ctx context.Context, texts []string) ([][]float32, error) {
out := make([][]float32, len(texts))
for i := range texts {
out[i], _ = f.Embed(ctx, texts[i])
}
return out, nil
}
func (f fixedEmbedder) Dimensions() int { return f.dims }
func (f fixedEmbedder) Close() error { return nil }
// TestHybridSearch_DeChunksToParent proves the query-time de-chunk
// contract: when the vector index holds chunk vectors, a chunk hit is
// mapped back to its parent symbol, the symbol appears at most once,
// and no synthetic "#chunk" ID ever leaks into the results.
func TestHybridSearch_DeChunksToParent(t *testing.T) {
const dims = 3
vec := NewVector(dims)
// Two symbols. "big.go::Big" is sub-chunked into three chunk
// vectors; "small.go::Small" has a single plain vector. All chunk
// vectors of Big point the same direction so a query hits several
// of them at once — the case de-chunking must collapse.
vec.Add("big.go::Big#chunk0", []float32{1, 0, 0})
vec.Add("big.go::Big#chunk1", []float32{0.99, 0.01, 0})
vec.Add("big.go::Big#chunk2", []float32{0.98, 0.02, 0})
vec.Add("small.go::Small", []float32{0.5, 0.5, 0})
vec.SetChunkMap(map[string]string{
"big.go::Big#chunk0": "big.go::Big",
"big.go::Big#chunk1": "big.go::Big",
"big.go::Big#chunk2": "big.go::Big",
})
text := NewBM25()
text.Add("big.go::Big", "Big", "big.go", "")
text.Add("small.go::Small", "Small", "small.go", "")
h := NewHybrid(text, vec, fixedEmbedder{dims: dims})
results := h.Search("anything", 10)
require.NotEmpty(t, results)
seen := map[string]int{}
for _, r := range results {
assert.NotContains(t, r.ID, "#chunk",
"a synthetic chunk ID must never leak into search results")
seen[r.ID]++
}
assert.Equal(t, 1, seen["big.go::Big"],
"the sub-chunked symbol must appear exactly once despite owning three chunk vectors")
assert.LessOrEqual(t, seen["small.go::Small"], 1)
}
// TestHybridSearch_DeChunkPreservesOrder asserts the de-chunk step
// keeps a symbol at the rank of its best-scoring chunk.
func TestHybridSearch_DeChunkPreservesOrder(t *testing.T) {
const dims = 3
vec := NewVector(dims)
// The query vector is {1,0,0}. "A" owns the closest chunk; "B"
// owns a chunk slightly further away. A must therefore outrank B.
vec.Add("a.go::A#chunk0", []float32{1, 0, 0})
vec.Add("b.go::B#chunk0", []float32{0.7, 0.7, 0})
vec.SetChunkMap(map[string]string{
"a.go::A#chunk0": "a.go::A",
"b.go::B#chunk0": "b.go::B",
})
// Empty text backend so only the vector channel decides ordering.
h := NewHybrid(NewBM25(), vec, fixedEmbedder{dims: dims})
h.SetAutoAlpha(false) // plain RRF — vector ranks drive the order
got := h.dechunkVectorIDs(vec.Search([]float32{1, 0, 0}, 8), 8)
require.Len(t, got, 2)
assert.Equal(t, "a.go::A", got[0], "the symbol with the nearest chunk must rank first")
assert.Equal(t, "b.go::B", got[1])
}
// TestHybridSearch_NoChunkMapUnaffected asserts that when no symbol is
// chunked, search behaves exactly as before — IDs pass through.
func TestHybridSearch_NoChunkMapUnaffected(t *testing.T) {
const dims = 3
vec := NewVector(dims)
vec.Add("a.go::A", []float32{1, 0, 0})
vec.Add("b.go::B", []float32{0, 1, 0})
require.False(t, vec.HasChunks(), "no SetChunkMap → HasChunks must be false")
h := NewHybrid(NewBM25(), vec, fixedEmbedder{dims: dims})
results := h.Search("anything", 10)
for _, r := range results {
assert.NotContains(t, r.ID, "#chunk")
}
}
// TestVectorBackend_ResolveChunk covers the chunk → parent lookup in
// isolation: a mapped ID resolves to its parent, an unmapped ID passes
// through unchanged.
func TestVectorBackend_ResolveChunk(t *testing.T) {
v := NewVector(2)
v.SetChunkMap(map[string]string{"f.go::F#chunk1": "f.go::F"})
parent, isChunk := v.ResolveChunk("f.go::F#chunk1")
assert.True(t, isChunk)
assert.Equal(t, "f.go::F", parent)
plain, isChunk := v.ResolveChunk("g.go::G")
assert.False(t, isChunk)
assert.Equal(t, "g.go::G", plain, "an unmapped ID must pass through unchanged")
}
// TestVectorBackend_ChunkMapSurvivesSaveLoad asserts the chunk map is
// persisted by Save and restored by LoadFrom — the daemon snapshot and
// the per-repo cache both rely on this so de-chunking still works after
// a restart.
func TestVectorBackend_ChunkMapSurvivesSaveLoad(t *testing.T) {
src := NewVector(3)
src.Add("big.go::Big#chunk0", []float32{1, 0, 0})
src.Add("big.go::Big#chunk1", []float32{0, 1, 0})
src.SetChunkMap(map[string]string{
"big.go::Big#chunk0": "big.go::Big",
"big.go::Big#chunk1": "big.go::Big",
})
var buf strings.Builder
require.NoError(t, src.Save(&stringWriter{&buf}))
dst := NewVector(3)
require.NoError(t, dst.LoadFrom(strings.NewReader(buf.String())))
require.True(t, dst.HasChunks(), "chunk map must survive a Save/Load round-trip")
parent, isChunk := dst.ResolveChunk("big.go::Big#chunk1")
assert.True(t, isChunk)
assert.Equal(t, "big.go::Big", parent)
}
// TestVectorBackend_LegacyBlobLoadsWithoutChunkMap asserts a legacy raw
// HNSW export (written before the framed format) still loads, with an
// empty chunk map — the back-compat path.
func TestVectorBackend_LegacyBlobLoadsWithoutChunkMap(t *testing.T) {
// A VectorBackend with no chunk map, saved, then a fresh backend
// loaded from a stream that has had the frame magic stripped to
// simulate a pre-framing blob.
src := NewVector(3)
src.Add("a.go::A", []float32{1, 0, 0})
var framed strings.Builder
require.NoError(t, src.Save(&stringWriter{&framed}))
raw := framed.String()
// Frame layout: 4-byte magic + 4-byte map length + map JSON + HNSW.
// Strip magic+length+"{}" (an empty map JSON) to get the bare HNSW.
require.Greater(t, len(raw), 10)
bare := raw[4+4+2:] // 4 magic, 4 length, 2 = len("{}")
dst := NewVector(3)
require.NoError(t, dst.LoadFrom(strings.NewReader(bare)),
"a legacy un-framed HNSW blob must still load")
assert.False(t, dst.HasChunks(), "a legacy blob has no chunk map")
}
// stringWriter adapts a strings.Builder to io.Writer for the tests
// above (strings.Builder already satisfies io.Writer, but the wrapper
// keeps the intent explicit).
type stringWriter struct{ b *strings.Builder }
func (w *stringWriter) Write(p []byte) (int, error) { return w.b.Write(p) }