880715fad4
- New internal/memory/ package: Store interface, InMemoryStore (cosine sim) - Tests: insert→search roundtrip, topK truncation, empty store, cosine edges - Wire into voice: memStore on reactiveHandler, insert note embedding after WriteNote - Memory Insert is best-effort, log-and-continue on error Co-Authored-By: opencode <opencode@anthropic.com>
81 lines
2.0 KiB
Go
81 lines
2.0 KiB
Go
package memory
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"testing"
|
|
)
|
|
|
|
func TestInsertAndSearch(t *testing.T) {
|
|
s := NewInMemoryStore()
|
|
ctx := context.Background()
|
|
|
|
// Insert a few vectors
|
|
s.Insert(ctx, "doc1", []float32{1, 0, 0}, nil)
|
|
s.Insert(ctx, "doc2", []float32{0, 1, 0}, nil)
|
|
s.Insert(ctx, "doc3", []float32{0, 0, 1}, map[string]string{"source": "note"})
|
|
|
|
// Search for something close to doc1
|
|
results, err := s.Search(ctx, []float32{0.9, 0.1, 0}, 5)
|
|
if err != nil {
|
|
t.Fatalf("Search: %v", err)
|
|
}
|
|
if len(results) != 3 {
|
|
t.Fatalf("expected 3 results, got %d", len(results))
|
|
}
|
|
if results[0].ID != "doc1" {
|
|
t.Errorf("nearest should be doc1, got %s", results[0].ID)
|
|
}
|
|
if math.Abs(results[0].Score-0.9) > 0.01 {
|
|
t.Errorf("doc1 score should be near 0.9, got %f", results[0].Score)
|
|
}
|
|
|
|
// Check metadata preserved
|
|
if results[2].Meta["source"] != "note" {
|
|
t.Errorf("doc3 meta.source = %q, want note", results[2].Meta["source"])
|
|
}
|
|
}
|
|
|
|
func TestTopKTruncation(t *testing.T) {
|
|
s := NewInMemoryStore()
|
|
ctx := context.Background()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
s.Insert(ctx, "", []float32{float32(i) / 10, 0, 0}, nil)
|
|
}
|
|
|
|
results, err := s.Search(ctx, []float32{1, 0, 0}, 3)
|
|
if err != nil {
|
|
t.Fatalf("Search: %v", err)
|
|
}
|
|
if len(results) != 3 {
|
|
t.Fatalf("expected 3 results with topK=3, got %d", len(results))
|
|
}
|
|
}
|
|
|
|
func TestEmptyStore(t *testing.T) {
|
|
s := NewInMemoryStore()
|
|
results, err := s.Search(context.Background(), []float32{1, 0, 0}, 5)
|
|
if err != nil {
|
|
t.Fatalf("Search on empty store: %v", err)
|
|
}
|
|
if len(results) != 0 {
|
|
t.Fatalf("expected 0 results, got %d", len(results))
|
|
}
|
|
}
|
|
|
|
func TestCosineEdgeCases(t *testing.T) {
|
|
if c := cosine(nil, []float32{1}); c != 0 {
|
|
t.Errorf("nil first: expected 0, got %f", c)
|
|
}
|
|
if c := cosine([]float32{1}, nil); c != 0 {
|
|
t.Errorf("nil second: expected 0, got %f", c)
|
|
}
|
|
if c := cosine([]float32{}, []float32{}); c != 0 {
|
|
t.Errorf("empty: expected 0, got %f", c)
|
|
}
|
|
if c := cosine([]float32{1, 2}, []float32{1, 2}); math.Abs(c-5) > 0.001 {
|
|
t.Errorf("dot(1,2;1,2) = %f, want 5", c)
|
|
}
|
|
}
|