feat: hybrid retrieval (BM25 + vettoriale, RRF) opt-in via hybrid=true

- sparse vector bm25 (modifier IDF) + indice TEXT su text
- migrazione automatica: create_vector_name su collection esistente + backfill dei punti privi di sparse
- write: sparse vector a ogni upsert (fastembed Qdrant/bm25)
- search: hybrid=true → prefetch denso (min_score anti-rumore) + sparso, fusione RRF; default invariato (punteggi cosine)
- qdrant-client 1.13.0 → 1.19.0 (create_vector_name, query_points; search rimosso in 1.19)
- fastembed 0.5.1 + pre-download modello BM25 nel Dockerfile (appuser)
- estensione: parametro hybrid in qmem_search
- verificato sul server: migrazione + backfill OK, termine esatto trovato con RRF 1.0 (istanza di test)
This commit is contained in:
Matteo Benedetto
2026-08-16 19:41:42 +02:00
parent 2d354bfde6
commit 7abced1566
4 changed files with 144 additions and 11 deletions
+9
View File
@@ -279,6 +279,14 @@ export default function qmemExtension(pi: ExtensionAPI) {
}),
),
top_k: Type.Optional(Type.Integer({ description: "Numero massimo di risultati (default: 5, max 20)." })),
hybrid: Type.Optional(
Type.Boolean({
description:
"True = hybrid retrieval (BM25 + vettoriale, fusione RRF): migliore recall su nomi propri, ID, codici, " +
"acronimi e termini esatti. I punteggi risultanti sono RRF, non cosine: interpretali come ranking, " +
"non come similarità. min_score resta applicato al ramo vettoriale (anti-rumore).",
}),
),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
const cfg = loadConfig();
@@ -302,6 +310,7 @@ export default function qmemExtension(pi: ExtensionAPI) {
include_superseded: p.include_superseded ?? false,
min_score: p.min_score ?? 0.45,
top_k: p.top_k ?? 5,
hybrid: p.hybrid ?? false,
},
signal,
);