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 }