The old model was a symmetric paraphrase model, so it scored "do these look alike" instead of "does this note answer this question". Also fixes the file mismatch: the Makefile, the deploy config and both evals now all name the same quantized file, and the quantized one is what gets measured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
This commit is contained in:
@@ -43,14 +43,17 @@ notes. Without it, the floor `HashEmbedder` is used — deterministic but weak
|
|||||||
(Russian recall rarely clears the confidence gate, many commands fall to
|
(Russian recall rarely clears the confidence gate, many commands fall to
|
||||||
"clarify").
|
"clarify").
|
||||||
|
|
||||||
**Download the embedder** (ONNX, ~90 MB):
|
**Download the embedder** (ONNX, ~120 MB):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make download-embedder
|
make download-embedder
|
||||||
```
|
```
|
||||||
|
|
||||||
This fetches `paraphrase-multilingual-MiniLM-L12-v2` (384-dim, 12-layer,
|
This fetches `multilingual-e5-small` (384-dim, 12-layer, Russian and English)
|
||||||
supports 50+ languages including Russian) to `models/embedder/`.
|
to `models/embedder/multilingual-e5-small/`. It is an asymmetric retrieval
|
||||||
|
model: the code puts `query: ` in front of a question and `passage: ` in front
|
||||||
|
of a stored note, which is how e5 was trained. The quantized file is the one
|
||||||
|
that is downloaded, deployed and measured.
|
||||||
|
|
||||||
**Also need ONNX Runtime** (`libonnxruntime.so`):
|
**Also need ONNX Runtime** (`libonnxruntime.so`):
|
||||||
|
|
||||||
@@ -64,8 +67,8 @@ sudo cp onnxruntime-linux-x64-1.15.1/lib/libonnxruntime.so* /usr/local/lib/
|
|||||||
```json
|
```json
|
||||||
"voice": {
|
"voice": {
|
||||||
"embedder": {
|
"embedder": {
|
||||||
"model_path": "models/embedder/model_quantized.onnx",
|
"model_path": "models/embedder/multilingual-e5-small/model_quantized.onnx",
|
||||||
"tokenizer_path": "models/embedder/tokenizer.json",
|
"tokenizer_path": "models/embedder/multilingual-e5-small/tokenizer.json",
|
||||||
"lib_path": "/usr/local/lib/libonnxruntime.so"
|
"lib_path": "/usr/local/lib/libonnxruntime.so"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,9 +115,13 @@ deps-piper:
|
|||||||
-o /tmp/piper.tar.gz
|
-o /tmp/piper.tar.gz
|
||||||
tar -xzf /tmp/piper.tar.gz -C deps/
|
tar -xzf /tmp/piper.tar.gz -C deps/
|
||||||
|
|
||||||
EMBEDDER_DIR := $(shell pwd)/models/embedder
|
# multilingual-e5-small: an asymmetric retrieval model. It is trained to match
|
||||||
EMBEDDER_MODEL_URL := https://huggingface.co/Xenova/paraphrase-multilingual-MiniLM-L12-v2/resolve/main/onnx/model_quantized.onnx
|
# a short question against a longer passage, which is what note recall is.
|
||||||
EMBEDDER_TOKENIZER_URL := https://huggingface.co/Xenova/paraphrase-multilingual-MiniLM-L12-v2/resolve/main/tokenizer.json
|
# The quantized file is the one we download, deploy and measure — see
|
||||||
|
# RECALL-EVAL-31-07-2026.md.
|
||||||
|
EMBEDDER_DIR := $(shell pwd)/models/embedder/multilingual-e5-small
|
||||||
|
EMBEDDER_MODEL_URL := https://huggingface.co/Xenova/multilingual-e5-small/resolve/main/onnx/model_quantized.onnx
|
||||||
|
EMBEDDER_TOKENIZER_URL := https://huggingface.co/Xenova/multilingual-e5-small/resolve/main/tokenizer.json
|
||||||
|
|
||||||
download-embedder:
|
download-embedder:
|
||||||
mkdir -p $(EMBEDDER_DIR)
|
mkdir -p $(EMBEDDER_DIR)
|
||||||
|
|||||||
+2
-2
@@ -36,8 +36,8 @@
|
|||||||
"stt": { "socket": "/run/maven/stt.sock", "lang": "ru" },
|
"stt": { "socket": "/run/maven/stt.sock", "lang": "ru" },
|
||||||
"tts": { "socket": "/run/maven/tts.sock", "lang": "ru" },
|
"tts": { "socket": "/run/maven/tts.sock", "lang": "ru" },
|
||||||
"embedder": {
|
"embedder": {
|
||||||
"model_path": "/opt/maven/models/embedder/model.onnx",
|
"model_path": "/opt/maven/models/embedder/multilingual-e5-small/model_quantized.onnx",
|
||||||
"tokenizer_path": "/opt/maven/models/embedder/tokenizer.json",
|
"tokenizer_path": "/opt/maven/models/embedder/multilingual-e5-small/tokenizer.json",
|
||||||
"lib_path": "/opt/maven/lib/libonnxruntime.so"
|
"lib_path": "/opt/maven/lib/libonnxruntime.so"
|
||||||
},
|
},
|
||||||
"tool_timeout": "30s",
|
"tool_timeout": "30s",
|
||||||
|
|||||||
@@ -117,18 +117,40 @@ type cachingEmbedder struct {
|
|||||||
seen map[string][]float32
|
seen map[string][]float32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ router.AsymmetricEmbedder = (*cachingEmbedder)(nil)
|
||||||
|
|
||||||
func (c *cachingEmbedder) Dim() int { return c.inner.Dim() }
|
func (c *cachingEmbedder) Dim() int { return c.inner.Dim() }
|
||||||
func (c *cachingEmbedder) Close() error { return nil } // the caller owns inner
|
func (c *cachingEmbedder) Close() error { return nil } // the caller owns inner
|
||||||
|
|
||||||
func (c *cachingEmbedder) Embed(ctx context.Context, text string) ([]float32, error) {
|
func (c *cachingEmbedder) Embed(ctx context.Context, text string) ([]float32, error) {
|
||||||
if v, ok := c.seen[text]; ok {
|
return c.cached(ctx, "embed:"+text, func() ([]float32, error) {
|
||||||
|
return c.inner.Embed(ctx, text)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// The two sides of an asymmetric embedder give different vectors for the same
|
||||||
|
// string, so the cache key has to say which side asked.
|
||||||
|
func (c *cachingEmbedder) EmbedQuery(ctx context.Context, text string) ([]float32, error) {
|
||||||
|
return c.cached(ctx, "query:"+text, func() ([]float32, error) {
|
||||||
|
return router.EmbedQuery(ctx, c.inner, text)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cachingEmbedder) EmbedPassage(ctx context.Context, text string) ([]float32, error) {
|
||||||
|
return c.cached(ctx, "passage:"+text, func() ([]float32, error) {
|
||||||
|
return router.EmbedPassage(ctx, c.inner, text)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cachingEmbedder) cached(_ context.Context, key string, embed func() ([]float32, error)) ([]float32, error) {
|
||||||
|
if v, ok := c.seen[key]; ok {
|
||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
v, err := c.inner.Embed(ctx, text)
|
v, err := embed()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
c.seen[text] = v
|
c.seen[key] = v
|
||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +327,7 @@ func scoreCase(ctx context.Context, emb router.Embedder, newStore NewStore, minS
|
|||||||
|
|
||||||
all := append(append([]StoredNote(nil), c.Notes...), filler...)
|
all := append(append([]StoredNote(nil), c.Notes...), filler...)
|
||||||
for _, n := range all {
|
for _, n := range all {
|
||||||
vec, err := emb.Embed(ctx, n.Text)
|
vec, err := router.EmbedPassage(ctx, emb, n.Text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Outcome{}, fmt.Errorf("%s: embed note %s: %w", c.ID, n.ID, err)
|
return Outcome{}, fmt.Errorf("%s: embed note %s: %w", c.ID, n.ID, err)
|
||||||
}
|
}
|
||||||
@@ -317,7 +339,7 @@ func scoreCase(ctx context.Context, emb router.Embedder, newStore NewStore, minS
|
|||||||
|
|
||||||
o := Outcome{Case: c}
|
o := Outcome{Case: c}
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
qvec, err := emb.Embed(ctx, c.Query)
|
qvec, err := router.EmbedQuery(ctx, emb, c.Query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
o.Latency = time.Since(start)
|
o.Latency = time.Since(start)
|
||||||
o.Err = err
|
o.Err = err
|
||||||
|
|||||||
@@ -246,8 +246,8 @@ func TestONNXRecall(t *testing.T) {
|
|||||||
if lib == "" {
|
if lib == "" {
|
||||||
t.Skip("MAVEN_ONNX_LIB unset — see AGENTS.md § Embedder model for intent routing")
|
t.Skip("MAVEN_ONNX_LIB unset — see AGENTS.md § Embedder model for intent routing")
|
||||||
}
|
}
|
||||||
model := filepath.Join("../../..", "models/embedder/model.onnx")
|
model := filepath.Join("../../..", "models/embedder/multilingual-e5-small/model_quantized.onnx")
|
||||||
tok := filepath.Join("../../..", "models/embedder/tokenizer.json")
|
tok := filepath.Join("../../..", "models/embedder/multilingual-e5-small/tokenizer.json")
|
||||||
for _, p := range []string{lib, model, tok} {
|
for _, p := range []string{lib, model, tok} {
|
||||||
if _, err := os.Stat(p); err != nil {
|
if _, err := os.Stat(p); err != nil {
|
||||||
t.Skipf("missing %s: %v", p, err)
|
t.Skipf("missing %s: %v", p, err)
|
||||||
|
|||||||
@@ -185,8 +185,8 @@ func TestONNXBaseline(t *testing.T) {
|
|||||||
if lib == "" {
|
if lib == "" {
|
||||||
t.Skip("MAVEN_ONNX_LIB unset — see AGENTS.md § Embedder model for intent routing")
|
t.Skip("MAVEN_ONNX_LIB unset — see AGENTS.md § Embedder model for intent routing")
|
||||||
}
|
}
|
||||||
model := filepath.Join("../../..", "models/embedder/model.onnx")
|
model := filepath.Join("../../..", "models/embedder/multilingual-e5-small/model_quantized.onnx")
|
||||||
tok := filepath.Join("../../..", "models/embedder/tokenizer.json")
|
tok := filepath.Join("../../..", "models/embedder/multilingual-e5-small/tokenizer.json")
|
||||||
for _, p := range []string{lib, model, tok} {
|
for _, p := range []string{lib, model, tok} {
|
||||||
if _, err := os.Stat(p); err != nil {
|
if _, err := os.Stat(p); err != nil {
|
||||||
t.Skipf("missing %s: %v", p, err)
|
t.Skipf("missing %s: %v", p, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user