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.
- package/README.md +438 -20
- package/RESEARCH.md +117 -37
- package/bin/cli.mjs +931 -36
- package/dist/collapse.d.ts +6 -0
- package/dist/collapse.js +20 -0
- package/dist/core.d.ts +84 -0
- package/dist/core.js +479 -0
- package/dist/frontmatter.d.ts +20 -0
- package/dist/frontmatter.js +150 -0
- package/dist/index-format.d.ts +23 -0
- package/dist/index-format.js +125 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +29 -0
- package/dist/indexer.d.ts +70 -0
- package/dist/indexer.js +452 -0
- package/dist/ivf.d.ts +33 -0
- package/dist/ivf.js +144 -0
- package/dist/lexical.d.ts +44 -0
- package/dist/lexical.js +226 -0
- package/dist/markdown-parser.d.ts +27 -0
- package/dist/markdown-parser.js +252 -0
- package/dist/mcp.d.ts +111 -0
- package/dist/mcp.js +216 -0
- package/dist/metrics.d.ts +43 -0
- package/dist/metrics.js +97 -0
- package/dist/models.d.ts +38 -0
- package/dist/models.js +181 -0
- package/dist/rerank.d.ts +10 -0
- package/dist/rerank.js +41 -0
- package/dist/search.d.ts +135 -0
- package/dist/search.js +492 -0
- package/dist/serve.d.ts +49 -0
- package/dist/serve.js +321 -0
- package/dist/tui.d.ts +17 -0
- package/dist/tui.js +196 -0
- package/dist/wikilinks.d.ts +47 -0
- package/dist/wikilinks.js +171 -0
- package/package.json +25 -2
- package/src/collapse.ts +26 -0
- package/src/core.ts +546 -0
- package/src/frontmatter.ts +190 -0
- package/src/index-format.ts +151 -0
- package/src/index.ts +55 -0
- package/src/indexer.ts +585 -0
- package/src/ivf.ts +190 -0
- package/src/lexical.ts +257 -0
- package/src/markdown-parser.ts +303 -0
- package/src/mcp.ts +243 -0
- package/src/metrics.ts +135 -0
- package/src/models.ts +219 -0
- package/src/rerank.ts +54 -0
- package/src/search.ts +628 -0
- package/src/serve.ts +372 -0
- package/src/tui.ts +227 -0
- package/src/wikilinks.ts +244 -0
- package/src/core.mjs +0 -171
- package/src/indexer.mjs +0 -197
- package/src/models.mjs +0 -67
- 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 +
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
##
|
|
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.
|