Record which embedder wrote the stored vectors and warn on a swap (#378)
The embedder moved from paraphrase-multilingual-MiniLM-L12-v2 to multilingual-e5-small. Both are 384-dimensional, so nothing in the code noticed: cosine between an old stored vector and a new query vector is noise, and recall degrades silently. So the DB now records the embedder that wrote its vectors. One value for the whole DB (migration #11, a small `meta` key/value table) rather than a column on every vector row: the backfill re-embeds every note and fact in one pass, so a per-row marker would hold the same string in every row and cost a column on two tables for nothing. The identity comes from the embedder itself via a new optional ID() method ("multilingual-e5-small@384", model file name plus dimension), so pointing the config at another model changes the string without anyone editing a constant. mavend logs a loud WARNING at startup naming both the stored and the configured embedder when they differ. Detection only — recall behaviour is unchanged. TODO(#378) in store.CheckEmbedder marks where the backfill will hook in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A fresh DB has no marker yet, so the current embedder is recorded and
|
||||
// nothing is flagged.
|
||||
func TestCheckEmbedderFreshDBRecords(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
stored, mismatch, err := s.CheckEmbedder(ctx, "multilingual-e5-small@384")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckEmbedder: %v", err)
|
||||
}
|
||||
if mismatch {
|
||||
t.Fatal("fresh DB reported a mismatch")
|
||||
}
|
||||
if stored != "multilingual-e5-small@384" {
|
||||
t.Fatalf("stored = %q", stored)
|
||||
}
|
||||
got, err := s.Meta(ctx, metaKeyEmbedderID)
|
||||
if err != nil {
|
||||
t.Fatalf("Meta: %v", err)
|
||||
}
|
||||
if got != "multilingual-e5-small@384" {
|
||||
t.Fatalf("marker not persisted, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Both models are 384-dim, so this is the only thing that catches the swap.
|
||||
func TestCheckEmbedderDifferentModelMismatch(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
if err := s.SetMeta(ctx, metaKeyEmbedderID, "paraphrase-multilingual-MiniLM-L12-v2@384"); err != nil {
|
||||
t.Fatalf("SetMeta: %v", err)
|
||||
}
|
||||
stored, mismatch, err := s.CheckEmbedder(ctx, "multilingual-e5-small@384")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckEmbedder: %v", err)
|
||||
}
|
||||
if !mismatch {
|
||||
t.Fatal("different embedder not detected")
|
||||
}
|
||||
if stored != "paraphrase-multilingual-MiniLM-L12-v2@384" {
|
||||
t.Fatalf("stored = %q", stored)
|
||||
}
|
||||
}
|
||||
|
||||
// The same embedder must never raise a false alarm, including on re-check.
|
||||
func TestCheckEmbedderSameModelNoAlarm(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
_, mismatch, err := s.CheckEmbedder(ctx, "multilingual-e5-small@384")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckEmbedder: %v", err)
|
||||
}
|
||||
if mismatch {
|
||||
t.Fatalf("false alarm on pass %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user