@delali/narsil 0.1.6 → 0.1.7
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 +130 -2
- package/dist/adapters/filesystem.d.ts +1 -1
- package/dist/adapters/filesystem.mjs +1 -1
- package/dist/adapters/indexeddb.d.ts +1 -1
- package/dist/adapters/indexeddb.mjs +1 -1
- package/dist/adapters/memory.d.ts +1 -1
- package/dist/{adapters-XODT2F3h.d.ts → adapters-BbRkoiu9.d.ts} +7 -1
- package/dist/{chunk-WYC635GI.mjs → chunk-I24SGLFS.mjs} +5 -6
- package/dist/{chunk-TFWBALU4.mjs → chunk-KHL4RPC3.mjs} +1957 -1053
- package/dist/{chunk-7GKPWGCN.mjs → chunk-R2C2EQGO.mjs} +12 -2
- package/dist/{chunk-GVN2W4VA.mjs → chunk-VVORJOXR.mjs} +273 -84
- package/dist/direct-executor-LBJLGMJ2.mjs +4 -0
- package/dist/embeddings/openai.d.ts +13 -0
- package/dist/embeddings/openai.mjs +248 -0
- package/dist/index.browser.mjs +7887 -5557
- package/dist/index.d.ts +83 -49
- package/dist/index.mjs +1886 -702
- package/dist/invalidation/broadcast-channel.d.ts +1 -1
- package/dist/invalidation/filesystem.d.ts +1 -1
- package/dist/invalidation/noop.d.ts +1 -1
- package/dist/languages/english.mjs +1 -1
- package/dist/vector/hnsw-build-worker.d.ts +9 -2
- package/dist/vector/hnsw-build-worker.mjs +54 -10
- package/dist/workers/entry.mjs +1 -1
- package/package.json +10 -3
- package/dist/direct-executor-6TSG7GWH.mjs +0 -4
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,872 @@
|
|
|
1
|
-
import { createDirectExecutor, createPartitionRouter,
|
|
2
|
-
export { getLanguage, registerLanguage } from './chunk-
|
|
3
|
-
export { isSimdAvailable } from './chunk-
|
|
4
|
-
import './chunk-
|
|
5
|
-
import {
|
|
6
|
-
export { ErrorCodes, NarsilError } from './chunk-
|
|
1
|
+
import { createDirectExecutor, createPartitionRouter, validateSchema, getLanguage, deserializePayloadV2, extractVectorFieldsFromSchema, tokenize, fanOutQuery, validateRequiredFields, flattenSchema, getNestedValue, detectRuntime, fnv1a, kWayMerge, createPartitionIndex } from './chunk-KHL4RPC3.mjs';
|
|
2
|
+
export { clearNormalizationCache, configureNormalizationCache, getLanguage, getNormalizationCacheSize, registerLanguage, resetNormalizationCache } from './chunk-KHL4RPC3.mjs';
|
|
3
|
+
export { isSimdAvailable } from './chunk-VVORJOXR.mjs';
|
|
4
|
+
import './chunk-I24SGLFS.mjs';
|
|
5
|
+
import { ErrorCodes, NarsilError } from './chunk-R2C2EQGO.mjs';
|
|
6
|
+
export { ErrorCodes, NarsilError } from './chunk-R2C2EQGO.mjs';
|
|
7
7
|
import { generateId, isValidWorkerResponse, createRequestId } from './chunk-HRABJLPB.mjs';
|
|
8
8
|
import { decode } from '@msgpack/msgpack';
|
|
9
9
|
|
|
10
|
+
// src/engine/embed.ts
|
|
11
|
+
function validateVector(vector, targetField, expectedDimensions) {
|
|
12
|
+
if (!(vector instanceof Float32Array)) {
|
|
13
|
+
throw new NarsilError(
|
|
14
|
+
ErrorCodes.EMBEDDING_FAILED,
|
|
15
|
+
`Adapter returned ${typeof vector} for field "${targetField}", expected Float32Array`,
|
|
16
|
+
{ field: targetField, returnedType: typeof vector }
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
if (vector.length !== expectedDimensions) {
|
|
20
|
+
throw new NarsilError(
|
|
21
|
+
ErrorCodes.EMBEDDING_DIMENSION_MISMATCH,
|
|
22
|
+
`Adapter returned ${vector.length}-dimensional vector for field "${targetField}", expected ${expectedDimensions}`,
|
|
23
|
+
{ field: targetField, expected: expectedDimensions, actual: vector.length }
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
for (let i = 0; i < vector.length; i++) {
|
|
27
|
+
if (Number.isNaN(vector[i])) {
|
|
28
|
+
throw new NarsilError(
|
|
29
|
+
ErrorCodes.EMBEDDING_FAILED,
|
|
30
|
+
`Adapter returned vector with NaN at index ${i} for field "${targetField}"`,
|
|
31
|
+
{ field: targetField, index: i }
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return vector;
|
|
36
|
+
}
|
|
37
|
+
function resolveFieldValue(document, path) {
|
|
38
|
+
if (!path.includes(".")) return document[path];
|
|
39
|
+
const segments = path.split(".");
|
|
40
|
+
let current = document;
|
|
41
|
+
for (const segment of segments) {
|
|
42
|
+
if (current === null || current === void 0 || typeof current !== "object") return void 0;
|
|
43
|
+
current = current[segment];
|
|
44
|
+
}
|
|
45
|
+
return current;
|
|
46
|
+
}
|
|
47
|
+
function setFieldValue(document, path, value) {
|
|
48
|
+
if (!path.includes(".")) {
|
|
49
|
+
document[path] = value;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const segments = path.split(".");
|
|
53
|
+
let current = document;
|
|
54
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
55
|
+
let next = current[segments[i]];
|
|
56
|
+
if (next === null || next === void 0 || typeof next !== "object") {
|
|
57
|
+
next = {};
|
|
58
|
+
current[segments[i]] = next;
|
|
59
|
+
}
|
|
60
|
+
current = next;
|
|
61
|
+
}
|
|
62
|
+
current[segments[segments.length - 1]] = value;
|
|
63
|
+
}
|
|
64
|
+
function deleteFieldValue(document, path) {
|
|
65
|
+
if (!path.includes(".")) {
|
|
66
|
+
delete document[path];
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const segments = path.split(".");
|
|
70
|
+
let current = document;
|
|
71
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
72
|
+
const next = current[segments[i]];
|
|
73
|
+
if (next === null || next === void 0 || typeof next !== "object") return;
|
|
74
|
+
current = next;
|
|
75
|
+
}
|
|
76
|
+
delete current[segments[segments.length - 1]];
|
|
77
|
+
}
|
|
78
|
+
function collectSourceText(document, sourceFields, targetField, documentIndex) {
|
|
79
|
+
const sources = Array.isArray(sourceFields) ? sourceFields : [sourceFields];
|
|
80
|
+
const parts = [];
|
|
81
|
+
for (const source of sources) {
|
|
82
|
+
const value = resolveFieldValue(document, source);
|
|
83
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
84
|
+
if (typeof value !== "string") {
|
|
85
|
+
throw new NarsilError(
|
|
86
|
+
ErrorCodes.DOC_VALIDATION_FAILED,
|
|
87
|
+
`Embedding source field "${source}" must be a string, got ${typeof value}`,
|
|
88
|
+
{ field: source, targetField }
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
parts.push(value);
|
|
92
|
+
}
|
|
93
|
+
if (parts.length === 0) {
|
|
94
|
+
const details = { field: targetField };
|
|
95
|
+
if (documentIndex !== void 0) {
|
|
96
|
+
details.documentIndex = documentIndex;
|
|
97
|
+
}
|
|
98
|
+
throw new NarsilError(
|
|
99
|
+
ErrorCodes.EMBEDDING_NO_SOURCE,
|
|
100
|
+
`All source fields for embedding target "${targetField}" are missing or empty`,
|
|
101
|
+
details
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return parts.join("\n");
|
|
105
|
+
}
|
|
106
|
+
function wrapAdapterError(err, targetField) {
|
|
107
|
+
if (err instanceof NarsilError) return err;
|
|
108
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
109
|
+
return new NarsilError(ErrorCodes.EMBEDDING_FAILED, `Embedding failed for field "${targetField}": ${message}`, {
|
|
110
|
+
field: targetField,
|
|
111
|
+
cause: message
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async function embedDocumentFields(document, embeddingConfig, adapter, signal) {
|
|
115
|
+
const embeddedFields = /* @__PURE__ */ new Set();
|
|
116
|
+
try {
|
|
117
|
+
for (const [targetField, sourceFields] of Object.entries(embeddingConfig.fields)) {
|
|
118
|
+
const existing = resolveFieldValue(document, targetField);
|
|
119
|
+
if (existing !== void 0 && existing !== null) continue;
|
|
120
|
+
const text = collectSourceText(document, sourceFields, targetField);
|
|
121
|
+
let vector;
|
|
122
|
+
try {
|
|
123
|
+
const raw = await adapter.embed(text, "document", signal);
|
|
124
|
+
vector = validateVector(raw, targetField, adapter.dimensions);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
throw wrapAdapterError(err, targetField);
|
|
127
|
+
}
|
|
128
|
+
setFieldValue(document, targetField, vector);
|
|
129
|
+
embeddedFields.add(targetField);
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
for (const field of embeddedFields) {
|
|
133
|
+
deleteFieldValue(document, field);
|
|
134
|
+
}
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
return embeddedFields;
|
|
138
|
+
}
|
|
139
|
+
var SOURCE_FAILURE_CODES = /* @__PURE__ */ new Set([ErrorCodes.EMBEDDING_NO_SOURCE, ErrorCodes.DOC_VALIDATION_FAILED]);
|
|
140
|
+
function isDocumentSourceError(err) {
|
|
141
|
+
return err instanceof NarsilError && SOURCE_FAILURE_CODES.has(err.code);
|
|
142
|
+
}
|
|
143
|
+
async function embedBatchDocumentFields(documents, embeddingConfig, adapter, signal) {
|
|
144
|
+
const embedded = /* @__PURE__ */ new Map();
|
|
145
|
+
const failed = /* @__PURE__ */ new Map();
|
|
146
|
+
try {
|
|
147
|
+
for (const [targetField, sourceFields] of Object.entries(embeddingConfig.fields)) {
|
|
148
|
+
const needsEmbedding = [];
|
|
149
|
+
for (let i = 0; i < documents.length; i++) {
|
|
150
|
+
if (failed.has(i)) continue;
|
|
151
|
+
const doc = documents[i];
|
|
152
|
+
const existing = resolveFieldValue(doc, targetField);
|
|
153
|
+
if (existing !== void 0 && existing !== null) continue;
|
|
154
|
+
try {
|
|
155
|
+
const text = collectSourceText(doc, sourceFields, targetField, i);
|
|
156
|
+
needsEmbedding.push({ docIndex: i, text });
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (isDocumentSourceError(err)) {
|
|
159
|
+
failed.set(i, err);
|
|
160
|
+
} else {
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (needsEmbedding.length === 0) continue;
|
|
166
|
+
const texts = needsEmbedding.map((entry) => entry.text);
|
|
167
|
+
let vectors;
|
|
168
|
+
try {
|
|
169
|
+
if (adapter.embedBatch) {
|
|
170
|
+
vectors = await adapter.embedBatch(texts, "document", signal);
|
|
171
|
+
} else {
|
|
172
|
+
vectors = [];
|
|
173
|
+
const concurrency = 8;
|
|
174
|
+
for (let start = 0; start < texts.length; start += concurrency) {
|
|
175
|
+
const chunk = texts.slice(start, start + concurrency);
|
|
176
|
+
const chunkResults = await Promise.all(chunk.map((t) => adapter.embed(t, "document", signal)));
|
|
177
|
+
vectors.push(...chunkResults);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} catch (err) {
|
|
181
|
+
throw wrapAdapterError(err, targetField);
|
|
182
|
+
}
|
|
183
|
+
if (vectors.length !== needsEmbedding.length) {
|
|
184
|
+
throw new NarsilError(
|
|
185
|
+
ErrorCodes.EMBEDDING_FAILED,
|
|
186
|
+
`Adapter returned ${vectors.length} vectors for ${needsEmbedding.length} inputs on field "${targetField}"`,
|
|
187
|
+
{ field: targetField, expected: needsEmbedding.length, actual: vectors.length }
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
for (let j = 0; j < needsEmbedding.length; j++) {
|
|
191
|
+
const { docIndex } = needsEmbedding[j];
|
|
192
|
+
const validated = validateVector(vectors[j], targetField, adapter.dimensions);
|
|
193
|
+
setFieldValue(documents[docIndex], targetField, validated);
|
|
194
|
+
let fieldSet = embedded.get(docIndex);
|
|
195
|
+
if (!fieldSet) {
|
|
196
|
+
fieldSet = /* @__PURE__ */ new Set();
|
|
197
|
+
embedded.set(docIndex, fieldSet);
|
|
198
|
+
}
|
|
199
|
+
fieldSet.add(targetField);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch (err) {
|
|
203
|
+
for (const [docIndex, fields] of embedded) {
|
|
204
|
+
for (const field of fields) {
|
|
205
|
+
deleteFieldValue(documents[docIndex], field);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
return { embedded, failed };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/engine/validation.ts
|
|
214
|
+
var INDEX_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
215
|
+
var MAX_INDEX_NAME_LENGTH = 256;
|
|
216
|
+
var MAX_DOC_ID_LENGTH = 512;
|
|
217
|
+
var BATCH_CHUNK_SIZE = 1e3;
|
|
218
|
+
var MAX_LIMIT = 1e4;
|
|
219
|
+
var MAX_OFFSET = 1e5;
|
|
220
|
+
var DEFAULT_LIMIT = 10;
|
|
221
|
+
var DEFAULT_OFFSET = 0;
|
|
222
|
+
function now() {
|
|
223
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
224
|
+
return performance.now();
|
|
225
|
+
}
|
|
226
|
+
return Date.now();
|
|
227
|
+
}
|
|
228
|
+
function validateIndexName(name) {
|
|
229
|
+
if (!name || name.length === 0) {
|
|
230
|
+
throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, "Index name must not be empty", { indexName: name });
|
|
231
|
+
}
|
|
232
|
+
if (name.length > MAX_INDEX_NAME_LENGTH) {
|
|
233
|
+
throw new NarsilError(
|
|
234
|
+
ErrorCodes.INDEX_NOT_FOUND,
|
|
235
|
+
`Index name must not exceed ${MAX_INDEX_NAME_LENGTH} characters`,
|
|
236
|
+
{
|
|
237
|
+
indexName: name,
|
|
238
|
+
length: name.length
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (!INDEX_NAME_PATTERN.test(name)) {
|
|
243
|
+
throw new NarsilError(
|
|
244
|
+
ErrorCodes.INDEX_NOT_FOUND,
|
|
245
|
+
`Index name "${name}" contains invalid characters; use alphanumeric, dots, hyphens, and underscores only`,
|
|
246
|
+
{ indexName: name }
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (name.includes("..")) {
|
|
250
|
+
throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index name "${name}" must not contain ".."`, { indexName: name });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function validateDocId(docId) {
|
|
254
|
+
if (!docId || docId.length === 0) {
|
|
255
|
+
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not be empty", { docId });
|
|
256
|
+
}
|
|
257
|
+
if (docId.length > MAX_DOC_ID_LENGTH) {
|
|
258
|
+
throw new NarsilError(
|
|
259
|
+
ErrorCodes.DOC_VALIDATION_FAILED,
|
|
260
|
+
`Document ID must not exceed ${MAX_DOC_ID_LENGTH} characters`,
|
|
261
|
+
{
|
|
262
|
+
docId,
|
|
263
|
+
length: docId.length
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
if (docId.includes("\0")) {
|
|
268
|
+
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not contain null bytes", { docId });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function clampLimit(limit) {
|
|
272
|
+
if (limit === void 0) return DEFAULT_LIMIT;
|
|
273
|
+
return Math.max(0, Math.min(limit, MAX_LIMIT));
|
|
274
|
+
}
|
|
275
|
+
function clampOffset(offset) {
|
|
276
|
+
if (offset === void 0) return DEFAULT_OFFSET;
|
|
277
|
+
return Math.max(0, Math.min(offset, MAX_OFFSET));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/engine/vector-coordinator.ts
|
|
281
|
+
function extractVectorFromDoc(document, fieldPath) {
|
|
282
|
+
const value = getNestedValue(document, fieldPath);
|
|
283
|
+
if (value === void 0 || value === null) return null;
|
|
284
|
+
if (value instanceof Float32Array) return value;
|
|
285
|
+
if (!Array.isArray(value)) return null;
|
|
286
|
+
const arr = new Float32Array(value.length);
|
|
287
|
+
for (let i = 0; i < value.length; i++) {
|
|
288
|
+
const n = value[i];
|
|
289
|
+
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
|
290
|
+
arr[i] = n;
|
|
291
|
+
}
|
|
292
|
+
return arr;
|
|
293
|
+
}
|
|
294
|
+
function deleteNestedValue(obj, path) {
|
|
295
|
+
if (!path.includes(".")) {
|
|
296
|
+
delete obj[path];
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const segments = path.split(".");
|
|
300
|
+
let current = obj;
|
|
301
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
302
|
+
const next = current[segments[i]];
|
|
303
|
+
if (next === null || next === void 0 || typeof next !== "object") return;
|
|
304
|
+
current = next;
|
|
305
|
+
}
|
|
306
|
+
delete current[segments[segments.length - 1]];
|
|
307
|
+
}
|
|
308
|
+
function vectorsEqual(a, b) {
|
|
309
|
+
if (!a) return false;
|
|
310
|
+
if (a.length !== b.length) return false;
|
|
311
|
+
for (let i = 0; i < a.length; i++) {
|
|
312
|
+
if (a[i] !== b[i]) return false;
|
|
313
|
+
}
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
function validateVectorDimensions(vectors, vecIndexes) {
|
|
317
|
+
for (const [fieldPath, vector] of vectors) {
|
|
318
|
+
const vecIndex = vecIndexes.get(fieldPath);
|
|
319
|
+
if (!vecIndex) continue;
|
|
320
|
+
if (vector.length !== vecIndex.dimension) {
|
|
321
|
+
throw new NarsilError(
|
|
322
|
+
ErrorCodes.VECTOR_DIMENSION_MISMATCH,
|
|
323
|
+
`Vector dimension mismatch for field "${fieldPath}": expected ${vecIndex.dimension}, got ${vector.length}`,
|
|
324
|
+
{ field: fieldPath, expected: vecIndex.dimension, received: vector.length }
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function prepareDocumentVectors(document, vectorFieldPaths, vecIndexes) {
|
|
330
|
+
if (vecIndexes.size === 0) {
|
|
331
|
+
return { partitionDoc: document, extractedVectors: /* @__PURE__ */ new Map() };
|
|
332
|
+
}
|
|
333
|
+
const extractedVectors = /* @__PURE__ */ new Map();
|
|
334
|
+
for (const fieldPath of vectorFieldPaths) {
|
|
335
|
+
const vec = extractVectorFromDoc(document, fieldPath);
|
|
336
|
+
if (vec) {
|
|
337
|
+
extractedVectors.set(fieldPath, vec);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (extractedVectors.size === 0) {
|
|
341
|
+
return { partitionDoc: document, extractedVectors };
|
|
342
|
+
}
|
|
343
|
+
const partitionDoc = shallowCopyExcluding(document, extractedVectors.keys());
|
|
344
|
+
return { partitionDoc, extractedVectors };
|
|
345
|
+
}
|
|
346
|
+
function shallowCopyExcluding(document, fieldPaths) {
|
|
347
|
+
const topLevelExcludes = /* @__PURE__ */ new Set();
|
|
348
|
+
const nestedPaths = [];
|
|
349
|
+
for (const path of fieldPaths) {
|
|
350
|
+
if (!path.includes(".")) {
|
|
351
|
+
topLevelExcludes.add(path);
|
|
352
|
+
} else {
|
|
353
|
+
nestedPaths.push(path);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const result = {};
|
|
357
|
+
for (const key of Object.keys(document)) {
|
|
358
|
+
if (topLevelExcludes.has(key)) continue;
|
|
359
|
+
result[key] = document[key];
|
|
360
|
+
}
|
|
361
|
+
for (const path of nestedPaths) {
|
|
362
|
+
const segments = path.split(".");
|
|
363
|
+
const topKey = segments[0];
|
|
364
|
+
if (!(topKey in result) || result[topKey] === null || typeof result[topKey] !== "object") continue;
|
|
365
|
+
const source = document[topKey];
|
|
366
|
+
let target = { ...source };
|
|
367
|
+
result[topKey] = target;
|
|
368
|
+
for (let i = 1; i < segments.length - 1; i++) {
|
|
369
|
+
const seg = segments[i];
|
|
370
|
+
const next = target[seg];
|
|
371
|
+
if (next === null || next === void 0 || typeof next !== "object") break;
|
|
372
|
+
const copy = { ...next };
|
|
373
|
+
target[seg] = copy;
|
|
374
|
+
target = copy;
|
|
375
|
+
}
|
|
376
|
+
delete target[segments[segments.length - 1]];
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
function insertDocumentVectors(docId, vectors, vecIndexes) {
|
|
381
|
+
const insertedFields = [];
|
|
382
|
+
try {
|
|
383
|
+
for (const [fieldPath, vector] of vectors) {
|
|
384
|
+
const vecIndex = vecIndexes.get(fieldPath);
|
|
385
|
+
if (!vecIndex) continue;
|
|
386
|
+
vecIndex.insert(docId, vector);
|
|
387
|
+
insertedFields.push(fieldPath);
|
|
388
|
+
}
|
|
389
|
+
} catch (err) {
|
|
390
|
+
for (const fieldPath of insertedFields) {
|
|
391
|
+
const vecIndex = vecIndexes.get(fieldPath);
|
|
392
|
+
if (vecIndex) {
|
|
393
|
+
vecIndex.remove(docId);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
throw err;
|
|
397
|
+
}
|
|
398
|
+
return insertedFields;
|
|
399
|
+
}
|
|
400
|
+
function removeDocumentVectors(docId, vecIndexes) {
|
|
401
|
+
for (const [, vecIndex] of vecIndexes) {
|
|
402
|
+
vecIndex.remove(docId);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function updateDocumentVectors(docId, vectors, vecIndexes) {
|
|
406
|
+
const updatedFields = [];
|
|
407
|
+
try {
|
|
408
|
+
for (const [fieldPath, newVec] of vectors) {
|
|
409
|
+
const vecIndex = vecIndexes.get(fieldPath);
|
|
410
|
+
if (!vecIndex) continue;
|
|
411
|
+
if (newVec === null) {
|
|
412
|
+
if (vecIndex.has(docId)) {
|
|
413
|
+
const oldVec2 = vecIndex.getVector(docId);
|
|
414
|
+
vecIndex.remove(docId);
|
|
415
|
+
updatedFields.push({ fieldPath, oldVec: oldVec2 });
|
|
416
|
+
}
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const oldVec = vecIndex.getVector(docId);
|
|
420
|
+
if (vectorsEqual(oldVec, newVec)) continue;
|
|
421
|
+
vecIndex.remove(docId);
|
|
422
|
+
try {
|
|
423
|
+
vecIndex.insert(docId, newVec);
|
|
424
|
+
updatedFields.push({ fieldPath, oldVec });
|
|
425
|
+
} catch (err) {
|
|
426
|
+
if (oldVec) {
|
|
427
|
+
vecIndex.insert(docId, oldVec);
|
|
428
|
+
}
|
|
429
|
+
throw err;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} catch (err) {
|
|
433
|
+
for (const { fieldPath, oldVec } of updatedFields) {
|
|
434
|
+
const vecIndex = vecIndexes.get(fieldPath);
|
|
435
|
+
if (!vecIndex) continue;
|
|
436
|
+
vecIndex.remove(docId);
|
|
437
|
+
if (oldVec) {
|
|
438
|
+
vecIndex.insert(docId, oldVec);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
throw err;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/engine/mutations.ts
|
|
446
|
+
async function insertDocument(ctx, indexName, document, docId, options) {
|
|
447
|
+
ctx.guardShutdown();
|
|
448
|
+
const entry = ctx.requireIndex(indexName);
|
|
449
|
+
const resolvedDocId = docId ?? ctx.idGenerator();
|
|
450
|
+
validateDocId(resolvedDocId);
|
|
451
|
+
if (ctx.bufferIfRebalancing(indexName, { action: "insert", docId: resolvedDocId, document, indexName })) {
|
|
452
|
+
return resolvedDocId;
|
|
453
|
+
}
|
|
454
|
+
await ctx.pluginRegistry.runHook("beforeInsert", { indexName, docId: resolvedDocId, document });
|
|
455
|
+
if (entry.config.required && entry.config.required.length > 0) {
|
|
456
|
+
validateRequiredFields(document, entry.config.required);
|
|
457
|
+
}
|
|
458
|
+
if (entry.embeddingAdapter && entry.config.embedding) {
|
|
459
|
+
await embedDocumentFields(
|
|
460
|
+
document,
|
|
461
|
+
entry.config.embedding,
|
|
462
|
+
entry.embeddingAdapter,
|
|
463
|
+
ctx.abortController.signal
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
const insertManager = ctx.requireManager(indexName);
|
|
467
|
+
const insertVecIndexes = insertManager.getVectorIndexes();
|
|
468
|
+
const { partitionDoc, extractedVectors } = prepareDocumentVectors(
|
|
469
|
+
document,
|
|
470
|
+
entry.vectorFieldPaths,
|
|
471
|
+
insertVecIndexes
|
|
472
|
+
);
|
|
473
|
+
if (extractedVectors.size > 0) {
|
|
474
|
+
validateVectorDimensions(extractedVectors, insertVecIndexes);
|
|
475
|
+
}
|
|
476
|
+
await ctx.executor.execute({
|
|
477
|
+
type: "insert",
|
|
478
|
+
indexName,
|
|
479
|
+
docId: resolvedDocId,
|
|
480
|
+
document: partitionDoc,
|
|
481
|
+
requestId: resolvedDocId,
|
|
482
|
+
skipClone: extractedVectors.size > 0 ? true : options?.skipClone
|
|
483
|
+
});
|
|
484
|
+
try {
|
|
485
|
+
insertDocumentVectors(resolvedDocId, extractedVectors, insertVecIndexes);
|
|
486
|
+
} catch (err) {
|
|
487
|
+
try {
|
|
488
|
+
await ctx.executor.execute({ type: "remove", indexName, docId: resolvedDocId, requestId: resolvedDocId });
|
|
489
|
+
} catch (rollbackErr) {
|
|
490
|
+
console.warn(
|
|
491
|
+
`Rollback failed for doc "${resolvedDocId}" during insert atomicity:`,
|
|
492
|
+
rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
throw err;
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
await ctx.pluginRegistry.runHook("afterInsert", { indexName, docId: resolvedDocId, document });
|
|
499
|
+
} catch (err) {
|
|
500
|
+
console.warn("afterInsert plugin hook error:", err instanceof Error ? err.message : String(err));
|
|
501
|
+
}
|
|
502
|
+
ctx.flushManager?.markDirty(indexName, 0);
|
|
503
|
+
await ctx.orchestrator.replicateToWorkers({
|
|
504
|
+
type: "insert",
|
|
505
|
+
indexName,
|
|
506
|
+
docId: resolvedDocId,
|
|
507
|
+
document,
|
|
508
|
+
requestId: `replicate-insert-${resolvedDocId}`,
|
|
509
|
+
skipClone: options?.skipClone
|
|
510
|
+
});
|
|
511
|
+
for (const fieldPath of extractedVectors.keys()) {
|
|
512
|
+
const vecIndex = insertVecIndexes.get(fieldPath);
|
|
513
|
+
if (vecIndex) {
|
|
514
|
+
vecIndex.scheduleBuild();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
await ctx.orchestrator.checkPromotion();
|
|
518
|
+
return resolvedDocId;
|
|
519
|
+
}
|
|
520
|
+
async function insertDocumentBatch(ctx, indexName, documents, options) {
|
|
521
|
+
ctx.guardShutdown();
|
|
522
|
+
const entry = ctx.requireIndex(indexName);
|
|
523
|
+
const succeeded = [];
|
|
524
|
+
const succeededDocs = [];
|
|
525
|
+
const failed = [];
|
|
526
|
+
const hasBeforeHook = ctx.pluginRegistry.hasHooks("beforeInsert");
|
|
527
|
+
const hasAfterHook = ctx.pluginRegistry.hasHooks("afterInsert");
|
|
528
|
+
const hasRequired = entry.config.required && entry.config.required.length > 0;
|
|
529
|
+
const hasEmbedding = entry.embeddingAdapter && entry.config.embedding;
|
|
530
|
+
const batchManager = ctx.requireManager(indexName);
|
|
531
|
+
const batchVecIndexes = batchManager.getVectorIndexes();
|
|
532
|
+
const batchVectorFieldPaths = batchVecIndexes.size > 0 ? entry.vectorFieldPaths : /* @__PURE__ */ new Set();
|
|
533
|
+
const touchedVectorFields = /* @__PURE__ */ new Set();
|
|
534
|
+
for (let chunkStart = 0; chunkStart < documents.length; chunkStart += BATCH_CHUNK_SIZE) {
|
|
535
|
+
if (ctx.abortController.signal.aborted) break;
|
|
536
|
+
const chunkEnd = Math.min(chunkStart + BATCH_CHUNK_SIZE, documents.length);
|
|
537
|
+
const chunkFailedIndexes = /* @__PURE__ */ new Set();
|
|
538
|
+
if (hasRequired) {
|
|
539
|
+
for (let i = chunkStart; i < chunkEnd; i++) {
|
|
540
|
+
try {
|
|
541
|
+
validateRequiredFields(documents[i], entry.config.required);
|
|
542
|
+
} catch (err) {
|
|
543
|
+
chunkFailedIndexes.add(i);
|
|
544
|
+
failed.push({
|
|
545
|
+
docId: "",
|
|
546
|
+
error: err instanceof NarsilError ? err : new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, String(err))
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (hasEmbedding) {
|
|
552
|
+
const embeddableSlice = [];
|
|
553
|
+
const embeddableOriginalIndexes = [];
|
|
554
|
+
for (let i = chunkStart; i < chunkEnd; i++) {
|
|
555
|
+
if (chunkFailedIndexes.has(i)) continue;
|
|
556
|
+
embeddableSlice.push(documents[i]);
|
|
557
|
+
embeddableOriginalIndexes.push(i);
|
|
558
|
+
}
|
|
559
|
+
if (embeddableSlice.length > 0) {
|
|
560
|
+
try {
|
|
561
|
+
const embedResult = await embedBatchDocumentFields(
|
|
562
|
+
embeddableSlice,
|
|
563
|
+
entry.config.embedding,
|
|
564
|
+
entry.embeddingAdapter,
|
|
565
|
+
ctx.abortController.signal
|
|
566
|
+
);
|
|
567
|
+
for (const [sliceIndex, error] of embedResult.failed) {
|
|
568
|
+
const originalIdx = embeddableOriginalIndexes[sliceIndex];
|
|
569
|
+
chunkFailedIndexes.add(originalIdx);
|
|
570
|
+
failed.push({ docId: "", error });
|
|
571
|
+
}
|
|
572
|
+
} catch (err) {
|
|
573
|
+
const embeddingError = err instanceof NarsilError ? err : new NarsilError(ErrorCodes.EMBEDDING_FAILED, String(err));
|
|
574
|
+
for (const originalIdx of embeddableOriginalIndexes) {
|
|
575
|
+
chunkFailedIndexes.add(originalIdx);
|
|
576
|
+
failed.push({ docId: "", error: embeddingError });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
for (let i = chunkStart; i < chunkEnd; i++) {
|
|
582
|
+
if (ctx.abortController.signal.aborted) break;
|
|
583
|
+
if (chunkFailedIndexes.has(i)) continue;
|
|
584
|
+
const batchDocId = ctx.idGenerator();
|
|
585
|
+
try {
|
|
586
|
+
validateDocId(batchDocId);
|
|
587
|
+
if (hasBeforeHook) {
|
|
588
|
+
await ctx.pluginRegistry.runHook("beforeInsert", { indexName, docId: batchDocId, document: documents[i] });
|
|
589
|
+
}
|
|
590
|
+
const { partitionDoc, extractedVectors } = prepareDocumentVectors(
|
|
591
|
+
documents[i],
|
|
592
|
+
batchVectorFieldPaths,
|
|
593
|
+
batchVecIndexes
|
|
594
|
+
);
|
|
595
|
+
if (extractedVectors.size > 0) {
|
|
596
|
+
validateVectorDimensions(extractedVectors, batchVecIndexes);
|
|
597
|
+
}
|
|
598
|
+
const result = ctx.executor.execute({
|
|
599
|
+
type: "insert",
|
|
600
|
+
indexName,
|
|
601
|
+
docId: batchDocId,
|
|
602
|
+
document: partitionDoc,
|
|
603
|
+
requestId: batchDocId,
|
|
604
|
+
skipClone: extractedVectors.size > 0 ? true : options?.skipClone
|
|
605
|
+
});
|
|
606
|
+
if (result && typeof result.then === "function") {
|
|
607
|
+
await result;
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
insertDocumentVectors(batchDocId, extractedVectors, batchVecIndexes);
|
|
611
|
+
} catch (vecErr) {
|
|
612
|
+
try {
|
|
613
|
+
await ctx.executor.execute({ type: "remove", indexName, docId: batchDocId, requestId: batchDocId });
|
|
614
|
+
} catch (rollbackErr) {
|
|
615
|
+
console.warn(
|
|
616
|
+
`Rollback failed for doc "${batchDocId}" during batch insert atomicity:`,
|
|
617
|
+
rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
throw vecErr;
|
|
621
|
+
}
|
|
622
|
+
for (const fieldPath of extractedVectors.keys()) {
|
|
623
|
+
touchedVectorFields.add(fieldPath);
|
|
624
|
+
}
|
|
625
|
+
if (hasAfterHook) {
|
|
626
|
+
try {
|
|
627
|
+
await ctx.pluginRegistry.runHook("afterInsert", { indexName, docId: batchDocId, document: documents[i] });
|
|
628
|
+
} catch (err) {
|
|
629
|
+
console.warn("afterInsert plugin hook error:", err instanceof Error ? err.message : String(err));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
succeeded.push(batchDocId);
|
|
633
|
+
succeededDocs.push(documents[i]);
|
|
634
|
+
} catch (err) {
|
|
635
|
+
failed.push({
|
|
636
|
+
docId: batchDocId,
|
|
637
|
+
error: err instanceof NarsilError ? err : new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, String(err))
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (chunkEnd < documents.length) {
|
|
642
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
ctx.flushManager?.markDirty(indexName, 0);
|
|
646
|
+
for (let i = 0; i < succeeded.length; i++) {
|
|
647
|
+
await ctx.orchestrator.replicateToWorkers({
|
|
648
|
+
type: "insert",
|
|
649
|
+
indexName,
|
|
650
|
+
docId: succeeded[i],
|
|
651
|
+
document: succeededDocs[i],
|
|
652
|
+
requestId: `replicate-insert-${succeeded[i]}`,
|
|
653
|
+
skipClone: options?.skipClone
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
for (const fieldPath of touchedVectorFields) {
|
|
657
|
+
const vecIndex = batchVecIndexes.get(fieldPath);
|
|
658
|
+
if (vecIndex) {
|
|
659
|
+
vecIndex.scheduleBuild();
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
await ctx.orchestrator.checkPromotion();
|
|
663
|
+
return { succeeded, failed };
|
|
664
|
+
}
|
|
665
|
+
async function removeDocument(ctx, indexName, docId) {
|
|
666
|
+
ctx.guardShutdown();
|
|
667
|
+
ctx.requireIndex(indexName);
|
|
668
|
+
validateDocId(docId);
|
|
669
|
+
if (ctx.bufferIfRebalancing(indexName, { action: "remove", docId, indexName })) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
await ctx.pluginRegistry.runHook("beforeRemove", { indexName, docId });
|
|
673
|
+
await ctx.executor.execute({ type: "remove", indexName, docId, requestId: docId });
|
|
674
|
+
const removeManager = ctx.requireManager(indexName);
|
|
675
|
+
const removeVecIndexes = removeManager.getVectorIndexes();
|
|
676
|
+
removeDocumentVectors(docId, removeVecIndexes);
|
|
677
|
+
try {
|
|
678
|
+
await ctx.pluginRegistry.runHook("afterRemove", { indexName, docId });
|
|
679
|
+
} catch (err) {
|
|
680
|
+
console.warn("afterRemove plugin hook error:", err instanceof Error ? err.message : String(err));
|
|
681
|
+
}
|
|
682
|
+
ctx.flushManager?.markDirty(indexName, 0);
|
|
683
|
+
await ctx.orchestrator.replicateToWorkers({
|
|
684
|
+
type: "remove",
|
|
685
|
+
indexName,
|
|
686
|
+
docId,
|
|
687
|
+
requestId: `replicate-remove-${docId}`
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
async function removeDocumentBatch(ctx, indexName, docIds) {
|
|
691
|
+
ctx.guardShutdown();
|
|
692
|
+
ctx.requireIndex(indexName);
|
|
693
|
+
const succeeded = [];
|
|
694
|
+
const failed = [];
|
|
695
|
+
const manager = ctx.requireManager(indexName);
|
|
696
|
+
manager.beginBatchRemove();
|
|
697
|
+
try {
|
|
698
|
+
for (let chunkStart = 0; chunkStart < docIds.length; chunkStart += BATCH_CHUNK_SIZE) {
|
|
699
|
+
const chunkEnd = Math.min(chunkStart + BATCH_CHUNK_SIZE, docIds.length);
|
|
700
|
+
for (let i = chunkStart; i < chunkEnd; i++) {
|
|
701
|
+
try {
|
|
702
|
+
await removeDocument(ctx, indexName, docIds[i]);
|
|
703
|
+
succeeded.push(docIds[i]);
|
|
704
|
+
} catch (err) {
|
|
705
|
+
failed.push({
|
|
706
|
+
docId: docIds[i],
|
|
707
|
+
error: err instanceof NarsilError ? err : new NarsilError(ErrorCodes.DOC_NOT_FOUND, String(err))
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (chunkEnd < docIds.length) {
|
|
712
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
} finally {
|
|
716
|
+
manager.endBatchRemove();
|
|
717
|
+
}
|
|
718
|
+
return { succeeded, failed };
|
|
719
|
+
}
|
|
720
|
+
async function updateDocument(ctx, indexName, docId, document) {
|
|
721
|
+
ctx.guardShutdown();
|
|
722
|
+
const entry = ctx.requireIndex(indexName);
|
|
723
|
+
validateDocId(docId);
|
|
724
|
+
if (ctx.bufferIfRebalancing(indexName, { action: "update", docId, document, indexName })) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (entry.embeddingAdapter && entry.config.embedding) {
|
|
728
|
+
await embedDocumentFields(
|
|
729
|
+
document,
|
|
730
|
+
entry.config.embedding,
|
|
731
|
+
entry.embeddingAdapter,
|
|
732
|
+
ctx.abortController.signal
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
const updateManager = ctx.requireManager(indexName);
|
|
736
|
+
const oldDocument = updateManager.get(docId);
|
|
737
|
+
const oldPartitionDoc = updateManager.getRef(docId);
|
|
738
|
+
const rollbackDoc = oldPartitionDoc ? structuredClone(oldPartitionDoc) : void 0;
|
|
739
|
+
await ctx.pluginRegistry.runHook("beforeUpdate", {
|
|
740
|
+
indexName,
|
|
741
|
+
docId,
|
|
742
|
+
oldDocument: oldDocument ?? {},
|
|
743
|
+
newDocument: document
|
|
744
|
+
});
|
|
745
|
+
const updateVecIndexes = updateManager.getVectorIndexes();
|
|
746
|
+
const updateExtractedVectors = /* @__PURE__ */ new Map();
|
|
747
|
+
if (updateVecIndexes.size > 0) {
|
|
748
|
+
const dimensionCheckVectors = /* @__PURE__ */ new Map();
|
|
749
|
+
for (const fieldPath of entry.vectorFieldPaths) {
|
|
750
|
+
const newVec = extractVectorFromDocForUpdate(document, fieldPath);
|
|
751
|
+
updateExtractedVectors.set(fieldPath, newVec);
|
|
752
|
+
if (newVec) {
|
|
753
|
+
dimensionCheckVectors.set(fieldPath, newVec);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (dimensionCheckVectors.size > 0) {
|
|
757
|
+
validateVectorDimensions(dimensionCheckVectors, updateVecIndexes);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const { partitionDoc } = prepareUpdatePartitionDoc(document, updateExtractedVectors);
|
|
761
|
+
await ctx.executor.execute({
|
|
762
|
+
type: "update",
|
|
763
|
+
indexName,
|
|
764
|
+
docId,
|
|
765
|
+
document: partitionDoc,
|
|
766
|
+
requestId: docId
|
|
767
|
+
});
|
|
768
|
+
try {
|
|
769
|
+
updateDocumentVectors(docId, updateExtractedVectors, updateVecIndexes);
|
|
770
|
+
} catch (err) {
|
|
771
|
+
if (rollbackDoc) {
|
|
772
|
+
try {
|
|
773
|
+
await ctx.executor.execute({
|
|
774
|
+
type: "update",
|
|
775
|
+
indexName,
|
|
776
|
+
docId,
|
|
777
|
+
document: rollbackDoc,
|
|
778
|
+
requestId: docId
|
|
779
|
+
});
|
|
780
|
+
} catch (rollbackErr) {
|
|
781
|
+
console.warn(
|
|
782
|
+
`Rollback failed for doc "${docId}" during update atomicity:`,
|
|
783
|
+
rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
throw err;
|
|
788
|
+
}
|
|
789
|
+
try {
|
|
790
|
+
await ctx.pluginRegistry.runHook("afterUpdate", {
|
|
791
|
+
indexName,
|
|
792
|
+
docId,
|
|
793
|
+
oldDocument: oldDocument ?? {},
|
|
794
|
+
newDocument: document
|
|
795
|
+
});
|
|
796
|
+
} catch (err) {
|
|
797
|
+
console.warn("afterUpdate plugin hook error:", err instanceof Error ? err.message : String(err));
|
|
798
|
+
}
|
|
799
|
+
ctx.flushManager?.markDirty(indexName, 0);
|
|
800
|
+
await ctx.orchestrator.replicateToWorkers({
|
|
801
|
+
type: "update",
|
|
802
|
+
indexName,
|
|
803
|
+
docId,
|
|
804
|
+
document,
|
|
805
|
+
requestId: `replicate-update-${docId}`
|
|
806
|
+
});
|
|
807
|
+
for (const [fieldPath, vec] of updateExtractedVectors) {
|
|
808
|
+
if (vec === null) continue;
|
|
809
|
+
const vecIndex = updateVecIndexes.get(fieldPath);
|
|
810
|
+
if (vecIndex) {
|
|
811
|
+
vecIndex.scheduleBuild();
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async function updateDocumentBatch(ctx, indexName, updates) {
|
|
816
|
+
ctx.guardShutdown();
|
|
817
|
+
const entry = ctx.requireIndex(indexName);
|
|
818
|
+
const succeeded = [];
|
|
819
|
+
const failed = [];
|
|
820
|
+
const updateBatchManager = ctx.requireManager(indexName);
|
|
821
|
+
const updateBatchVecIndexes = updateBatchManager.getVectorIndexes();
|
|
822
|
+
const touchedVectorFields = /* @__PURE__ */ new Set();
|
|
823
|
+
for (let chunkStart = 0; chunkStart < updates.length; chunkStart += BATCH_CHUNK_SIZE) {
|
|
824
|
+
const chunkEnd = Math.min(chunkStart + BATCH_CHUNK_SIZE, updates.length);
|
|
825
|
+
for (let i = chunkStart; i < chunkEnd; i++) {
|
|
826
|
+
try {
|
|
827
|
+
await updateDocument(ctx, indexName, updates[i].docId, updates[i].document);
|
|
828
|
+
succeeded.push(updates[i].docId);
|
|
829
|
+
if (updateBatchVecIndexes.size > 0) {
|
|
830
|
+
for (const fieldPath of entry.vectorFieldPaths) {
|
|
831
|
+
const vec = extractVectorFromDocForUpdate(updates[i].document, fieldPath);
|
|
832
|
+
if (vec !== null) {
|
|
833
|
+
touchedVectorFields.add(fieldPath);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
} catch (err) {
|
|
838
|
+
failed.push({
|
|
839
|
+
docId: updates[i].docId,
|
|
840
|
+
error: err instanceof NarsilError ? err : new NarsilError(ErrorCodes.DOC_NOT_FOUND, String(err))
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (chunkEnd < updates.length) {
|
|
845
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
for (const fieldPath of touchedVectorFields) {
|
|
849
|
+
const vecIndex = updateBatchVecIndexes.get(fieldPath);
|
|
850
|
+
if (vecIndex) {
|
|
851
|
+
vecIndex.scheduleBuild();
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return { succeeded, failed };
|
|
855
|
+
}
|
|
856
|
+
function extractVectorFromDocForUpdate(document, fieldPath) {
|
|
857
|
+
return extractVectorFromDoc(document, fieldPath);
|
|
858
|
+
}
|
|
859
|
+
function prepareUpdatePartitionDoc(document, extractedVectors) {
|
|
860
|
+
if (extractedVectors.size === 0) {
|
|
861
|
+
return { partitionDoc: document };
|
|
862
|
+
}
|
|
863
|
+
const partitionDoc = structuredClone(document);
|
|
864
|
+
for (const fieldPath of extractedVectors.keys()) {
|
|
865
|
+
deleteNestedValue(partitionDoc, fieldPath);
|
|
866
|
+
}
|
|
867
|
+
return { partitionDoc };
|
|
868
|
+
}
|
|
869
|
+
|
|
10
870
|
// src/workers/worker-executor.ts
|
|
11
871
|
var DEFAULT_BACKPRESSURE_LIMIT = 100;
|
|
12
872
|
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
@@ -544,8 +1404,89 @@ function highlightField(text, queryTokens, language, options) {
|
|
|
544
1404
|
return { snippet: result, positions: mergedPositions };
|
|
545
1405
|
}
|
|
546
1406
|
|
|
1407
|
+
// src/search/fusion.ts
|
|
1408
|
+
function reciprocalRankFusion(lists, options) {
|
|
1409
|
+
const k = options.k > 0 ? options.k : 60;
|
|
1410
|
+
const scores = /* @__PURE__ */ new Map();
|
|
1411
|
+
const docData = /* @__PURE__ */ new Map();
|
|
1412
|
+
for (const list of lists) {
|
|
1413
|
+
for (let rank = 0; rank < list.length; rank++) {
|
|
1414
|
+
const doc = list[rank];
|
|
1415
|
+
const rrfContribution = 1 / (k + rank + 1);
|
|
1416
|
+
scores.set(doc.docId, (scores.get(doc.docId) ?? 0) + rrfContribution);
|
|
1417
|
+
if (!docData.has(doc.docId)) {
|
|
1418
|
+
docData.set(doc.docId, doc);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
const result = [];
|
|
1423
|
+
for (const [docId, score] of scores) {
|
|
1424
|
+
const original = docData.get(docId);
|
|
1425
|
+
if (!original) continue;
|
|
1426
|
+
result.push({
|
|
1427
|
+
docId,
|
|
1428
|
+
score,
|
|
1429
|
+
termFrequencies: original.termFrequencies,
|
|
1430
|
+
fieldLengths: original.fieldLengths,
|
|
1431
|
+
idf: original.idf
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
result.sort((a, b) => b.score - a.score || a.docId.localeCompare(b.docId));
|
|
1435
|
+
return result;
|
|
1436
|
+
}
|
|
1437
|
+
function linearCombination(textResults, vectorResults, options) {
|
|
1438
|
+
const { alpha } = options;
|
|
1439
|
+
const normalizedText = minMaxNormalize(textResults);
|
|
1440
|
+
const normalizedVector = minMaxNormalize(vectorResults);
|
|
1441
|
+
const textScoreMap = /* @__PURE__ */ new Map();
|
|
1442
|
+
for (const doc of normalizedText) {
|
|
1443
|
+
textScoreMap.set(doc.docId, doc.score);
|
|
1444
|
+
}
|
|
1445
|
+
const textDocMap = /* @__PURE__ */ new Map();
|
|
1446
|
+
for (const doc of textResults) {
|
|
1447
|
+
textDocMap.set(doc.docId, doc);
|
|
1448
|
+
}
|
|
1449
|
+
const vectorScoreMap = /* @__PURE__ */ new Map();
|
|
1450
|
+
for (const doc of normalizedVector) {
|
|
1451
|
+
vectorScoreMap.set(doc.docId, doc.score);
|
|
1452
|
+
}
|
|
1453
|
+
const allDocIds = /* @__PURE__ */ new Set();
|
|
1454
|
+
for (const doc of textResults) allDocIds.add(doc.docId);
|
|
1455
|
+
for (const doc of vectorResults) allDocIds.add(doc.docId);
|
|
1456
|
+
const result = [];
|
|
1457
|
+
for (const docId of allDocIds) {
|
|
1458
|
+
const tScore = textScoreMap.get(docId) ?? 0;
|
|
1459
|
+
const vScore = vectorScoreMap.get(docId) ?? 0;
|
|
1460
|
+
const combined = alpha * vScore + (1 - alpha) * tScore;
|
|
1461
|
+
const original = textDocMap.get(docId);
|
|
1462
|
+
result.push({
|
|
1463
|
+
docId,
|
|
1464
|
+
score: combined,
|
|
1465
|
+
termFrequencies: original?.termFrequencies ?? {},
|
|
1466
|
+
fieldLengths: original?.fieldLengths ?? {},
|
|
1467
|
+
idf: original?.idf ?? {}
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
result.sort((a, b) => b.score - a.score || a.docId.localeCompare(b.docId));
|
|
1471
|
+
return result;
|
|
1472
|
+
}
|
|
1473
|
+
function minMaxNormalize(docs) {
|
|
1474
|
+
if (docs.length === 0) return [];
|
|
1475
|
+
let min = docs[0].score;
|
|
1476
|
+
let max = docs[0].score;
|
|
1477
|
+
for (let i = 1; i < docs.length; i++) {
|
|
1478
|
+
if (docs[i].score < min) min = docs[i].score;
|
|
1479
|
+
if (docs[i].score > max) max = docs[i].score;
|
|
1480
|
+
}
|
|
1481
|
+
const range = max - min;
|
|
1482
|
+
if (range === 0) {
|
|
1483
|
+
return docs.map((d) => ({ docId: d.docId, score: 1 }));
|
|
1484
|
+
}
|
|
1485
|
+
return docs.map((d) => ({ docId: d.docId, score: (d.score - min) / range }));
|
|
1486
|
+
}
|
|
1487
|
+
|
|
547
1488
|
// src/search/grouping.ts
|
|
548
|
-
function
|
|
1489
|
+
function getNestedValue2(obj, path) {
|
|
549
1490
|
const segments = path.split(".");
|
|
550
1491
|
let current = obj;
|
|
551
1492
|
for (const segment of segments) {
|
|
@@ -569,7 +1510,7 @@ function applyGrouping(hits, group, getDocument) {
|
|
|
569
1510
|
const fieldValues = [];
|
|
570
1511
|
const valuesRecord = {};
|
|
571
1512
|
for (const field of group.fields) {
|
|
572
|
-
const value = doc ?
|
|
1513
|
+
const value = doc ? getNestedValue2(doc, field) : void 0;
|
|
573
1514
|
fieldValues.push(value);
|
|
574
1515
|
valuesRecord[field] = value;
|
|
575
1516
|
}
|
|
@@ -735,7 +1676,7 @@ function applyPinning(hits, pinned, resolveDoc) {
|
|
|
735
1676
|
}
|
|
736
1677
|
|
|
737
1678
|
// src/search/sorting.ts
|
|
738
|
-
function
|
|
1679
|
+
function getNestedValue3(obj, path) {
|
|
739
1680
|
const segments = path.split(".");
|
|
740
1681
|
let current = obj;
|
|
741
1682
|
for (const segment of segments) {
|
|
@@ -763,7 +1704,7 @@ function applySorting(hits, sort, getDocument) {
|
|
|
763
1704
|
);
|
|
764
1705
|
continue;
|
|
765
1706
|
}
|
|
766
|
-
const values = sortFields.map(([field]) =>
|
|
1707
|
+
const values = sortFields.map(([field]) => getNestedValue3(doc, field));
|
|
767
1708
|
valueCache.set(hit.id, values);
|
|
768
1709
|
}
|
|
769
1710
|
const sorted = hits.slice();
|
|
@@ -801,98 +1742,172 @@ function applySorting(hits, sort, getDocument) {
|
|
|
801
1742
|
return sorted;
|
|
802
1743
|
}
|
|
803
1744
|
|
|
804
|
-
// src/engine/validation.ts
|
|
805
|
-
var INDEX_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
806
|
-
var MAX_INDEX_NAME_LENGTH = 256;
|
|
807
|
-
var MAX_DOC_ID_LENGTH = 512;
|
|
808
|
-
var BATCH_CHUNK_SIZE = 1e3;
|
|
809
|
-
var MAX_LIMIT = 1e4;
|
|
810
|
-
var MAX_OFFSET = 1e5;
|
|
811
|
-
var DEFAULT_LIMIT = 10;
|
|
812
|
-
var DEFAULT_OFFSET = 0;
|
|
813
|
-
function now() {
|
|
814
|
-
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
815
|
-
return performance.now();
|
|
816
|
-
}
|
|
817
|
-
return Date.now();
|
|
818
|
-
}
|
|
819
|
-
function validateIndexName(name) {
|
|
820
|
-
if (!name || name.length === 0) {
|
|
821
|
-
throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, "Index name must not be empty", { indexName: name });
|
|
822
|
-
}
|
|
823
|
-
if (name.length > MAX_INDEX_NAME_LENGTH) {
|
|
824
|
-
throw new NarsilError(
|
|
825
|
-
ErrorCodes.INDEX_NOT_FOUND,
|
|
826
|
-
`Index name must not exceed ${MAX_INDEX_NAME_LENGTH} characters`,
|
|
827
|
-
{
|
|
828
|
-
indexName: name,
|
|
829
|
-
length: name.length
|
|
830
|
-
}
|
|
831
|
-
);
|
|
832
|
-
}
|
|
833
|
-
if (!INDEX_NAME_PATTERN.test(name)) {
|
|
834
|
-
throw new NarsilError(
|
|
835
|
-
ErrorCodes.INDEX_NOT_FOUND,
|
|
836
|
-
`Index name "${name}" contains invalid characters; use alphanumeric, dots, hyphens, and underscores only`,
|
|
837
|
-
{ indexName: name }
|
|
838
|
-
);
|
|
839
|
-
}
|
|
840
|
-
if (name.includes("..")) {
|
|
841
|
-
throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index name "${name}" must not contain ".."`, { indexName: name });
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
function validateDocId(docId) {
|
|
845
|
-
if (!docId || docId.length === 0) {
|
|
846
|
-
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not be empty", { docId });
|
|
847
|
-
}
|
|
848
|
-
if (docId.length > MAX_DOC_ID_LENGTH) {
|
|
849
|
-
throw new NarsilError(
|
|
850
|
-
ErrorCodes.DOC_VALIDATION_FAILED,
|
|
851
|
-
`Document ID must not exceed ${MAX_DOC_ID_LENGTH} characters`,
|
|
852
|
-
{
|
|
853
|
-
docId,
|
|
854
|
-
length: docId.length
|
|
855
|
-
}
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
if (docId.includes("\0")) {
|
|
859
|
-
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not contain null bytes", { docId });
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
function clampLimit(limit) {
|
|
863
|
-
if (limit === void 0) return DEFAULT_LIMIT;
|
|
864
|
-
return Math.max(0, Math.min(limit, MAX_LIMIT));
|
|
865
|
-
}
|
|
866
|
-
function clampOffset(offset) {
|
|
867
|
-
if (offset === void 0) return DEFAULT_OFFSET;
|
|
868
|
-
return Math.max(0, Math.min(offset, MAX_OFFSET));
|
|
869
|
-
}
|
|
870
|
-
|
|
871
1745
|
// src/engine/query.ts
|
|
872
|
-
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1746
|
+
function collectFilterDocIds(manager, params, schema) {
|
|
1747
|
+
const filterDocIds = /* @__PURE__ */ new Set();
|
|
1748
|
+
if (!params.filters) return filterDocIds;
|
|
1749
|
+
for (const partition of manager.getAllPartitions()) {
|
|
1750
|
+
const partitionFiltered = partition.applyFilters(params.filters, schema);
|
|
1751
|
+
for (const docId of partitionFiltered) {
|
|
1752
|
+
filterDocIds.add(docId);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
return filterDocIds;
|
|
1756
|
+
}
|
|
1757
|
+
function vectorResultsToScored(results) {
|
|
1758
|
+
return results.map((r) => ({
|
|
1759
|
+
docId: r.docId,
|
|
1760
|
+
score: r.score,
|
|
1761
|
+
termFrequencies: {},
|
|
1762
|
+
fieldLengths: {},
|
|
1763
|
+
idf: {}
|
|
1764
|
+
}));
|
|
1765
|
+
}
|
|
1766
|
+
function resolveVectorIndex(manager, fieldName) {
|
|
1767
|
+
return manager.getVectorIndexes().get(fieldName);
|
|
1768
|
+
}
|
|
1769
|
+
function executeVectorSearch(params, manager, config, limit, offset) {
|
|
1770
|
+
const vectorConfig = params.vector;
|
|
1771
|
+
if (!vectorConfig || !vectorConfig.value) {
|
|
1772
|
+
return { scored: [], totalMatched: 0 };
|
|
1773
|
+
}
|
|
1774
|
+
const vecIndex = resolveVectorIndex(manager, vectorConfig.field);
|
|
1775
|
+
if (!vecIndex) {
|
|
1776
|
+
return { scored: [], totalMatched: 0 };
|
|
1777
|
+
}
|
|
1778
|
+
let filterDocIds;
|
|
1779
|
+
if (params.filters) {
|
|
1780
|
+
filterDocIds = collectFilterDocIds(manager, params, config.schema);
|
|
1781
|
+
if (filterDocIds.size === 0) {
|
|
1782
|
+
return { scored: [], totalMatched: 0 };
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
const queryVec = new Float32Array(vectorConfig.value);
|
|
1786
|
+
const k = limit + offset + 1;
|
|
1787
|
+
const results = vecIndex.search(queryVec, k, {
|
|
1788
|
+
metric: vectorConfig.metric ?? "cosine",
|
|
1789
|
+
minSimilarity: vectorConfig.similarity ?? 0,
|
|
1790
|
+
filterDocIds,
|
|
1791
|
+
efSearch: vectorConfig.efSearch
|
|
1792
|
+
});
|
|
1793
|
+
const scored = vectorResultsToScored(results);
|
|
1794
|
+
return { scored, totalMatched: scored.length };
|
|
1795
|
+
}
|
|
1796
|
+
async function executeHybridSearch(params, manager, language, config, workerSearch, indexName, limit, offset) {
|
|
1797
|
+
const { vector: _vector, mode: _mode, hybrid: _hybrid, ...textOnlyParams } = params;
|
|
1798
|
+
let filterDocIds;
|
|
1799
|
+
if (params.filters) {
|
|
1800
|
+
filterDocIds = collectFilterDocIds(manager, params, config.schema);
|
|
1801
|
+
if (filterDocIds.size === 0) {
|
|
1802
|
+
return { scored: [], totalMatched: 0 };
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
let textFanOutResult;
|
|
1806
|
+
const textWorkerResult = workerSearch ? await workerSearch(indexName, textOnlyParams) : null;
|
|
1807
|
+
if (textWorkerResult) {
|
|
1808
|
+
textFanOutResult = textWorkerResult;
|
|
881
1809
|
} else {
|
|
882
1810
|
const searchOptions = {
|
|
883
1811
|
bm25Params: config.bm25,
|
|
884
1812
|
stopWords: config.stopWords,
|
|
885
1813
|
customTokenizer: config.tokenizer
|
|
886
1814
|
};
|
|
887
|
-
|
|
1815
|
+
textFanOutResult = await fanOutQuery(
|
|
888
1816
|
manager,
|
|
889
|
-
|
|
1817
|
+
textOnlyParams,
|
|
890
1818
|
language,
|
|
891
1819
|
config.schema,
|
|
892
1820
|
{ scoringMode: params.scoring ?? config.defaultScoring ?? "local" },
|
|
893
1821
|
searchOptions
|
|
894
1822
|
);
|
|
895
1823
|
}
|
|
1824
|
+
const vectorConfig = params.vector;
|
|
1825
|
+
if (!vectorConfig || !vectorConfig.value) {
|
|
1826
|
+
return {
|
|
1827
|
+
scored: textFanOutResult.scored,
|
|
1828
|
+
totalMatched: textFanOutResult.totalMatched,
|
|
1829
|
+
facets: textFanOutResult.facets
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
let vectorScored = [];
|
|
1833
|
+
const vecIndex = resolveVectorIndex(manager, vectorConfig.field);
|
|
1834
|
+
if (vecIndex) {
|
|
1835
|
+
const queryVec = new Float32Array(vectorConfig.value);
|
|
1836
|
+
const vectorK = limit + offset + 1;
|
|
1837
|
+
const vectorResults = vecIndex.search(queryVec, vectorK, {
|
|
1838
|
+
metric: vectorConfig.metric ?? "cosine",
|
|
1839
|
+
minSimilarity: vectorConfig.similarity ?? 0,
|
|
1840
|
+
filterDocIds,
|
|
1841
|
+
efSearch: vectorConfig.efSearch
|
|
1842
|
+
});
|
|
1843
|
+
vectorScored = vectorResultsToScored(vectorResults);
|
|
1844
|
+
}
|
|
1845
|
+
const hybridConfig = params.hybrid ?? {};
|
|
1846
|
+
const strategy = hybridConfig.strategy ?? "rrf";
|
|
1847
|
+
let fusedScored;
|
|
1848
|
+
if (strategy === "rrf") {
|
|
1849
|
+
const rrfK = hybridConfig.k !== void 0 && hybridConfig.k > 0 ? hybridConfig.k : 60;
|
|
1850
|
+
fusedScored = reciprocalRankFusion([textFanOutResult.scored, vectorScored], { k: rrfK });
|
|
1851
|
+
} else {
|
|
1852
|
+
const alpha = clampAlpha(hybridConfig.alpha);
|
|
1853
|
+
fusedScored = linearCombination(textFanOutResult.scored, vectorScored, { alpha });
|
|
1854
|
+
}
|
|
1855
|
+
if (params.minScore !== void 0 && params.minScore > 0) {
|
|
1856
|
+
const threshold = params.minScore;
|
|
1857
|
+
fusedScored = fusedScored.filter((doc) => doc.score >= threshold);
|
|
1858
|
+
}
|
|
1859
|
+
if (filterDocIds) {
|
|
1860
|
+
fusedScored = fusedScored.filter((doc) => filterDocIds.has(doc.docId));
|
|
1861
|
+
}
|
|
1862
|
+
return {
|
|
1863
|
+
scored: fusedScored,
|
|
1864
|
+
totalMatched: fusedScored.length,
|
|
1865
|
+
facets: textFanOutResult.facets
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
function clampAlpha(alpha) {
|
|
1869
|
+
if (alpha === void 0) return 0.5;
|
|
1870
|
+
if (!Number.isFinite(alpha)) return 0.5;
|
|
1871
|
+
if (alpha < 0) return 0;
|
|
1872
|
+
if (alpha > 1) return 1;
|
|
1873
|
+
return alpha;
|
|
1874
|
+
}
|
|
1875
|
+
async function executeQuery(params, context) {
|
|
1876
|
+
const { manager, language, config, workerSearch, indexName } = context;
|
|
1877
|
+
const startTime = now();
|
|
1878
|
+
const limit = clampLimit(params.limit);
|
|
1879
|
+
const offset = clampOffset(params.offset);
|
|
1880
|
+
const hasTerm = params.term !== void 0 && params.term.trim().length > 0;
|
|
1881
|
+
const hasVector = params.vector !== void 0 && params.vector.value !== void 0;
|
|
1882
|
+
const isHybridMode = params.mode === "hybrid" || hasTerm && hasVector;
|
|
1883
|
+
const isVectorOnly = (params.mode === "vector" || hasVector && !hasTerm) && !isHybridMode;
|
|
1884
|
+
const requestedVectorField = params.vector?.field;
|
|
1885
|
+
const hasGlobalVectorIndex = requestedVectorField !== void 0 && manager.getVectorIndexes().has(requestedVectorField);
|
|
1886
|
+
let fanOutResult;
|
|
1887
|
+
if (isVectorOnly && hasGlobalVectorIndex) {
|
|
1888
|
+
fanOutResult = executeVectorSearch(params, manager, config, limit, offset);
|
|
1889
|
+
} else if (isHybridMode && hasGlobalVectorIndex) {
|
|
1890
|
+
fanOutResult = await executeHybridSearch(params, manager, language, config, workerSearch, indexName, limit, offset);
|
|
1891
|
+
} else {
|
|
1892
|
+
const workerResult = workerSearch ? await workerSearch(indexName, params) : null;
|
|
1893
|
+
if (workerResult) {
|
|
1894
|
+
fanOutResult = workerResult;
|
|
1895
|
+
} else {
|
|
1896
|
+
const searchOptions = {
|
|
1897
|
+
bm25Params: config.bm25,
|
|
1898
|
+
stopWords: config.stopWords,
|
|
1899
|
+
customTokenizer: config.tokenizer
|
|
1900
|
+
};
|
|
1901
|
+
fanOutResult = await fanOutQuery(
|
|
1902
|
+
manager,
|
|
1903
|
+
params,
|
|
1904
|
+
language,
|
|
1905
|
+
config.schema,
|
|
1906
|
+
{ scoringMode: params.scoring ?? config.defaultScoring ?? "local" },
|
|
1907
|
+
searchOptions
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
896
1911
|
const needsFullHits = params.sort !== void 0 || params.group !== void 0 || params.pinned !== void 0 || params.searchAfter !== void 0;
|
|
897
1912
|
let hits;
|
|
898
1913
|
if (needsFullHits) {
|
|
@@ -915,94 +1930,546 @@ async function executeQuery(params, context) {
|
|
|
915
1930
|
};
|
|
916
1931
|
}
|
|
917
1932
|
}
|
|
918
|
-
if (params.sort) {
|
|
919
|
-
hits = applySorting(hits, params.sort, (docId) => manager.getRef(docId));
|
|
1933
|
+
if (params.sort) {
|
|
1934
|
+
hits = applySorting(hits, params.sort, (docId) => manager.getRef(docId));
|
|
1935
|
+
}
|
|
1936
|
+
let groups;
|
|
1937
|
+
if (params.group) {
|
|
1938
|
+
groups = applyGrouping(hits, params.group, (docId) => manager.getRef(docId));
|
|
1939
|
+
}
|
|
1940
|
+
if (params.pinned) {
|
|
1941
|
+
hits = applyPinning(hits, params.pinned, (docId) => {
|
|
1942
|
+
const doc = manager.getRef(docId);
|
|
1943
|
+
if (!doc) return void 0;
|
|
1944
|
+
return { id: docId, score: 0, document: doc };
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
const { paginated, nextCursor } = applyPagination(hits, limit, offset, params.searchAfter);
|
|
1948
|
+
for (const hit of paginated) {
|
|
1949
|
+
hit.document = manager.get(hit.id) ?? {};
|
|
1950
|
+
}
|
|
1951
|
+
if (groups) {
|
|
1952
|
+
for (const group of groups) {
|
|
1953
|
+
for (const hit of group.hits) {
|
|
1954
|
+
hit.document = manager.get(hit.id) ?? {};
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
if (params.highlight) {
|
|
1959
|
+
applyHighlights(paginated, params, language, config);
|
|
1960
|
+
}
|
|
1961
|
+
const elapsed = now() - startTime;
|
|
1962
|
+
return {
|
|
1963
|
+
hits: paginated,
|
|
1964
|
+
count: fanOutResult.totalMatched,
|
|
1965
|
+
elapsed,
|
|
1966
|
+
cursor: nextCursor,
|
|
1967
|
+
facets: fanOutResult.facets,
|
|
1968
|
+
groups
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
async function executePreflight(params, context) {
|
|
1972
|
+
const { manager, language, config, workerSearch, indexName } = context;
|
|
1973
|
+
const startTime = now();
|
|
1974
|
+
const hasTerm = params.term !== void 0 && params.term.trim().length > 0;
|
|
1975
|
+
const hasVector = params.vector !== void 0 && params.vector.value !== void 0;
|
|
1976
|
+
const isHybridMode = params.mode === "hybrid" || hasTerm && hasVector;
|
|
1977
|
+
const isVectorOnly = (params.mode === "vector" || hasVector && !hasTerm) && !isHybridMode;
|
|
1978
|
+
const requestedVectorField = params.vector?.field;
|
|
1979
|
+
const hasGlobalVectorIndex = requestedVectorField !== void 0 && manager.getVectorIndexes().has(requestedVectorField);
|
|
1980
|
+
let totalMatched;
|
|
1981
|
+
const preflightLimit = 1e3;
|
|
1982
|
+
const preflightOffset = 0;
|
|
1983
|
+
if (isVectorOnly && hasGlobalVectorIndex) {
|
|
1984
|
+
const result = executeVectorSearch(params, manager, config, preflightLimit, preflightOffset);
|
|
1985
|
+
totalMatched = result.totalMatched;
|
|
1986
|
+
} else if (isHybridMode && hasGlobalVectorIndex) {
|
|
1987
|
+
const result = await executeHybridSearch(
|
|
1988
|
+
params,
|
|
1989
|
+
manager,
|
|
1990
|
+
language,
|
|
1991
|
+
config,
|
|
1992
|
+
workerSearch,
|
|
1993
|
+
indexName,
|
|
1994
|
+
preflightLimit,
|
|
1995
|
+
preflightOffset
|
|
1996
|
+
);
|
|
1997
|
+
totalMatched = result.totalMatched;
|
|
1998
|
+
} else {
|
|
1999
|
+
const workerResult = workerSearch ? await workerSearch(indexName, params) : null;
|
|
2000
|
+
if (workerResult) {
|
|
2001
|
+
totalMatched = workerResult.totalMatched;
|
|
2002
|
+
} else {
|
|
2003
|
+
const searchOptions = {
|
|
2004
|
+
bm25Params: config.bm25,
|
|
2005
|
+
stopWords: config.stopWords,
|
|
2006
|
+
customTokenizer: config.tokenizer
|
|
2007
|
+
};
|
|
2008
|
+
const fanOutResult = await fanOutQuery(
|
|
2009
|
+
manager,
|
|
2010
|
+
params,
|
|
2011
|
+
language,
|
|
2012
|
+
config.schema,
|
|
2013
|
+
{ scoringMode: params.scoring ?? config.defaultScoring ?? "local" },
|
|
2014
|
+
searchOptions
|
|
2015
|
+
);
|
|
2016
|
+
totalMatched = fanOutResult.totalMatched;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
const elapsed = now() - startTime;
|
|
2020
|
+
return { count: totalMatched, elapsed };
|
|
2021
|
+
}
|
|
2022
|
+
function applyHighlights(hits, params, language, config) {
|
|
2023
|
+
if (!params.highlight) return;
|
|
2024
|
+
const queryTokenResult = tokenize(params.term ?? "", language, {
|
|
2025
|
+
stem: true,
|
|
2026
|
+
removeStopWords: true,
|
|
2027
|
+
customTokenizer: config.tokenizer,
|
|
2028
|
+
stopWordOverride: config.stopWords
|
|
2029
|
+
});
|
|
2030
|
+
for (const hit of hits) {
|
|
2031
|
+
const highlights = {};
|
|
2032
|
+
for (const field of params.highlight.fields) {
|
|
2033
|
+
const doc = hit.document;
|
|
2034
|
+
const fieldValue = doc[field];
|
|
2035
|
+
if (typeof fieldValue === "string") {
|
|
2036
|
+
highlights[field] = highlightField(fieldValue, queryTokenResult.tokens, language, {
|
|
2037
|
+
preTag: params.highlight.preTag,
|
|
2038
|
+
postTag: params.highlight.postTag,
|
|
2039
|
+
maxSnippetLength: params.highlight.maxSnippetLength
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
hit.highlights = highlights;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
// src/partitioning/write-ahead-queue.ts
|
|
2048
|
+
function createWriteAheadQueue(maxSize = 1e4) {
|
|
2049
|
+
const entries = [];
|
|
2050
|
+
let nextSequence = 1;
|
|
2051
|
+
return {
|
|
2052
|
+
push(entry) {
|
|
2053
|
+
if (entries.length >= maxSize) {
|
|
2054
|
+
throw new NarsilError(
|
|
2055
|
+
ErrorCodes.PARTITION_REBALANCING_BACKPRESSURE,
|
|
2056
|
+
`Write-ahead queue is full (${maxSize} entries). Retry after rebalancing completes.`,
|
|
2057
|
+
{ maxSize, currentSize: entries.length }
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
const seq = nextSequence++;
|
|
2061
|
+
entries.push({ ...entry, sequenceNumber: seq });
|
|
2062
|
+
return seq;
|
|
2063
|
+
},
|
|
2064
|
+
drain() {
|
|
2065
|
+
const result = entries.splice(0);
|
|
2066
|
+
result.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
|
2067
|
+
return result;
|
|
2068
|
+
},
|
|
2069
|
+
clear() {
|
|
2070
|
+
entries.length = 0;
|
|
2071
|
+
},
|
|
2072
|
+
get size() {
|
|
2073
|
+
return entries.length;
|
|
2074
|
+
},
|
|
2075
|
+
get isFull() {
|
|
2076
|
+
return entries.length >= maxSize;
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// src/engine/rebalance-executor.ts
|
|
2082
|
+
async function executeRebalance(manager, indexName, targetPartitionCount, ctx) {
|
|
2083
|
+
if (targetPartitionCount <= 0 || !Number.isInteger(targetPartitionCount)) {
|
|
2084
|
+
throw new NarsilError(
|
|
2085
|
+
ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
|
|
2086
|
+
`Target partition count must be a positive integer, got ${targetPartitionCount}`,
|
|
2087
|
+
{ targetPartitionCount }
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
if (ctx.rebalancingIndexes.has(indexName)) {
|
|
2091
|
+
throw new NarsilError(
|
|
2092
|
+
ErrorCodes.PARTITION_REBALANCING_BACKPRESSURE,
|
|
2093
|
+
`Index "${indexName}" is already being rebalanced`
|
|
2094
|
+
);
|
|
2095
|
+
}
|
|
2096
|
+
if (targetPartitionCount === manager.partitionCount) {
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
const waq = createWriteAheadQueue();
|
|
2100
|
+
ctx.waqMap.set(indexName, waq);
|
|
2101
|
+
ctx.rebalancingIndexes.add(indexName);
|
|
2102
|
+
try {
|
|
2103
|
+
const oldCount = manager.partitionCount;
|
|
2104
|
+
await ctx.rebalancer.rebalance(manager, targetPartitionCount, ctx.router, (progress) => {
|
|
2105
|
+
if (progress.phase === "complete") {
|
|
2106
|
+
const handlers = ctx.eventHandlers.get("partitionRebalance");
|
|
2107
|
+
if (handlers) {
|
|
2108
|
+
for (const handler of handlers) {
|
|
2109
|
+
handler({ indexName, oldCount, newCount: targetPartitionCount });
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
});
|
|
2114
|
+
const entries = waq.drain();
|
|
2115
|
+
const appliedSeqs = ctx.lastAppliedSeqMap.get(indexName) ?? /* @__PURE__ */ new Map();
|
|
2116
|
+
const rebalanceEntry = ctx.requireIndex(indexName);
|
|
2117
|
+
const rebalanceVecIndexes = manager.getVectorIndexes();
|
|
2118
|
+
const rebalanceVectorFieldPaths = rebalanceVecIndexes.size > 0 ? rebalanceEntry.vectorFieldPaths : /* @__PURE__ */ new Set();
|
|
2119
|
+
for (const entry of entries) {
|
|
2120
|
+
const partitionLastSeq = appliedSeqs.get(0) ?? 0;
|
|
2121
|
+
if (entry.sequenceNumber <= partitionLastSeq) continue;
|
|
2122
|
+
try {
|
|
2123
|
+
if (entry.action === "insert" && entry.document) {
|
|
2124
|
+
const { partitionDoc, extractedVectors } = prepareDocumentVectors(
|
|
2125
|
+
entry.document,
|
|
2126
|
+
rebalanceVectorFieldPaths,
|
|
2127
|
+
rebalanceVecIndexes
|
|
2128
|
+
);
|
|
2129
|
+
manager.insert(entry.docId, partitionDoc);
|
|
2130
|
+
if (extractedVectors.size > 0) {
|
|
2131
|
+
insertDocumentVectors(entry.docId, extractedVectors, rebalanceVecIndexes);
|
|
2132
|
+
}
|
|
2133
|
+
} else if (entry.action === "remove") {
|
|
2134
|
+
manager.remove(entry.docId);
|
|
2135
|
+
removeDocumentVectors(entry.docId, rebalanceVecIndexes);
|
|
2136
|
+
} else if (entry.action === "update" && entry.document) {
|
|
2137
|
+
const replayUpdateVectors = /* @__PURE__ */ new Map();
|
|
2138
|
+
for (const fieldPath of rebalanceVectorFieldPaths) {
|
|
2139
|
+
const newVec = extractVectorFromDoc(entry.document, fieldPath);
|
|
2140
|
+
replayUpdateVectors.set(fieldPath, newVec);
|
|
2141
|
+
}
|
|
2142
|
+
const replayUpdateDoc = replayUpdateVectors.size > 0 ? structuredClone(entry.document) : entry.document;
|
|
2143
|
+
if (replayUpdateVectors.size > 0) {
|
|
2144
|
+
for (const fieldPath of replayUpdateVectors.keys()) {
|
|
2145
|
+
deleteNestedValue(replayUpdateDoc, fieldPath);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
manager.update(entry.docId, replayUpdateDoc);
|
|
2149
|
+
for (const [fieldPath, newVec] of replayUpdateVectors) {
|
|
2150
|
+
const vecIndex = rebalanceVecIndexes.get(fieldPath);
|
|
2151
|
+
if (!vecIndex) continue;
|
|
2152
|
+
if (newVec === null) {
|
|
2153
|
+
if (vecIndex.has(entry.docId)) {
|
|
2154
|
+
vecIndex.remove(entry.docId);
|
|
2155
|
+
}
|
|
2156
|
+
} else {
|
|
2157
|
+
const oldVec = vecIndex.getVector(entry.docId);
|
|
2158
|
+
if (!vectorsEqual(oldVec, newVec)) {
|
|
2159
|
+
vecIndex.remove(entry.docId);
|
|
2160
|
+
vecIndex.insert(entry.docId, newVec);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
} catch (replayErr) {
|
|
2166
|
+
const isDuplicate = replayErr instanceof NarsilError && replayErr.code === ErrorCodes.DOC_ALREADY_EXISTS;
|
|
2167
|
+
const isMissing = replayErr instanceof NarsilError && replayErr.code === ErrorCodes.DOC_NOT_FOUND;
|
|
2168
|
+
if (!isDuplicate && !isMissing) {
|
|
2169
|
+
console.warn(`WAQ replay failed for ${entry.action} on doc "${entry.docId}":`, replayErr);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
appliedSeqs.set(0, entry.sequenceNumber);
|
|
2173
|
+
}
|
|
2174
|
+
ctx.lastAppliedSeqMap.set(indexName, appliedSeqs);
|
|
2175
|
+
} finally {
|
|
2176
|
+
ctx.rebalancingIndexes.delete(indexName);
|
|
2177
|
+
ctx.waqMap.delete(indexName);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// src/engine/resolve-vector-text.ts
|
|
2182
|
+
async function resolveVectorText(params, embeddingAdapter, signal) {
|
|
2183
|
+
if (!params.vector) return params;
|
|
2184
|
+
if (params.vector.text === void 0 && params.vector.value === void 0) return params;
|
|
2185
|
+
if (params.vector.value !== void 0 && params.vector.text === void 0) return params;
|
|
2186
|
+
if (params.vector.text !== void 0 && params.vector.value !== void 0) {
|
|
2187
|
+
throw new NarsilError(ErrorCodes.EMBEDDING_CONFIG_INVALID, "Vector query cannot have both 'text' and 'value'");
|
|
2188
|
+
}
|
|
2189
|
+
if (!embeddingAdapter) {
|
|
2190
|
+
throw new NarsilError(
|
|
2191
|
+
ErrorCodes.EMBEDDING_CONFIG_INVALID,
|
|
2192
|
+
"Vector query with 'text' requires an embedding adapter on the index or instance"
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
if (typeof params.vector.text !== "string") {
|
|
2196
|
+
throw new NarsilError(
|
|
2197
|
+
ErrorCodes.DOC_VALIDATION_FAILED,
|
|
2198
|
+
`Vector query 'text' must be a string, got ${typeof params.vector.text}`
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
if (params.vector.text.length === 0) {
|
|
2202
|
+
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Vector query 'text' must not be empty");
|
|
2203
|
+
}
|
|
2204
|
+
const raw = await embeddingAdapter.embed(params.vector.text, "query", signal);
|
|
2205
|
+
if (!(raw instanceof Float32Array)) {
|
|
2206
|
+
throw new NarsilError(
|
|
2207
|
+
ErrorCodes.EMBEDDING_FAILED,
|
|
2208
|
+
`Adapter returned ${typeof raw} for query embedding, expected Float32Array`
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
if (raw.length !== embeddingAdapter.dimensions) {
|
|
2212
|
+
throw new NarsilError(
|
|
2213
|
+
ErrorCodes.EMBEDDING_DIMENSION_MISMATCH,
|
|
2214
|
+
`Adapter returned ${raw.length}-dimensional vector for query, expected ${embeddingAdapter.dimensions}`,
|
|
2215
|
+
{ expected: embeddingAdapter.dimensions, actual: raw.length }
|
|
2216
|
+
);
|
|
2217
|
+
}
|
|
2218
|
+
const resolved = { ...params.vector, value: Array.from(raw) };
|
|
2219
|
+
delete resolved.text;
|
|
2220
|
+
return { ...params, vector: resolved };
|
|
2221
|
+
}
|
|
2222
|
+
var VALID_HNSW_METRICS = /* @__PURE__ */ new Set(["cosine", "dotProduct", "euclidean"]);
|
|
2223
|
+
function validateHnswMetric(value) {
|
|
2224
|
+
if (value === void 0 || value === null) return void 0;
|
|
2225
|
+
if (typeof value === "string" && VALID_HNSW_METRICS.has(value)) {
|
|
2226
|
+
return value;
|
|
2227
|
+
}
|
|
2228
|
+
return void 0;
|
|
2229
|
+
}
|
|
2230
|
+
function wireToPartition(raw) {
|
|
2231
|
+
const documents = {};
|
|
2232
|
+
for (const [docId, doc] of Object.entries(raw.documents ?? {})) {
|
|
2233
|
+
documents[docId] = {
|
|
2234
|
+
fields: doc.fields,
|
|
2235
|
+
fieldLengths: doc.field_lengths ?? {}
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
const invertedIndex = {};
|
|
2239
|
+
for (const [token, list] of Object.entries(raw.inverted_index ?? {})) {
|
|
2240
|
+
invertedIndex[token] = {
|
|
2241
|
+
docFrequency: list.doc_freq,
|
|
2242
|
+
postings: (list.postings ?? []).map((p) => ({
|
|
2243
|
+
docId: p.doc_id,
|
|
2244
|
+
termFrequency: p.term_freq,
|
|
2245
|
+
field: p.field,
|
|
2246
|
+
positions: p.positions ?? []
|
|
2247
|
+
}))
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
const numeric = {};
|
|
2251
|
+
for (const [field, entries] of Object.entries(raw.field_indexes?.numeric ?? {})) {
|
|
2252
|
+
numeric[field] = entries.map((e) => ({ value: e.value, docId: e.doc_id }));
|
|
2253
|
+
}
|
|
2254
|
+
const boolean = {};
|
|
2255
|
+
for (const [field, idx] of Object.entries(raw.field_indexes?.boolean ?? {})) {
|
|
2256
|
+
boolean[field] = { trueDocs: idx.true_docs ?? [], falseDocs: idx.false_docs ?? [] };
|
|
2257
|
+
}
|
|
2258
|
+
const geopoint = {};
|
|
2259
|
+
for (const [field, entries] of Object.entries(raw.field_indexes?.geopoint ?? {})) {
|
|
2260
|
+
geopoint[field] = entries.map((e) => ({ lat: e.lat, lon: e.lon, docId: e.doc_id }));
|
|
2261
|
+
}
|
|
2262
|
+
const vectorData = {};
|
|
2263
|
+
for (const [field, data] of Object.entries(raw.vector_data ?? {})) {
|
|
2264
|
+
vectorData[field] = {
|
|
2265
|
+
dimension: data.dimension,
|
|
2266
|
+
vectors: (data.vectors ?? []).map((v) => ({ docId: v.doc_id, vector: v.vector })),
|
|
2267
|
+
hnswGraph: data.hnsw_graph ? {
|
|
2268
|
+
entryPoint: data.hnsw_graph.entry_point,
|
|
2269
|
+
maxLayer: data.hnsw_graph.max_layer,
|
|
2270
|
+
m: data.hnsw_graph.m,
|
|
2271
|
+
efConstruction: data.hnsw_graph.ef_construction,
|
|
2272
|
+
metric: validateHnswMetric(data.hnsw_graph.metric),
|
|
2273
|
+
nodes: data.hnsw_graph.nodes
|
|
2274
|
+
} : null,
|
|
2275
|
+
sq8: data.sq8 ? {
|
|
2276
|
+
alpha: data.sq8.alpha,
|
|
2277
|
+
offset: data.sq8.offset,
|
|
2278
|
+
quantizedVectors: data.sq8.quantized_vectors,
|
|
2279
|
+
vectorSums: data.sq8.vector_sums,
|
|
2280
|
+
vectorSumSqs: data.sq8.vector_sum_sqs
|
|
2281
|
+
} : null
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
return {
|
|
2285
|
+
indexName: raw.index_name,
|
|
2286
|
+
partitionId: raw.partition_id,
|
|
2287
|
+
totalPartitions: raw.total_partitions,
|
|
2288
|
+
language: raw.language ?? "english",
|
|
2289
|
+
schema: raw.schema ?? {},
|
|
2290
|
+
docCount: raw.doc_count ?? 0,
|
|
2291
|
+
avgDocLength: raw.avg_doc_length ?? 0,
|
|
2292
|
+
documents,
|
|
2293
|
+
invertedIndex,
|
|
2294
|
+
fieldIndexes: {
|
|
2295
|
+
numeric,
|
|
2296
|
+
boolean,
|
|
2297
|
+
enum: raw.field_indexes?.enum ?? {},
|
|
2298
|
+
geopoint
|
|
2299
|
+
},
|
|
2300
|
+
vectorData,
|
|
2301
|
+
statistics: {
|
|
2302
|
+
totalDocuments: raw.statistics?.total_documents ?? 0,
|
|
2303
|
+
totalFieldLengths: raw.statistics?.total_field_lengths ?? {},
|
|
2304
|
+
averageFieldLengths: raw.statistics?.average_field_lengths ?? {},
|
|
2305
|
+
docFrequencies: raw.statistics?.doc_frequencies ?? {}
|
|
2306
|
+
}
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
function deserializePayloadV1(data) {
|
|
2310
|
+
const raw = decode(data);
|
|
2311
|
+
return wireToPartition(raw);
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
// src/engine/snapshot.ts
|
|
2315
|
+
async function createSnapshot(manager, entry) {
|
|
2316
|
+
const partitionBuffers = [];
|
|
2317
|
+
for (let i = 0; i < manager.partitionCount; i++) {
|
|
2318
|
+
partitionBuffers.push(manager.serializePartitionToBytes(i));
|
|
2319
|
+
}
|
|
2320
|
+
const snapshotVecIndexes = manager.getVectorIndexes();
|
|
2321
|
+
const vectorPayloads = {};
|
|
2322
|
+
for (const [fieldPath, vecIndex] of snapshotVecIndexes) {
|
|
2323
|
+
vectorPayloads[fieldPath] = vecIndex.serialize();
|
|
2324
|
+
}
|
|
2325
|
+
const { encode: encode2 } = await import('@msgpack/msgpack');
|
|
2326
|
+
return encode2({
|
|
2327
|
+
version: 2,
|
|
2328
|
+
schema: entry.config.schema,
|
|
2329
|
+
language: entry.language.name,
|
|
2330
|
+
partitions: partitionBuffers,
|
|
2331
|
+
vectorIndexes: vectorPayloads
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
async function restoreFromSnapshot(indexName, data, executor, indexRegistry, getVectorFieldPaths2, dropIndex, requireManager) {
|
|
2335
|
+
if (!(data instanceof Uint8Array)) {
|
|
2336
|
+
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Snapshot data must be a Uint8Array");
|
|
2337
|
+
}
|
|
2338
|
+
const { decode: decode2 } = await import('@msgpack/msgpack');
|
|
2339
|
+
const envelope = decode2(data);
|
|
2340
|
+
if (envelope.version !== 1 && envelope.version !== 2) {
|
|
2341
|
+
throw new NarsilError(
|
|
2342
|
+
ErrorCodes.DOC_VALIDATION_FAILED,
|
|
2343
|
+
`Unsupported snapshot version: ${envelope.version}. Expected version 1 or 2`,
|
|
2344
|
+
{ version: envelope.version }
|
|
2345
|
+
);
|
|
920
2346
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
groups = applyGrouping(hits, params.group, (docId) => manager.getRef(docId));
|
|
2347
|
+
if (!envelope.schema || typeof envelope.schema !== "object") {
|
|
2348
|
+
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Invalid snapshot: missing or invalid schema");
|
|
924
2349
|
}
|
|
925
|
-
if (
|
|
926
|
-
|
|
927
|
-
const doc = manager.getRef(docId);
|
|
928
|
-
if (!doc) return void 0;
|
|
929
|
-
return { id: docId, score: 0, document: doc };
|
|
930
|
-
});
|
|
2350
|
+
if (!envelope.language || typeof envelope.language !== "string") {
|
|
2351
|
+
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Invalid snapshot: missing or invalid language");
|
|
931
2352
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
hit.document = manager.get(hit.id) ?? {};
|
|
2353
|
+
if (!Array.isArray(envelope.partitions)) {
|
|
2354
|
+
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Invalid snapshot: missing partitions");
|
|
935
2355
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
2356
|
+
const language = getLanguage(envelope.language);
|
|
2357
|
+
const schema = envelope.schema;
|
|
2358
|
+
validateSchema(schema);
|
|
2359
|
+
if (indexRegistry.has(indexName)) {
|
|
2360
|
+
await dropIndex(indexName);
|
|
2361
|
+
}
|
|
2362
|
+
const indexConfig = { schema, language: envelope.language };
|
|
2363
|
+
executor.createIndex(indexName, indexConfig, language);
|
|
2364
|
+
const vectorFieldPaths = getVectorFieldPaths2(schema);
|
|
2365
|
+
indexRegistry.set(indexName, { config: indexConfig, language, embeddingAdapter: null, vectorFieldPaths });
|
|
2366
|
+
try {
|
|
2367
|
+
const manager = requireManager(indexName);
|
|
2368
|
+
while (manager.partitionCount < envelope.partitions.length) {
|
|
2369
|
+
manager.addPartition();
|
|
2370
|
+
}
|
|
2371
|
+
const deserializePartitionPayload = envelope.version === 2 ? deserializePayloadV2 : deserializePayloadV1;
|
|
2372
|
+
for (let i = 0; i < envelope.partitions.length; i++) {
|
|
2373
|
+
const partition = deserializePartitionPayload(envelope.partitions[i]);
|
|
2374
|
+
manager.deserializePartition(i, partition);
|
|
2375
|
+
}
|
|
2376
|
+
if (envelope.vectorIndexes) {
|
|
2377
|
+
const restoreVecIndexes = manager.getVectorIndexes();
|
|
2378
|
+
for (const [fieldPath, payload] of Object.entries(envelope.vectorIndexes)) {
|
|
2379
|
+
const vecIndex = restoreVecIndexes.get(fieldPath);
|
|
2380
|
+
if (vecIndex) {
|
|
2381
|
+
vecIndex.deserialize(payload);
|
|
2382
|
+
}
|
|
940
2383
|
}
|
|
941
2384
|
}
|
|
2385
|
+
} catch (err) {
|
|
2386
|
+
try {
|
|
2387
|
+
executor.dropIndex(indexName);
|
|
2388
|
+
indexRegistry.delete(indexName);
|
|
2389
|
+
} catch (_) {
|
|
2390
|
+
}
|
|
2391
|
+
throw err;
|
|
942
2392
|
}
|
|
943
|
-
|
|
944
|
-
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
// src/engine/suggest.ts
|
|
2396
|
+
function executeSuggest(manager, language, params) {
|
|
2397
|
+
const t0 = performance.now();
|
|
2398
|
+
const limit = Math.max(1, Math.min(params.limit ?? 10, 100));
|
|
2399
|
+
const rawPrefix = params.prefix.trim();
|
|
2400
|
+
if (rawPrefix.length === 0) {
|
|
2401
|
+
return { terms: [], elapsed: performance.now() - t0 };
|
|
2402
|
+
}
|
|
2403
|
+
const unstemmed = tokenize(rawPrefix, language, { stem: false, removeStopWords: false });
|
|
2404
|
+
const lastToken = unstemmed.tokens.length > 0 ? unstemmed.tokens[unstemmed.tokens.length - 1].token : rawPrefix.toLowerCase();
|
|
2405
|
+
if (lastToken.length === 0) {
|
|
2406
|
+
return { terms: [], elapsed: performance.now() - t0 };
|
|
2407
|
+
}
|
|
2408
|
+
const stemmed = language.stemmer ? language.stemmer(lastToken) : lastToken;
|
|
2409
|
+
const prefixes = stemmed !== lastToken ? [lastToken, stemmed] : [lastToken];
|
|
2410
|
+
const partitions = manager.getAllPartitions();
|
|
2411
|
+
const merged = /* @__PURE__ */ new Map();
|
|
2412
|
+
const perPartitionLimit = limit * 2;
|
|
2413
|
+
for (const partition of partitions) {
|
|
2414
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2415
|
+
for (const prefix of prefixes) {
|
|
2416
|
+
const suggestions = partition.suggestTerms(prefix, perPartitionLimit);
|
|
2417
|
+
for (const s of suggestions) {
|
|
2418
|
+
if (seen.has(s.term)) continue;
|
|
2419
|
+
seen.add(s.term);
|
|
2420
|
+
merged.set(s.term, (merged.get(s.term) ?? 0) + s.documentFrequency);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
const terms = Array.from(merged.entries()).map(([term, documentFrequency]) => ({ term, documentFrequency })).sort((a, b) => b.documentFrequency - a.documentFrequency);
|
|
2425
|
+
if (terms.length > limit) terms.length = limit;
|
|
2426
|
+
return { terms, elapsed: performance.now() - t0 };
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// src/engine/vector-maintenance.ts
|
|
2430
|
+
function resolveVectorIndexes(manager, indexName, fieldName) {
|
|
2431
|
+
const all = manager.getVectorIndexes();
|
|
2432
|
+
if (fieldName === void 0) return all;
|
|
2433
|
+
const idx = all.get(fieldName);
|
|
2434
|
+
if (!idx) {
|
|
2435
|
+
throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Vector field "${fieldName}" not found in index "${indexName}"`, {
|
|
2436
|
+
indexName,
|
|
2437
|
+
fieldName
|
|
2438
|
+
});
|
|
945
2439
|
}
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
count: fanOutResult.totalMatched,
|
|
950
|
-
elapsed,
|
|
951
|
-
cursor: nextCursor,
|
|
952
|
-
facets: fanOutResult.facets,
|
|
953
|
-
groups
|
|
954
|
-
};
|
|
2440
|
+
const single = /* @__PURE__ */ new Map();
|
|
2441
|
+
single.set(fieldName, idx);
|
|
2442
|
+
return single;
|
|
955
2443
|
}
|
|
956
|
-
|
|
957
|
-
const
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
const workerResult = workerSearch ? await workerSearch(indexName, params) : null;
|
|
961
|
-
if (workerResult) {
|
|
962
|
-
totalMatched = workerResult.totalMatched;
|
|
963
|
-
} else {
|
|
964
|
-
const searchOptions = {
|
|
965
|
-
bm25Params: config.bm25,
|
|
966
|
-
stopWords: config.stopWords,
|
|
967
|
-
customTokenizer: config.tokenizer
|
|
968
|
-
};
|
|
969
|
-
const fanOutResult = await fanOutQuery(
|
|
970
|
-
manager,
|
|
971
|
-
params,
|
|
972
|
-
language,
|
|
973
|
-
config.schema,
|
|
974
|
-
{ scoringMode: params.scoring ?? config.defaultScoring ?? "local" },
|
|
975
|
-
searchOptions
|
|
976
|
-
);
|
|
977
|
-
totalMatched = fanOutResult.totalMatched;
|
|
2444
|
+
function compactVectors(manager, indexName, fieldName) {
|
|
2445
|
+
const indexes = resolveVectorIndexes(manager, indexName, fieldName);
|
|
2446
|
+
for (const [, idx] of indexes) {
|
|
2447
|
+
idx.compact();
|
|
978
2448
|
}
|
|
979
|
-
const elapsed = now() - startTime;
|
|
980
|
-
return { count: totalMatched, elapsed };
|
|
981
2449
|
}
|
|
982
|
-
function
|
|
983
|
-
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
removeStopWords: true,
|
|
987
|
-
customTokenizer: config.tokenizer,
|
|
988
|
-
stopWordOverride: config.stopWords
|
|
989
|
-
});
|
|
990
|
-
for (const hit of hits) {
|
|
991
|
-
const highlights = {};
|
|
992
|
-
for (const field of params.highlight.fields) {
|
|
993
|
-
const doc = hit.document;
|
|
994
|
-
const fieldValue = doc[field];
|
|
995
|
-
if (typeof fieldValue === "string") {
|
|
996
|
-
highlights[field] = highlightField(fieldValue, queryTokenResult.tokens, language, {
|
|
997
|
-
preTag: params.highlight.preTag,
|
|
998
|
-
postTag: params.highlight.postTag,
|
|
999
|
-
maxSnippetLength: params.highlight.maxSnippetLength
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
hit.highlights = highlights;
|
|
2450
|
+
async function optimizeVectors(manager, indexName, fieldName) {
|
|
2451
|
+
const indexes = resolveVectorIndexes(manager, indexName, fieldName);
|
|
2452
|
+
for (const [, idx] of indexes) {
|
|
2453
|
+
await idx.optimize();
|
|
1004
2454
|
}
|
|
1005
2455
|
}
|
|
2456
|
+
function getVectorMaintenanceStatus(manager) {
|
|
2457
|
+
const indexes = manager.getVectorIndexes();
|
|
2458
|
+
const results = [];
|
|
2459
|
+
for (const [field, idx] of indexes) {
|
|
2460
|
+
const status = idx.maintenanceStatus();
|
|
2461
|
+
results.push({
|
|
2462
|
+
fieldName: field,
|
|
2463
|
+
tombstoneRatio: status.tombstoneRatio,
|
|
2464
|
+
graphCount: status.graphCount,
|
|
2465
|
+
bufferSize: status.bufferSize,
|
|
2466
|
+
building: status.building,
|
|
2467
|
+
estimatedCompactMs: status.estimatedCompactMs,
|
|
2468
|
+
estimatedOptimizeMs: status.estimatedOptimizeMs
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
return results;
|
|
2472
|
+
}
|
|
1006
2473
|
|
|
1007
2474
|
// src/partitioning/rebalancer.ts
|
|
1008
2475
|
var CHUNK_SIZE = 1e3;
|
|
@@ -1094,40 +2561,6 @@ function createRebalancer() {
|
|
|
1094
2561
|
};
|
|
1095
2562
|
}
|
|
1096
2563
|
|
|
1097
|
-
// src/partitioning/write-ahead-queue.ts
|
|
1098
|
-
function createWriteAheadQueue(maxSize = 1e4) {
|
|
1099
|
-
const entries = [];
|
|
1100
|
-
let nextSequence = 1;
|
|
1101
|
-
return {
|
|
1102
|
-
push(entry) {
|
|
1103
|
-
if (entries.length >= maxSize) {
|
|
1104
|
-
throw new NarsilError(
|
|
1105
|
-
ErrorCodes.PARTITION_REBALANCING_BACKPRESSURE,
|
|
1106
|
-
`Write-ahead queue is full (${maxSize} entries). Retry after rebalancing completes.`,
|
|
1107
|
-
{ maxSize, currentSize: entries.length }
|
|
1108
|
-
);
|
|
1109
|
-
}
|
|
1110
|
-
const seq = nextSequence++;
|
|
1111
|
-
entries.push({ ...entry, sequenceNumber: seq });
|
|
1112
|
-
return seq;
|
|
1113
|
-
},
|
|
1114
|
-
drain() {
|
|
1115
|
-
const result = entries.splice(0);
|
|
1116
|
-
result.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
|
1117
|
-
return result;
|
|
1118
|
-
},
|
|
1119
|
-
clear() {
|
|
1120
|
-
entries.length = 0;
|
|
1121
|
-
},
|
|
1122
|
-
get size() {
|
|
1123
|
-
return entries.length;
|
|
1124
|
-
},
|
|
1125
|
-
get isFull() {
|
|
1126
|
-
return entries.length >= maxSize;
|
|
1127
|
-
}
|
|
1128
|
-
};
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
2564
|
// src/persistence/flush-manager.ts
|
|
1132
2565
|
var DEFAULT_INTERVAL = 5e3;
|
|
1133
2566
|
var DEFAULT_MUTATION_THRESHOLD = 1e3;
|
|
@@ -1234,141 +2667,148 @@ function createFlushManager(config, getPartitionData, getInstanceId) {
|
|
|
1234
2667
|
}
|
|
1235
2668
|
startTimer();
|
|
1236
2669
|
}
|
|
1237
|
-
async function shutdown() {
|
|
1238
|
-
isShuttingDown = true;
|
|
1239
|
-
if (flushTimerId !== null) {
|
|
1240
|
-
clearInterval(flushTimerId);
|
|
1241
|
-
flushTimerId = null;
|
|
1242
|
-
}
|
|
1243
|
-
await flush();
|
|
1244
|
-
}
|
|
1245
|
-
return {
|
|
1246
|
-
markDirty,
|
|
1247
|
-
flush,
|
|
1248
|
-
shutdown
|
|
1249
|
-
};
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
// src/plugins/registry.ts
|
|
1253
|
-
function isThenable(value) {
|
|
1254
|
-
return value !== null && typeof value === "object" && typeof value.then === "function";
|
|
1255
|
-
}
|
|
1256
|
-
async function continueAsync(pending, hooks, startIndex, context) {
|
|
1257
|
-
await pending;
|
|
1258
|
-
for (let i = startIndex; i < hooks.length; i++) {
|
|
1259
|
-
await hooks[i](context);
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
function createPluginRegistry() {
|
|
1263
|
-
const plugins = [];
|
|
1264
|
-
return {
|
|
1265
|
-
register(plugin) {
|
|
1266
|
-
plugins.push(plugin);
|
|
1267
|
-
},
|
|
1268
|
-
hasHooks(hookName) {
|
|
1269
|
-
return plugins.some((p) => typeof p[hookName] === "function");
|
|
1270
|
-
},
|
|
1271
|
-
runHook(hookName, context) {
|
|
1272
|
-
const hooks = [];
|
|
1273
|
-
for (const plugin of plugins) {
|
|
1274
|
-
const hook = plugin[hookName];
|
|
1275
|
-
if (typeof hook === "function") {
|
|
1276
|
-
hooks.push(hook.bind(plugin));
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
if (hooks.length === 0) return;
|
|
1280
|
-
for (let i = 0; i < hooks.length; i++) {
|
|
1281
|
-
const result = hooks[i](context);
|
|
1282
|
-
if (isThenable(result)) {
|
|
1283
|
-
return continueAsync(result, hooks, i + 1, context);
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
1289
|
-
var VALID_HNSW_METRICS = /* @__PURE__ */ new Set(["cosine", "dotProduct", "euclidean"]);
|
|
1290
|
-
function validateHnswMetric(value) {
|
|
1291
|
-
if (value === void 0 || value === null) return void 0;
|
|
1292
|
-
if (typeof value === "string" && VALID_HNSW_METRICS.has(value)) {
|
|
1293
|
-
return value;
|
|
1294
|
-
}
|
|
1295
|
-
return void 0;
|
|
1296
|
-
}
|
|
1297
|
-
function wireToPartition(raw) {
|
|
1298
|
-
const documents = {};
|
|
1299
|
-
for (const [docId, doc] of Object.entries(raw.documents ?? {})) {
|
|
1300
|
-
documents[docId] = {
|
|
1301
|
-
fields: doc.fields,
|
|
1302
|
-
fieldLengths: doc.field_lengths ?? {}
|
|
1303
|
-
};
|
|
1304
|
-
}
|
|
1305
|
-
const invertedIndex = {};
|
|
1306
|
-
for (const [token, list] of Object.entries(raw.inverted_index ?? {})) {
|
|
1307
|
-
invertedIndex[token] = {
|
|
1308
|
-
docFrequency: list.doc_freq,
|
|
1309
|
-
postings: (list.postings ?? []).map((p) => ({
|
|
1310
|
-
docId: p.doc_id,
|
|
1311
|
-
termFrequency: p.term_freq,
|
|
1312
|
-
field: p.field,
|
|
1313
|
-
positions: p.positions ?? []
|
|
1314
|
-
}))
|
|
1315
|
-
};
|
|
1316
|
-
}
|
|
1317
|
-
const numeric = {};
|
|
1318
|
-
for (const [field, entries] of Object.entries(raw.field_indexes?.numeric ?? {})) {
|
|
1319
|
-
numeric[field] = entries.map((e) => ({ value: e.value, docId: e.doc_id }));
|
|
1320
|
-
}
|
|
1321
|
-
const boolean = {};
|
|
1322
|
-
for (const [field, idx] of Object.entries(raw.field_indexes?.boolean ?? {})) {
|
|
1323
|
-
boolean[field] = { trueDocs: idx.true_docs ?? [], falseDocs: idx.false_docs ?? [] };
|
|
1324
|
-
}
|
|
1325
|
-
const geopoint = {};
|
|
1326
|
-
for (const [field, entries] of Object.entries(raw.field_indexes?.geopoint ?? {})) {
|
|
1327
|
-
geopoint[field] = entries.map((e) => ({ lat: e.lat, lon: e.lon, docId: e.doc_id }));
|
|
1328
|
-
}
|
|
1329
|
-
const vectorData = {};
|
|
1330
|
-
for (const [field, data] of Object.entries(raw.vector_data ?? {})) {
|
|
1331
|
-
vectorData[field] = {
|
|
1332
|
-
dimension: data.dimension,
|
|
1333
|
-
vectors: (data.vectors ?? []).map((v) => ({ docId: v.doc_id, vector: v.vector })),
|
|
1334
|
-
hnswGraph: data.hnsw_graph ? {
|
|
1335
|
-
entryPoint: data.hnsw_graph.entry_point,
|
|
1336
|
-
maxLayer: data.hnsw_graph.max_layer,
|
|
1337
|
-
m: data.hnsw_graph.m,
|
|
1338
|
-
efConstruction: data.hnsw_graph.ef_construction,
|
|
1339
|
-
metric: validateHnswMetric(data.hnsw_graph.metric),
|
|
1340
|
-
nodes: data.hnsw_graph.nodes
|
|
1341
|
-
} : null
|
|
1342
|
-
};
|
|
1343
|
-
}
|
|
2670
|
+
async function shutdown() {
|
|
2671
|
+
isShuttingDown = true;
|
|
2672
|
+
if (flushTimerId !== null) {
|
|
2673
|
+
clearInterval(flushTimerId);
|
|
2674
|
+
flushTimerId = null;
|
|
2675
|
+
}
|
|
2676
|
+
await flush();
|
|
2677
|
+
}
|
|
1344
2678
|
return {
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
2679
|
+
markDirty,
|
|
2680
|
+
flush,
|
|
2681
|
+
shutdown
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
// src/plugins/registry.ts
|
|
2686
|
+
function isThenable(value) {
|
|
2687
|
+
return value !== null && typeof value === "object" && typeof value.then === "function";
|
|
2688
|
+
}
|
|
2689
|
+
async function continueAsync(pending, hooks, startIndex, context) {
|
|
2690
|
+
await pending;
|
|
2691
|
+
for (let i = startIndex; i < hooks.length; i++) {
|
|
2692
|
+
await hooks[i](context);
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
function createPluginRegistry() {
|
|
2696
|
+
const plugins = [];
|
|
2697
|
+
return {
|
|
2698
|
+
register(plugin) {
|
|
2699
|
+
plugins.push(plugin);
|
|
1359
2700
|
},
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
2701
|
+
hasHooks(hookName) {
|
|
2702
|
+
return plugins.some((p) => typeof p[hookName] === "function");
|
|
2703
|
+
},
|
|
2704
|
+
runHook(hookName, context) {
|
|
2705
|
+
const hooks = [];
|
|
2706
|
+
for (const plugin of plugins) {
|
|
2707
|
+
const hook = plugin[hookName];
|
|
2708
|
+
if (typeof hook === "function") {
|
|
2709
|
+
hooks.push(hook.bind(plugin));
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
if (hooks.length === 0) return;
|
|
2713
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
2714
|
+
const result = hooks[i](context);
|
|
2715
|
+
if (isThenable(result)) {
|
|
2716
|
+
return continueAsync(result, hooks, i + 1, context);
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
1366
2719
|
}
|
|
1367
2720
|
};
|
|
1368
2721
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
2722
|
+
|
|
2723
|
+
// src/schema/embedding-validator.ts
|
|
2724
|
+
var VECTOR_DIMENSION_PATTERN = /^vector\[(\d+)]$/;
|
|
2725
|
+
function validateEmbeddingConfig(embeddingConfig, schema, instanceAdapter) {
|
|
2726
|
+
const fieldEntries = Object.keys(embeddingConfig.fields);
|
|
2727
|
+
if (fieldEntries.length === 0) {
|
|
2728
|
+
throw new NarsilError(
|
|
2729
|
+
ErrorCodes.EMBEDDING_CONFIG_INVALID,
|
|
2730
|
+
"Embedding config must define at least one field mapping"
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
const resolvedAdapter = embeddingConfig.adapter ?? instanceAdapter;
|
|
2734
|
+
if (!resolvedAdapter) {
|
|
2735
|
+
throw new NarsilError(
|
|
2736
|
+
ErrorCodes.EMBEDDING_CONFIG_INVALID,
|
|
2737
|
+
"Embedding config requires an adapter (set on the index or the instance)"
|
|
2738
|
+
);
|
|
2739
|
+
}
|
|
2740
|
+
if (typeof resolvedAdapter.dimensions !== "number" || !Number.isInteger(resolvedAdapter.dimensions) || resolvedAdapter.dimensions <= 0) {
|
|
2741
|
+
throw new NarsilError(
|
|
2742
|
+
ErrorCodes.EMBEDDING_CONFIG_INVALID,
|
|
2743
|
+
`Adapter dimensions must be a positive integer, got ${resolvedAdapter.dimensions}`
|
|
2744
|
+
);
|
|
2745
|
+
}
|
|
2746
|
+
const flatFields = flattenSchema(schema);
|
|
2747
|
+
for (const [targetField, sourceFields] of Object.entries(embeddingConfig.fields)) {
|
|
2748
|
+
const targetType = flatFields[targetField];
|
|
2749
|
+
if (targetType === void 0) {
|
|
2750
|
+
throw new NarsilError(
|
|
2751
|
+
ErrorCodes.EMBEDDING_CONFIG_INVALID,
|
|
2752
|
+
`Embedding target field "${targetField}" does not exist in the schema`,
|
|
2753
|
+
{ field: targetField }
|
|
2754
|
+
);
|
|
2755
|
+
}
|
|
2756
|
+
const vectorMatch = VECTOR_DIMENSION_PATTERN.exec(targetType);
|
|
2757
|
+
if (!vectorMatch) {
|
|
2758
|
+
throw new NarsilError(
|
|
2759
|
+
ErrorCodes.EMBEDDING_CONFIG_INVALID,
|
|
2760
|
+
`Embedding target field "${targetField}" must be a vector type, got "${targetType}"`,
|
|
2761
|
+
{ field: targetField, type: targetType }
|
|
2762
|
+
);
|
|
2763
|
+
}
|
|
2764
|
+
const schemaDimension = Number.parseInt(vectorMatch[1], 10);
|
|
2765
|
+
if (schemaDimension !== resolvedAdapter.dimensions) {
|
|
2766
|
+
throw new NarsilError(
|
|
2767
|
+
ErrorCodes.EMBEDDING_DIMENSION_MISMATCH,
|
|
2768
|
+
`Embedding target field "${targetField}" has dimension ${schemaDimension} but the adapter produces ${resolvedAdapter.dimensions}-dimensional vectors`,
|
|
2769
|
+
{ field: targetField, expected: schemaDimension, actual: resolvedAdapter.dimensions }
|
|
2770
|
+
);
|
|
2771
|
+
}
|
|
2772
|
+
const sources = Array.isArray(sourceFields) ? sourceFields : [sourceFields];
|
|
2773
|
+
if (sources.length === 0) {
|
|
2774
|
+
throw new NarsilError(
|
|
2775
|
+
ErrorCodes.EMBEDDING_CONFIG_INVALID,
|
|
2776
|
+
`Embedding target field "${targetField}" must have at least one source field`,
|
|
2777
|
+
{ field: targetField }
|
|
2778
|
+
);
|
|
2779
|
+
}
|
|
2780
|
+
for (const source of sources) {
|
|
2781
|
+
const sourceType = flatFields[source];
|
|
2782
|
+
if (sourceType === void 0) {
|
|
2783
|
+
throw new NarsilError(
|
|
2784
|
+
ErrorCodes.EMBEDDING_CONFIG_INVALID,
|
|
2785
|
+
`Embedding source field "${source}" does not exist in the schema`,
|
|
2786
|
+
{ field: source, targetField }
|
|
2787
|
+
);
|
|
2788
|
+
}
|
|
2789
|
+
if (sourceType !== "string") {
|
|
2790
|
+
throw new NarsilError(
|
|
2791
|
+
ErrorCodes.EMBEDDING_CONFIG_INVALID,
|
|
2792
|
+
`Embedding source field "${source}" must be a string type, got "${sourceType}"`,
|
|
2793
|
+
{ field: source, type: sourceType, targetField }
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
return resolvedAdapter;
|
|
2799
|
+
}
|
|
2800
|
+
function validateRequiredFieldsInSchema(required, schema) {
|
|
2801
|
+
if (required.length === 0) return;
|
|
2802
|
+
const flatFields = flattenSchema(schema);
|
|
2803
|
+
for (const field of required) {
|
|
2804
|
+
if (flatFields[field] === void 0) {
|
|
2805
|
+
throw new NarsilError(
|
|
2806
|
+
ErrorCodes.EMBEDDING_CONFIG_INVALID,
|
|
2807
|
+
`Required field "${field}" does not exist in the schema`,
|
|
2808
|
+
{ field }
|
|
2809
|
+
);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
1372
2812
|
}
|
|
1373
2813
|
|
|
1374
2814
|
// src/workers/promoter.ts
|
|
@@ -1410,6 +2850,9 @@ function createExecutionPromoter(config) {
|
|
|
1410
2850
|
}
|
|
1411
2851
|
|
|
1412
2852
|
// src/narsil.ts
|
|
2853
|
+
function getVectorFieldPaths(schema) {
|
|
2854
|
+
return new Set(extractVectorFieldsFromSchema(schema).keys());
|
|
2855
|
+
}
|
|
1413
2856
|
async function createNarsil(config) {
|
|
1414
2857
|
const executor = createDirectExecutor();
|
|
1415
2858
|
const promoter = createExecutionPromoter({
|
|
@@ -1418,9 +2861,7 @@ async function createNarsil(config) {
|
|
|
1418
2861
|
});
|
|
1419
2862
|
const pluginRegistry = createPluginRegistry();
|
|
1420
2863
|
if (config?.plugins) {
|
|
1421
|
-
for (const plugin of config.plugins)
|
|
1422
|
-
pluginRegistry.register(plugin);
|
|
1423
|
-
}
|
|
2864
|
+
for (const plugin of config.plugins) pluginRegistry.register(plugin);
|
|
1424
2865
|
}
|
|
1425
2866
|
let flushManager = null;
|
|
1426
2867
|
if (config?.persistence) {
|
|
@@ -1447,13 +2888,12 @@ async function createNarsil(config) {
|
|
|
1447
2888
|
const indexRegistry = /* @__PURE__ */ new Map();
|
|
1448
2889
|
const eventHandlers = /* @__PURE__ */ new Map();
|
|
1449
2890
|
let isShutdown = false;
|
|
2891
|
+
const abortController = new AbortController();
|
|
1450
2892
|
const orchestrator = createWorkerOrchestrator(config, executor, promoter, indexRegistry, {
|
|
1451
2893
|
onPromotion(workerCount, reason) {
|
|
1452
2894
|
const handlers = eventHandlers.get("workerPromote");
|
|
1453
2895
|
if (handlers) {
|
|
1454
|
-
for (const handler of handlers) {
|
|
1455
|
-
handler({ workerCount, reason });
|
|
1456
|
-
}
|
|
2896
|
+
for (const handler of handlers) handler({ workerCount, reason });
|
|
1457
2897
|
}
|
|
1458
2898
|
}
|
|
1459
2899
|
});
|
|
@@ -1478,7 +2918,8 @@ async function createNarsil(config) {
|
|
|
1478
2918
|
if (!rebalancingIndexes.has(indexName)) return false;
|
|
1479
2919
|
const waq = waqMap.get(indexName);
|
|
1480
2920
|
if (!waq) return false;
|
|
1481
|
-
|
|
2921
|
+
const clonedEntry = entry.document ? { ...entry, document: structuredClone(entry.document) } : entry;
|
|
2922
|
+
waq.push(clonedEntry);
|
|
1482
2923
|
return true;
|
|
1483
2924
|
}
|
|
1484
2925
|
function requireManager(indexName) {
|
|
@@ -1488,6 +2929,27 @@ async function createNarsil(config) {
|
|
|
1488
2929
|
}
|
|
1489
2930
|
return manager;
|
|
1490
2931
|
}
|
|
2932
|
+
const mutationCtx = {
|
|
2933
|
+
executor,
|
|
2934
|
+
pluginRegistry,
|
|
2935
|
+
flushManager,
|
|
2936
|
+
orchestrator,
|
|
2937
|
+
idGenerator,
|
|
2938
|
+
abortController,
|
|
2939
|
+
guardShutdown,
|
|
2940
|
+
requireIndex,
|
|
2941
|
+
requireManager,
|
|
2942
|
+
bufferIfRebalancing
|
|
2943
|
+
};
|
|
2944
|
+
const rebalanceCtx = {
|
|
2945
|
+
rebalancer,
|
|
2946
|
+
router: rebalanceRouter,
|
|
2947
|
+
waqMap,
|
|
2948
|
+
rebalancingIndexes,
|
|
2949
|
+
lastAppliedSeqMap,
|
|
2950
|
+
eventHandlers,
|
|
2951
|
+
requireIndex
|
|
2952
|
+
};
|
|
1491
2953
|
const narsil = {
|
|
1492
2954
|
async createIndex(name, indexConfig) {
|
|
1493
2955
|
guardShutdown();
|
|
@@ -1496,9 +2958,22 @@ async function createNarsil(config) {
|
|
|
1496
2958
|
throw new NarsilError(ErrorCodes.INDEX_ALREADY_EXISTS, `Index "${name}" already exists`, { indexName: name });
|
|
1497
2959
|
}
|
|
1498
2960
|
validateSchema(indexConfig.schema);
|
|
2961
|
+
let resolvedEmbeddingAdapter = null;
|
|
2962
|
+
if (indexConfig.embedding) {
|
|
2963
|
+
resolvedEmbeddingAdapter = validateEmbeddingConfig(indexConfig.embedding, indexConfig.schema, config?.embedding);
|
|
2964
|
+
}
|
|
2965
|
+
if (indexConfig.required && indexConfig.required.length > 0) {
|
|
2966
|
+
validateRequiredFieldsInSchema(indexConfig.required, indexConfig.schema);
|
|
2967
|
+
}
|
|
1499
2968
|
const language = getLanguage(indexConfig.language ?? "english");
|
|
1500
2969
|
executor.createIndex(name, indexConfig, language);
|
|
1501
|
-
|
|
2970
|
+
const vectorFieldPaths = getVectorFieldPaths(indexConfig.schema);
|
|
2971
|
+
indexRegistry.set(name, {
|
|
2972
|
+
config: indexConfig,
|
|
2973
|
+
language,
|
|
2974
|
+
embeddingAdapter: resolvedEmbeddingAdapter,
|
|
2975
|
+
vectorFieldPaths
|
|
2976
|
+
});
|
|
1502
2977
|
await pluginRegistry.runHook("onIndexCreate", { indexName: name, config: indexConfig });
|
|
1503
2978
|
},
|
|
1504
2979
|
async dropIndex(name) {
|
|
@@ -1525,12 +3000,12 @@ async function createNarsil(config) {
|
|
|
1525
3000
|
guardShutdown();
|
|
1526
3001
|
const entry = requireIndex(indexName);
|
|
1527
3002
|
const manager = executor.getManager(indexName);
|
|
1528
|
-
const
|
|
3003
|
+
const mem = manager?.estimateMemoryBytes() ?? 0;
|
|
1529
3004
|
return {
|
|
1530
3005
|
documentCount: manager?.countDocuments() ?? 0,
|
|
1531
3006
|
partitionCount: manager?.partitionCount ?? 0,
|
|
1532
|
-
memoryBytes:
|
|
1533
|
-
indexSizeBytes:
|
|
3007
|
+
memoryBytes: mem,
|
|
3008
|
+
indexSizeBytes: mem,
|
|
1534
3009
|
language: entry.language.name,
|
|
1535
3010
|
schema: entry.config.schema
|
|
1536
3011
|
};
|
|
@@ -1538,197 +3013,25 @@ async function createNarsil(config) {
|
|
|
1538
3013
|
getPartitionStats(indexName) {
|
|
1539
3014
|
guardShutdown();
|
|
1540
3015
|
requireIndex(indexName);
|
|
1541
|
-
|
|
1542
|
-
return manager?.getPartitionStats() ?? [];
|
|
3016
|
+
return executor.getManager(indexName)?.getPartitionStats() ?? [];
|
|
1543
3017
|
},
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
requireIndex(indexName);
|
|
1547
|
-
const resolvedDocId = docId ?? idGenerator();
|
|
1548
|
-
validateDocId(resolvedDocId);
|
|
1549
|
-
if (bufferIfRebalancing(indexName, { action: "insert", docId: resolvedDocId, document, indexName })) {
|
|
1550
|
-
return resolvedDocId;
|
|
1551
|
-
}
|
|
1552
|
-
await pluginRegistry.runHook("beforeInsert", { indexName, docId: resolvedDocId, document });
|
|
1553
|
-
await executor.execute({
|
|
1554
|
-
type: "insert",
|
|
1555
|
-
indexName,
|
|
1556
|
-
docId: resolvedDocId,
|
|
1557
|
-
document,
|
|
1558
|
-
requestId: resolvedDocId,
|
|
1559
|
-
skipClone: options?.skipClone
|
|
1560
|
-
});
|
|
1561
|
-
try {
|
|
1562
|
-
await pluginRegistry.runHook("afterInsert", { indexName, docId: resolvedDocId, document });
|
|
1563
|
-
} catch (err) {
|
|
1564
|
-
console.warn("afterInsert plugin hook error:", err);
|
|
1565
|
-
}
|
|
1566
|
-
flushManager?.markDirty(indexName, 0);
|
|
1567
|
-
await orchestrator.replicateToWorkers({
|
|
1568
|
-
type: "insert",
|
|
1569
|
-
indexName,
|
|
1570
|
-
docId: resolvedDocId,
|
|
1571
|
-
document,
|
|
1572
|
-
requestId: `replicate-insert-${resolvedDocId}`,
|
|
1573
|
-
skipClone: options?.skipClone
|
|
1574
|
-
});
|
|
1575
|
-
await orchestrator.checkPromotion();
|
|
1576
|
-
return resolvedDocId;
|
|
3018
|
+
insert(indexName, document, docId, options) {
|
|
3019
|
+
return insertDocument(mutationCtx, indexName, document, docId, options);
|
|
1577
3020
|
},
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
requireIndex(indexName);
|
|
1581
|
-
const succeeded = [];
|
|
1582
|
-
const failed = [];
|
|
1583
|
-
const hasBeforeHook = pluginRegistry.hasHooks("beforeInsert");
|
|
1584
|
-
const hasAfterHook = pluginRegistry.hasHooks("afterInsert");
|
|
1585
|
-
for (let chunkStart = 0; chunkStart < documents.length; chunkStart += BATCH_CHUNK_SIZE) {
|
|
1586
|
-
const chunkEnd = Math.min(chunkStart + BATCH_CHUNK_SIZE, documents.length);
|
|
1587
|
-
for (let i = chunkStart; i < chunkEnd; i++) {
|
|
1588
|
-
const docId = idGenerator();
|
|
1589
|
-
try {
|
|
1590
|
-
validateDocId(docId);
|
|
1591
|
-
if (hasBeforeHook) {
|
|
1592
|
-
await pluginRegistry.runHook("beforeInsert", { indexName, docId, document: documents[i] });
|
|
1593
|
-
}
|
|
1594
|
-
const result = executor.execute({
|
|
1595
|
-
type: "insert",
|
|
1596
|
-
indexName,
|
|
1597
|
-
docId,
|
|
1598
|
-
document: documents[i],
|
|
1599
|
-
requestId: docId,
|
|
1600
|
-
skipClone: options?.skipClone
|
|
1601
|
-
});
|
|
1602
|
-
if (result && typeof result.then === "function") {
|
|
1603
|
-
await result;
|
|
1604
|
-
}
|
|
1605
|
-
if (hasAfterHook) {
|
|
1606
|
-
try {
|
|
1607
|
-
await pluginRegistry.runHook("afterInsert", { indexName, docId, document: documents[i] });
|
|
1608
|
-
} catch (err) {
|
|
1609
|
-
console.warn("afterInsert plugin hook error:", err);
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
succeeded.push(docId);
|
|
1613
|
-
} catch (err) {
|
|
1614
|
-
failed.push({
|
|
1615
|
-
docId,
|
|
1616
|
-
error: err instanceof NarsilError ? err : new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, String(err))
|
|
1617
|
-
});
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
if (chunkEnd < documents.length) {
|
|
1621
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
flushManager?.markDirty(indexName, 0);
|
|
1625
|
-
await orchestrator.checkPromotion();
|
|
1626
|
-
return { succeeded, failed };
|
|
3021
|
+
insertBatch(indexName, documents, options) {
|
|
3022
|
+
return insertDocumentBatch(mutationCtx, indexName, documents, options);
|
|
1627
3023
|
},
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
requireIndex(indexName);
|
|
1631
|
-
validateDocId(docId);
|
|
1632
|
-
if (bufferIfRebalancing(indexName, { action: "remove", docId, indexName })) {
|
|
1633
|
-
return;
|
|
1634
|
-
}
|
|
1635
|
-
await pluginRegistry.runHook("beforeRemove", { indexName, docId });
|
|
1636
|
-
await executor.execute({ type: "remove", indexName, docId, requestId: docId });
|
|
1637
|
-
try {
|
|
1638
|
-
await pluginRegistry.runHook("afterRemove", { indexName, docId });
|
|
1639
|
-
} catch (err) {
|
|
1640
|
-
console.warn("afterRemove plugin hook error:", err);
|
|
1641
|
-
}
|
|
1642
|
-
flushManager?.markDirty(indexName, 0);
|
|
1643
|
-
await orchestrator.replicateToWorkers({
|
|
1644
|
-
type: "remove",
|
|
1645
|
-
indexName,
|
|
1646
|
-
docId,
|
|
1647
|
-
requestId: `replicate-remove-${docId}`
|
|
1648
|
-
});
|
|
3024
|
+
remove(indexName, docId) {
|
|
3025
|
+
return removeDocument(mutationCtx, indexName, docId);
|
|
1649
3026
|
},
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
requireIndex(indexName);
|
|
1653
|
-
const succeeded = [];
|
|
1654
|
-
const failed = [];
|
|
1655
|
-
for (let chunkStart = 0; chunkStart < docIds.length; chunkStart += BATCH_CHUNK_SIZE) {
|
|
1656
|
-
const chunkEnd = Math.min(chunkStart + BATCH_CHUNK_SIZE, docIds.length);
|
|
1657
|
-
for (let i = chunkStart; i < chunkEnd; i++) {
|
|
1658
|
-
try {
|
|
1659
|
-
await narsil.remove(indexName, docIds[i]);
|
|
1660
|
-
succeeded.push(docIds[i]);
|
|
1661
|
-
} catch (err) {
|
|
1662
|
-
failed.push({
|
|
1663
|
-
docId: docIds[i],
|
|
1664
|
-
error: err instanceof NarsilError ? err : new NarsilError(ErrorCodes.DOC_NOT_FOUND, String(err))
|
|
1665
|
-
});
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
if (chunkEnd < docIds.length) {
|
|
1669
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
return { succeeded, failed };
|
|
3027
|
+
removeBatch(indexName, docIds) {
|
|
3028
|
+
return removeDocumentBatch(mutationCtx, indexName, docIds);
|
|
1673
3029
|
},
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
requireIndex(indexName);
|
|
1677
|
-
validateDocId(docId);
|
|
1678
|
-
if (bufferIfRebalancing(indexName, { action: "update", docId, document, indexName })) {
|
|
1679
|
-
return;
|
|
1680
|
-
}
|
|
1681
|
-
const manager = executor.getManager(indexName);
|
|
1682
|
-
const oldDocument = manager?.get(docId);
|
|
1683
|
-
await pluginRegistry.runHook("beforeUpdate", {
|
|
1684
|
-
indexName,
|
|
1685
|
-
docId,
|
|
1686
|
-
oldDocument: oldDocument ?? {},
|
|
1687
|
-
newDocument: document
|
|
1688
|
-
});
|
|
1689
|
-
await executor.execute({ type: "update", indexName, docId, document, requestId: docId });
|
|
1690
|
-
try {
|
|
1691
|
-
await pluginRegistry.runHook("afterUpdate", {
|
|
1692
|
-
indexName,
|
|
1693
|
-
docId,
|
|
1694
|
-
oldDocument: oldDocument ?? {},
|
|
1695
|
-
newDocument: document
|
|
1696
|
-
});
|
|
1697
|
-
} catch (err) {
|
|
1698
|
-
console.warn("afterUpdate plugin hook error:", err);
|
|
1699
|
-
}
|
|
1700
|
-
flushManager?.markDirty(indexName, 0);
|
|
1701
|
-
await orchestrator.replicateToWorkers({
|
|
1702
|
-
type: "update",
|
|
1703
|
-
indexName,
|
|
1704
|
-
docId,
|
|
1705
|
-
document,
|
|
1706
|
-
requestId: `replicate-update-${docId}`
|
|
1707
|
-
});
|
|
3030
|
+
update(indexName, docId, document) {
|
|
3031
|
+
return updateDocument(mutationCtx, indexName, docId, document);
|
|
1708
3032
|
},
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
requireIndex(indexName);
|
|
1712
|
-
const succeeded = [];
|
|
1713
|
-
const failed = [];
|
|
1714
|
-
for (let chunkStart = 0; chunkStart < updates.length; chunkStart += BATCH_CHUNK_SIZE) {
|
|
1715
|
-
const chunkEnd = Math.min(chunkStart + BATCH_CHUNK_SIZE, updates.length);
|
|
1716
|
-
for (let i = chunkStart; i < chunkEnd; i++) {
|
|
1717
|
-
try {
|
|
1718
|
-
await narsil.update(indexName, updates[i].docId, updates[i].document);
|
|
1719
|
-
succeeded.push(updates[i].docId);
|
|
1720
|
-
} catch (err) {
|
|
1721
|
-
failed.push({
|
|
1722
|
-
docId: updates[i].docId,
|
|
1723
|
-
error: err instanceof NarsilError ? err : new NarsilError(ErrorCodes.DOC_NOT_FOUND, String(err))
|
|
1724
|
-
});
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
if (chunkEnd < updates.length) {
|
|
1728
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
return { succeeded, failed };
|
|
3033
|
+
updateBatch(indexName, updates) {
|
|
3034
|
+
return updateDocumentBatch(mutationCtx, indexName, updates);
|
|
1732
3035
|
},
|
|
1733
3036
|
async get(indexName, docId) {
|
|
1734
3037
|
guardShutdown();
|
|
@@ -1741,9 +3044,7 @@ async function createNarsil(config) {
|
|
|
1741
3044
|
const result = /* @__PURE__ */ new Map();
|
|
1742
3045
|
for (const docId of docIds) {
|
|
1743
3046
|
const doc = await narsil.get(indexName, docId);
|
|
1744
|
-
if (doc !== void 0)
|
|
1745
|
-
result.set(docId, doc);
|
|
1746
|
-
}
|
|
3047
|
+
if (doc !== void 0) result.set(docId, doc);
|
|
1747
3048
|
}
|
|
1748
3049
|
return result;
|
|
1749
3050
|
},
|
|
@@ -1761,9 +3062,10 @@ async function createNarsil(config) {
|
|
|
1761
3062
|
guardShutdown();
|
|
1762
3063
|
const entry = requireIndex(indexName);
|
|
1763
3064
|
const manager = requireManager(indexName);
|
|
1764
|
-
await
|
|
3065
|
+
const resolvedParams = await resolveVectorText(params, entry.embeddingAdapter, abortController.signal);
|
|
3066
|
+
await pluginRegistry.runHook("beforeSearch", { indexName, params: resolvedParams });
|
|
1765
3067
|
const workerSearch = orchestrator.isPromoted() ? orchestrator.searchViaWorker.bind(orchestrator) : void 0;
|
|
1766
|
-
const result = await executeQuery(
|
|
3068
|
+
const result = await executeQuery(resolvedParams, {
|
|
1767
3069
|
manager,
|
|
1768
3070
|
language: entry.language,
|
|
1769
3071
|
config: entry.config,
|
|
@@ -1773,7 +3075,7 @@ async function createNarsil(config) {
|
|
|
1773
3075
|
try {
|
|
1774
3076
|
await pluginRegistry.runHook("afterSearch", {
|
|
1775
3077
|
indexName,
|
|
1776
|
-
params,
|
|
3078
|
+
params: resolvedParams,
|
|
1777
3079
|
results: result
|
|
1778
3080
|
});
|
|
1779
3081
|
} catch (err) {
|
|
@@ -1785,8 +3087,9 @@ async function createNarsil(config) {
|
|
|
1785
3087
|
guardShutdown();
|
|
1786
3088
|
const entry = requireIndex(indexName);
|
|
1787
3089
|
const manager = requireManager(indexName);
|
|
3090
|
+
const resolvedParams = await resolveVectorText(params, entry.embeddingAdapter, abortController.signal);
|
|
1788
3091
|
const workerSearch = orchestrator.isPromoted() ? orchestrator.searchViaWorker.bind(orchestrator) : void 0;
|
|
1789
|
-
return executePreflight(
|
|
3092
|
+
return executePreflight(resolvedParams, {
|
|
1790
3093
|
manager,
|
|
1791
3094
|
language: entry.language,
|
|
1792
3095
|
config: entry.config,
|
|
@@ -1796,105 +3099,23 @@ async function createNarsil(config) {
|
|
|
1796
3099
|
},
|
|
1797
3100
|
async suggest(indexName, params) {
|
|
1798
3101
|
guardShutdown();
|
|
1799
|
-
|
|
1800
|
-
const manager = requireManager(indexName);
|
|
1801
|
-
const t0 = performance.now();
|
|
1802
|
-
const limit = Math.max(1, Math.min(params.limit ?? 10, 100));
|
|
1803
|
-
const rawPrefix = params.prefix.trim();
|
|
1804
|
-
if (rawPrefix.length === 0) {
|
|
1805
|
-
return { terms: [], elapsed: performance.now() - t0 };
|
|
1806
|
-
}
|
|
1807
|
-
const unstemmed = tokenize(rawPrefix, entry.language, { stem: false, removeStopWords: false });
|
|
1808
|
-
const lastToken = unstemmed.tokens.length > 0 ? unstemmed.tokens[unstemmed.tokens.length - 1].token : rawPrefix.toLowerCase();
|
|
1809
|
-
if (lastToken.length === 0) {
|
|
1810
|
-
return { terms: [], elapsed: performance.now() - t0 };
|
|
1811
|
-
}
|
|
1812
|
-
const stemmed = entry.language.stemmer ? entry.language.stemmer(lastToken) : lastToken;
|
|
1813
|
-
const prefixes = stemmed !== lastToken ? [lastToken, stemmed] : [lastToken];
|
|
1814
|
-
const partitions = manager.getAllPartitions();
|
|
1815
|
-
const merged = /* @__PURE__ */ new Map();
|
|
1816
|
-
const perPartitionLimit = limit * 2;
|
|
1817
|
-
for (const partition of partitions) {
|
|
1818
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1819
|
-
for (const prefix of prefixes) {
|
|
1820
|
-
const suggestions = partition.suggestTerms(prefix, perPartitionLimit);
|
|
1821
|
-
for (const s of suggestions) {
|
|
1822
|
-
if (seen.has(s.term)) continue;
|
|
1823
|
-
seen.add(s.term);
|
|
1824
|
-
merged.set(s.term, (merged.get(s.term) ?? 0) + s.documentFrequency);
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
const terms = Array.from(merged.entries()).map(([term, documentFrequency]) => ({ term, documentFrequency })).sort((a, b) => b.documentFrequency - a.documentFrequency);
|
|
1829
|
-
if (terms.length > limit) terms.length = limit;
|
|
1830
|
-
return { terms, elapsed: performance.now() - t0 };
|
|
3102
|
+
return executeSuggest(requireManager(indexName), requireIndex(indexName).language, params);
|
|
1831
3103
|
},
|
|
1832
3104
|
async snapshot(indexName) {
|
|
1833
3105
|
guardShutdown();
|
|
1834
|
-
|
|
1835
|
-
const manager = requireManager(indexName);
|
|
1836
|
-
const partitionBuffers = [];
|
|
1837
|
-
for (let i = 0; i < manager.partitionCount; i++) {
|
|
1838
|
-
partitionBuffers.push(manager.serializePartitionToBytes(i));
|
|
1839
|
-
}
|
|
1840
|
-
const { encode: encode2 } = await import('@msgpack/msgpack');
|
|
1841
|
-
return encode2({
|
|
1842
|
-
version: 2,
|
|
1843
|
-
schema: entry.config.schema,
|
|
1844
|
-
language: entry.language.name,
|
|
1845
|
-
partitions: partitionBuffers
|
|
1846
|
-
});
|
|
3106
|
+
return createSnapshot(requireManager(indexName), requireIndex(indexName));
|
|
1847
3107
|
},
|
|
1848
3108
|
async restore(indexName, data) {
|
|
1849
3109
|
guardShutdown();
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
{ version: envelope.version }
|
|
1860
|
-
);
|
|
1861
|
-
}
|
|
1862
|
-
if (!envelope.schema || typeof envelope.schema !== "object") {
|
|
1863
|
-
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Invalid snapshot: missing or invalid schema");
|
|
1864
|
-
}
|
|
1865
|
-
if (!envelope.language || typeof envelope.language !== "string") {
|
|
1866
|
-
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Invalid snapshot: missing or invalid language");
|
|
1867
|
-
}
|
|
1868
|
-
if (!Array.isArray(envelope.partitions)) {
|
|
1869
|
-
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Invalid snapshot: missing partitions");
|
|
1870
|
-
}
|
|
1871
|
-
const language = getLanguage(envelope.language);
|
|
1872
|
-
const schema = envelope.schema;
|
|
1873
|
-
validateSchema(schema);
|
|
1874
|
-
if (indexRegistry.has(indexName)) {
|
|
1875
|
-
await narsil.dropIndex(indexName);
|
|
1876
|
-
}
|
|
1877
|
-
const indexConfig = { schema, language: envelope.language };
|
|
1878
|
-
executor.createIndex(indexName, indexConfig, language);
|
|
1879
|
-
indexRegistry.set(indexName, { config: indexConfig, language });
|
|
1880
|
-
try {
|
|
1881
|
-
const manager = requireManager(indexName);
|
|
1882
|
-
while (manager.partitionCount < envelope.partitions.length) {
|
|
1883
|
-
manager.addPartition();
|
|
1884
|
-
}
|
|
1885
|
-
const deserializePartitionPayload = envelope.version === 2 ? deserializePayloadV2 : deserializePayloadV1;
|
|
1886
|
-
for (let i = 0; i < envelope.partitions.length; i++) {
|
|
1887
|
-
const partition = deserializePartitionPayload(envelope.partitions[i]);
|
|
1888
|
-
manager.deserializePartition(i, partition);
|
|
1889
|
-
}
|
|
1890
|
-
} catch (err) {
|
|
1891
|
-
try {
|
|
1892
|
-
executor.dropIndex(indexName);
|
|
1893
|
-
indexRegistry.delete(indexName);
|
|
1894
|
-
} catch (_) {
|
|
1895
|
-
}
|
|
1896
|
-
throw err;
|
|
1897
|
-
}
|
|
3110
|
+
return restoreFromSnapshot(
|
|
3111
|
+
indexName,
|
|
3112
|
+
data,
|
|
3113
|
+
executor,
|
|
3114
|
+
indexRegistry,
|
|
3115
|
+
getVectorFieldPaths,
|
|
3116
|
+
narsil.dropIndex.bind(narsil),
|
|
3117
|
+
requireManager
|
|
3118
|
+
);
|
|
1898
3119
|
},
|
|
1899
3120
|
async clear(indexName) {
|
|
1900
3121
|
guardShutdown();
|
|
@@ -1905,65 +3126,7 @@ async function createNarsil(config) {
|
|
|
1905
3126
|
async rebalance(indexName, targetPartitionCount) {
|
|
1906
3127
|
guardShutdown();
|
|
1907
3128
|
requireIndex(indexName);
|
|
1908
|
-
|
|
1909
|
-
if (targetPartitionCount <= 0 || !Number.isInteger(targetPartitionCount)) {
|
|
1910
|
-
throw new NarsilError(
|
|
1911
|
-
ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
|
|
1912
|
-
`Target partition count must be a positive integer, got ${targetPartitionCount}`,
|
|
1913
|
-
{ targetPartitionCount }
|
|
1914
|
-
);
|
|
1915
|
-
}
|
|
1916
|
-
if (rebalancingIndexes.has(indexName)) {
|
|
1917
|
-
throw new NarsilError(
|
|
1918
|
-
ErrorCodes.PARTITION_REBALANCING_BACKPRESSURE,
|
|
1919
|
-
`Index "${indexName}" is already being rebalanced`
|
|
1920
|
-
);
|
|
1921
|
-
}
|
|
1922
|
-
if (targetPartitionCount === manager.partitionCount) {
|
|
1923
|
-
return;
|
|
1924
|
-
}
|
|
1925
|
-
const waq = createWriteAheadQueue();
|
|
1926
|
-
waqMap.set(indexName, waq);
|
|
1927
|
-
rebalancingIndexes.add(indexName);
|
|
1928
|
-
try {
|
|
1929
|
-
const oldCount = manager.partitionCount;
|
|
1930
|
-
await rebalancer.rebalance(manager, targetPartitionCount, rebalanceRouter, (progress) => {
|
|
1931
|
-
if (progress.phase === "complete") {
|
|
1932
|
-
const handlers = eventHandlers.get("partitionRebalance");
|
|
1933
|
-
if (handlers) {
|
|
1934
|
-
for (const handler of handlers) {
|
|
1935
|
-
handler({ indexName, oldCount, newCount: targetPartitionCount });
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
});
|
|
1940
|
-
const entries = waq.drain();
|
|
1941
|
-
const appliedSeqs = lastAppliedSeqMap.get(indexName) ?? /* @__PURE__ */ new Map();
|
|
1942
|
-
for (const entry of entries) {
|
|
1943
|
-
const partitionLastSeq = appliedSeqs.get(0) ?? 0;
|
|
1944
|
-
if (entry.sequenceNumber <= partitionLastSeq) continue;
|
|
1945
|
-
try {
|
|
1946
|
-
if (entry.action === "insert" && entry.document) {
|
|
1947
|
-
manager.insert(entry.docId, entry.document);
|
|
1948
|
-
} else if (entry.action === "remove") {
|
|
1949
|
-
manager.remove(entry.docId);
|
|
1950
|
-
} else if (entry.action === "update" && entry.document) {
|
|
1951
|
-
manager.update(entry.docId, entry.document);
|
|
1952
|
-
}
|
|
1953
|
-
} catch (replayErr) {
|
|
1954
|
-
const isDuplicate = replayErr instanceof NarsilError && replayErr.code === ErrorCodes.DOC_ALREADY_EXISTS;
|
|
1955
|
-
const isMissing = replayErr instanceof NarsilError && replayErr.code === ErrorCodes.DOC_NOT_FOUND;
|
|
1956
|
-
if (!isDuplicate && !isMissing) {
|
|
1957
|
-
console.warn(`WAQ replay failed for ${entry.action} on doc "${entry.docId}":`, replayErr);
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
appliedSeqs.set(0, entry.sequenceNumber);
|
|
1961
|
-
}
|
|
1962
|
-
lastAppliedSeqMap.set(indexName, appliedSeqs);
|
|
1963
|
-
} finally {
|
|
1964
|
-
rebalancingIndexes.delete(indexName);
|
|
1965
|
-
waqMap.delete(indexName);
|
|
1966
|
-
}
|
|
3129
|
+
return executeRebalance(requireManager(indexName), indexName, targetPartitionCount, rebalanceCtx);
|
|
1967
3130
|
},
|
|
1968
3131
|
async updatePartitionConfig(indexName, partitionConfig) {
|
|
1969
3132
|
guardShutdown();
|
|
@@ -1988,29 +3151,35 @@ async function createNarsil(config) {
|
|
|
1988
3151
|
);
|
|
1989
3152
|
}
|
|
1990
3153
|
}
|
|
1991
|
-
if (!entry.config.partitions) {
|
|
1992
|
-
|
|
1993
|
-
}
|
|
1994
|
-
if (partitionConfig.maxDocsPerPartition !== void 0) {
|
|
3154
|
+
if (!entry.config.partitions) entry.config.partitions = {};
|
|
3155
|
+
if (partitionConfig.maxDocsPerPartition !== void 0)
|
|
1995
3156
|
entry.config.partitions.maxDocsPerPartition = partitionConfig.maxDocsPerPartition;
|
|
1996
|
-
|
|
1997
|
-
if (partitionConfig.maxPartitions !== void 0) {
|
|
3157
|
+
if (partitionConfig.maxPartitions !== void 0)
|
|
1998
3158
|
entry.config.partitions.maxPartitions = partitionConfig.maxPartitions;
|
|
1999
|
-
}
|
|
2000
3159
|
},
|
|
2001
3160
|
getMemoryStats() {
|
|
2002
3161
|
const workerStats = orchestrator.getMemoryStats();
|
|
2003
3162
|
let mainThreadBytes = 0;
|
|
2004
3163
|
for (const [name] of indexRegistry) {
|
|
2005
|
-
const
|
|
2006
|
-
if (
|
|
2007
|
-
mainThreadBytes += manager.estimateMemoryBytes();
|
|
2008
|
-
}
|
|
3164
|
+
const mgr = executor.getManager(name);
|
|
3165
|
+
if (mgr) mainThreadBytes += mgr.estimateMemoryBytes();
|
|
2009
3166
|
}
|
|
2010
|
-
return {
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
3167
|
+
return { totalBytes: mainThreadBytes + workerStats.totalBytes, workers: workerStats.workers };
|
|
3168
|
+
},
|
|
3169
|
+
async compactVectors(indexName, fieldName) {
|
|
3170
|
+
guardShutdown();
|
|
3171
|
+
requireIndex(indexName);
|
|
3172
|
+
compactVectors(requireManager(indexName), indexName, fieldName);
|
|
3173
|
+
},
|
|
3174
|
+
async optimizeVectors(indexName, fieldName) {
|
|
3175
|
+
guardShutdown();
|
|
3176
|
+
requireIndex(indexName);
|
|
3177
|
+
await optimizeVectors(requireManager(indexName), indexName, fieldName);
|
|
3178
|
+
},
|
|
3179
|
+
vectorMaintenanceStatus(indexName) {
|
|
3180
|
+
guardShutdown();
|
|
3181
|
+
requireIndex(indexName);
|
|
3182
|
+
return getVectorMaintenanceStatus(requireManager(indexName));
|
|
2014
3183
|
},
|
|
2015
3184
|
on(event, handler) {
|
|
2016
3185
|
const key = event;
|
|
@@ -2026,16 +3195,31 @@ async function createNarsil(config) {
|
|
|
2026
3195
|
const handlers = eventHandlers.get(key);
|
|
2027
3196
|
if (handlers) {
|
|
2028
3197
|
handlers.delete(handler);
|
|
2029
|
-
if (handlers.size === 0)
|
|
2030
|
-
eventHandlers.delete(key);
|
|
2031
|
-
}
|
|
3198
|
+
if (handlers.size === 0) eventHandlers.delete(key);
|
|
2032
3199
|
}
|
|
2033
3200
|
},
|
|
2034
3201
|
async shutdown() {
|
|
2035
3202
|
if (isShutdown) return;
|
|
2036
3203
|
isShutdown = true;
|
|
2037
|
-
|
|
2038
|
-
|
|
3204
|
+
abortController.abort();
|
|
3205
|
+
for (const [name] of indexRegistry) {
|
|
3206
|
+
const mgr = executor.getManager(name);
|
|
3207
|
+
if (mgr) {
|
|
3208
|
+
for (const [, vecIdx] of mgr.getVectorIndexes()) {
|
|
3209
|
+
vecIdx.dispose();
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
if (flushManager) await flushManager.shutdown();
|
|
3214
|
+
const adaptersToShutdown = /* @__PURE__ */ new Set();
|
|
3215
|
+
for (const [, entry] of indexRegistry) {
|
|
3216
|
+
if (entry.embeddingAdapter?.shutdown) adaptersToShutdown.add(entry.embeddingAdapter);
|
|
3217
|
+
}
|
|
3218
|
+
for (const adapter of adaptersToShutdown) {
|
|
3219
|
+
try {
|
|
3220
|
+
await adapter.shutdown?.();
|
|
3221
|
+
} catch (_) {
|
|
3222
|
+
}
|
|
2039
3223
|
}
|
|
2040
3224
|
await orchestrator.shutdown();
|
|
2041
3225
|
await executor.shutdown();
|