md-semantic-search 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +438 -20
  2. package/RESEARCH.md +117 -37
  3. package/bin/cli.mjs +931 -36
  4. package/dist/collapse.d.ts +6 -0
  5. package/dist/collapse.js +20 -0
  6. package/dist/core.d.ts +84 -0
  7. package/dist/core.js +479 -0
  8. package/dist/frontmatter.d.ts +20 -0
  9. package/dist/frontmatter.js +150 -0
  10. package/dist/index-format.d.ts +23 -0
  11. package/dist/index-format.js +125 -0
  12. package/dist/index.d.ts +29 -0
  13. package/dist/index.js +29 -0
  14. package/dist/indexer.d.ts +70 -0
  15. package/dist/indexer.js +452 -0
  16. package/dist/ivf.d.ts +33 -0
  17. package/dist/ivf.js +144 -0
  18. package/dist/lexical.d.ts +44 -0
  19. package/dist/lexical.js +226 -0
  20. package/dist/markdown-parser.d.ts +27 -0
  21. package/dist/markdown-parser.js +252 -0
  22. package/dist/mcp.d.ts +111 -0
  23. package/dist/mcp.js +216 -0
  24. package/dist/metrics.d.ts +43 -0
  25. package/dist/metrics.js +97 -0
  26. package/dist/models.d.ts +38 -0
  27. package/dist/models.js +181 -0
  28. package/dist/rerank.d.ts +10 -0
  29. package/dist/rerank.js +41 -0
  30. package/dist/search.d.ts +135 -0
  31. package/dist/search.js +492 -0
  32. package/dist/serve.d.ts +49 -0
  33. package/dist/serve.js +321 -0
  34. package/dist/tui.d.ts +17 -0
  35. package/dist/tui.js +196 -0
  36. package/dist/wikilinks.d.ts +47 -0
  37. package/dist/wikilinks.js +171 -0
  38. package/package.json +25 -2
  39. package/src/collapse.ts +26 -0
  40. package/src/core.ts +546 -0
  41. package/src/frontmatter.ts +190 -0
  42. package/src/index-format.ts +151 -0
  43. package/src/index.ts +55 -0
  44. package/src/indexer.ts +585 -0
  45. package/src/ivf.ts +190 -0
  46. package/src/lexical.ts +257 -0
  47. package/src/markdown-parser.ts +303 -0
  48. package/src/mcp.ts +243 -0
  49. package/src/metrics.ts +135 -0
  50. package/src/models.ts +219 -0
  51. package/src/rerank.ts +54 -0
  52. package/src/search.ts +628 -0
  53. package/src/serve.ts +372 -0
  54. package/src/tui.ts +227 -0
  55. package/src/wikilinks.ts +244 -0
  56. package/src/core.mjs +0 -171
  57. package/src/indexer.mjs +0 -197
  58. package/src/models.mjs +0 -67
  59. package/src/search.mjs +0 -107
package/RESEARCH.md CHANGED
@@ -12,7 +12,7 @@ auditable and reproducible.
12
12
  > ranked an irrelevant page *above* the correct one. `e5-base` fixed it.
13
13
  > - **Quantization is not the problem** people assume — int8 vs fp32 changed
14
14
  > cosine scores by <0.003 and never changed the ranking.
15
- > - **Hybrid (vector + keyword via RRF)** beats either alone: vectors catch
15
+ > - **Hybrid (vector + lexical via RRF)** beats either alone: vectors catch
16
16
  > paraphrases, keywords catch exact identifiers.
17
17
 
18
18
  ---
@@ -98,55 +98,135 @@ This is why the default is `e5-base`, not the tempting lightweight `e5-small`.
98
98
  ## 5. Design decisions that follow
99
99
 
100
100
  ### E5 prefixes are mandatory
101
- The E5 family is trained with instruction prefixes: documents must be embedded
102
- as `passage: <text>` and queries as `query: <text>`. Omitting them measurably
103
- degrades retrieval. The model registry encodes this per-model, because
104
- **`bge-m3` is the opposite — it wants no prefix at all.** Getting this wrong is
105
- a silent quality bug.
106
-
107
- ### Chunk by heading, not by page
108
- Embedding a whole multi-section page into one vector blurs distinct topics.
109
- We split on Markdown headings (`#`..`######`); sections over ~1400 chars split
110
- again on blank lines. Search then returns the relevant *section*, with its
111
- heading, not just "the page where the word appears somewhere."
112
-
113
- ### Hybrid ranking via Reciprocal Rank Fusion
114
- Pure vectors miss exact identifiers (`win32`, `TextIOWrapper`, error codes);
115
- pure keywords miss paraphrases. We compute two rankings — cosine and lexical
116
- term-overlap — and fuse them with RRF (`score = Σ 1/(k + rank)`, `k=60`). RRF
117
- needs no score normalization or weight tuning, which is what makes it robust
118
- across very different score scales.
119
-
120
- ### Stop-words protect the lexical lane
121
- Cross-lingual queries share function words ("при", "the", "кода") with
122
- irrelevant documents, letting lexical overlap promote noise. A small ru/en
123
- stop-list is removed before lexical scoring so RRF fuses signal, not filler.
124
-
125
- ### Incremental by content hash
126
- Each file's md5 is stored. Re-indexing reuses embeddings for unchanged files
127
- and only re-embeds what changed — a no-op re-index of 46 files is sub-second.
128
- Changing the model invalidates all vectors and forces a clean rebuild
129
- (dimensions and prefix semantics differ).
130
-
131
- ## 6. Reproducing
101
+ `multilingual-e5-*` models require `"query: "` before queries and `"passage: "`
102
+ before documents. Omitting them degrades cosine separation.
103
+
104
+ ### Lexical + vector fusion (RRF)
105
+ Vector search catches concepts ("frozen console" → "win32 stdin"), but can blur
106
+ exact function names or error codes (`E_FAIL_WIN32`). Combined rank via
107
+ Reciprocal Rank Fusion ($k=60$) yields the best overall precision.
108
+
109
+ ## 6. Reranking architecture (issue #15)
110
+
111
+ First-pass vector + lexical retrieval is shallow: query and document are
112
+ embedded independently. A cross-encoder reranker (`Xenova/bge-reranker-base`)
113
+ processes the query and document together through a sequence classification
114
+ head.
115
+
116
+ - **Lazy loading:** The reranker model is loaded only when `--rerank` is passed.
117
+ - **Candidate pool:** First-pass retrieval returns candidate pool $k_{\text{pool}} = \max(20, k \times 3)$, which is then re-ordered by the cross-encoder.
118
+ - **Head semantics:** `bge-reranker-base` uses a single-class output head (`logits[0]`). Softmax must **not** be applied over single-class heads (which would yield 1.0 for all inputs).
119
+
120
+ ## 7. Golden-set benchmark (issue #56)
121
+
122
+ The experiments above are qualitative. To make quality claims reproducible and
123
+ regression-detectable, the repo ships a **frozen golden set**: a small synthetic
124
+ RU/EN corpus plus graded relevance judgements.
125
+
126
+ - `bench/corpus/` — 16 markdown files, 8 topics × 2 languages (tokens, i18n,
127
+ windows stdin, db failover, model cache, obsidian links, backups, search
128
+ index/server). Content is deliberately plain so embedding quality, not
129
+ domain vocabulary, drives the scores.
130
+ - `bench/fixtures/dev-golden.json` — 60 queries across six categories
131
+ (`natural-question`, `paraphrase`, `keyword`, `alias`, `hard-negative`,
132
+ `identifier`), 48 dev / 8 test / 4 holdout. Grades: 3 = direct answer,
133
+ 2 = useful support, 1 = related, 0 = hard negative. The fixture pins
134
+ `corpusHash` (sha256 of paths+content) — a changed corpus fails the run.
135
+ - `bench/fixture.mjs` — schema-v1 validator, `loadFixture`, `corpusFingerprint`,
136
+ deterministic `splitIntoSlices`.
137
+ - `src/metrics.mjs` — nDCG@k (graded, log2 discount), MRR, Hit@k, Recall@k,
138
+ `queryMetrics`/`aggregateMetrics`, win/tie/loss for paired model comparison.
139
+ - `scripts/run-bench.mjs` — builds the index over the frozen corpus, runs every
140
+ query of the selected slice, prints aggregate + per-category metrics.
141
+
142
+ ```bash
143
+ node scripts/run-bench.mjs --slice dev # full dev eval (k=10, e5-base)
144
+ node scripts/run-bench.mjs --fast --json # CI smoke (k=3)
145
+ node scripts/run-bench.mjs --model bge-m3 --slice dev > /tmp/bge-m3.json
146
+ node scripts/run-bench.mjs --model e5-base --slice dev > /tmp/e5-base.json
147
+ ```
148
+
149
+ Baseline on dev (e5-base, hybrid RRF, k=10, 2026-08-14):
150
+
151
+ | Metric | Value |
152
+ |--------|-------|
153
+ | nDCG@10 | 0.9254 |
154
+ | MRR | 0.9167 |
155
+ | Hit@10 | 0.9792 |
156
+ | Recall@10 | 1.0000 |
157
+
158
+ ## 8. Reproducing
132
159
 
133
160
  ```bash
134
- # Build with the default, then with the heavy model, and compare a known query:
135
161
  mdss index --db ./your-wiki
136
162
  mdss search --db ./your-wiki --semantic "a paraphrase of something you know is in there"
137
163
 
138
164
  mdss index --db ./your-wiki --model bge-m3
139
165
  mdss search --db ./your-wiki --semantic "the same paraphrase"
140
- ```
141
166
 
142
- Use `--semantic` to see the raw vector ranking (no lexical fusion) when
143
- evaluating a model — that isolates embedding quality from the keyword lane.
167
+ mdss index --db ./your-wiki --model qwen3-embedding-0.6b
168
+ mdss search --db ./your-wiki --semantic "the same paraphrase"
169
+ ```
144
170
 
145
- ## 7. Recommendations
171
+ ## 9. Recommendations
146
172
 
147
173
  | If you want… | Use |
148
174
  |--------------|-----|
149
175
  | Sensible default, fast, ~280 MB | `e5-base` (default) |
150
176
  | Maximum cross-lingual quality, disk/time no object | `bge-m3` |
177
+ | Evaluate a newer 0.6B multilingual model locally | `qwen3-embedding-0.6b` (~613 MB q8) |
151
178
  | Smallest footprint and your queries are same-language & literal | `e5-small` (with eyes open) |
152
179
  | Best precision on exact identifiers | keep hybrid on (don't pass `--semantic`) |
180
+ | Sharpest top-k on a larger corpus | add `--rerank` (cross-encoder, +280 MB) |
181
+
182
+ ---
183
+
184
+ ## 10. Research Evaluation: Lightweight Multilingual Reranker Tier (Issue #51)
185
+
186
+ ### Context & Decision Gate
187
+ - **Baseline:** `Xenova/bge-reranker-base` (278MB q8, XLM-RoBERTa architecture, single-class logit head).
188
+ - **Rejected Candidate:** `cross-encoder/ms-marco-MiniLM-L6-v2` is English-only and collapses on Russian/bilingual technical queries despite smaller size.
189
+ - **Criteria for New Candidate:**
190
+ 1. Verified ONNX/Transformers.js artifact with `q8` quantization.
191
+ 2. Proven multilingual / cross-lingual ranking capability on RU/EN.
192
+ 3. Commercial MIT/Apache-2.0 compatible license.
193
+ 4. Non-inferiority on the golden set (#56): nDCG@10 drop $\le 0.01$ vs BGE-base.
194
+ 5. Single-class raw logit verification (no 1-class softmax wrapper).
195
+
196
+ If no candidate meets the multilingual non-inferiority gate, `bge-reranker-base` remains the sole default reranker.
197
+
198
+ ---
199
+
200
+ ## 11. Research Evaluation: Persisted Vector int8 & Matryoshka (MRL) Scaling (Issue #52)
201
+
202
+ ### Disambiguation & Rules
203
+ - **Model Inference Quantization (`dtype: q8`):** Already used for transformer weight inference in `src/core.mjs`.
204
+ - **Stored Vector Quantization:** Compressing stored document float vectors (`Float32Array`) to `int8` with per-vector scale factors.
205
+ - **Matryoshka Representation Learning (MRL):** Truncating embedding output dimension (e.g., 1024 → 256).
206
+ - **Contract Rule:** Dimension truncation is allowed **only** for models with explicit MRL training contracts (e.g. `Qwen3-Embedding-0.6B`). Models like `BGE-M3` do **not** support arbitrary dimension slicing without severe recall degradation.
207
+
208
+ ### Scale Trigger
209
+ - Do not introduce persisted vector quantization or MRL index format breaking changes until corpus size exceeds **100,000 chunks** or stored index size dominates disk/RAM budgets beyond 500 MB.
210
+
211
+ ---
212
+
213
+ ## 12. Research Evaluation: Late Chunking vs Heading-Path Context (Issue #53)
214
+
215
+ ### Baseline vs Late Chunking
216
+ - **Current Baseline:** `mdss` prepends document title + full heading hierarchy path (`headingPath: ['Section', 'Subsection']`) to every chunk before tokenization.
217
+ - **Late Chunking Concept:** Passing full documents through a long-context transformer, extracting token-level hidden states, and pooling chunk token spans post-hoc.
218
+ - **Evaluation Gate:** Late chunking adds high memory overhead, token-offset alignment complexity, and custom model batching. It will be prototyped **only if** heading-path contextualization exhibits a measured context-recall deficit on long-document ablation benchmarks.
219
+
220
+ ---
221
+
222
+ ## 13. Research Evaluation: WASM SIMD & ANN/IVF Scale Triggers (Issues #31 & #32)
223
+
224
+ ### WebAssembly SIMD Kernel Gate (Issue #31)
225
+ - **Baseline:** Linear sweep over contiguous `Float32Array` runtime vector buffer (Issue #30).
226
+ - **Trigger:** WebAssembly SIMD dot-product kernels will be considered **only if** CPU profiling on >50,000 chunks shows JS float multiplication accounts for >40% of warm search latency.
227
+ - **Constraint:** Zero native Node C++ bindings; must maintain pure JS fallback for non-WASM environments.
228
+
229
+ ### ANN / IVF Scale Threshold (Issue #32)
230
+ - **Baseline:** Exact $O(N \cdot d)$ linear sweep.
231
+ - **Trigger:** ANN (Inverted File / HNSW) indexing is a scale contingency. It shall **not** be introduced until corpus size exceeds **100,000+ chunks** and exact warm sweep latency exceeds 50 ms.
232
+ - **Requirement:** Exact pre/post-filter correctness must be preserved; small knowledge bases (<100k chunks) will always use exact linear search.