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/src/rerank.ts
CHANGED
|
@@ -3,32 +3,129 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { resolveSessionOptions, retryWithBackoff, emitDownloadEvent } from './core.js';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
export interface RerankerTokenizer {
|
|
7
|
+
(queries: string[], options: { text_pair: string[]; padding: boolean; truncation: boolean }): unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RerankerModelOutput {
|
|
11
|
+
logits: {
|
|
12
|
+
tolist(): unknown;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RerankerModel {
|
|
17
|
+
(inputs: unknown): Promise<RerankerModelOutput>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LoadedReranker {
|
|
21
|
+
tokenizer: RerankerTokenizer;
|
|
22
|
+
model: RerankerModel;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const _rerankers = new Map<string, LoadedReranker>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reranker registry (issue #203) — the cross-encoder counterpart of the
|
|
29
|
+
* embedder MODELS registry. Each entry is an explicit adapter: repo id,
|
|
30
|
+
* pinned revision, runtime dtype, and the tokenizer context budget the
|
|
31
|
+
* pair-truncation relies on. The loaded model must be a single-logit
|
|
32
|
+
* sequence classifier (logits[0] = relevance score) — the extraction in
|
|
33
|
+
* rerankScores is shared by every entry.
|
|
34
|
+
*/
|
|
35
|
+
export interface RerankerAdapter {
|
|
36
|
+
id: string;
|
|
37
|
+
revision?: string;
|
|
38
|
+
dtype: string;
|
|
39
|
+
maxTokens: number;
|
|
40
|
+
family?: string;
|
|
41
|
+
note?: string;
|
|
42
|
+
}
|
|
7
43
|
|
|
8
|
-
export const
|
|
44
|
+
export const RERANKERS: Record<string, RerankerAdapter> = {
|
|
45
|
+
'gte-reranker-modernbert': {
|
|
46
|
+
id: 'Alibaba-NLP/gte-reranker-modernbert-base',
|
|
47
|
+
revision: 'f7481e6055501a30fb19d090657df9ec1f79ab2c',
|
|
48
|
+
dtype: 'q8',
|
|
49
|
+
maxTokens: 8192,
|
|
50
|
+
family: 'gte',
|
|
51
|
+
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.',
|
|
52
|
+
},
|
|
53
|
+
'bge-reranker-base': {
|
|
54
|
+
id: 'Xenova/bge-reranker-base',
|
|
55
|
+
dtype: 'q8',
|
|
56
|
+
maxTokens: 512,
|
|
57
|
+
family: 'bge',
|
|
58
|
+
note: '~278 MB q8. The pre-#203 default; kept for reproducibility (pin via --rerank-model bge-reranker-base).',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const DEFAULT_RERANKER = 'gte-reranker-modernbert';
|
|
63
|
+
|
|
64
|
+
/** Legacy export — the default adapter's repo id (kept for back-compat). */
|
|
65
|
+
export const RERANK_MODEL = RERANKERS[DEFAULT_RERANKER].id;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve a reranker alias / raw repo id (optionally `id@revision`) to an
|
|
69
|
+
* adapter. Unknown ids are rejected — unlike embedders there is no
|
|
70
|
+
* "unknown adapter" escape hatch, because the single-logit contract cannot
|
|
71
|
+
* be guessed safely.
|
|
72
|
+
*/
|
|
73
|
+
export function resolveReranker(name?: string | null): { alias: string; adapter: RerankerAdapter } {
|
|
74
|
+
if (!name || name === DEFAULT_RERANKER) {
|
|
75
|
+
return { alias: DEFAULT_RERANKER, adapter: RERANKERS[DEFAULT_RERANKER] };
|
|
76
|
+
}
|
|
77
|
+
if (RERANKERS[name]) return { alias: name, adapter: RERANKERS[name] };
|
|
78
|
+
let id = name;
|
|
79
|
+
let revision: string | undefined;
|
|
80
|
+
const at = name.indexOf('@');
|
|
81
|
+
if (at > 0) {
|
|
82
|
+
id = name.slice(0, at);
|
|
83
|
+
revision = name.slice(at + 1) || undefined;
|
|
84
|
+
}
|
|
85
|
+
for (const [alias, adapter] of Object.entries(RERANKERS)) {
|
|
86
|
+
if (adapter.id === id) {
|
|
87
|
+
return { alias, adapter: revision ? { ...adapter, revision } : adapter };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
throw new Error(
|
|
91
|
+
`unknown reranker "${name}" — available: ${Object.keys(RERANKERS).join(', ')} (or register it in RERANKERS, issue #203)`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
9
94
|
|
|
10
95
|
export async function getReranker(
|
|
11
96
|
cacheDir: string,
|
|
12
97
|
offline: boolean = false,
|
|
13
|
-
retryOpts: { maxRetries?: number; delays?: number[]; log?: (msg: string) => void } = {}
|
|
14
|
-
|
|
98
|
+
retryOpts: { maxRetries?: number; delays?: number[]; log?: (msg: string) => void } = {},
|
|
99
|
+
rerankerName?: string | null
|
|
100
|
+
): Promise<LoadedReranker> {
|
|
101
|
+
const { adapter } = resolveReranker(rerankerName);
|
|
15
102
|
const sessionOptions = resolveSessionOptions();
|
|
16
103
|
const sessionKey = sessionOptions ? JSON.stringify(sessionOptions) : 'default';
|
|
17
|
-
const key = `${
|
|
104
|
+
const key = `${adapter.id}@${adapter.revision || 'main'}|${adapter.dtype}|${offline ? 'off' : 'on'}|${sessionKey}`;
|
|
18
105
|
if (_rerankers.has(key)) return _rerankers.get(key)!;
|
|
19
106
|
|
|
20
|
-
const loadPipeline = async () => {
|
|
21
|
-
const { AutoModelForSequenceClassification, AutoTokenizer, env } = await import(
|
|
107
|
+
const loadPipeline = async (): Promise<LoadedReranker> => {
|
|
108
|
+
const { AutoModelForSequenceClassification, AutoTokenizer, env } = (await import(
|
|
109
|
+
'@huggingface/transformers'
|
|
110
|
+
)) as unknown as {
|
|
111
|
+
AutoModelForSequenceClassification: { from_pretrained(id: string, opts: Record<string, unknown>): Promise<RerankerModel> };
|
|
112
|
+
AutoTokenizer: { from_pretrained(id: string, opts: Record<string, unknown>): Promise<RerankerTokenizer> };
|
|
113
|
+
env: { cacheDir?: string; allowRemoteModels?: boolean };
|
|
114
|
+
};
|
|
22
115
|
if (cacheDir) env.cacheDir = cacheDir;
|
|
23
116
|
env.allowRemoteModels = !offline;
|
|
24
|
-
const tokenizer = await AutoTokenizer.from_pretrained(
|
|
117
|
+
const tokenizer = await AutoTokenizer.from_pretrained(adapter.id, {
|
|
118
|
+
...(adapter.revision ? { revision: adapter.revision } : {}),
|
|
119
|
+
progress_callback: emitDownloadEvent,
|
|
120
|
+
});
|
|
25
121
|
const loadOpts: Record<string, unknown> = {
|
|
26
|
-
dtype:
|
|
122
|
+
dtype: adapter.dtype,
|
|
27
123
|
progress_callback: emitDownloadEvent,
|
|
124
|
+
...(adapter.revision ? { revision: adapter.revision } : {}),
|
|
28
125
|
...(sessionOptions ? { session_options: sessionOptions } : {}),
|
|
29
126
|
};
|
|
30
|
-
const model = await AutoModelForSequenceClassification.from_pretrained(
|
|
31
|
-
const r = { tokenizer, model };
|
|
127
|
+
const model = await AutoModelForSequenceClassification.from_pretrained(adapter.id, loadOpts);
|
|
128
|
+
const r: LoadedReranker = { tokenizer, model };
|
|
32
129
|
_rerankers.set(key, r);
|
|
33
130
|
return r;
|
|
34
131
|
};
|
|
@@ -44,12 +141,22 @@ export async function rerankScores(
|
|
|
44
141
|
query: string,
|
|
45
142
|
passages: string[],
|
|
46
143
|
cacheDir: string,
|
|
47
|
-
offline: boolean = false
|
|
144
|
+
offline: boolean = false,
|
|
145
|
+
rerankerName?: string | null
|
|
48
146
|
): Promise<number[]> {
|
|
49
147
|
if (passages.length === 0) return [];
|
|
50
|
-
const { tokenizer, model } = await getReranker(cacheDir, offline);
|
|
148
|
+
const { tokenizer, model } = await getReranker(cacheDir, offline, {}, rerankerName);
|
|
51
149
|
const queries = new Array(passages.length).fill(query);
|
|
52
150
|
const inputs = tokenizer(queries, { text_pair: passages, padding: true, truncation: true });
|
|
53
151
|
const outputs = await model(inputs);
|
|
54
|
-
|
|
152
|
+
const rawLogits = outputs?.logits?.tolist();
|
|
153
|
+
if (!Array.isArray(rawLogits)) {
|
|
154
|
+
throw new Error('reranker model returned invalid logits structure');
|
|
155
|
+
}
|
|
156
|
+
return rawLogits.map((row: unknown, idx: number) => {
|
|
157
|
+
if (!Array.isArray(row) || typeof row[0] !== 'number' || !Number.isFinite(row[0])) {
|
|
158
|
+
throw new Error(`reranker model returned non-finite score at index ${idx}`);
|
|
159
|
+
}
|
|
160
|
+
return row[0];
|
|
161
|
+
});
|
|
55
162
|
}
|