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.
Files changed (75) hide show
  1. package/README.md +132 -16
  2. package/RESEARCH.md +207 -3
  3. package/bin/cli.mjs +449 -13
  4. package/dist/binary-format.js +9 -0
  5. package/dist/completions.js +11 -1
  6. package/dist/core.d.ts +42 -4
  7. package/dist/core.js +208 -14
  8. package/dist/federation.d.ts +16 -1
  9. package/dist/federation.js +33 -16
  10. package/dist/filter.d.ts +7 -0
  11. package/dist/filter.js +13 -0
  12. package/dist/hnsw.d.ts +96 -0
  13. package/dist/hnsw.js +326 -0
  14. package/dist/index-format.js +8 -1
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.js +4 -4
  17. package/dist/indexer.d.ts +18 -1
  18. package/dist/indexer.js +92 -18
  19. package/dist/ivf.d.ts +14 -7
  20. package/dist/ivf.js +126 -18
  21. package/dist/lexical.d.ts +10 -0
  22. package/dist/lexical.js +98 -15
  23. package/dist/markdown-parser.d.ts +62 -0
  24. package/dist/markdown-parser.js +227 -30
  25. package/dist/mcp.d.ts +229 -7
  26. package/dist/mcp.js +743 -44
  27. package/dist/models.d.ts +8 -2
  28. package/dist/models.js +84 -32
  29. package/dist/providers.d.ts +56 -0
  30. package/dist/providers.js +305 -0
  31. package/dist/quantization.d.ts +12 -0
  32. package/dist/quantization.js +54 -0
  33. package/dist/rag.d.ts +132 -7
  34. package/dist/rag.js +314 -38
  35. package/dist/repair.d.ts +1 -1
  36. package/dist/repair.js +53 -2
  37. package/dist/rerank.d.ts +51 -6
  38. package/dist/rerank.js +69 -10
  39. package/dist/search.d.ts +49 -3
  40. package/dist/search.js +387 -78
  41. package/dist/serve.d.ts +35 -0
  42. package/dist/serve.js +574 -58
  43. package/dist/tui.d.ts +14 -0
  44. package/dist/tui.js +132 -15
  45. package/dist/watcher.js +23 -12
  46. package/dist/webui.d.ts +3 -3
  47. package/dist/webui.js +225 -5
  48. package/dist/wikilinks.d.ts +31 -0
  49. package/dist/wikilinks.js +185 -1
  50. package/package.json +3 -3
  51. package/src/binary-format.ts +13 -0
  52. package/src/completions.ts +11 -1
  53. package/src/core.ts +249 -13
  54. package/src/federation.ts +51 -17
  55. package/src/filter.ts +14 -0
  56. package/src/hnsw.ts +414 -0
  57. package/src/index-format.ts +7 -1
  58. package/src/index.ts +7 -2
  59. package/src/indexer.ts +112 -19
  60. package/src/ivf.ts +157 -20
  61. package/src/lexical.ts +91 -13
  62. package/src/markdown-parser.ts +279 -37
  63. package/src/mcp.ts +820 -47
  64. package/src/models.ts +97 -35
  65. package/src/providers.ts +345 -0
  66. package/src/quantization.ts +61 -0
  67. package/src/rag.ts +451 -34
  68. package/src/repair.ts +51 -2
  69. package/src/rerank.ts +121 -14
  70. package/src/search.ts +424 -82
  71. package/src/serve.ts +589 -66
  72. package/src/tui.ts +142 -16
  73. package/src/watcher.ts +21 -10
  74. package/src/webui.ts +225 -5
  75. 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
- const _rerankers = new Map<string, { tokenizer: any; model: any }>();
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 RERANK_MODEL = 'Xenova/bge-reranker-base';
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
- ): Promise<{ tokenizer: any; model: any }> {
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 = `${RERANK_MODEL}|${offline ? 'off' : 'on'}|${sessionKey}`;
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('@huggingface/transformers');
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(RERANK_MODEL, { progress_callback: emitDownloadEvent });
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: 'q8',
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(RERANK_MODEL, loadOpts);
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
- return (outputs.logits.tolist() as number[][]).map((row) => row[0]);
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
  }