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>
96 lines
1.9 KiB
Go
96 lines
1.9 KiB
Go
package memory
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"sync"
|
|
)
|
|
|
|
// Result is a single search hit.
|
|
type Result struct {
|
|
ID string
|
|
Score float64
|
|
Meta map[string]string
|
|
}
|
|
|
|
// Store is a vector memory interface.
|
|
type Store interface {
|
|
Insert(ctx context.Context, id string, vec []float32, meta map[string]string) error
|
|
Search(ctx context.Context, vec []float32, topK int) ([]Result, error)
|
|
}
|
|
|
|
// item is a single stored vector with metadata.
|
|
type item struct {
|
|
id string
|
|
vec []float32
|
|
meta map[string]string
|
|
}
|
|
|
|
// InMemoryStore implements Store with cosine similarity search.
|
|
type InMemoryStore struct {
|
|
mu sync.RWMutex
|
|
items []item
|
|
}
|
|
|
|
func NewInMemoryStore() *InMemoryStore {
|
|
return &InMemoryStore{}
|
|
}
|
|
|
|
func (s *InMemoryStore) Insert(_ context.Context, id string, vec []float32, meta map[string]string) error {
|
|
s.mu.Lock()
|
|
s.items = append(s.items, item{id: id, vec: vec, meta: meta})
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (s *InMemoryStore) Search(_ context.Context, vec []float32, topK int) ([]Result, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
if topK <= 0 {
|
|
topK = 10
|
|
}
|
|
|
|
type scored struct {
|
|
id string
|
|
score float64
|
|
meta map[string]string
|
|
}
|
|
|
|
scores := make([]scored, 0, len(s.items))
|
|
for _, it := range s.items {
|
|
score := cosine(vec, it.vec)
|
|
scores = append(scores, scored{id: it.id, score: score, meta: it.meta})
|
|
}
|
|
|
|
sort.Slice(scores, func(i, j int) bool {
|
|
return scores[i].score > scores[j].score // descending
|
|
})
|
|
|
|
if topK > len(scores) {
|
|
topK = len(scores)
|
|
}
|
|
|
|
out := make([]Result, topK)
|
|
for i := 0; i < topK; i++ {
|
|
out[i] = Result{
|
|
ID: scores[i].id,
|
|
Score: scores[i].score,
|
|
Meta: scores[i].meta,
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// cosine similarity (dot product, assumes L2-normalized vectors).
|
|
func cosine(a, b []float32) float64 {
|
|
if len(a) != len(b) || len(a) == 0 {
|
|
return 0
|
|
}
|
|
var dot float64
|
|
for i := range a {
|
|
dot += float64(a[i]) * float64(b[i])
|
|
}
|
|
return dot
|
|
}
|