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/dist/rerank.d.ts
CHANGED
|
@@ -1,10 +1,55 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface RerankerTokenizer {
|
|
2
|
+
(queries: string[], options: {
|
|
3
|
+
text_pair: string[];
|
|
4
|
+
padding: boolean;
|
|
5
|
+
truncation: boolean;
|
|
6
|
+
}): unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface RerankerModelOutput {
|
|
9
|
+
logits: {
|
|
10
|
+
tolist(): unknown;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface RerankerModel {
|
|
14
|
+
(inputs: unknown): Promise<RerankerModelOutput>;
|
|
15
|
+
}
|
|
16
|
+
export interface LoadedReranker {
|
|
17
|
+
tokenizer: RerankerTokenizer;
|
|
18
|
+
model: RerankerModel;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Reranker registry (issue #203) — the cross-encoder counterpart of the
|
|
22
|
+
* embedder MODELS registry. Each entry is an explicit adapter: repo id,
|
|
23
|
+
* pinned revision, runtime dtype, and the tokenizer context budget the
|
|
24
|
+
* pair-truncation relies on. The loaded model must be a single-logit
|
|
25
|
+
* sequence classifier (logits[0] = relevance score) — the extraction in
|
|
26
|
+
* rerankScores is shared by every entry.
|
|
27
|
+
*/
|
|
28
|
+
export interface RerankerAdapter {
|
|
29
|
+
id: string;
|
|
30
|
+
revision?: string;
|
|
31
|
+
dtype: string;
|
|
32
|
+
maxTokens: number;
|
|
33
|
+
family?: string;
|
|
34
|
+
note?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare const RERANKERS: Record<string, RerankerAdapter>;
|
|
37
|
+
export declare const DEFAULT_RERANKER = "gte-reranker-modernbert";
|
|
38
|
+
/** Legacy export — the default adapter's repo id (kept for back-compat). */
|
|
39
|
+
export declare const RERANK_MODEL: string;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a reranker alias / raw repo id (optionally `id@revision`) to an
|
|
42
|
+
* adapter. Unknown ids are rejected — unlike embedders there is no
|
|
43
|
+
* "unknown adapter" escape hatch, because the single-logit contract cannot
|
|
44
|
+
* be guessed safely.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveReranker(name?: string | null): {
|
|
47
|
+
alias: string;
|
|
48
|
+
adapter: RerankerAdapter;
|
|
49
|
+
};
|
|
2
50
|
export declare function getReranker(cacheDir: string, offline?: boolean, retryOpts?: {
|
|
3
51
|
maxRetries?: number;
|
|
4
52
|
delays?: number[];
|
|
5
53
|
log?: (msg: string) => void;
|
|
6
|
-
}): Promise<
|
|
7
|
-
|
|
8
|
-
model: any;
|
|
9
|
-
}>;
|
|
10
|
-
export declare function rerankScores(query: string, passages: string[], cacheDir: string, offline?: boolean): Promise<number[]>;
|
|
54
|
+
}, rerankerName?: string | null): Promise<LoadedReranker>;
|
|
55
|
+
export declare function rerankScores(query: string, passages: string[], cacheDir: string, offline?: boolean, rerankerName?: string | null): Promise<number[]>;
|
package/dist/rerank.js
CHANGED
|
@@ -3,25 +3,75 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { resolveSessionOptions, retryWithBackoff, emitDownloadEvent } from './core.js';
|
|
5
5
|
const _rerankers = new Map();
|
|
6
|
-
export const
|
|
7
|
-
|
|
6
|
+
export const RERANKERS = {
|
|
7
|
+
'gte-reranker-modernbert': {
|
|
8
|
+
id: 'Alibaba-NLP/gte-reranker-modernbert-base',
|
|
9
|
+
revision: 'f7481e6055501a30fb19d090657df9ec1f79ab2c',
|
|
10
|
+
dtype: 'q8',
|
|
11
|
+
maxTokens: 8192,
|
|
12
|
+
family: 'gte',
|
|
13
|
+
note: 'Default since issue #203 — ~144 MB q8, Apache-2.0, ModernBERT 8k context. Golden set: nDCG@10 0.9318 vs bge-reranker-base 0.8839.',
|
|
14
|
+
},
|
|
15
|
+
'bge-reranker-base': {
|
|
16
|
+
id: 'Xenova/bge-reranker-base',
|
|
17
|
+
dtype: 'q8',
|
|
18
|
+
maxTokens: 512,
|
|
19
|
+
family: 'bge',
|
|
20
|
+
note: '~278 MB q8. The pre-#203 default; kept for reproducibility (pin via --rerank-model bge-reranker-base).',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
export const DEFAULT_RERANKER = 'gte-reranker-modernbert';
|
|
24
|
+
/** Legacy export — the default adapter's repo id (kept for back-compat). */
|
|
25
|
+
export const RERANK_MODEL = RERANKERS[DEFAULT_RERANKER].id;
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a reranker alias / raw repo id (optionally `id@revision`) to an
|
|
28
|
+
* adapter. Unknown ids are rejected — unlike embedders there is no
|
|
29
|
+
* "unknown adapter" escape hatch, because the single-logit contract cannot
|
|
30
|
+
* be guessed safely.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveReranker(name) {
|
|
33
|
+
if (!name || name === DEFAULT_RERANKER) {
|
|
34
|
+
return { alias: DEFAULT_RERANKER, adapter: RERANKERS[DEFAULT_RERANKER] };
|
|
35
|
+
}
|
|
36
|
+
if (RERANKERS[name])
|
|
37
|
+
return { alias: name, adapter: RERANKERS[name] };
|
|
38
|
+
let id = name;
|
|
39
|
+
let revision;
|
|
40
|
+
const at = name.indexOf('@');
|
|
41
|
+
if (at > 0) {
|
|
42
|
+
id = name.slice(0, at);
|
|
43
|
+
revision = name.slice(at + 1) || undefined;
|
|
44
|
+
}
|
|
45
|
+
for (const [alias, adapter] of Object.entries(RERANKERS)) {
|
|
46
|
+
if (adapter.id === id) {
|
|
47
|
+
return { alias, adapter: revision ? { ...adapter, revision } : adapter };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`unknown reranker "${name}" — available: ${Object.keys(RERANKERS).join(', ')} (or register it in RERANKERS, issue #203)`);
|
|
51
|
+
}
|
|
52
|
+
export async function getReranker(cacheDir, offline = false, retryOpts = {}, rerankerName) {
|
|
53
|
+
const { adapter } = resolveReranker(rerankerName);
|
|
8
54
|
const sessionOptions = resolveSessionOptions();
|
|
9
55
|
const sessionKey = sessionOptions ? JSON.stringify(sessionOptions) : 'default';
|
|
10
|
-
const key = `${
|
|
56
|
+
const key = `${adapter.id}@${adapter.revision || 'main'}|${adapter.dtype}|${offline ? 'off' : 'on'}|${sessionKey}`;
|
|
11
57
|
if (_rerankers.has(key))
|
|
12
58
|
return _rerankers.get(key);
|
|
13
59
|
const loadPipeline = async () => {
|
|
14
|
-
const { AutoModelForSequenceClassification, AutoTokenizer, env } = await import('@huggingface/transformers');
|
|
60
|
+
const { AutoModelForSequenceClassification, AutoTokenizer, env } = (await import('@huggingface/transformers'));
|
|
15
61
|
if (cacheDir)
|
|
16
62
|
env.cacheDir = cacheDir;
|
|
17
63
|
env.allowRemoteModels = !offline;
|
|
18
|
-
const tokenizer = await AutoTokenizer.from_pretrained(
|
|
64
|
+
const tokenizer = await AutoTokenizer.from_pretrained(adapter.id, {
|
|
65
|
+
...(adapter.revision ? { revision: adapter.revision } : {}),
|
|
66
|
+
progress_callback: emitDownloadEvent,
|
|
67
|
+
});
|
|
19
68
|
const loadOpts = {
|
|
20
|
-
dtype:
|
|
69
|
+
dtype: adapter.dtype,
|
|
21
70
|
progress_callback: emitDownloadEvent,
|
|
71
|
+
...(adapter.revision ? { revision: adapter.revision } : {}),
|
|
22
72
|
...(sessionOptions ? { session_options: sessionOptions } : {}),
|
|
23
73
|
};
|
|
24
|
-
const model = await AutoModelForSequenceClassification.from_pretrained(
|
|
74
|
+
const model = await AutoModelForSequenceClassification.from_pretrained(adapter.id, loadOpts);
|
|
25
75
|
const r = { tokenizer, model };
|
|
26
76
|
_rerankers.set(key, r);
|
|
27
77
|
return r;
|
|
@@ -31,12 +81,21 @@ export async function getReranker(cacheDir, offline = false, retryOpts = {}) {
|
|
|
31
81
|
}
|
|
32
82
|
return await retryWithBackoff(loadPipeline, retryOpts);
|
|
33
83
|
}
|
|
34
|
-
export async function rerankScores(query, passages, cacheDir, offline = false) {
|
|
84
|
+
export async function rerankScores(query, passages, cacheDir, offline = false, rerankerName) {
|
|
35
85
|
if (passages.length === 0)
|
|
36
86
|
return [];
|
|
37
|
-
const { tokenizer, model } = await getReranker(cacheDir, offline);
|
|
87
|
+
const { tokenizer, model } = await getReranker(cacheDir, offline, {}, rerankerName);
|
|
38
88
|
const queries = new Array(passages.length).fill(query);
|
|
39
89
|
const inputs = tokenizer(queries, { text_pair: passages, padding: true, truncation: true });
|
|
40
90
|
const outputs = await model(inputs);
|
|
41
|
-
|
|
91
|
+
const rawLogits = outputs?.logits?.tolist();
|
|
92
|
+
if (!Array.isArray(rawLogits)) {
|
|
93
|
+
throw new Error('reranker model returned invalid logits structure');
|
|
94
|
+
}
|
|
95
|
+
return rawLogits.map((row, idx) => {
|
|
96
|
+
if (!Array.isArray(row) || typeof row[0] !== 'number' || !Number.isFinite(row[0])) {
|
|
97
|
+
throw new Error(`reranker model returned non-finite score at index ${idx}`);
|
|
98
|
+
}
|
|
99
|
+
return row[0];
|
|
100
|
+
});
|
|
42
101
|
}
|
package/dist/search.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { ModelDescriptor } from './core.js';
|
|
|
2
2
|
import { LexicalIndex } from './lexical.js';
|
|
3
3
|
import { DocumentMetadata } from './frontmatter.js';
|
|
4
4
|
import { IVFIndex } from './ivf.js';
|
|
5
|
+
import { HnswIndex } from './hnsw.js';
|
|
5
6
|
import { RelationshipGraph } from './wikilinks.js';
|
|
6
7
|
import { FilterNode } from './filter.js';
|
|
7
8
|
export { tokenize } from './lexical.js';
|
|
@@ -13,6 +14,17 @@ export declare function rrf(rankings: Array<Array<{
|
|
|
13
14
|
idx: number;
|
|
14
15
|
score: number;
|
|
15
16
|
}>>, k?: number, weights?: number[]): Map<number, number>;
|
|
17
|
+
/**
|
|
18
|
+
* issue #191: bounded top-k selection over a dense sweep — a capped min-heap
|
|
19
|
+
* instead of materializing one object per chunk and fully sorting. Ties break
|
|
20
|
+
* toward the smaller idx, which is exactly what a stable sort by
|
|
21
|
+
* (score desc) over idx-ordered input produces, so the selected pool and its
|
|
22
|
+
* order are identical to `[...items].sort((a, b) => b.score - a.score).slice(0, k)`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function topKByScore(n: number, scoreAt: (i: number) => number, k: number): Array<{
|
|
25
|
+
idx: number;
|
|
26
|
+
score: number;
|
|
27
|
+
}>;
|
|
16
28
|
export interface IndexChunk {
|
|
17
29
|
file: string;
|
|
18
30
|
title: string;
|
|
@@ -74,9 +86,18 @@ export interface RuntimeIndexState {
|
|
|
74
86
|
model: ModelDescriptor;
|
|
75
87
|
queryCache: QueryEmbeddingCache;
|
|
76
88
|
ivf?: IVFIndex;
|
|
89
|
+
/** issue #201: optional HNSW graph sidecar (hnsw.json), parallel to ivf. */
|
|
90
|
+
hnsw?: HnswIndex;
|
|
77
91
|
graph?: RelationshipGraph;
|
|
78
92
|
pageRank?: Map<string, number>;
|
|
93
|
+
/**
|
|
94
|
+
* issue #190: precomputed sqrt(sum(c^2)) for int8 vectors (zero-copy binary
|
|
95
|
+
* load only) — lets the dense sweep compute the dot product alone.
|
|
96
|
+
*/
|
|
97
|
+
chunkNorms?: Float32Array;
|
|
79
98
|
}
|
|
99
|
+
/** issue #190 tool/test seam: the cached int8 sweep norms, when present. */
|
|
100
|
+
export declare function getRuntimeChunkNorms(index: IndexFile): Float32Array | undefined;
|
|
80
101
|
export declare function getRuntimeGraph(runtime: RuntimeIndexState): {
|
|
81
102
|
graph: RelationshipGraph;
|
|
82
103
|
pageRank: Map<string, number>;
|
|
@@ -84,7 +105,9 @@ export declare function getRuntimeGraph(runtime: RuntimeIndexState): {
|
|
|
84
105
|
/**
|
|
85
106
|
* Issue #127 pure helpers — exported for deterministic unit tests.
|
|
86
107
|
*/
|
|
87
|
-
/** Frontmatter `
|
|
108
|
+
/** Frontmatter `updated` → `created` → file mtime; null when nothing parses.
|
|
109
|
+
* Issue #164: `updated` wins — long-lived notes that were recently revised
|
|
110
|
+
* must not be penalized by their original creation date. */
|
|
88
111
|
export declare function resolvePassageAgeMs(meta: {
|
|
89
112
|
created?: unknown;
|
|
90
113
|
updated?: unknown;
|
|
@@ -97,13 +120,23 @@ export declare function recencyBoost(ageMs: number | null, halfLifeDays: number,
|
|
|
97
120
|
* Injectable fetch for tests.
|
|
98
121
|
*/
|
|
99
122
|
export declare function hydePassage(endpoint: string, query: string, model?: string, fetchImpl?: typeof fetch): Promise<string | null>;
|
|
123
|
+
/**
|
|
124
|
+
* issue #203: LLM paraphrases for multi-query expansion. ONE OpenAI-compatible
|
|
125
|
+
* chat completion asking for N alternative phrasings; returns clean, deduped,
|
|
126
|
+
* non-empty variants (the original query excluded). Returns [] on ANY failure —
|
|
127
|
+
* multi-query must degrade to the single-query baseline silently, exactly like
|
|
128
|
+
* HyDE. Injectable fetch for tests.
|
|
129
|
+
*/
|
|
130
|
+
export declare function paraphraseQueries(endpoint: string, query: string, count: number, model?: string, fetchImpl?: typeof fetch): Promise<string[]>;
|
|
100
131
|
/**
|
|
101
132
|
* Salient-term extraction for PRF (issue #125): tokens that appear in the
|
|
102
133
|
* top-m feedback passages, are not query terms or stopwords, and are ranked
|
|
103
134
|
* by (tf-in-feedback × distinct-passage coverage). Pure + deterministic.
|
|
104
135
|
*/
|
|
105
136
|
export declare function extractExpansionTerms(topTexts: string[], queryTerms: string[], maxTerms: number): string[];
|
|
106
|
-
export declare function loadIndex(indexDir: string
|
|
137
|
+
export declare function loadIndex(indexDir: string, opts?: {
|
|
138
|
+
checkStale?: boolean;
|
|
139
|
+
}): {
|
|
107
140
|
index: IndexFile;
|
|
108
141
|
model: ModelDescriptor;
|
|
109
142
|
};
|
|
@@ -147,10 +180,23 @@ export interface SearchOptions {
|
|
|
147
180
|
llmEndpoint?: string;
|
|
148
181
|
/** issue #125: model name for the HyDE chat call. */
|
|
149
182
|
llmModel?: string;
|
|
150
|
-
|
|
183
|
+
/**
|
|
184
|
+
* issue #203: multi-query expansion — N total query variants (original +
|
|
185
|
+
* N-1 LLM paraphrases), each embedded, rankings fused with union-RRF.
|
|
186
|
+
* Requires `llmEndpoint`; without one (or on any LLM failure) it degrades
|
|
187
|
+
* to the single-query baseline. 0/1/undefined = off.
|
|
188
|
+
*/
|
|
189
|
+
multiQuery?: number;
|
|
190
|
+
/** issue #203: reranker registry alias (`bge-reranker-base` default, `gte-reranker-modernbert`, ...). */
|
|
191
|
+
rerankModel?: string;
|
|
192
|
+
/** issue #201: ANN engine — `true` = default engine, or explicit 'ivf' | 'hnsw'. */
|
|
193
|
+
ann?: boolean | 'ivf' | 'hnsw';
|
|
151
194
|
nprobe?: number;
|
|
195
|
+
/** issue #201: HNSW ef-search breadth (default 64). */
|
|
196
|
+
ef?: number;
|
|
152
197
|
graphBoost?: number;
|
|
153
198
|
filter?: string | FilterNode;
|
|
199
|
+
dimensions?: number;
|
|
154
200
|
}
|
|
155
201
|
export interface SearchResultHit {
|
|
156
202
|
file: string;
|