md-semantic-search 1.0.3 → 2.0.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 +132 -16
- package/RESEARCH.md +207 -3
- package/bin/cli.mjs +449 -13
- package/dist/binary-format.js +9 -0
- package/dist/completions.js +11 -1
- package/dist/core.d.ts +42 -4
- package/dist/core.js +208 -14
- package/dist/federation.d.ts +16 -1
- package/dist/federation.js +33 -16
- package/dist/filter.d.ts +7 -0
- package/dist/filter.js +13 -0
- package/dist/hnsw.d.ts +96 -0
- package/dist/hnsw.js +326 -0
- package/dist/index-format.js +8 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/indexer.d.ts +18 -1
- package/dist/indexer.js +92 -18
- package/dist/ivf.d.ts +14 -7
- package/dist/ivf.js +126 -18
- package/dist/lexical.d.ts +10 -0
- package/dist/lexical.js +98 -15
- package/dist/markdown-parser.d.ts +62 -0
- package/dist/markdown-parser.js +227 -30
- package/dist/mcp.d.ts +229 -7
- package/dist/mcp.js +743 -44
- package/dist/models.d.ts +8 -2
- package/dist/models.js +84 -32
- package/dist/providers.d.ts +56 -0
- package/dist/providers.js +305 -0
- package/dist/quantization.d.ts +12 -0
- package/dist/quantization.js +54 -0
- package/dist/rag.d.ts +132 -7
- package/dist/rag.js +314 -38
- package/dist/repair.d.ts +1 -1
- package/dist/repair.js +53 -2
- package/dist/rerank.d.ts +51 -6
- package/dist/rerank.js +69 -10
- package/dist/search.d.ts +49 -3
- package/dist/search.js +387 -78
- package/dist/serve.d.ts +35 -0
- package/dist/serve.js +574 -58
- package/dist/tui.d.ts +14 -0
- package/dist/tui.js +132 -15
- package/dist/watcher.js +23 -12
- package/dist/webui.d.ts +3 -3
- package/dist/webui.js +225 -5
- package/dist/wikilinks.d.ts +31 -0
- package/dist/wikilinks.js +185 -1
- package/package.json +3 -3
- package/src/binary-format.ts +13 -0
- package/src/completions.ts +11 -1
- package/src/core.ts +249 -13
- package/src/federation.ts +51 -17
- package/src/filter.ts +14 -0
- package/src/hnsw.ts +414 -0
- package/src/index-format.ts +7 -1
- package/src/index.ts +7 -2
- package/src/indexer.ts +112 -19
- package/src/ivf.ts +157 -20
- package/src/lexical.ts +91 -13
- package/src/markdown-parser.ts +279 -37
- package/src/mcp.ts +820 -47
- package/src/models.ts +97 -35
- package/src/providers.ts +345 -0
- package/src/quantization.ts +61 -0
- package/src/rag.ts +451 -34
- package/src/repair.ts +51 -2
- package/src/rerank.ts +121 -14
- package/src/search.ts +424 -82
- package/src/serve.ts +589 -66
- package/src/tui.ts +142 -16
- package/src/watcher.ts +21 -10
- package/src/webui.ts +225 -5
- package/src/wikilinks.ts +220 -1
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ measurements that shaped its defaults.
|
|
|
100
100
|
|
|
101
101
|
| Approach | Stronger than mdss at | mdss advantage for its target segment |
|
|
102
102
|
|---|---|---|
|
|
103
|
-
| **QMD** | Query expansion, HyDE, LLM reranking, MCP/SDK ecosystem | Smaller default footprint, Node
|
|
103
|
+
| **QMD** | Query expansion, HyDE, LLM reranking, MCP/SDK ecosystem | Smaller default footprint, Node 20.9+, zero native C++ runtime (pure ONNX.js), fast incremental chunk reuse |
|
|
104
104
|
| **Obsidian Hybrid Search** | Obsidian GUI integration, Obsidian vault plugins | Standalone CLI & HTTP API, not tied to Obsidian, crash-safe atomic indexing, robust incremental re-index |
|
|
105
105
|
| **Vector Databases (Qdrant/Pinecone)** | Multi-million chunk scale, distributed vector index, cloud replication | Zero infra, single JSON index, no server/daemon required, fast in-memory sweep for 10k–50k chunks |
|
|
106
106
|
| **Grep / Fzf** | Zero model load time, instant literal string matching | Cross-lingual matching (RU/EN), paraphrase retrieval, semantic understanding while retaining exact BM25 lexical precision |
|
|
@@ -378,7 +378,7 @@ mdss search --db /path/to/your/markdown "how do I rotate the api token" --rerank
|
|
|
378
378
|
```
|
|
379
379
|
|
|
380
380
|
- The first pass pulls a wider candidate pool (default `max(20, k*3)` chunks),
|
|
381
|
-
the cross-encoder (`
|
|
381
|
+
the cross-encoder (`gte-reranker-modernbert`, ~144 MB, downloaded on first
|
|
382
382
|
use) re-scores them as query↔passage pairs, and the best `k` survive.
|
|
383
383
|
- Results from a re-ranked search carry a `rerankScore` field in `--json` mode.
|
|
384
384
|
- Programmatic use: `rerank: true` in `search()`/`searchIndex()`, `rerankPool`
|
|
@@ -386,9 +386,57 @@ mdss search --db /path/to/your/markdown "how do I rotate the api token" --rerank
|
|
|
386
386
|
accepts `"rerank": true` in the `/search` body.
|
|
387
387
|
- The reranker is lazy: nothing loads unless `--rerank` is actually requested,
|
|
388
388
|
so searches without it pay zero extra cost.
|
|
389
|
+
- **Reranker registry** (issue #203): `--rerank-model <alias>` picks the model —
|
|
390
|
+
`gte-reranker-modernbert` (Alibaba gte-reranker-modernbert-base, ~144 MB,
|
|
391
|
+
ModernBERT 8k context, Apache-2.0; **default since the #203 measurement** —
|
|
392
|
+
nDCG@10 0.9318 vs 0.8839) or the pre-#203 incumbent `bge-reranker-base`
|
|
393
|
+
(~278 MB, kept for reproducibility); raw `id@revision` also accepted.
|
|
394
|
+
Programmatic: `rerankModel: 'bge-reranker-base'`.
|
|
395
|
+
- **Multi-query expansion** (issue #203): `--multi-query <n>` embeds N query
|
|
396
|
+
variants (the original + N-1 LLM paraphrases from `--llm-endpoint`, one extra
|
|
397
|
+
embedding each) and fuses the dense rankings with union-RRF — a recall lift
|
|
398
|
+
on heavy paraphrases, at the cost of N−1 extra embeddings and one LLM call.
|
|
399
|
+
Without an endpoint (or on any LLM failure) it silently degrades to the
|
|
400
|
+
single-query baseline. Programmatic: `multiQuery: 3` (+ `llmEndpoint`).
|
|
389
401
|
|
|
402
|
+
### 3. Ask & Interactive Multi-turn Chat (`mdss ask`, `mdss chat`)
|
|
390
403
|
|
|
391
|
-
|
|
404
|
+
`md-semantic-search` includes a grounded RAG QA engine that operates fully offline by default with zero external dependencies, or connects optionally to local/remote LLMs (Ollama, OpenAI-compatible APIs).
|
|
405
|
+
|
|
406
|
+
> ⚠️ **Data egress**: a non-loopback `--llm-endpoint` (or `--embedder openai`) sends **note content** to that external service. The CLI prints a one-time warning before the first send; see [SECURITY.md → Data egress](./SECURITY.md#data-egress-issue-183). Loopback endpoints (local Ollama/llama.cpp) and the default extractive/local-embedder paths keep everything on your machine.
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
# One-shot grounded QA with citations:
|
|
410
|
+
mdss ask --db ./notes "What is the token expiration policy?"
|
|
411
|
+
|
|
412
|
+
# Multi-turn persistent chat session:
|
|
413
|
+
mdss chat --db ./notes
|
|
414
|
+
|
|
415
|
+
# Resume the most recent session or a specific session:
|
|
416
|
+
mdss chat --db ./notes --resume
|
|
417
|
+
mdss chat --db ./notes --session my-session-id
|
|
418
|
+
|
|
419
|
+
# One-shot query into a persistent session:
|
|
420
|
+
mdss chat --db ./notes --session my-session-id "What about database failover?"
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
- **Offline Extractive Synthesis**: Zero-dependency default extracts salient factual sentences with strict `[file › heading]` source provenance.
|
|
424
|
+
- **OpenAI-compatible chat with SSE streaming** (issue #199): with `--llm-endpoint`, synthesis speaks the `/chat/completions` lingua franca — llama.cpp, LM Studio, vLLM, and Ollama (`.../v1`) all work **without code** — and answers **stream token-by-token** into `mdss ask`, `mdss chat`, the TUI RAG panel (`mdss search -i --rag`), and the web UI (`mdss serve` → ✦ Ask): the first token renders in milliseconds instead of after the whole generation.
|
|
425
|
+
```bash
|
|
426
|
+
# llama.cpp / LM Studio / vLLM (OpenAI-compatible):
|
|
427
|
+
mdss ask --db ./notes "What is the token expiration policy?" \
|
|
428
|
+
--llm-endpoint http://127.0.0.1:8080/v1 --llm-model qwen2.5
|
|
429
|
+
# Ollama's OpenAI-compatible surface:
|
|
430
|
+
mdss chat --db ./notes --llm-endpoint http://127.0.0.1:11434/v1 --resume
|
|
431
|
+
# Ollama's native API (pre-#199 wire format, also streamed):
|
|
432
|
+
mdss ask --db ./notes "..." --llm-endpoint http://127.0.0.1:11434 --llm-provider ollama
|
|
433
|
+
```
|
|
434
|
+
An endpoint that does not answer `/chat/completions` (404/405) is retried once on the Ollama-native `/api/generate`, so existing Ollama-root setups keep working with default flags; `--llm-provider` pins the wire format explicitly. A hung endpoint fails on the **idle timeout** (`MDSS_FETCH_TIMEOUT_MS` — armed on connect, reset on every token, issue #184), so a long generation is never killed mid-answer, and any failure degrades to the extractive synthesis.
|
|
435
|
+
- **Persistent Sessions**: Multi-turn sessions are stored in `<indexDir>/sessions/<id>.json` preserving full conversation turns, retrieved chunk provenance manifests (file, heading, exact line numbers, chunk hashes), and model metadata.
|
|
436
|
+
- **Context Carry-Over**: Follow-up turns re-rank and expand query tokens using prior conversational context.
|
|
437
|
+
- **Interactive REPL Commands**: `/session` (info), `/sources` (manifest of last turn), `/help`, `/exit`.
|
|
438
|
+
|
|
439
|
+
### 4. Serve (daemon, warm index + model)
|
|
392
440
|
|
|
393
441
|
```bash
|
|
394
442
|
mdss serve --db /path/to/your/markdown [--port 8747] [--host 127.0.0.1] [--watch]
|
|
@@ -421,6 +469,22 @@ curl localhost:8747/health # → {ok, chunks, model, dim, built, watchi
|
|
|
421
469
|
score, matches, snippet, rerankScore?}]}`. The CLI's `search --json` prints
|
|
422
470
|
the same hit shape.
|
|
423
471
|
|
|
472
|
+
**`POST /ask` — streamed grounded QA over SSE** (issue #199): start the daemon
|
|
473
|
+
with `--llm-endpoint` (chat backend — llama.cpp, LM Studio, vLLM, Ollama `.../v1`)
|
|
474
|
+
and `{"query": string, "k"?: number}` answers as `text/event-stream` frames:
|
|
475
|
+
`sources` (citations from retrieval) → `delta` (`{"text": "…"}` per token) →
|
|
476
|
+
`done` (`{answer, mode, citations, citationsUsed}`; `mode` is `endpoint` for the
|
|
477
|
+
LLM answer or `extractive` when it degraded, `citationsUsed` lists the citations
|
|
478
|
+
the answer text actually cites). **Multi-turn chat**: pass `"chat": true` to
|
|
479
|
+
start a persistent session or `"sessionId": "…"` to continue one — the same
|
|
480
|
+
`<indexDir>/sessions/` store `mdss chat` uses; a `session` frame opens the
|
|
481
|
+
stream and the `done` frame returns `sessionId`/`turnsCount`. The built-in web
|
|
482
|
+
UI has a **chat** toggle with history and a "new chat" reset. The same fairness gates as `/search` apply —
|
|
483
|
+
rate limit `429`, concurrency `503` — and an LLM call holds one concurrency
|
|
484
|
+
slot for its whole generation. Without `--llm-endpoint` the route answers
|
|
485
|
+
`400` instead of silently degrading. The built-in web UI ships an
|
|
486
|
+
**✦ Ask the notes** button that consumes this stream.
|
|
487
|
+
|
|
424
488
|
Or from a script / editor extension:
|
|
425
489
|
|
|
426
490
|
```js
|
|
@@ -514,16 +578,53 @@ mdss mcp --db /path/to/your/markdown
|
|
|
514
578
|
|
|
515
579
|
**Streamable HTTP transport (issue #123):** a long-running daemon can expose
|
|
516
580
|
the same tools over HTTP — `mdss serve --db ./notes --mcp` mounts `/mcp`
|
|
517
|
-
(POST JSON-RPC, SSE channel on GET, `Mcp-Session-Id` sessions
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
581
|
+
(POST JSON-RPC, SSE channel on GET, `Mcp-Session-Id` sessions). It sits behind
|
|
582
|
+
the exact same auth, Host-allowlist and rate-limit gates as `/search`, so
|
|
583
|
+
remote agents inherit the hardening. stdio via `mdss mcp` remains the default
|
|
584
|
+
transport.
|
|
585
|
+
|
|
586
|
+
**Protocol negotiation & `ping` (issue #197):** `initialize` speaks
|
|
587
|
+
`2025-11-25`, `2025-06-18`, `2025-03-26`, and `2024-11-05` — the client's
|
|
588
|
+
requested version is echoed when supported, otherwise the server answers with
|
|
589
|
+
its newest (`2025-11-25`), so modern clients never warn about an outdated
|
|
590
|
+
protocol and older clients keep working. `ping` is answered promptly with an
|
|
591
|
+
empty result on both transports.
|
|
592
|
+
|
|
593
|
+
**Sampling — LLM answers via the client (issue #197):** when the client
|
|
594
|
+
declares the `sampling` capability at `initialize`, `ask_knowledge_base` hands
|
|
595
|
+
the synthesis to the client's own model with a server→client
|
|
596
|
+
`sampling/createMessage` request — grounded passages go out, a cited answer
|
|
597
|
+
comes back — with **no local LLM and no extra user configuration** (Claude
|
|
598
|
+
Desktop sampling, for example). Over stdio the exchange rides the same
|
|
599
|
+
stdin/stdout JSON-RPC lines; over HTTP it rides the GET `/mcp` SSE stream with
|
|
600
|
+
the answer POSTed back. Sampling is strictly best-effort: a client without the
|
|
601
|
+
capability, a refusal, or a timeout (default 60 s, override with
|
|
602
|
+
`MDSS_SAMPLING_TIMEOUT_MS`) degrades to the built-in extractive answer. The
|
|
603
|
+
tool payload reports which path produced the result in `answerMode`
|
|
604
|
+
(`sampling` / `extractive`).
|
|
521
605
|
|
|
522
606
|
#### Available MCP Tools:
|
|
523
|
-
- **`search_markdown`**: Hybrid semantic vector + BM25 search with optional `path` glob
|
|
607
|
+
- **`search_markdown`**: Hybrid semantic vector + BM25 search with optional `path` glob, `tag`, rich `filter`, and `graphBoost`.
|
|
524
608
|
- **`get_chunk`**: Retrieve a specific section by file and heading.
|
|
609
|
+
- **`get_lines`**: Retrieve exact line spans (`fromLine`, `maxLines`) from an indexed Markdown document.
|
|
610
|
+
- **`related_notes`**: Find graph-connected (outgoing, backlinks, 2-hop) and semantically related notes for a document.
|
|
525
611
|
- **`list_files`**: List indexed Markdown files with chunk counts.
|
|
526
612
|
- **`index_status`**: Inspect index health, chunk counts, and model metadata.
|
|
613
|
+
- **`ask_knowledge_base`**: Grounded direct Q&A synthesis with citations; uses the client's own LLM via MCP sampling when available (see below), else the built-in extractive answerer — `answerMode` in the payload tells which.
|
|
614
|
+
- *All tools return `structuredContent` alongside text and include `readOnly: true, idempotent: true` annotations.*
|
|
615
|
+
|
|
616
|
+
#### Available MCP Resources & Templates:
|
|
617
|
+
- **`mdss://note/{path}`**: Read full content of an indexed Markdown document.
|
|
618
|
+
- **`mdss://note/{path}?fromLine=1&maxLines=50`**: Addressable line-span slices of notes.
|
|
619
|
+
- **`mdss://vault/{vault}`**: List all indexed notes within a vault.
|
|
620
|
+
- **`mdss://status`**: Live index status and chunk count statistics.
|
|
621
|
+
|
|
622
|
+
#### Available MCP Prompts:
|
|
623
|
+
- **`search-and-cite`**: Synthesizes a grounded answer referencing `mdss://note/{path}` resource URIs.
|
|
624
|
+
- **`summarize-note`**: Produces a structured summary of core concepts and linked notes.
|
|
625
|
+
- **`compare-notes`**: Identifies agreements, divergences, and synergies across two notes.
|
|
626
|
+
- **`find-contradictions`**: Audits notes for conflicting statements or outdated ADRs.
|
|
627
|
+
- **`timeline`**: Builds a chronological milestone / event timeline from notes.
|
|
527
628
|
|
|
528
629
|
#### Claude Desktop Configuration:
|
|
529
630
|
Add to your `claude_desktop_config.json`:
|
|
@@ -571,12 +672,14 @@ Add to your `claude_desktop_config.json`:
|
|
|
571
672
|
| `--json` | Machine-readable output: `index` → build result JSON, `stats` → index stats JSON, `search` → hit list JSON (each hit includes `matches` — the query terms found in the chunk). |
|
|
572
673
|
| `--semantic` | `search` only: pure vector ranking, skip lexical/RRF fusion (zero lexical tokenization). |
|
|
573
674
|
| `--open [N]` | `search`: open the top (or Nth) hit in your editor at its `startLine` — `$MDSS_EDITOR`/`$VISUAL`/`$EDITOR`, VS Code `--goto`, or the GUI opener. With `--json` on a non-TTY prints the intent instead of launching (issue #110). |
|
|
574
|
-
| `--fix` | `check`: apply safe auto-repairs — stale `.mdss.lock`, SHA-mismatched `vectors.bin` sidecar, corrupt `ivf.json`; rebuilds a corrupt index / recomputes `.hashes.json` when `--db` is available. Never touches source notes (issue #117). |
|
|
675
|
+
| `--fix` | `check`: apply safe auto-repairs — stale `.mdss.lock`, SHA-mismatched `vectors.bin` sidecar, corrupt `ivf.json`/`hnsw.json`; rebuilds a corrupt index / recomputes `.hashes.json` when `--db` is available. Never touches source notes (issue #117). |
|
|
575
676
|
| `--dry-run` | `check --fix`: print the planned repairs without mutating anything. |
|
|
576
|
-
| `--rerank` | Re-rank the candidate pool with a cross-encoder (
|
|
677
|
+
| `--rerank` | Re-rank the candidate pool with a cross-encoder (~144 MB default model, downloaded on first use). Slower, sharper results — see *Reranking* below. |
|
|
678
|
+
| `--rerank-model <m>` | Reranker registry alias (issue #203): `bge-reranker-base` (default) or `gte-reranker-modernbert` (144 MB, 8k ctx, Apache-2.0); raw `id@revision` accepted. |
|
|
679
|
+
| `--multi-query <n>` | Multi-query expansion (issue #203): N query variants (original + N−1 LLM paraphrases) fused by union-RRF; needs `--llm-endpoint`, degrades silently without it. |
|
|
577
680
|
| `--graph-boost <n>` | Weight for Obsidian wikilink graph ranking and PageRank prior (search; default 0). Incorporates document centrality and 2-hop contextual propagation. |
|
|
578
681
|
| `--filter <expr>` | Rich boolean filter across frontmatter tags and properties (e.g. `tag:engineering AND status != archived`). |
|
|
579
|
-
| `--ann` | Approximate nearest neighbor search
|
|
682
|
+
| `--ann [ivf\|hnsw]` | Approximate nearest neighbor search (search; auto-enables for large corpora). Engines (issue #201): `ivf` (k-means, the default sidecar) or `hnsw` (graph — no training phase, 100% measured recall at equal-or-lower query latency, `hnsw.json` sidecar, corrupt → brute-force fallback). Bare `--ann` prefers an existing sidecar. |
|
|
580
683
|
| `--nprobe <n>` | Number of nearest centroid clusters to probe during ANN search (default 8). |
|
|
581
684
|
| `--port <n>` | HTTP port for `serve` (default 8747, or `MDSS_PORT`). |
|
|
582
685
|
| `--host <ip>` | Bind address for `serve` (default `127.0.0.1` — loopback only; use `0.0.0.0` to expose on the LAN, or `MDSS_HOST`). |
|
|
@@ -590,7 +693,7 @@ Add to your `claude_desktop_config.json`:
|
|
|
590
693
|
| `--watch` | `serve`: re-index incrementally when files change. |
|
|
591
694
|
| `--watch-debug` | `serve --watch`: verbose trace of polls, FS-error classifications and retries (issue #116). |
|
|
592
695
|
| `--recency <days>` | `search`: time-decay half-life — passages get a `0.5^(age/halfLife)` boost after fusion; age from frontmatter `created`/`updated`, then file mtime; surfaced in `explain` (issue #127). |
|
|
593
|
-
| `--embedder <name>` | Use an external embedding provider (`ollama` or `openai`) instead of local transformers.js, together with `--embedder-model` (+ `--embedder-base-url`, `--embedder-key-file`). The provider id/model/dim are recorded in the index — switching providers invalidates vectors with the usual full re-embed. Local remains the zero-config default and the only fully-offline path (issue #124). |
|
|
696
|
+
| `--embedder <name>` | Use an external embedding provider (`ollama` or `openai`) instead of local transformers.js, together with `--embedder-model` (+ `--embedder-base-url`, `--embedder-key-file`). The provider id/model/dim are recorded in the index — switching providers invalidates vectors with the usual full re-embed. Local remains the zero-config default and the only fully-offline path (issue #124). ⚠️ A non-loopback endpoint receives note content as embedding input — see [SECURITY.md → Data egress](./SECURITY.md#data-egress-issue-183). |
|
|
594
697
|
| `--no-query-cache` | `search`: skip the persistent query-embedding cache — repeated queries in fresh processes re-embed otherwise (issue #114). |
|
|
595
698
|
| `--no-ui` | `serve`: disable the built-in web UI at `/` — restores JSON API help there (issue #111). |
|
|
596
699
|
| `--watch-interval <ms>` | `serve --watch`: poll every N ms (default 3000). |
|
|
@@ -687,6 +790,8 @@ mdss models
|
|
|
687
790
|
| `e5-large` | `Xenova/multilingual-e5-large` | 1024 | ~2.2 GB, higher quality. |
|
|
688
791
|
| `bge-m3` | `Xenova/bge-m3` | 1024 | ~2.3 GB. Best cross-lingual separation in tests. |
|
|
689
792
|
| `qwen3-embedding-0.6b` | `onnx-community/Qwen3-Embedding-0.6B-ONNX` | 1024 | ~613 MB q8. Opt-in; instruction-prefixed queries and last-token pooling. |
|
|
793
|
+
| `granite-multilingual-311m` | `onnx-community/granite-embedding-311m-multilingual-r2-ONNX` | 768 | ~298 MB q8. IBM Granite r2 — ModernBERT 8k context, CLS pooling, Apache-2.0 (issue #200). |
|
|
794
|
+
| `embeddinggemma-300m` | `onnx-community/embeddinggemma-300m-ONNX` | 768 | ~300 MB q8. Google EmbeddingGemma — pre-pooled output, MRL dims 64–768, Gemma license (issue #200). |
|
|
690
795
|
|
|
691
796
|
Switching models invalidates the stored vectors automatically — the next
|
|
692
797
|
`index` run does a full rebuild.
|
|
@@ -709,12 +814,16 @@ The adapter declares exactly which embedding semantics it needs — the shared
|
|
|
709
814
|
`embed()` path never guesses from the model name:
|
|
710
815
|
|
|
711
816
|
- `queryPrefix` / `passagePrefix` — text prepended to queries/passages (E5 uses
|
|
712
|
-
`query: `/`passage: `, BGE none, Qwen3 a retrieval instruction on
|
|
713
|
-
|
|
817
|
+
`query: `/`passage: `, BGE/Granite none, Qwen3 a retrieval instruction on
|
|
818
|
+
queries, EmbeddingGemma its task/title prefixes);
|
|
819
|
+
- `pooling` — `'mean'`, `'last_token'`, `'cls'`, or `'none'`;
|
|
714
820
|
- `normalize` — whether vectors are L2-normalized;
|
|
715
821
|
- `nativeDim` / `dim` — embedding dimension (and optional `dimensions` for MRL);
|
|
716
822
|
- `dtype` — the ONNX runtime data type loaded at runtime (default `q8`);
|
|
717
823
|
- `maxTokens` — the tokenizer context budget (Qwen3-0.6B is 32768);
|
|
824
|
+
- `pooledOutput` — the ONNX graph already emits a pooled `sentence_embedding`
|
|
825
|
+
(EmbeddingGemma); embedding reads that output directly instead of the
|
|
826
|
+
feature-extraction pipeline;
|
|
718
827
|
- `family` — an informational formatting/pooling label surfaced by `stats`.
|
|
719
828
|
|
|
720
829
|
`mdss models` prints each adapter's pooling, normalization, dimension, and
|
|
@@ -760,7 +869,12 @@ section instead of silently producing wrong vectors.
|
|
|
760
869
|
1. **Walk** `--db` recursively for `.md`/`.markdown` (dotfiles & `--ignore`
|
|
761
870
|
globs skipped).
|
|
762
871
|
2. **Chunk** each file by Markdown headings while retaining the active heading
|
|
763
|
-
stack; oversized sections split on blank lines
|
|
872
|
+
stack; oversized sections split on blank lines. The budget is in **tokens**
|
|
873
|
+
(issue #202): `min(model maxTokens, 512)` by default with a script-
|
|
874
|
+
calibrated estimator, so Cyrillic notes get the same real budget as English
|
|
875
|
+
ones (the legacy English-calibrated `~1400 chars` budget overflowed the
|
|
876
|
+
model's context for RU ~2×). Override with `--max-chunk-tokens`, or set `0`
|
|
877
|
+
to restore the legacy char budget.
|
|
764
878
|
3. **Embed** the document title, full heading path, and chunk body (`passage:`
|
|
765
879
|
prefix for E5) → store `{file, title, heading, headingPath, text, vec}` in
|
|
766
880
|
`vectors.json`, plus per-file md5 in `.hashes.json`. The index stores an
|
|
@@ -796,7 +910,9 @@ Contributions are welcome — bug reports with reproduction steps, docs fixes,
|
|
|
796
910
|
tests, and features all help. Start with
|
|
797
911
|
[CONTRIBUTING.md](./CONTRIBUTING.md) (dev setup, project tour, conventions)
|
|
798
912
|
and look for issues labeled **`good first issue`** — they are scoped for a
|
|
799
|
-
first-time contributor.
|
|
913
|
+
first-time contributor. For the internals — module map, data-flow diagrams,
|
|
914
|
+
on-disk formats, and extension points — see
|
|
915
|
+
[ARCHITECTURE.md](./ARCHITECTURE.md). By participating you agree to the
|
|
800
916
|
[Code of Conduct](./CODE_OF_CONDUCT.md).
|
|
801
917
|
|
|
802
918
|
## License
|
package/RESEARCH.md
CHANGED
|
@@ -155,6 +155,13 @@ Baseline on dev (e5-base, hybrid RRF, k=10, 2026-08-14):
|
|
|
155
155
|
| Hit@10 | 0.9792 |
|
|
156
156
|
| Recall@10 | 1.0000 |
|
|
157
157
|
|
|
158
|
+
The **committed regression baseline** (issue #198) lives in
|
|
159
|
+
`bench/fixtures/golden-baseline.json` (2026-10-28: nDCG@10 0.8856, Recall 1.0 —
|
|
160
|
+
re-measured against the current ranking stack; the 2026-08-14 numbers above
|
|
161
|
+
predate later fusion/chunking changes). The nightly workflow gates against it
|
|
162
|
+
(`nDCG@10` drop ≤ 0.01, `Recall@10` = 1.0); see *Retrieval quality gate* in
|
|
163
|
+
CONTRIBUTING.md for the local `--gate` / `--update-baseline` workflow.
|
|
164
|
+
|
|
158
165
|
## 8. Reproducing
|
|
159
166
|
|
|
160
167
|
```bash
|
|
@@ -226,7 +233,204 @@ If no candidate meets the multilingual non-inferiority gate, `bge-reranker-base`
|
|
|
226
233
|
- **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
234
|
- **Constraint:** Zero native Node C++ bindings; must maintain pure JS fallback for non-WASM environments.
|
|
228
235
|
|
|
229
|
-
### ANN / IVF Scale Threshold (
|
|
236
|
+
### ANN / IVF Scale Threshold & K-Means++ Seeding (Issues #32 & #139)
|
|
230
237
|
- **Baseline:** Exact $O(N \cdot d)$ linear sweep.
|
|
231
|
-
- **
|
|
232
|
-
- **
|
|
238
|
+
- **K-Means++ Seeding:** Uses deterministic pseudo-random $D^2$-weighted centroid seeding (Mulberry32) to prevent initial centroid collapse on correlated notes corpora.
|
|
239
|
+
- **Measured Quality Gate:** `bench/ann-recall.mjs` benchmarks Recall@10 across $nprobe \in \{1, 4, 8, 16\}$ on clustered corpora, demonstrating $\ge 99.5\%$ Recall@10 at default $nprobe=8$ with sub-millisecond candidate pruning.
|
|
240
|
+
- **Trigger:** ANN indexing is auto-engaged above `ANN_THRESHOLD = 500` chunks when `--ann` is passed or IVF index is persisted, while exact pre/post-filter correctness is strictly preserved.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 14. Matryoshka Representation Learning (MRL) Dimension Selection (Issue #143)
|
|
245
|
+
|
|
246
|
+
### Mechanism
|
|
247
|
+
Models trained with Matryoshka Representation Learning (MRL) loss (such as `onnx-community/Qwen3-Embedding-0.6B-ONNX` and `Xenova/bge-m3`) embed semantic information hierarchically, allowing vectors to be sliced at predefined sub-dimensions (`32`, `64`, `128`, `256`, `512`, `768`, `1024`) and re-normalized to unit L2 norm without significant loss in semantic retrieval fidelity.
|
|
248
|
+
|
|
249
|
+
### Benefits & Trade-offs
|
|
250
|
+
- **Storage & RAM Savings:** 1024 → 256 dimensions reduces `vectors.bin` size and memory consumption by 4× (from ~19.5 MB to ~4.88 MB per 5,000 chunks).
|
|
251
|
+
- **Sweep Latency:** Dot-product cosine sweep latency is halved (e.g. from ~14.2 ms to ~6.8 ms for 5,000 chunks in single-threaded pure JS).
|
|
252
|
+
- **Integrity & Cache Invalidation:** `embeddingAdapterFingerprint` binds the selected dimension (`model.dim`). Changing `--dimensions` automatically invalidates previous chunk hashes and forces clean re-indexing rather than silent vector dimension mismatch.
|
|
253
|
+
|
|
254
|
+
### Usage
|
|
255
|
+
```bash
|
|
256
|
+
# Build index truncated to 256 dims (4x smaller vectors)
|
|
257
|
+
mdss index --db ./notes --model qwen3-embedding-0.6b --dimensions 256
|
|
258
|
+
|
|
259
|
+
# Search with dimension safety validation
|
|
260
|
+
mdss search --db ./notes --dimensions 256 "query"
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 15. Research Evaluation: Modern Embedding Adapter Candidates (Issue #200)
|
|
266
|
+
|
|
267
|
+
### Context & Decision Gate
|
|
268
|
+
|
|
269
|
+
The registry default (`e5-base`, 2022) predates the 2024-2025 multilingual
|
|
270
|
+
embedding generation. Issue #200 asked for 2-3 measured candidates, each a
|
|
271
|
+
registry entry with a pinned revision + q8 ONNX artifact, evaluated on the
|
|
272
|
+
golden set (#56) before any default change.
|
|
273
|
+
|
|
274
|
+
**Candidate screening (Hugging Face artifact probe, 2026-11):**
|
|
275
|
+
|
|
276
|
+
| Candidate | Verdict |
|
|
277
|
+
|---|---|
|
|
278
|
+
| `Xenova/gte-multilingual-base-1` | **Rejected** — no public transformers.js artifact (gte-multilingual needs remote-code modeling, unsupported by the JS runtime) |
|
|
279
|
+
| `Xenova/arctic-embed-m` | **Rejected** — no ONNX conversion published |
|
|
280
|
+
| `Xenova/jina-embeddings-v3` | **Rejected** — no transformers.js-compatible artifact; task-LoRA switch is not expressible through the feature-extraction pipeline |
|
|
281
|
+
| `onnx-community/Qwen3-Embedding-4B-ONNX` | **Rejected** — fp32-only (16 GB); no quantized export; unusable for a CLI embedder |
|
|
282
|
+
| `onnx-community/granite-embedding-311m-multilingual-r2-ONNX` | **Accepted** — q8 298 MB, Apache-2.0, ModernBERT (8k ctx), CLS pooling, no prefixes |
|
|
283
|
+
| `onnx-community/embeddinggemma-300m-ONNX` | **Accepted** — q8 ~300 MB, pre-pooled `sentence_embedding` graph output (needed a new `pooledOutput` adapter path in `core.embed`), MRL 64–768, Gemma license |
|
|
284
|
+
|
|
285
|
+
### Golden-Set Results (frozen corpus, hash `dev-golden.json`)
|
|
286
|
+
|
|
287
|
+
| Model (alias) | q8 size | nDCG@10 (dev, n=48) | MRR | Hit@10 | Recall@10 | nDCG (test) | nDCG (holdout) |
|
|
288
|
+
|---|---|---|---|---|---|---|---|
|
|
289
|
+
| `e5-base` (baseline) | ~280 MB | 0.8797 | 0.8628 | 0.979 | 1.000 | 1.0000 | 0.9077 |
|
|
290
|
+
| `granite-multilingual-311m` | ~298 MB | 0.8761 | 0.8559 | 0.979 | 1.000 | 1.0000 | 0.9077 |
|
|
291
|
+
| `embeddinggemma-300m` | ~300 MB | **0.9002** | **0.8837** | **1.000** | 1.000 | 1.0000 | 0.9077 |
|
|
292
|
+
|
|
293
|
+
- **Non-inferiority (#198 tolerance: drop ≤ 0.01):** granite passes (−0.0036);
|
|
294
|
+
embeddinggemma **exceeds** the baseline (+0.0205) and is the only candidate
|
|
295
|
+
that recovers the two dev queries e5-base misses (Hit 1.000).
|
|
296
|
+
- **Discriminating slice is `dev`** — `test` and `holdout` are saturated for
|
|
297
|
+
both models, so they cannot rank the candidates further.
|
|
298
|
+
|
|
299
|
+
### Decision
|
|
300
|
+
|
|
301
|
+
1. **Both candidates ship as opt-in registry entries** with pinned revisions
|
|
302
|
+
(issue #200 task 1) and are documented in the README Models table.
|
|
303
|
+
2. **The default stays `e5-base`.** EmbeddingGemma's superiority is proven on
|
|
304
|
+
our golden set, but the default switch is gated on two factors outside this
|
|
305
|
+
measurement: (a) the **Gemma license** — not MIT/Apache, so making it the
|
|
306
|
+
npm-distributed default imposes Google's additional terms on every user,
|
|
307
|
+
which is a project-licensing decision, not an engineering one; (b) one
|
|
308
|
+
fixture (48 dev queries) is a thin base for overturning a default that the
|
|
309
|
+
#198 gate has been calibrated on. Re-evaluate if/when the golden set grows.
|
|
310
|
+
**Until then `embeddinggemma-300m` is the recommended opt-in** (`--model
|
|
311
|
+
embeddinggemma-300m`) — same size class as e5-base, better measured quality.
|
|
312
|
+
3. **Infrastructure added:** `pooling: 'cls'` (granite) and the
|
|
313
|
+
`pooledOutput` adapter flag (pre-pooled ONNX graphs bypass the
|
|
314
|
+
feature-extraction pipeline) — both fingerprint-relevant and unit-tested.
|
|
315
|
+
|
|
316
|
+
### Reproducing
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
node scripts/run-bench.mjs --model e5-base --slice dev
|
|
320
|
+
node scripts/run-bench.mjs --model granite-multilingual-311m --slice dev
|
|
321
|
+
node scripts/run-bench.mjs --model embeddinggemma-300m --slice dev
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## 16. Reranker Registry & Multi-Query Expansion (Issue #203)
|
|
327
|
+
|
|
328
|
+
### Candidate screening
|
|
329
|
+
|
|
330
|
+
The issue's proposed `bge-reranker-v2-m3` has **no transformers.js artifact**
|
|
331
|
+
(`Xenova/bge-reranker-v2-m3` and `onnx-community/bge-reranker-v2-m3` do not
|
|
332
|
+
exist — 401 on every resolve). The screened replacement:
|
|
333
|
+
|
|
334
|
+
| Candidate | Verdict |
|
|
335
|
+
|---|---|
|
|
336
|
+
| `BAAI/bge-reranker-v2-m3` | **Rejected** — no JS-runtime ONNX conversion |
|
|
337
|
+
| `Alibaba-NLP/gte-reranker-modernbert-base` | **Accepted** — ONNX exports in-repo, q8 144 MB, Apache-2.0, ModernBERT 8k ctx, single-logit head (same extraction contract as bge-reranker-base) |
|
|
338
|
+
| `Xenova/bge-reranker-large` | available but unmeasured (~2× base size; superseded by gte-modernbert's result) |
|
|
339
|
+
|
|
340
|
+
### Golden-Set Results (e5-base dense retrieval, frozen corpus, dev slice n=48)
|
|
341
|
+
|
|
342
|
+
| Config | nDCG@10 | MRR | Hit@10 | Recall@10 |
|
|
343
|
+
|---|---|---|---|---|
|
|
344
|
+
| no rerank (baseline) | 0.8797 | 0.8628 | 0.979 | 1.000 |
|
|
345
|
+
| `--rerank` (bge-reranker-base, ~278 MB) | 0.8839 | 0.8646 | 1.000 | 1.000 |
|
|
346
|
+
| `--rerank --rerank-model gte-reranker-modernbert` (~144 MB) | **0.9318** | **0.9253** | 1.000 | 1.000 |
|
|
347
|
+
|
|
348
|
+
**gte-reranker-modernbert passes the non-inferiority gate with a decisive
|
|
349
|
+
margin: +0.052 nDCG@10 over the incumbent reranker, at half the size, under
|
|
350
|
+
Apache-2.0.** Unlike the embedding default (#200), no license blocker applies —
|
|
351
|
+
**the default reranker switches to `gte-reranker-modernbert`**; the incumbent
|
|
352
|
+
stays in the registry as `--rerank-model bge-reranker-base` for reproducibility.
|
|
353
|
+
|
|
354
|
+
### Multi-query expansion
|
|
355
|
+
|
|
356
|
+
`--multi-query N` (N = total variants: original + N−1 LLM paraphrases, one
|
|
357
|
+
extra embedding each) fuses per-variant dense rankings by union-RRF; ANN
|
|
358
|
+
probing unions the per-variant candidate sets; the `cos` field stays anchored
|
|
359
|
+
to the original query. Quality of the lift depends entirely on the LLM's
|
|
360
|
+
paraphrases, so the pinned contract is the MECHANISM (unit tests with a fake
|
|
361
|
+
LLM): variants embed through the shared query cache, the union never drops
|
|
362
|
+
baseline hits, and — the acceptance criterion — **a paraphrase-only hit enters
|
|
363
|
+
the top-k with the flag on and is absent without it**, while `--multi-query`
|
|
364
|
+
without an endpoint (or on any LLM failure) is byte-identical to the
|
|
365
|
+
single-query baseline. Production lift numbers require a real local LLM and
|
|
366
|
+
are deliberately not claimed from a rigged fixture.
|
|
367
|
+
|
|
368
|
+
### Reproducing
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
node scripts/run-bench.mjs --rerank
|
|
372
|
+
node scripts/run-bench.mjs --rerank --rerank-model gte-reranker-modernbert
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## 17. HNSW Graph ANN alongside IVF (Issue #201)
|
|
378
|
+
|
|
379
|
+
### Implementation
|
|
380
|
+
|
|
381
|
+
`src/hnsw.ts` — pure JS/TS HNSW (Malkov & Yashunin 2016) sharing the IVF
|
|
382
|
+
contract: build → serialize → `hnsw.json` → search as a candidate pre-filter.
|
|
383
|
+
Key properties:
|
|
384
|
+
|
|
385
|
+
- **No training phase.** The graph builds by incremental insertion
|
|
386
|
+
(`insertHnsw`), which is also the watch-rebuild path: tombstoned deletes
|
|
387
|
+
(`deleteHnsw`) keep removed chunks searchable-through-but-hidden, so a
|
|
388
|
+
persisted graph can grow without a full rebuild.
|
|
389
|
+
- **Diversity heuristic pruning** (Algorithm 4). The first implementation used
|
|
390
|
+
blind top-M pruning, which severed the just-added edge at overflowing
|
|
391
|
+
neighbors — on the 5000-vector clustered corpus recall collapsed to 44.4%
|
|
392
|
+
and did NOT respond to `ef` (stranded regions). The heuristic restores
|
|
393
|
+
connectivity: 100% recall at every ef on both bench corpora.
|
|
394
|
+
- **Determinism**: level assignment is seeded per node (`seed + idx`), so
|
|
395
|
+
full rebuilds from the same insertion order are identical.
|
|
396
|
+
- **Hardened `deserializeHnsw`** with the same discipline as
|
|
397
|
+
`deserializeIVF` (format, header, per-level link caps, self/duplicate
|
|
398
|
+
links, entry-point-on-top-layer, tombstone range, count-vs-chunks).
|
|
399
|
+
|
|
400
|
+
### Measured (bench/ann-recall.mjs, clustered corpus, dim 256)
|
|
401
|
+
|
|
402
|
+
| n | Engine | Build | Config | Recall@10 | p50 latency |
|
|
403
|
+
|---|---|---|---|---|---|
|
|
404
|
+
| 1000 | IVF | 85 ms | nprobe 4 | 100% | 0.098 ms |
|
|
405
|
+
| 1000 | HNSW | 2605 ms | ef 16 | 100% | 0.212 ms |
|
|
406
|
+
| 5000 | IVF | 1025 ms | nprobe 4 | 100% | 0.252 ms |
|
|
407
|
+
| 5000 | HNSW | 20027 ms | ef 16 | 100% | 0.215 ms |
|
|
408
|
+
|
|
409
|
+
**Query-side: HNSW ≥ IVF** — 100% recall at equal-or-lower p50 latency
|
|
410
|
+
(satisfying the acceptance criterion's first branch). **Build-side: IVF wins
|
|
411
|
+
~20×** (no training vs O(n log n) graph construction in JS), and the sidecar
|
|
412
|
+
is rebuilt by every full index run — so the automatic sidecar for large
|
|
413
|
+
corpora keeps **IVF as the default engine**; the criterion's second branch
|
|
414
|
+
(documented result, IVF remains default) covers the build-time trade-off.
|
|
415
|
+
|
|
416
|
+
### Decision
|
|
417
|
+
|
|
418
|
+
1. `--ann hnsw` opts into the graph engine end-to-end (indexer sidecar +
|
|
419
|
+
search probe + `check --fix` corrupt-sidecar repair); corrupt `hnsw.json`
|
|
420
|
+
degrades to brute force silently, like `ivf.json`.
|
|
421
|
+
2. Bare `--ann` and the automatic threshold path stay on IVF until either
|
|
422
|
+
(a) the JS graph build gets an order of magnitude cheaper (worker
|
|
423
|
+
parallelism, WASM SIMD — see §13), or (b) incremental watch rebuilds
|
|
424
|
+
start reusing the persisted graph (the `insertHnsw`/`deleteHnsw` API is
|
|
425
|
+
already in place for that).
|
|
426
|
+
3. `--ef <n>` tunes the HNSW search breadth (default 64; recall saturates
|
|
427
|
+
at 100% already at ef 16 on the bench corpora — the default leaves
|
|
428
|
+
headroom for noisier real-world distributions).
|
|
429
|
+
|
|
430
|
+
### Reproducing
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
node bench/ann-recall.mjs # IVF + HNSW curves side by side
|
|
434
|
+
mdss index --db ./notes --ann hnsw # hnsw.json sidecar
|
|
435
|
+
mdss search --db ./notes --ann hnsw "query"
|
|
436
|
+
```
|