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/dist/rerank.d.ts CHANGED
@@ -1,10 +1,55 @@
1
- export declare const RERANK_MODEL = "Xenova/bge-reranker-base";
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
- tokenizer: any;
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 RERANK_MODEL = 'Xenova/bge-reranker-base';
7
- export async function getReranker(cacheDir, offline = false, retryOpts = {}) {
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 = `${RERANK_MODEL}|${offline ? 'off' : 'on'}|${sessionKey}`;
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(RERANK_MODEL, { progress_callback: emitDownloadEvent });
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: 'q8',
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(RERANK_MODEL, loadOpts);
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
- return outputs.logits.tolist().map((row) => row[0]);
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 `created` → `updated` → file mtime; null when nothing parses. */
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
- ann?: boolean;
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;