Commit 92f1344
feat(rag): hybrid retrieval + multilingual refinements on top of vector RAG (#246)
> **Stacked on `feat/221-rag-sources`.** Base this PR against that
branch, not `main`, and merge it after the base lands. The diff here is
only the retrieval-quality layer (~16 files); everything else belongs to
the base PR.
Turns the vector-only baseline into hybrid retrieval and adds the
language refinements. Same on-device guarantee — nothing new leaves the
device; this is purely a quality upgrade to how chunks are found and
ranked.
## What it adds on top of the baseline
- **Keyword index** (`database/keywordIndex.ts`) — a parallel FTS5/BM25
table keyed by the same `documentId:chunkIndex` chunk id, built
alongside the vector index and torn down with it. On first launch after
upgrading from the vector baseline it **backfills from the existing
`vectors` table**, so documents imported before this PR become
keyword-searchable without a re-import.
- **Hybrid retrieval** (`utils/hybridRetrieval.ts`, replacing the
vanilla `retrieve`) — semantic vector search + keyword BM25, fused with
Reciprocal Rank Fusion, then re-ranked (term-coverage boost → MMR
diversification → adaptive-k) before neighbor expansion.
- **Multilingual no-answer detection** — Polish refusal patterns added
to the existing English ones, so a PL "brak informacji" reply cites
nothing instead of over-citing.
- **`HybridRetriever`** — the app↔library boundary: a thin wrapper
binding store + embeddings into one `retrieve(query, options)` call.
## Why hybrid (the evidence)
BM25 recovers exact-match recall — names, codes, rare tokens — that
embeddings miss; vectors recover paraphrase. An offline eval harness
(real LFM 2.5 + FTS5, frozen embeddings, ~1000 queries) confirms hybrid
beats pure vector on exact and mixed queries, at parity on
pure-paraphrase. RRF fuses the two rankings without tuning a per-query
linear weight. The semantic floor those numbers were measured against
(`0.40` + `0.25` top-keep floor) is the calibrated gate that ships in
the base PR, so the measured recall is what this branch actually
delivers.
> Re-run the on-device eval before merging: the keyword stemming
(`utils/queryTerms.ts`) and the RRF/coverage/adaptive-k knobs are
exactly what the harness measures, so any tuning here should be
revalidated against it rather than trusted on unit tests alone.
## Key decisions / trade-offs
- **No cross-encoder re-ranker.** Re-ranking is RRF + term-coverage +
MMR + adaptive-k — all cheap and on-device. A cross-encoder would
improve precision but costs a second model and per-candidate inference
on a phone.
- **`HybridRetriever` has no interface and no generic.** One
implementation, one caller. Notably *not* `implements VectorStore`: the
hybrid is read-only and its `ContextChunk` output drops id/embedding, so
coercing it to a `QueryResult` would change results. Extract an
interface when a second retriever appears.
- **Retrieval constants live in `constants/retrieval.ts`** with
per-value rationale — the fusion weights, MMR lambda, coverage alpha,
adaptive-k ratios and semantic floor each document where the number
comes from.
## Known limitations
- **FTS5 degradation is soft.** If the native build lacks FTS5,
`ensureKeywordIndex` logs a warning and hybrid search silently falls
back to vector-only. Open question: leave as-is / surface a signal /
fail fast — depends on whether a supported build without FTS5 exists.
- **Query processing is PL/EN-tuned.** `utils/queryTerms.ts` hardcodes
Polish morphology (ł/Ł folding, stem-prefix) and the no-answer detection
is PL/EN. Other languages work but retrieve measurably worse. Follow-up:
a `LanguageAdapter` with a neutral fallback.
## How to test locally
1. **Exact-match win** — attach a doc containing a rare token (an error
code, a surname) and query it verbatim; the chunk should surface even
when semantic similarity alone would gate it out.
2. **PL refusal** — ask (in Polish) something the docs don't cover;
expect a no-answer reply with zero citations.
3. **Diversity** — a spanning multi-doc query should not return five
near-duplicate chunks from one file (MMR + per-file cap).
4. **FTS5 fallback** — on a build without FTS5, retrieval still returns
results (vector-only) with a logged warning.
5. **Backfill after upgrade** — open a build that imported documents on
the vector baseline, then upgrade to this branch; the pre-existing
documents are keyword-searchable (verbatim rare-token query surfaces
them) with no re-import.
---------
Co-authored-by: Norbert Klockiewicz <Nklockiewicz12@gmail.com>1 parent 700363f commit 92f1344
87 files changed
Lines changed: 10655 additions & 2098 deletions
File tree
- __mocks__
- @gorhom
- __tests__
- app/(drawer)/chat
- assets/icons
- components
- bottomSheets
- chat-screen
- constants
- context
- database
- hooks
- ios
- store
- utils
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
2 | | - | |
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
3 | 9 | | |
4 | | - | |
5 | | - | |
6 | | - | |
7 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
8 | 24 | | |
9 | 25 | | |
10 | | - | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
11 | 41 | | |
12 | | - | |
13 | | - | |
14 | | - | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
| 5 | + | |
4 | 6 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
76 | 76 | | |
77 | 77 | | |
78 | 78 | | |
| 79 | + | |
79 | 80 | | |
80 | 81 | | |
81 | 82 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
3 | 6 | | |
4 | 7 | | |
5 | 8 | | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
6 | 13 | | |
7 | 14 | | |
8 | 15 | | |
| |||
13 | 20 | | |
14 | 21 | | |
15 | 22 | | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
21 | | - | |
22 | | - | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
23 | 35 | | |
24 | 36 | | |
25 | 37 | | |
26 | | - | |
| 38 | + | |
27 | 39 | | |
28 | 40 | | |
29 | 41 | | |
| |||
45 | 57 | | |
46 | 58 | | |
47 | 59 | | |
48 | | - | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
49 | 66 | | |
50 | 67 | | |
51 | 68 | | |
| |||
63 | 80 | | |
64 | 81 | | |
65 | 82 | | |
66 | | - | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
67 | 90 | | |
68 | 91 | | |
69 | 92 | | |
| |||
77 | 100 | | |
78 | 101 | | |
79 | 102 | | |
80 | | - | |
81 | | - | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
82 | 111 | | |
83 | 112 | | |
84 | 113 | | |
| |||
91 | 120 | | |
92 | 121 | | |
93 | 122 | | |
94 | | - | |
| 123 | + | |
95 | 124 | | |
96 | 125 | | |
97 | 126 | | |
| |||
114 | 143 | | |
115 | 144 | | |
116 | 145 | | |
117 | | - | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
118 | 158 | | |
119 | 159 | | |
120 | 160 | | |
| |||
175 | 215 | | |
176 | 216 | | |
177 | 217 | | |
178 | | - | |
| 218 | + | |
179 | 219 | | |
180 | 220 | | |
181 | 221 | | |
| |||
187 | 227 | | |
188 | 228 | | |
189 | 229 | | |
190 | | - | |
191 | | - | |
192 | | - | |
193 | | - | |
194 | | - | |
195 | | - | |
196 | | - | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
197 | 244 | | |
198 | 245 | | |
199 | 246 | | |
200 | 247 | | |
| 248 | + | |
201 | 249 | | |
202 | 250 | | |
203 | 251 | | |
204 | 252 | | |
205 | 253 | | |
206 | | - | |
| 254 | + | |
207 | 255 | | |
208 | 256 | | |
209 | 257 | | |
| |||
293 | 341 | | |
294 | 342 | | |
295 | 343 | | |
296 | | - | |
297 | | - | |
298 | | - | |
299 | | - | |
300 | | - | |
301 | | - | |
302 | | - | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
303 | 356 | | |
304 | 357 | | |
305 | 358 | | |
306 | 359 | | |
307 | 360 | | |
308 | 361 | | |
309 | | - | |
310 | | - | |
311 | | - | |
312 | | - | |
313 | | - | |
314 | | - | |
315 | | - | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
316 | 374 | | |
317 | 375 | | |
318 | 376 | | |
| |||
332 | 390 | | |
333 | 391 | | |
334 | 392 | | |
335 | | - | |
| 393 | + | |
336 | 394 | | |
337 | 395 | | |
338 | 396 | | |
| |||
405 | 463 | | |
406 | 464 | | |
407 | 465 | | |
408 | | - | |
| 466 | + | |
409 | 467 | | |
410 | 468 | | |
411 | 469 | | |
| |||
434 | 492 | | |
435 | 493 | | |
436 | 494 | | |
437 | | - | |
| 495 | + | |
438 | 496 | | |
439 | | - | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
440 | 504 | | |
441 | 505 | | |
442 | 506 | | |
| |||
0 commit comments