@monoes/monograph 1.2.8 → 1.2.9
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/dist/src/index.d.ts +0 -7
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +0 -7
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp-tools/index.d.ts +0 -1
- package/dist/src/mcp-tools/index.d.ts.map +1 -1
- package/dist/src/mcp-tools/index.js +0 -1
- package/dist/src/mcp-tools/index.js.map +1 -1
- package/dist/src/search/hybrid-query.d.ts +11 -17
- package/dist/src/search/hybrid-query.d.ts.map +1 -1
- package/dist/src/search/hybrid-query.js +10 -95
- package/dist/src/search/hybrid-query.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/index.ts +0 -7
- package/src/mcp-tools/index.ts +0 -1
- package/src/search/hybrid-query.ts +13 -120
- package/dist/src/__tests__/mcp-tools/embed.test.d.ts +0 -2
- package/dist/src/__tests__/mcp-tools/embed.test.d.ts.map +0 -1
- package/dist/src/__tests__/mcp-tools/embed.test.js +0 -12
- package/dist/src/__tests__/mcp-tools/embed.test.js.map +0 -1
- package/dist/src/__tests__/search/embed-stale.test.d.ts +0 -2
- package/dist/src/__tests__/search/embed-stale.test.d.ts.map +0 -1
- package/dist/src/__tests__/search/embed-stale.test.js +0 -38
- package/dist/src/__tests__/search/embed-stale.test.js.map +0 -1
- package/dist/src/__tests__/search/exact-search.test.d.ts +0 -2
- package/dist/src/__tests__/search/exact-search.test.d.ts.map +0 -1
- package/dist/src/__tests__/search/exact-search.test.js +0 -49
- package/dist/src/__tests__/search/exact-search.test.js.map +0 -1
- package/dist/src/mcp-tools/embed.d.ts +0 -25
- package/dist/src/mcp-tools/embed.d.ts.map +0 -1
- package/dist/src/mcp-tools/embed.js +0 -33
- package/dist/src/mcp-tools/embed.js.map +0 -1
- package/dist/src/pipeline/phases/embeddings.d.ts +0 -6
- package/dist/src/pipeline/phases/embeddings.d.ts.map +0 -1
- package/dist/src/pipeline/phases/embeddings.js +0 -12
- package/dist/src/pipeline/phases/embeddings.js.map +0 -1
- package/dist/src/search/device-config.d.ts +0 -16
- package/dist/src/search/device-config.d.ts.map +0 -1
- package/dist/src/search/device-config.js +0 -22
- package/dist/src/search/device-config.js.map +0 -1
- package/dist/src/search/embed-batch.d.ts +0 -30
- package/dist/src/search/embed-batch.d.ts.map +0 -1
- package/dist/src/search/embed-batch.js +0 -99
- package/dist/src/search/embed-batch.js.map +0 -1
- package/dist/src/search/embedder.d.ts +0 -26
- package/dist/src/search/embedder.d.ts.map +0 -1
- package/dist/src/search/embedder.js +0 -49
- package/dist/src/search/embedder.js.map +0 -1
- package/dist/src/search/exact-search.d.ts +0 -10
- package/dist/src/search/exact-search.d.ts.map +0 -1
- package/dist/src/search/exact-search.js +0 -28
- package/dist/src/search/exact-search.js.map +0 -1
- package/dist/src/search/http-embedder.d.ts +0 -17
- package/dist/src/search/http-embedder.d.ts.map +0 -1
- package/dist/src/search/http-embedder.js +0 -57
- package/dist/src/search/http-embedder.js.map +0 -1
- package/dist/src/storage/embedding-store.d.ts +0 -29
- package/dist/src/storage/embedding-store.d.ts.map +0 -1
- package/dist/src/storage/embedding-store.js +0 -77
- package/dist/src/storage/embedding-store.js.map +0 -1
- package/src/__tests__/mcp-tools/embed.test.ts +0 -13
- package/src/__tests__/search/embed-stale.test.ts +0 -42
- package/src/__tests__/search/exact-search.test.ts +0 -54
- package/src/mcp-tools/embed.ts +0 -56
- package/src/pipeline/phases/embeddings.ts +0 -17
- package/src/search/device-config.ts +0 -31
- package/src/search/embed-batch.ts +0 -151
- package/src/search/embedder.ts +0 -60
- package/src/search/exact-search.ts +0 -45
- package/src/search/http-embedder.ts +0 -78
- package/src/storage/embedding-store.ts +0 -108
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Batch-embed all symbol nodes that don't yet have an embedding stored.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type Database from 'better-sqlite3';
|
|
6
|
-
import type { EmbedderFn } from './embedder.js';
|
|
7
|
-
import type { EmbedDeviceConfig } from './device-config.js';
|
|
8
|
-
import type { HttpEmbedderConfig } from './http-embedder.js';
|
|
9
|
-
import { upsertEmbedding, ensureEmbeddingSchema, countEmbeddings } from '../storage/embedding-store.js';
|
|
10
|
-
import { embedText } from './embedder.js';
|
|
11
|
-
|
|
12
|
-
const BATCH_SIZE = 32;
|
|
13
|
-
|
|
14
|
-
export interface EmbedBatchConfig {
|
|
15
|
-
device?: EmbedDeviceConfig;
|
|
16
|
-
remote?: HttpEmbedderConfig; // if set, use HttpEmbedder instead of local
|
|
17
|
-
batchSize?: number; // override default batch size
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Embed a list of text strings and return an array of embedding vectors.
|
|
22
|
-
*
|
|
23
|
-
* If `config.remote` is provided, delegates to `HttpEmbedder`.
|
|
24
|
-
* Otherwise falls back to the local HuggingFace embedder.
|
|
25
|
-
*
|
|
26
|
-
* @param texts - Strings to embed
|
|
27
|
-
* @param config - Optional device/remote/batchSize config
|
|
28
|
-
*/
|
|
29
|
-
export async function embedBatch(
|
|
30
|
-
texts: string[],
|
|
31
|
-
config?: EmbedBatchConfig,
|
|
32
|
-
): Promise<number[][]> {
|
|
33
|
-
// Store device config in a local variable (passed through for future use)
|
|
34
|
-
const _device = config?.device;
|
|
35
|
-
void _device;
|
|
36
|
-
|
|
37
|
-
if (config?.remote) {
|
|
38
|
-
const { HttpEmbedder } = await import('./http-embedder.js');
|
|
39
|
-
const embedder = new HttpEmbedder(config.remote);
|
|
40
|
-
return embedder.embedBatch(texts);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Local embedder path
|
|
44
|
-
const { getEmbedder } = await import('./embedder.js');
|
|
45
|
-
const embedderFn = await getEmbedder();
|
|
46
|
-
const batchSize = config?.batchSize ?? BATCH_SIZE;
|
|
47
|
-
const results: number[][] = [];
|
|
48
|
-
|
|
49
|
-
for (let i = 0; i < texts.length; i += batchSize) {
|
|
50
|
-
const batch = texts.slice(i, i + batchSize);
|
|
51
|
-
const batchResults = await Promise.all(
|
|
52
|
-
batch.map(async (text) => {
|
|
53
|
-
const vector = await embedText(text, embedderFn);
|
|
54
|
-
return Array.from(vector);
|
|
55
|
-
}),
|
|
56
|
-
);
|
|
57
|
-
results.push(...batchResults);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return results;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface EmbedAllResult {
|
|
64
|
-
embedded: number;
|
|
65
|
-
skipped: number;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Embed all nodes that are missing embeddings.
|
|
70
|
-
*
|
|
71
|
-
* Nodes are fetched in batches of BATCH_SIZE to avoid loading the entire DB
|
|
72
|
-
* into memory at once.
|
|
73
|
-
*
|
|
74
|
-
* @param db - Open monograph database
|
|
75
|
-
* @param embedder - Feature extraction pipeline function
|
|
76
|
-
* @param force - When true, re-embed nodes even if they already have an embedding
|
|
77
|
-
*/
|
|
78
|
-
const CODE_LABELS = new Set(['Function', 'Method', 'Class', 'Module', 'Interface', 'Enum', 'Struct', 'Constructor', 'Variable', 'Const', 'TypeAlias']);
|
|
79
|
-
|
|
80
|
-
export async function embedAll(
|
|
81
|
-
db: Database.Database,
|
|
82
|
-
embedder: EmbedderFn,
|
|
83
|
-
force = false,
|
|
84
|
-
codeOnly = false,
|
|
85
|
-
): Promise<EmbedAllResult> {
|
|
86
|
-
let embedded = 0;
|
|
87
|
-
let skipped = 0;
|
|
88
|
-
|
|
89
|
-
// Fetch all node IDs and their name+normLabel (used as the text to embed)
|
|
90
|
-
const rows = db
|
|
91
|
-
.prepare(
|
|
92
|
-
`SELECT id, name, norm_label, label, file_path
|
|
93
|
-
FROM nodes
|
|
94
|
-
ORDER BY rowid`,
|
|
95
|
-
)
|
|
96
|
-
.all() as { id: string; name: string; norm_label: string; label: string; file_path: string | null }[];
|
|
97
|
-
|
|
98
|
-
const filteredRows = codeOnly ? rows.filter((r) => CODE_LABELS.has(r.label)) : rows;
|
|
99
|
-
|
|
100
|
-
// Build set of node IDs that already have embeddings
|
|
101
|
-
const existingIds = force
|
|
102
|
-
? new Set<string>()
|
|
103
|
-
: new Set(
|
|
104
|
-
(
|
|
105
|
-
db.prepare('SELECT node_id FROM embeddings').all() as { node_id: string }[]
|
|
106
|
-
).map((r) => r.node_id),
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
// Filter to only rows that need embedding
|
|
110
|
-
const toEmbed = filteredRows.filter((r) => !existingIds.has(r.id));
|
|
111
|
-
skipped = filteredRows.length - toEmbed.length;
|
|
112
|
-
|
|
113
|
-
// Ensure schema migration runs once before any upsert — not per-row.
|
|
114
|
-
if (toEmbed.length > 0) ensureEmbeddingSchema(db);
|
|
115
|
-
|
|
116
|
-
// Process in batches
|
|
117
|
-
for (let i = 0; i < toEmbed.length; i += BATCH_SIZE) {
|
|
118
|
-
const batch = toEmbed.slice(i, i + BATCH_SIZE);
|
|
119
|
-
await Promise.all(
|
|
120
|
-
batch.map(async (row) => {
|
|
121
|
-
const text = buildNodeText(row);
|
|
122
|
-
try {
|
|
123
|
-
const vector = await embedText(text, embedder);
|
|
124
|
-
upsertEmbedding(db, row.id, vector);
|
|
125
|
-
embedded++;
|
|
126
|
-
} catch {
|
|
127
|
-
// Skip nodes that fail to embed (e.g. empty name)
|
|
128
|
-
skipped++;
|
|
129
|
-
}
|
|
130
|
-
}),
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return { embedded, skipped };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
138
|
-
|
|
139
|
-
function buildNodeText(row: {
|
|
140
|
-
name: string;
|
|
141
|
-
norm_label: string;
|
|
142
|
-
label: string;
|
|
143
|
-
file_path: string | null;
|
|
144
|
-
}): string {
|
|
145
|
-
const parts: string[] = [row.name, row.label];
|
|
146
|
-
if (row.norm_label && row.norm_label !== row.name) parts.push(row.norm_label);
|
|
147
|
-
if (row.file_path) parts.push(row.file_path);
|
|
148
|
-
return parts.join(' ');
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export { countEmbeddings };
|
package/src/search/embedder.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lazy singleton for the HuggingFace FeatureExtractionPipeline.
|
|
3
|
-
*
|
|
4
|
-
* Uses dynamic import so that:
|
|
5
|
-
* 1. Tests can mock `getEmbedder` without installing the package.
|
|
6
|
-
* 2. @huggingface/transformers is optional at import time; callers receive a
|
|
7
|
-
* clear error if the package is unavailable.
|
|
8
|
-
*
|
|
9
|
-
* Model: Snowflake/snowflake-arctic-embed-xs (384 dimensions)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
-
export type EmbedderFn = (text: string | string[], options?: Record<string, unknown>) => Promise<any>;
|
|
14
|
-
|
|
15
|
-
let cachedEmbedder: EmbedderFn | null = null;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Returns (and caches) a FeatureExtractionPipeline for
|
|
19
|
-
* 'Snowflake/snowflake-arctic-embed-xs'.
|
|
20
|
-
*
|
|
21
|
-
* Throws a descriptive error if @huggingface/transformers is not installed.
|
|
22
|
-
*/
|
|
23
|
-
export async function getEmbedder(): Promise<EmbedderFn> {
|
|
24
|
-
if (cachedEmbedder) return cachedEmbedder;
|
|
25
|
-
|
|
26
|
-
let pipeline: (task: string, model: string, opts?: Record<string, unknown>) => Promise<EmbedderFn>;
|
|
27
|
-
try {
|
|
28
|
-
// Dynamic import so the dep is optional at bundle time
|
|
29
|
-
const hf = await import('@huggingface/transformers');
|
|
30
|
-
pipeline = hf.pipeline as typeof pipeline;
|
|
31
|
-
} catch {
|
|
32
|
-
throw new Error(
|
|
33
|
-
'@huggingface/transformers is not installed. ' +
|
|
34
|
-
'Run `npm install @huggingface/transformers` to enable embedding support.',
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
cachedEmbedder = await pipeline('feature-extraction', 'Snowflake/snowflake-arctic-embed-xs', {
|
|
39
|
-
dtype: 'fp32',
|
|
40
|
-
});
|
|
41
|
-
return cachedEmbedder;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Reset the cached singleton — used only in tests. */
|
|
45
|
-
export function resetEmbedderCache(): void {
|
|
46
|
-
cachedEmbedder = null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Embed a single text string and return a 384-dim Float32Array.
|
|
51
|
-
* Pooling: mean. Normalisation: L2 (so cosine ≡ dot product).
|
|
52
|
-
*/
|
|
53
|
-
export async function embedText(
|
|
54
|
-
text: string,
|
|
55
|
-
embedder: EmbedderFn,
|
|
56
|
-
): Promise<Float32Array> {
|
|
57
|
-
const output = await embedder(text, { pooling: 'mean', normalize: true });
|
|
58
|
-
// HuggingFace Tensor: output.data is Float32Array
|
|
59
|
-
return output.data as Float32Array;
|
|
60
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type Database from 'better-sqlite3';
|
|
2
|
-
|
|
3
|
-
export interface ExactSearchResult {
|
|
4
|
-
id: string;
|
|
5
|
-
score: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface ExactSearchOptions {
|
|
9
|
-
limit?: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function cosineSimilarity(a: Float32Array, b: Float32Array): number {
|
|
13
|
-
let dot = 0, normA = 0, normB = 0;
|
|
14
|
-
const len = Math.min(a.length, b.length);
|
|
15
|
-
for (let i = 0; i < len; i++) {
|
|
16
|
-
dot += a[i]! * b[i]!;
|
|
17
|
-
normA += a[i]! * a[i]!;
|
|
18
|
-
normB += b[i]! * b[i]!;
|
|
19
|
-
}
|
|
20
|
-
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
21
|
-
return denom === 0 ? 0 : dot / denom;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function exactVectorSearch(
|
|
25
|
-
db: Database.Database,
|
|
26
|
-
queryVector: Float32Array,
|
|
27
|
-
options: ExactSearchOptions = {},
|
|
28
|
-
): ExactSearchResult[] {
|
|
29
|
-
const { limit = 10 } = options;
|
|
30
|
-
|
|
31
|
-
const rows = db.prepare(`
|
|
32
|
-
SELECT e.node_id as id, e.vector FROM embeddings e
|
|
33
|
-
INNER JOIN nodes n ON n.id = e.node_id
|
|
34
|
-
`).all() as { id: string; vector: Buffer }[];
|
|
35
|
-
|
|
36
|
-
if (rows.length === 0) return [];
|
|
37
|
-
|
|
38
|
-
return rows
|
|
39
|
-
.map(row => {
|
|
40
|
-
const vec = new Float32Array(row.vector.buffer, row.vector.byteOffset, row.vector.byteLength / 4);
|
|
41
|
-
return { id: row.id, score: cosineSimilarity(queryVector, vec) };
|
|
42
|
-
})
|
|
43
|
-
.sort((a, b) => b.score - a.score)
|
|
44
|
-
.slice(0, limit);
|
|
45
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
export interface HttpEmbedderConfig {
|
|
2
|
-
endpoint: string;
|
|
3
|
-
model?: string;
|
|
4
|
-
apiKey?: string;
|
|
5
|
-
timeoutMs?: number;
|
|
6
|
-
batchSize?: number;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface EmbedResponse {
|
|
10
|
-
embedding: number[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class HttpEmbedder {
|
|
14
|
-
private config: Required<HttpEmbedderConfig>;
|
|
15
|
-
|
|
16
|
-
constructor(config: HttpEmbedderConfig) {
|
|
17
|
-
this.config = {
|
|
18
|
-
model: '',
|
|
19
|
-
apiKey: '',
|
|
20
|
-
timeoutMs: 30_000,
|
|
21
|
-
batchSize: 32,
|
|
22
|
-
...config,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async embedOne(text: string): Promise<number[]> {
|
|
27
|
-
const controller = new AbortController();
|
|
28
|
-
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const headers: Record<string, string> = {
|
|
32
|
-
'Content-Type': 'application/json',
|
|
33
|
-
};
|
|
34
|
-
if (this.config.apiKey) {
|
|
35
|
-
headers['Authorization'] = `Bearer ${this.config.apiKey}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const body: Record<string, unknown> = { prompt: text };
|
|
39
|
-
if (this.config.model) {
|
|
40
|
-
body['model'] = this.config.model;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const response = await fetch(this.config.endpoint, {
|
|
44
|
-
method: 'POST',
|
|
45
|
-
headers,
|
|
46
|
-
body: JSON.stringify(body),
|
|
47
|
-
signal: controller.signal,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
if (!response.ok) {
|
|
51
|
-
throw new Error(`HTTP embedder: ${response.status} ${response.statusText}`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const data = (await response.json()) as Partial<EmbedResponse>;
|
|
55
|
-
if (!data.embedding) {
|
|
56
|
-
throw new Error('HTTP embedder: no embedding in response');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return data.embedding;
|
|
60
|
-
} finally {
|
|
61
|
-
clearTimeout(timer);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async embedBatch(texts: string[]): Promise<number[][]> {
|
|
66
|
-
const results: number[][] = [];
|
|
67
|
-
const batchSize = this.config.batchSize;
|
|
68
|
-
|
|
69
|
-
for (let i = 0; i < texts.length; i += batchSize) {
|
|
70
|
-
const batch = texts.slice(i, i + batchSize);
|
|
71
|
-
for (const text of batch) {
|
|
72
|
-
results.push(await this.embedOne(text));
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return results;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import type Database from 'better-sqlite3';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* CRUD operations for the embeddings table.
|
|
5
|
-
*
|
|
6
|
-
* Vectors are stored as BLOBs (raw Float32Array bytes) and reconstructed on read.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Ensure the embeddings table has the content_hash column introduced in a later
|
|
11
|
-
* schema version. Call this ONCE before a batch of upserts rather than inside
|
|
12
|
-
* upsertEmbedding itself to avoid running ALTER TABLE on every row write.
|
|
13
|
-
*/
|
|
14
|
-
export function ensureEmbeddingSchema(db: Database.Database): void {
|
|
15
|
-
try {
|
|
16
|
-
db.exec('ALTER TABLE embeddings ADD COLUMN content_hash TEXT');
|
|
17
|
-
} catch {
|
|
18
|
-
// Column already exists — ignore.
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function upsertEmbedding(
|
|
23
|
-
db: Database.Database,
|
|
24
|
-
nodeId: string,
|
|
25
|
-
vector: Float32Array,
|
|
26
|
-
contentHash?: string,
|
|
27
|
-
): void {
|
|
28
|
-
const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
|
|
29
|
-
db
|
|
30
|
-
.prepare(
|
|
31
|
-
'INSERT OR REPLACE INTO embeddings (node_id, vector, content_hash) VALUES (?, ?, ?)',
|
|
32
|
-
)
|
|
33
|
-
.run(nodeId, buf, contentHash ?? null);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Bulk-upsert multiple embeddings in a single transaction.
|
|
38
|
-
* Calls ensureEmbeddingSchema once before writing, then wraps all inserts in
|
|
39
|
-
* a transaction for 10-100x faster throughput vs per-row upsertEmbedding calls.
|
|
40
|
-
*/
|
|
41
|
-
export function batchUpsertEmbeddings(
|
|
42
|
-
db: Database.Database,
|
|
43
|
-
entries: Array<{ nodeId: string; vector: Float32Array; contentHash?: string }>,
|
|
44
|
-
): void {
|
|
45
|
-
if (entries.length === 0) return;
|
|
46
|
-
ensureEmbeddingSchema(db);
|
|
47
|
-
const stmt = db.prepare(
|
|
48
|
-
'INSERT OR REPLACE INTO embeddings (node_id, vector, content_hash) VALUES (?, ?, ?)',
|
|
49
|
-
);
|
|
50
|
-
const insertMany = db.transaction(
|
|
51
|
-
(rows: Array<{ nodeId: string; vector: Float32Array; contentHash?: string }>) => {
|
|
52
|
-
for (const e of rows) {
|
|
53
|
-
const buf = Buffer.from(e.vector.buffer, e.vector.byteOffset, e.vector.byteLength);
|
|
54
|
-
stmt.run(e.nodeId, buf, e.contentHash ?? null);
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
);
|
|
58
|
-
insertMany(entries);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function getEmbeddingContentHash(db: Database.Database, nodeId: string): string | null {
|
|
62
|
-
const row = db
|
|
63
|
-
.prepare('SELECT content_hash FROM embeddings WHERE node_id = ?')
|
|
64
|
-
.get(nodeId) as { content_hash: string | null } | undefined;
|
|
65
|
-
return row?.content_hash ?? null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function isEmbeddingStale(
|
|
69
|
-
db: Database.Database,
|
|
70
|
-
nodeId: string,
|
|
71
|
-
currentHash: string,
|
|
72
|
-
): boolean {
|
|
73
|
-
const stored = getEmbeddingContentHash(db, nodeId);
|
|
74
|
-
return stored !== currentHash;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function getEmbedding(db: Database.Database, nodeId: string): Float32Array | null {
|
|
78
|
-
const row = db.prepare('SELECT vector FROM embeddings WHERE node_id = ?').get(nodeId) as
|
|
79
|
-
| { vector: Buffer }
|
|
80
|
-
| undefined;
|
|
81
|
-
if (!row) return null;
|
|
82
|
-
return bufToFloat32(row.vector);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function getAllEmbeddings(db: Database.Database): Map<string, Float32Array> {
|
|
86
|
-
const rows = db.prepare('SELECT node_id, vector FROM embeddings').all() as {
|
|
87
|
-
node_id: string;
|
|
88
|
-
vector: Buffer;
|
|
89
|
-
}[];
|
|
90
|
-
const result = new Map<string, Float32Array>();
|
|
91
|
-
for (const row of rows) {
|
|
92
|
-
result.set(row.node_id, bufToFloat32(row.vector));
|
|
93
|
-
}
|
|
94
|
-
return result;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function countEmbeddings(db: Database.Database): number {
|
|
98
|
-
const row = db.prepare('SELECT COUNT(*) AS n FROM embeddings').get() as { n: number };
|
|
99
|
-
return row.n;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
function bufToFloat32(buf: Buffer): Float32Array {
|
|
105
|
-
// Copy to a fresh ArrayBuffer so slice offset doesn't cause issues
|
|
106
|
-
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
107
|
-
return new Float32Array(ab);
|
|
108
|
-
}
|