Swap the embedder for an asymmetric retriever (e5-small) #154
@@ -32,7 +32,11 @@ See `docs/rearchitecture.md` for the target architecture, `docs/design.md` for t
|
|||||||
GPU and the workstation has 16GB of VRAM. So the resident model, STT and TTS become preferred
|
GPU and the workstation has 16GB of VRAM. So the resident model, STT and TTS become preferred
|
||||||
remotes with a floor on homesrv. The workstation is never assumed up. Fall back silently when
|
remotes with a floor on homesrv. The workstation is never assumed up. Fall back silently when
|
||||||
it would only do the job better. Name the gap when the 1.7B cannot do it at all. The embedder
|
it would only do the job better. Name the gap when the 1.7B cannot do it at all. The embedder
|
||||||
stays on homesrv permanently, because it backs that floor. Read `docs/offload.md` before
|
stays on homesrv permanently, because it backs that floor. It is multilingual-e5-small,
|
||||||
|
quantized and asymmetric — `EmbedQuery` and `EmbedPassage` apply the `query:`/`passage:`
|
||||||
|
prefixes it was trained with, and calling plain `Embed` on a note is a bug. It replaced
|
||||||
|
MiniLM and bought ten points of recall@1 and 2.5× the speed; see
|
||||||
|
`docs/evals/2026-08-04-recall-e5-small.md`. Read `docs/offload.md` before
|
||||||
touching a daemon seam or adding a model caller. Vikunja #483 is the umbrella, #484 to #487
|
touching a daemon seam or adding a model caller. Vikunja #483 is the umbrella, #484 to #487
|
||||||
are the work.
|
are the work.
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ deps-piper:
|
|||||||
# multilingual-e5-small: an asymmetric retrieval model. It is trained to match
|
# multilingual-e5-small: an asymmetric retrieval model. It is trained to match
|
||||||
# a short question against a longer passage, which is what note recall is.
|
# a short question against a longer passage, which is what note recall is.
|
||||||
# The quantized file is the one we download, deploy and measure — see
|
# The quantized file is the one we download, deploy and measure — see
|
||||||
# docs/evals/2026-07-31-recall.md.
|
# docs/evals/2026-08-04-recall-e5-small.md for what the swap bought.
|
||||||
EMBEDDER_DIR := $(shell pwd)/models/embedder/multilingual-e5-small
|
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_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
|
EMBEDDER_TOKENIZER_URL := https://huggingface.co/Xenova/multilingual-e5-small/resolve/main/tokenizer.json
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Note recall after the e5-small swap — 04-08-2026
|
||||||
|
|
||||||
|
Closes Vikunja #371, which asked for the swap and for this re-measurement. The embedder is no
|
||||||
|
longer paraphrase-multilingual-MiniLM-L12-v2. It is **multilingual-e5-small**, quantized, with the
|
||||||
|
`query:` / `passage:` prefixes it was trained with (`internal/router/onnxembedder.go`,
|
||||||
|
`EmbedQuery` / `EmbedPassage`). `deploy/mavend.json` loads
|
||||||
|
`models/embedder/multilingual-e5-small/model_quantized.onnx`, which is the same file `make
|
||||||
|
download-embedder` fetches and the same file this run measured.
|
||||||
|
|
||||||
|
- Fixture + scorer: `internal/memory/recalleval/` — 32 cases now, not 30
|
||||||
|
- Reproduce: `make eval-recall`
|
||||||
|
- Commit: `b6abb19`
|
||||||
|
- Gate as deployed: `query_min_score` 0.55, `query_min_margin` 0.008
|
||||||
|
|
||||||
|
The fixture grew since 31-07, so the case counts are not comparable row for row. The percentages
|
||||||
|
are.
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
| | 31-07 MiniLM (onnx) | 04-08 e5-small (onnx) |
|
||||||
|
|---|---|---|
|
||||||
|
| **recall@1** | 60.0% (15/25) | **70.4% (19/27)** |
|
||||||
|
| recall@3 | 80.0% (20/25) | **85.2% (23/27)** |
|
||||||
|
| **answered after the gate** | 48.0% (12/25) | **63.0% (17/27)** |
|
||||||
|
| **false recall** | 1/5 (20%) | **0/5** |
|
||||||
|
| wrong note on top / tie on top | 10 / 0 | 8 / 0 |
|
||||||
|
| ranked first, then silenced by the gate | 3 | 2 |
|
||||||
|
| `hard` cases passed | 2/11 | 5/12 |
|
||||||
|
| RU / EN passed | 13/24 / 3/6 | 18/26 / 4/6 |
|
||||||
|
| latency p50 / p95 / max | 59ms / 148ms / 194ms | **23ms / 41ms / 62ms** |
|
||||||
|
|
||||||
|
The hash ratchet CI runs is unchanged in kind and still answers nothing after the gate: recall@1
|
||||||
|
37.0%, recall@3 74.1%, 0/27 answered, 0/5 false. It is lexical and exists so CI has a deterministic
|
||||||
|
floor. Never compare a hash number to an ONNX one.
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
### 1. The swap paid on every axis at once, including latency
|
||||||
|
|
||||||
|
Ten points of recall@1, fifteen points of *answered*, the one false recall gone, and it is 2.5×
|
||||||
|
faster because the quantized e5-small is 118MB against the 470MB fp32 file the old config loaded.
|
||||||
|
Finding 3 of the 31-07 eval predicted the recall half and said nothing about speed; the speed came
|
||||||
|
from fixing the second half of that finding, which was that the deployed path loaded a different
|
||||||
|
file than the download target.
|
||||||
|
|
||||||
|
The concrete case that eval named is fixed. "из-за чего кончилось место" no longer returns the
|
||||||
|
guitar-chords filler note. It now returns a homelab note, `n2` at 0.884, and the wanted note is
|
||||||
|
still not in the top 3 — so the query moved from absurd to merely wrong. That is the shape of what
|
||||||
|
is left.
|
||||||
|
|
||||||
|
### 2. The score distributions still overlap. The margin is what separates them
|
||||||
|
|
||||||
|
This is the part of #371's premise that did not come true. Right-note-first top-1 scores run
|
||||||
|
0.791 / 0.857 / 0.890 (min / median / max). Must-stay-silent top-1 scores run 0.795 / 0.815 /
|
||||||
|
0.835. The silent cases sit *inside* the answering range, so no value of `query_min_score` keeps
|
||||||
|
every real recall and rejects every false one — the same verdict as 31-07, at a higher and tighter
|
||||||
|
band of scores.
|
||||||
|
|
||||||
|
What separates them is the second-place gap. Margin top1-top2 for a right first hit: median 0.024.
|
||||||
|
For a must-be-silent case: median 0.002, max 0.019. A false recall is a note that beats its
|
||||||
|
neighbours by nothing, because nothing in the store is about the question. The sweep:
|
||||||
|
|
||||||
|
| margin | answered | false recall |
|
||||||
|
|---|---|---|
|
||||||
|
| 0.000 | 18/27 (67%) | 3/5 |
|
||||||
|
| 0.005 | 17/27 (63%) | 1/5 |
|
||||||
|
| **0.008 (deployed)** | **17/27 (63%)** | **0/5** |
|
||||||
|
| 0.010 | 15/27 (56%) | 0/5 |
|
||||||
|
| 0.015 | 12/27 (44%) | 0/5 |
|
||||||
|
|
||||||
|
0.008 is the knee: it is the smallest margin that silences all five, and the next step up costs two
|
||||||
|
real answers for nothing. The score gate contributes almost nothing on its own — every value from
|
||||||
|
0.00 to 0.70 answers the same 18 and admits the same 3 — so `query_min_score` is now close to inert
|
||||||
|
and the margin is the live dial. Leave both where they are; #412 is where a further sweep belongs.
|
||||||
|
|
||||||
|
### 3. What is left is a retrieval problem, not a gate problem
|
||||||
|
|
||||||
|
Eight cases put the wrong note on top, and the failures cluster: `hard` 5/12, `preference` 5/9,
|
||||||
|
`homelab` 8/13. Four of the eight have the right note in the top 3, so a reranker would collect
|
||||||
|
them; the other four do not, so nothing downstream can. Two more rank first and are silenced by the
|
||||||
|
margin — `en-hard-024` at 0.826 with margin 0.023, and `ru-home-026` at 0.846 with margin 0.001,
|
||||||
|
which is a genuine near-tie against a second note that is also plausible.
|
||||||
|
|
||||||
|
Preference queries are the weakest class in a way that is not about the model. "когда запускать
|
||||||
|
резервное копирование" and "как мне присылать оповещения" both return a fact, not the note that
|
||||||
|
states the preference. Facts and notes are searched in one pass since #373, so a confidently-scored
|
||||||
|
fact wins a question that a note answers better. That is a ranking policy question and it belongs
|
||||||
|
in its own task, not in a threshold.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Maven — Re-architecture (Qwen3 resident model, revised 2026-07-18)
|
# Maven — Re-architecture (Qwen3 resident model, revised 2026-07-18)
|
||||||
|
|
||||||
*Last verified: 2026-08-02 @ 7079a24. Living doc: correct it in place, do not append.*
|
*Last verified: 2026-08-04 @ b6abb19. Living doc: correct it in place, do not append.*
|
||||||
|
|
||||||
> Supersedes the classifier-first routing model. Agreed in a design session
|
> Supersedes the classifier-first routing model. Agreed in a design session
|
||||||
> after diagnosing that homesrv deploys with a **stub phraser** (no LLM
|
> after diagnosing that homesrv deploys with a **stub phraser** (no LLM
|
||||||
@@ -40,8 +40,10 @@ utterance
|
|||||||
are deferred until the main feature set is complete.
|
are deferred until the main feature set is complete.
|
||||||
- **Embedder demoted from router to tool** — it now backs `memory.search`
|
- **Embedder demoted from router to tool** — it now backs `memory.search`
|
||||||
(RAG) and gives the router a cheap "similar past notes/intents" hint. The
|
(RAG) and gives the router a cheap "similar past notes/intents" hint. The
|
||||||
router no longer depends on it clearing a threshold. Upgrade MiniLM → bge-m3
|
router no longer depends on it clearing a threshold. The MiniLM upgrade is
|
||||||
for better RU retrieval later (model swap, not architecture).
|
done: it is multilingual-e5-small, asymmetric, with the `query:`/`passage:`
|
||||||
|
prefixes (Vikunja #371, `docs/evals/2026-08-04-recall-e5-small.md`). A
|
||||||
|
further swap is a model swap, not architecture (Vikunja #412).
|
||||||
|
|
||||||
### Router output
|
### Router output
|
||||||
- Constrained structured JSON action `{tool, args, escalate}` — NOT free-form
|
- Constrained structured JSON action `{tool, args, escalate}` — NOT free-form
|
||||||
|
|||||||
Reference in New Issue
Block a user