@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/dist/index.mjs CHANGED
@@ -1,12 +1,872 @@
1
- import { createDirectExecutor, createPartitionRouter, getLanguage, validateSchema, deserializePayloadV2, tokenize, fanOutQuery, createPartitionIndex, detectRuntime, fnv1a, kWayMerge } from './chunk-TFWBALU4.mjs';
2
- export { getLanguage, registerLanguage } from './chunk-TFWBALU4.mjs';
3
- export { isSimdAvailable } from './chunk-GVN2W4VA.mjs';
4
- import './chunk-WYC635GI.mjs';
5
- import { NarsilError, ErrorCodes } from './chunk-7GKPWGCN.mjs';
6
- export { ErrorCodes, NarsilError } from './chunk-7GKPWGCN.mjs';
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 getNestedValue(obj, path) {
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 ? getNestedValue(doc, field) : void 0;
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 getNestedValue2(obj, path) {
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]) => getNestedValue2(doc, 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
- async function executeQuery(params, context) {
873
- const { manager, language, config, workerSearch, indexName } = context;
874
- const startTime = now();
875
- const limit = clampLimit(params.limit);
876
- const offset = clampOffset(params.offset);
877
- let fanOutResult;
878
- const workerResult = workerSearch ? await workerSearch(indexName, params) : null;
879
- if (workerResult) {
880
- fanOutResult = workerResult;
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
- fanOutResult = await fanOutQuery(
1815
+ textFanOutResult = await fanOutQuery(
888
1816
  manager,
889
- params,
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
- let groups;
922
- if (params.group) {
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 (params.pinned) {
926
- hits = applyPinning(hits, params.pinned, (docId) => {
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
- const { paginated, nextCursor } = applyPagination(hits, limit, offset, params.searchAfter);
933
- for (const hit of paginated) {
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
- if (groups) {
937
- for (const group of groups) {
938
- for (const hit of group.hits) {
939
- hit.document = manager.get(hit.id) ?? {};
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
- if (params.highlight) {
944
- applyHighlights(paginated, params, language, config);
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 elapsed = now() - startTime;
947
- return {
948
- hits: paginated,
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
- async function executePreflight(params, context) {
957
- const { manager, language, config, workerSearch, indexName } = context;
958
- const startTime = now();
959
- let totalMatched;
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 applyHighlights(hits, params, language, config) {
983
- if (!params.highlight) return;
984
- const queryTokenResult = tokenize(params.term ?? "", language, {
985
- stem: true,
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
- indexName: raw.index_name,
1346
- partitionId: raw.partition_id,
1347
- totalPartitions: raw.total_partitions,
1348
- language: raw.language ?? "english",
1349
- schema: raw.schema ?? {},
1350
- docCount: raw.doc_count ?? 0,
1351
- avgDocLength: raw.avg_doc_length ?? 0,
1352
- documents,
1353
- invertedIndex,
1354
- fieldIndexes: {
1355
- numeric,
1356
- boolean,
1357
- enum: raw.field_indexes?.enum ?? {},
1358
- geopoint
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
- vectorData,
1361
- statistics: {
1362
- totalDocuments: raw.statistics?.total_documents ?? 0,
1363
- totalFieldLengths: raw.statistics?.total_field_lengths ?? {},
1364
- averageFieldLengths: raw.statistics?.average_field_lengths ?? {},
1365
- docFrequencies: raw.statistics?.doc_frequencies ?? {}
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
- function deserializePayloadV1(data) {
1370
- const raw = decode(data);
1371
- return wireToPartition(raw);
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
- waq.push(entry);
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
- indexRegistry.set(name, { config: indexConfig, language });
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 estimatedMemory = manager?.estimateMemoryBytes() ?? 0;
3003
+ const mem = manager?.estimateMemoryBytes() ?? 0;
1529
3004
  return {
1530
3005
  documentCount: manager?.countDocuments() ?? 0,
1531
3006
  partitionCount: manager?.partitionCount ?? 0,
1532
- memoryBytes: estimatedMemory,
1533
- indexSizeBytes: estimatedMemory,
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
- const manager = executor.getManager(indexName);
1542
- return manager?.getPartitionStats() ?? [];
3016
+ return executor.getManager(indexName)?.getPartitionStats() ?? [];
1543
3017
  },
1544
- async insert(indexName, document, docId, options) {
1545
- guardShutdown();
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
- async insertBatch(indexName, documents, options) {
1579
- guardShutdown();
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
- async remove(indexName, docId) {
1629
- guardShutdown();
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
- async removeBatch(indexName, docIds) {
1651
- guardShutdown();
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
- async update(indexName, docId, document) {
1675
- guardShutdown();
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
- async updateBatch(indexName, updates) {
1710
- guardShutdown();
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 pluginRegistry.runHook("beforeSearch", { indexName, params });
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(params, {
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(params, {
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
- const entry = requireIndex(indexName);
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
- const entry = requireIndex(indexName);
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
- if (!(data instanceof Uint8Array)) {
1851
- throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Snapshot data must be a Uint8Array");
1852
- }
1853
- const { decode: decode2 } = await import('@msgpack/msgpack');
1854
- const envelope = decode2(data);
1855
- if (envelope.version !== 1 && envelope.version !== 2) {
1856
- throw new NarsilError(
1857
- ErrorCodes.DOC_VALIDATION_FAILED,
1858
- `Unsupported snapshot version: ${envelope.version}. Expected version 1 or 2`,
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
- const manager = requireManager(indexName);
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
- entry.config.partitions = {};
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 manager = executor.getManager(name);
2006
- if (manager) {
2007
- mainThreadBytes += manager.estimateMemoryBytes();
2008
- }
3164
+ const mgr = executor.getManager(name);
3165
+ if (mgr) mainThreadBytes += mgr.estimateMemoryBytes();
2009
3166
  }
2010
- return {
2011
- totalBytes: mainThreadBytes + workerStats.totalBytes,
2012
- workers: workerStats.workers
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
- if (flushManager) {
2038
- await flushManager.shutdown();
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();