Files
Algis DumbrisandClaude Opus 4.6 5dcb9e6149 feat: implement semantic search with embedding pipeline
Add HNSW-based vector search with configurable embedding providers
(OpenAI, Ollama) and automatic FTS5 fallback when no provider is
configured. Background pipeline embeds messages asynchronously on
ingest, stores vectors in a pure-Go HNSW index, and retries on
failure with exponential backoff. The search_messages MCP tool now
supports search_mode (auto/semantic/fulltext) and returns ranked
results with similarity scores. All existing tests continue to pass,
CGO_ENABLED=0 cross-compilation verified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 12:14:36 +02:00

111 lines
2.7 KiB
Go

package embedding
import (
"context"
"testing"
)
func TestMockProvider_Embed(t *testing.T) {
provider := NewMockProvider(128)
t.Run("returns correct dimensions", func(t *testing.T) {
vec, err := provider.Embed(context.Background(), "test text")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(vec) != 128 {
t.Errorf("vector length = %d, want 128", len(vec))
}
})
t.Run("batch embedding", func(t *testing.T) {
texts := []string{"hello", "world", "test"}
vecs, err := provider.EmbedBatch(context.Background(), texts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(vecs) != 3 {
t.Errorf("batch result length = %d, want 3", len(vecs))
}
for i, vec := range vecs {
if len(vec) != 128 {
t.Errorf("vector %d length = %d, want 128", i, len(vec))
}
}
})
t.Run("empty batch", func(t *testing.T) {
vecs, err := provider.EmbedBatch(context.Background(), nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(vecs) != 0 {
t.Errorf("empty batch result length = %d, want 0", len(vecs))
}
})
t.Run("custom embed func", func(t *testing.T) {
p := NewMockProvider(3)
p.SetEmbedFunc(func(ctx context.Context, text string) ([]float32, error) {
return []float32{1.0, 2.0, 3.0}, nil
})
vec, err := p.Embed(context.Background(), "anything")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if vec[0] != 1.0 || vec[1] != 2.0 || vec[2] != 3.0 {
t.Errorf("unexpected vector: %v", vec)
}
})
}
func TestNewProvider_Factory(t *testing.T) {
t.Run("unknown provider", func(t *testing.T) {
_, err := NewProvider("unknown", "", "")
if err == nil {
t.Error("expected error for unknown provider")
}
})
t.Run("empty provider", func(t *testing.T) {
_, err := NewProvider("", "", "")
if err == nil {
t.Error("expected error for empty provider")
}
})
t.Run("openai without key", func(t *testing.T) {
_, err := NewProvider("openai", "", "")
if err == nil {
t.Error("expected error for openai without API key")
}
})
t.Run("openai with key", func(t *testing.T) {
p, err := NewProvider("openai", "test-key", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if p.Name() != "openai" {
t.Errorf("name = %q, want openai", p.Name())
}
if p.Dimensions() != 1536 {
t.Errorf("dimensions = %d, want 1536", p.Dimensions())
}
})
t.Run("ollama", func(t *testing.T) {
p, err := NewProvider("ollama", "", "http://localhost:11434")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if p.Name() != "ollama" {
t.Errorf("name = %q, want ollama", p.Name())
}
if p.Dimensions() != 768 {
t.Errorf("dimensions = %d, want 768", p.Dimensions())
}
})
}