@delali/narsil 0.2.4-next.52 → 0.2.4-next.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +8 -4
  2. package/dist/adapters/filesystem.mjs +1 -1
  3. package/dist/adapters/indexeddb.mjs +1 -1
  4. package/dist/{capabilities-CuTSXvbN.d.ts → capabilities-DlYw0Ov0.d.ts} +13 -2
  5. package/dist/{chunk-U36IDJN7.mjs → chunk-4NVPQ6S5.mjs} +2 -2
  6. package/dist/{chunk-WM7IS45M.mjs → chunk-5T6AOR37.mjs} +2 -2
  7. package/dist/{chunk-ULJ36FK4.mjs → chunk-BH5SYB5W.mjs} +73 -46
  8. package/dist/{chunk-3GWP2TO6.mjs → chunk-D2S642IV.mjs} +4 -2
  9. package/dist/{chunk-P4ELXVJQ.mjs → chunk-FRQVMFY7.mjs} +7 -1
  10. package/dist/{chunk-ZT5BRDFW.mjs → chunk-L225SHNB.mjs} +2 -2
  11. package/dist/{chunk-72AXTIBW.mjs → chunk-NTZYA3SJ.mjs} +3 -3
  12. package/dist/{chunk-EY2ULUMT.mjs → chunk-RGS7FMCB.mjs} +25 -5
  13. package/dist/{chunk-YPYULM5E.mjs → chunk-TVT5V4E7.mjs} +84 -33
  14. package/dist/{chunk-T62YOSGS.mjs → chunk-U2AL3SON.mjs} +16 -98
  15. package/dist/chunk-UTOFOJOR.mjs +135 -0
  16. package/dist/{chunk-2FO7KZ6K.mjs → chunk-XNQAIO74.mjs} +53 -8
  17. package/dist/{client-BMJWVxOP.d.ts → client-CQUqt9Ir.d.ts} +13 -4
  18. package/dist/client.d.ts +8 -8
  19. package/dist/client.mjs +20 -14
  20. package/dist/{config-qMLpi5T4.d.ts → config-CNas6wJH.d.ts} +2 -2
  21. package/dist/{config-BcYZkdMw.d.ts → config-bXxnJrQc.d.ts} +80 -35
  22. package/dist/direct-executor-RBA5O7EN.mjs +12 -0
  23. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  24. package/dist/distribution/coordinator/etcd.mjs +1 -1
  25. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  26. package/dist/distribution/transport/grpc.mjs +1 -1
  27. package/dist/distribution.d.ts +8 -8
  28. package/dist/distribution.mjs +12 -12
  29. package/dist/embeddings/openai.mjs +1 -1
  30. package/dist/{engine-zhms1qId.d.ts → engine-Dj4I91Jv.d.ts} +21 -8
  31. package/dist/index.browser.mjs +169 -72
  32. package/dist/index.d.ts +7 -7
  33. package/dist/index.mjs +10 -10
  34. package/dist/{memory-OmgkX_Z2.d.ts → memory-xrXOZMbw.d.ts} +58 -13
  35. package/dist/persistence/durability/checkpoint-worker.d.ts +1 -1
  36. package/dist/persistence/durability/checkpoint-worker.mjs +10 -10
  37. package/dist/react.d.ts +11 -11
  38. package/dist/react.mjs +16 -4
  39. package/dist/{schema-OSWaaZzx.d.ts → schema-mLpXZBEh.d.ts} +13 -6
  40. package/dist/server.d.ts +13 -10
  41. package/dist/server.mjs +68 -20
  42. package/dist/{tasks-DiF1c9v2.d.ts → tasks-Df73Tjgq.d.ts} +9 -5
  43. package/dist/{thread-TLCX43WV.mjs → thread-5UP6MGED.mjs} +8 -8
  44. package/dist/{types-LEzMyjVX.d.ts → types-BTIVO8DI.d.ts} +1 -1
  45. package/dist/vector/search-worker.d.ts +1 -1
  46. package/dist/vector/search-worker.mjs +3 -3
  47. package/dist/workers/entry.mjs +23 -10
  48. package/package.json +7 -7
  49. package/dist/chunk-WFKIGEBY.mjs +0 -11
  50. package/dist/direct-executor-UJC36S2D.mjs +0 -12
@@ -1,6 +1,7 @@
1
1
  import { encode, decode } from '@msgpack/msgpack';
2
2
 
3
3
  // src/errors.ts
4
+ var NARSIL_ERROR_MARKER = /* @__PURE__ */ Symbol.for("@delali/narsil.NarsilError");
4
5
  var ErrorCodes = {
5
6
  SCHEMA_INVALID_TYPE: "SCHEMA_INVALID_TYPE",
6
7
  SCHEMA_DEPTH_EXCEEDED: "SCHEMA_DEPTH_EXCEEDED",
@@ -18,6 +19,7 @@ var ErrorCodes = {
18
19
  WORKER_CRASHED: "WORKER_CRASHED",
19
20
  WORKER_BUSY: "WORKER_BUSY",
20
21
  WORKER_TIMEOUT: "WORKER_TIMEOUT",
22
+ WORKER_ACTION_FAILED: "WORKER_ACTION_FAILED",
21
23
  PERSISTENCE_SAVE_FAILED: "PERSISTENCE_SAVE_FAILED",
22
24
  PERSISTENCE_LOAD_FAILED: "PERSISTENCE_LOAD_FAILED",
23
25
  PERSISTENCE_DELETE_FAILED: "PERSISTENCE_DELETE_FAILED",
@@ -137,8 +139,12 @@ var NarsilError = class extends Error {
137
139
  this.name = "NarsilError";
138
140
  this.code = code;
139
141
  this.details = details ?? {};
142
+ Object.defineProperty(this, NARSIL_ERROR_MARKER, { value: true });
140
143
  }
141
144
  };
145
+ function isNarsilError(value) {
146
+ return typeof value === "object" && value !== null && NARSIL_ERROR_MARKER in value;
147
+ }
142
148
  function createNarsilError(code, message, details) {
143
149
  return new NarsilError(code, message, details);
144
150
  }
@@ -14325,7 +14331,11 @@ function createHNSWIndex(dimension, store, config, quantizer, handles) {
14325
14331
  throw new Error(`Cannot insert HNSW node: vector for "${docId}" not found in VectorStore`);
14326
14332
  }
14327
14333
  if (entry.vector.length !== dimension) {
14328
- throw new Error(`Vector dimension mismatch: expected ${dimension}, got ${entry.vector.length}`);
14334
+ throw new NarsilError(
14335
+ ErrorCodes.VECTOR_DIMENSION_MISMATCH,
14336
+ `Vector dimension mismatch: expected ${dimension}, got ${entry.vector.length}`,
14337
+ { docId, expected: dimension, received: entry.vector.length }
14338
+ );
14329
14339
  }
14330
14340
  insertNode(state, ord);
14331
14341
  }
@@ -16182,8 +16192,9 @@ function completeGraph(state) {
16182
16192
  }
16183
16193
  function maintenanceStatus(state) {
16184
16194
  const storeSize = state.store.size;
16185
- const tombstoneRatio = storeSize > 0 ? state.tombstones.size / storeSize : 0;
16186
16195
  const graphCount = state.hnsw ? 1 : 0;
16196
+ const graphNodes = state.hnsw === null ? storeSize : state.hnsw.size + state.hnsw.tombstoneCount;
16197
+ const tombstoneRatio = graphNodes > 0 ? state.tombstones.size / graphNodes : 0;
16187
16198
  const estimatedCompactMs = Math.round(state.tombstones.size * ESTIMATED_MS_PER_TOMBSTONE * state.dimensionScale);
16188
16199
  const optimizeVectorCount = state.hnsw === null || graphNeedsRebuild(state) ? storeSize : state.buffer.size;
16189
16200
  const estimatedOptimizeMs = Math.round(optimizeVectorCount * ESTIMATED_MS_PER_VECTOR_REBUILD * state.dimensionScale);
@@ -16402,7 +16413,11 @@ function search2(state, query, k, options) {
16402
16413
  }
16403
16414
  function searchWithFilter(state, query, k, options, filter) {
16404
16415
  if (query.length !== state.dimension) {
16405
- throw new Error(`Vector dimension mismatch: expected ${state.dimension}, got ${query.length}`);
16416
+ throw new NarsilError(
16417
+ ErrorCodes.VECTOR_DIMENSION_MISMATCH,
16418
+ `Vector dimension mismatch: expected ${state.dimension}, got ${query.length}`,
16419
+ { expected: state.dimension, received: query.length }
16420
+ );
16406
16421
  }
16407
16422
  const currentLiveSize = liveSize(state);
16408
16423
  if (currentLiveSize === 0) return [];
@@ -19034,7 +19049,9 @@ function createDirectExecutor(options) {
19034
19049
  }
19035
19050
  case "memoryReport": {
19036
19051
  const usage = typeof process !== "undefined" && process.memoryUsage ? process.memoryUsage() : {};
19037
- return { ...usage, heapLimit: null };
19052
+ const heap = readHeapStatistics();
19053
+ const heapLimit = heap === null ? null : heap.configuredLimitBytes ?? heap.limitBytes;
19054
+ return { ...usage, heapLimit };
19038
19055
  }
19039
19056
  case "shutdown": {
19040
19057
  for (const [, entry] of indexes) {
@@ -19910,7 +19927,7 @@ async function unpackEnvelope(data, maxAcceptedVersion = CURRENT_ENVELOPE_VERSIO
19910
19927
  if (header.envelopeFormatVersion > maxAcceptedVersion) {
19911
19928
  throw new NarsilError(
19912
19929
  ErrorCodes.ENVELOPE_VERSION_MISMATCH,
19913
- `This data was written by Narsil envelope format v${header.envelopeFormatVersion} and requires a newer version of Narsil. You are running ${VERSION}.`
19930
+ `This data holds Narsil envelope format v${header.envelopeFormatVersion}, while this build of @delali/narsil reads up to v${maxAcceptedVersion}, so upgrade the package to read it.`
19914
19931
  );
19915
19932
  }
19916
19933
  if (header.flags.encryptionEnabled) {
@@ -22443,6 +22460,8 @@ async function spawnWorker3() {
22443
22460
  if (typeof worker.unref === "function") {
22444
22461
  worker.unref();
22445
22462
  }
22463
+ worker.on("error", () => discardWorker(worker));
22464
+ worker.on("exit", () => discardWorker(worker));
22446
22465
  spawnedWorkerCount += 1;
22447
22466
  return worker;
22448
22467
  } catch {
@@ -24082,6 +24101,8 @@ var BATCH_CHUNK_SIZE = 1e3;
24082
24101
  var DEFAULT_LIMIT = DEFAULT_PAGE_SIZE;
24083
24102
  var DEFAULT_OFFSET = 0;
24084
24103
  var INDEX_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
24104
+ var ABSOLUTE_WINDOWS_PATH_PATTERN = /^[a-zA-Z]:[\\/]/;
24105
+ var MODULE_URL_SCHEME_PATTERN = /^(file|https?|data|node):/;
24085
24106
  var MAX_INDEX_NAME_LENGTH = 255;
24086
24107
  var MAX_DOC_ID_LENGTH = 512;
24087
24108
  var MIN_SWEEP_INTERVAL_MS = 100;
@@ -24644,12 +24665,20 @@ function createInvalidationIntegration(deps) {
24644
24665
  }
24645
24666
 
24646
24667
  // src/engine/heap-pressure.ts
24668
+ function spentHeapFraction(heap) {
24669
+ if (heap.configuredLimitBytes !== null) return heap.usedBytes / heap.configuredLimitBytes;
24670
+ if (heap.availableBytes === null) return heap.usedBytes / heap.limitBytes;
24671
+ return 1 - heap.availableBytes / heap.limitBytes;
24672
+ }
24673
+ function ceilingBytes(heap) {
24674
+ return heap.configuredLimitBytes ?? heap.limitBytes;
24675
+ }
24647
24676
  function createHeapPressureNotifier(deps) {
24648
24677
  let warned = false;
24649
24678
  function check(indexName) {
24650
24679
  const heap = deps.readHeap();
24651
24680
  if (heap === null) return;
24652
- const fraction = heap.usedBytes / heap.limitBytes;
24681
+ const fraction = spentHeapFraction(heap);
24653
24682
  if (warned) {
24654
24683
  if (fraction < HEAP_PRESSURE_REARM_FRACTION) warned = false;
24655
24684
  return;
@@ -24659,7 +24688,7 @@ function createHeapPressureNotifier(deps) {
24659
24688
  deps.emit({
24660
24689
  indexName,
24661
24690
  heapUsed: heap.usedBytes,
24662
- heapLimit: heap.limitBytes,
24691
+ heapLimit: ceilingBytes(heap),
24663
24692
  estimatedMemoryBytes: deps.estimateIndexBytes(indexName)
24664
24693
  });
24665
24694
  }
@@ -25407,7 +25436,7 @@ function createWorkerExecutor(worker, config) {
25407
25436
  clearTimeout(entry.timeoutId);
25408
25437
  pending.delete(response.requestId);
25409
25438
  if (response.type === "error") {
25410
- entry.reject(new NarsilError(response.code, response.message));
25439
+ entry.reject(new NarsilError(response.code, response.message, response.details));
25411
25440
  } else {
25412
25441
  entry.resolve(response.data);
25413
25442
  }
@@ -25963,6 +25992,7 @@ function scheduleRequestThreadPoolRestart(state) {
25963
25992
  if (typeof timer.unref === "function") timer.unref();
25964
25993
  }
25965
25994
  async function serveRequestsOnWorkers(state, listener) {
25995
+ if (!state.workersEnabled) return 0;
25966
25996
  state.requestThreads = listener;
25967
25997
  state.keywordWorkerCount = resolveRequestThreadCount(state.config?.workers?.count);
25968
25998
  if (state.vectorCopyPolicy !== void 0) {
@@ -25985,6 +26015,7 @@ async function serveRequestsOnWorkers(state, listener) {
25985
26015
  return pool.workerCount;
25986
26016
  }
25987
26017
  function requestThreadCountOf(state) {
26018
+ if (!state.workersEnabled) return 0;
25988
26019
  return resolveRequestThreadCount(state.config?.workers?.count);
25989
26020
  }
25990
26021
  function stopRequestThreads(state) {
@@ -26694,32 +26725,37 @@ function now() {
26694
26725
  return Date.now();
26695
26726
  }
26696
26727
  function validateIndexName(name) {
26697
- if (!name || name.length === 0) {
26698
- throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, "Index name must not be empty", { indexName: name });
26728
+ const given = name;
26729
+ if (typeof given !== "string" || name.length === 0) {
26730
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "Index name must be a string of at least one character", {
26731
+ received: given === null ? "null" : typeof given
26732
+ });
26699
26733
  }
26700
26734
  if (name.length > MAX_INDEX_NAME_LENGTH) {
26701
- throw new NarsilError(
26702
- ErrorCodes.INDEX_NOT_FOUND,
26703
- `Index name must not exceed ${MAX_INDEX_NAME_LENGTH} characters`,
26704
- {
26705
- indexName: name,
26706
- length: name.length
26707
- }
26708
- );
26735
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, `Index name must not exceed ${MAX_INDEX_NAME_LENGTH} characters`, {
26736
+ indexName: name,
26737
+ length: name.length
26738
+ });
26709
26739
  }
26710
26740
  if (!INDEX_NAME_PATTERN.test(name)) {
26711
26741
  throw new NarsilError(
26712
- ErrorCodes.INDEX_NOT_FOUND,
26742
+ ErrorCodes.CONFIG_INVALID,
26713
26743
  `Index name "${name}" contains invalid characters; use alphanumeric, dots, hyphens, and underscores only`,
26714
26744
  { indexName: name }
26715
26745
  );
26716
26746
  }
26717
26747
  if (name.includes("..")) {
26718
- throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index name "${name}" must not contain ".."`, { indexName: name });
26748
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, `Index name "${name}" must not contain ".."`, { indexName: name });
26719
26749
  }
26720
26750
  }
26721
26751
  function validateDocId(docId) {
26722
- if (!docId || docId.length === 0) {
26752
+ const given = docId;
26753
+ if (typeof given !== "string") {
26754
+ throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must be a string", {
26755
+ received: given === null ? "null" : Array.isArray(given) ? "array" : typeof given
26756
+ });
26757
+ }
26758
+ if (docId.length === 0) {
26723
26759
  throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not be empty", { docId });
26724
26760
  }
26725
26761
  if (docId.length > MAX_DOC_ID_LENGTH) {
@@ -26760,6 +26796,16 @@ function requirePositiveSafeInteger(field, value) {
26760
26796
  throw new NarsilError(ErrorCodes.CONFIG_INVALID, `${field} must be a positive safe integer`, { field, value });
26761
26797
  }
26762
26798
  }
26799
+ function validateBootstrapModule(bootstrapModule) {
26800
+ if (bootstrapModule === void 0) return;
26801
+ const absolute = bootstrapModule.startsWith("/") || ABSOLUTE_WINDOWS_PATH_PATTERN.test(bootstrapModule) || MODULE_URL_SCHEME_PATTERN.test(bootstrapModule);
26802
+ if (absolute) return;
26803
+ throw new NarsilError(
26804
+ ErrorCodes.CONFIG_INVALID,
26805
+ `workers.bootstrapModule is "${bootstrapModule}", which a worker would resolve against Narsil's own directory, so pass an absolute path or a URL such as new URL('./bootstrap.js', import.meta.url).href`,
26806
+ { bootstrapModule }
26807
+ );
26808
+ }
26763
26809
  function validateWorkerConfig(workers, lifecycle) {
26764
26810
  if (workers === void 0) return;
26765
26811
  requirePositiveSafeInteger("workers.count", workers.count);
@@ -26770,6 +26816,7 @@ function validateWorkerConfig(workers, lifecycle) {
26770
26816
  mainCopyQueries: workers.mainCopyQueries
26771
26817
  });
26772
26818
  }
26819
+ validateBootstrapModule(workers.bootstrapModule);
26773
26820
  const closeAfter = lifecycle?.idleTimeoutMs;
26774
26821
  if (workers.idleTimeoutMs !== void 0 && closeAfter !== void 0 && workers.idleTimeoutMs > closeAfter) {
26775
26822
  throw new NarsilError(
@@ -27036,6 +27083,16 @@ function createEngineCore(config, hooks) {
27036
27083
  }
27037
27084
 
27038
27085
  // src/engine/index-lifecycle.ts
27086
+ async function runLifecycleHook(core, hookName, context) {
27087
+ try {
27088
+ await core.pluginRegistry.runHook(hookName, context);
27089
+ } catch (err) {
27090
+ console.warn(`${hookName} plugin hook error:`, err instanceof Error ? err.message : String(err));
27091
+ }
27092
+ }
27093
+ async function announceIndexCreated(core, name, config) {
27094
+ await runLifecycleHook(core, "onIndexCreate", { indexName: name, config });
27095
+ }
27039
27096
  async function createEngineIndex(core, config, name, indexConfig, indexUuid) {
27040
27097
  core.guardShutdown();
27041
27098
  validateIndexName(name);
@@ -27107,7 +27164,7 @@ async function createEngineIndex(core, config, name, indexConfig, indexUuid) {
27107
27164
  if (core.durability) {
27108
27165
  await core.durability.manager.persistMetadata(name);
27109
27166
  }
27110
- await core.pluginRegistry.runHook("onIndexCreate", { indexName: name, config: indexConfig });
27167
+ await runLifecycleHook(core, "onIndexCreate", { indexName: name, config: indexConfig });
27111
27168
  }
27112
27169
  async function dropEngineIndex(core, name) {
27113
27170
  core.guardShutdown();
@@ -27127,7 +27184,7 @@ async function dropEngineIndex(core, name) {
27127
27184
  core.watermarkNotifier.forget(name);
27128
27185
  core.analysisRebuild.clearStale(name);
27129
27186
  });
27130
- await core.pluginRegistry.runHook("onIndexDrop", { indexName: name, config: entry.config });
27187
+ await runLifecycleHook(core, "onIndexDrop", { indexName: name, config: entry.config });
27131
27188
  }
27132
27189
  function registerEngineEmbeddingAdapter(core, name, adapter) {
27133
27190
  core.guardShutdown();
@@ -27549,7 +27606,15 @@ async function awaitWriteVisibility(ctx, indexName) {
27549
27606
  async function insertDocument(ctx, indexName, document2, docId, options) {
27550
27607
  ctx.guardShutdown();
27551
27608
  const entry = ctx.requireIndex(indexName);
27552
- const resolvedDocId = docId ?? providedDocId(document2) ?? ctx.idGenerator();
27609
+ const embeddedDocId = providedDocId(document2);
27610
+ if (docId !== void 0 && embeddedDocId !== void 0 && embeddedDocId !== docId) {
27611
+ throw new NarsilError(
27612
+ ErrorCodes.DOC_VALIDATION_FAILED,
27613
+ `The call names document id "${docId}" while the document's own id field is "${embeddedDocId}", so the stored document would contradict the key that it is stored under`,
27614
+ { docId, documentId: embeddedDocId }
27615
+ );
27616
+ }
27617
+ const resolvedDocId = docId ?? embeddedDocId ?? ctx.idGenerator();
27553
27618
  validateDocId(resolvedDocId);
27554
27619
  await ctx.pluginRegistry.runHook("beforeInsert", { indexName, docId: resolvedDocId, document: document2 });
27555
27620
  if (entry.config.required && entry.config.required.length > 0) {
@@ -27820,13 +27885,12 @@ async function admitBatchDocuments(ctx, indexName, documents, failed) {
27820
27885
  // src/engine/mutations/insert-batch-documents.ts
27821
27886
  async function applyAdmittedDocuments(ctx, indexName, admitted, options, failed) {
27822
27887
  const hasAfterHook = ctx.pluginRegistry.hasHooks("afterInsert");
27823
- const chunkSize = hasAfterHook ? 1 : BATCH_CHUNK_SIZE;
27824
27888
  const succeeded = [];
27825
27889
  const buffered = /* @__PURE__ */ new Set();
27826
27890
  const touchedVectorFields = /* @__PURE__ */ new Set();
27827
- for (let chunkStart = 0; chunkStart < admitted.length; chunkStart += chunkSize) {
27891
+ for (let chunkStart = 0; chunkStart < admitted.length; chunkStart += BATCH_CHUNK_SIZE) {
27828
27892
  if (ctx.abortController.signal.aborted) break;
27829
- const chunk = admitted.slice(chunkStart, chunkStart + chunkSize);
27893
+ const chunk = admitted.slice(chunkStart, chunkStart + BATCH_CHUNK_SIZE);
27830
27894
  const applications = await applyInsertChunk(ctx, indexName, chunk, options);
27831
27895
  for (let i = 0; i < chunk.length; i++) {
27832
27896
  const doc = chunk[i];
@@ -27853,8 +27917,8 @@ async function applyAdmittedDocuments(ctx, indexName, admitted, options, failed)
27853
27917
  }
27854
27918
  succeeded.push(doc.docId);
27855
27919
  }
27856
- const applied = chunkStart + chunkSize;
27857
- if (applied < admitted.length && applied % BATCH_CHUNK_SIZE === 0) {
27920
+ const applied = chunkStart + BATCH_CHUNK_SIZE;
27921
+ if (applied < admitted.length) {
27858
27922
  await new Promise((r) => setTimeout(r, 0));
27859
27923
  }
27860
27924
  }
@@ -28223,7 +28287,6 @@ async function insertDocumentBatch(ctx, indexName, documents, options) {
28223
28287
  const failed = [];
28224
28288
  const hasBeforeHook = ctx.pluginRegistry.hasHooks("beforeInsert");
28225
28289
  const hasAfterHook = ctx.pluginRegistry.hasHooks("afterInsert");
28226
- const documentByDocument = hasBeforeHook || hasAfterHook;
28227
28290
  const required = entry.config.required;
28228
28291
  const batchManager = ctx.requireManager(indexName);
28229
28292
  const batchVecIndexes = batchManager.getVectorIndexes();
@@ -28303,9 +28366,6 @@ async function insertDocumentBatch(ctx, indexName, documents, options) {
28303
28366
  } catch (err) {
28304
28367
  failed.push({ docId: batchDocId, error: asBatchInsertError(err) });
28305
28368
  }
28306
- if (documentByDocument && prepared.length > 0) {
28307
- await applyPrepared(prepared.splice(0, prepared.length));
28308
- }
28309
28369
  }
28310
28370
  if (prepared.length > 0) await applyPrepared(prepared);
28311
28371
  if (chunkEnd < documents.length) {
@@ -28420,12 +28480,10 @@ async function removeDocumentBatch(ctx, indexName, docIds, options) {
28420
28480
  const succeeded = [];
28421
28481
  const failed = [];
28422
28482
  const manager = ctx.requireManager(indexName);
28423
- const hooked = ctx.pluginRegistry.hasHooks("beforeRemove") || ctx.pluginRegistry.hasHooks("afterRemove");
28424
- const chunkSize = hooked ? 1 : BATCH_CHUNK_SIZE;
28425
28483
  manager.beginBatchRemove();
28426
28484
  try {
28427
- for (let chunkStart = 0; chunkStart < docIds.length; chunkStart += chunkSize) {
28428
- const chunkEnd = Math.min(chunkStart + chunkSize, docIds.length);
28485
+ for (let chunkStart = 0; chunkStart < docIds.length; chunkStart += BATCH_CHUNK_SIZE) {
28486
+ const chunkEnd = Math.min(chunkStart + BATCH_CHUNK_SIZE, docIds.length);
28429
28487
  const prepared = [];
28430
28488
  for (let i = chunkStart; i < chunkEnd; i++) {
28431
28489
  try {
@@ -28452,13 +28510,14 @@ async function removeDocumentBatch(ctx, indexName, docIds, options) {
28452
28510
  if (!removal.buffered) await settleRemoval(ctx, indexName, removal.docId);
28453
28511
  succeeded.push(removal.docId);
28454
28512
  }
28455
- if (chunkEnd < docIds.length && chunkEnd % BATCH_CHUNK_SIZE === 0) {
28513
+ if (chunkEnd < docIds.length) {
28456
28514
  await new Promise((r) => setTimeout(r, 0));
28457
28515
  }
28458
28516
  }
28459
28517
  } finally {
28460
28518
  manager.endBatchRemove();
28461
28519
  }
28520
+ ctx.checkHeapPressure(indexName);
28462
28521
  if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
28463
28522
  return { succeeded, failed };
28464
28523
  }
@@ -28619,10 +28678,8 @@ async function updateDocumentBatch(ctx, indexName, updates, options) {
28619
28678
  const updateBatchManager = ctx.requireManager(indexName);
28620
28679
  const updateBatchVecIndexes = updateBatchManager.getVectorIndexes();
28621
28680
  const touchedVectorFields = /* @__PURE__ */ new Set();
28622
- const hooked = ctx.pluginRegistry.hasHooks("beforeUpdate") || ctx.pluginRegistry.hasHooks("afterUpdate");
28623
- const chunkSize = hooked ? 1 : BATCH_CHUNK_SIZE;
28624
- for (let chunkStart = 0; chunkStart < updates.length; chunkStart += chunkSize) {
28625
- const chunkEnd = Math.min(chunkStart + chunkSize, updates.length);
28681
+ for (let chunkStart = 0; chunkStart < updates.length; chunkStart += BATCH_CHUNK_SIZE) {
28682
+ const chunkEnd = Math.min(chunkStart + BATCH_CHUNK_SIZE, updates.length);
28626
28683
  const prepared = [];
28627
28684
  for (let i = chunkStart; i < chunkEnd; i++) {
28628
28685
  try {
@@ -28652,7 +28709,7 @@ async function updateDocumentBatch(ctx, indexName, updates, options) {
28652
28709
  }
28653
28710
  succeeded.push(update.docId);
28654
28711
  }
28655
- if (chunkEnd < updates.length && chunkEnd % BATCH_CHUNK_SIZE === 0) {
28712
+ if (chunkEnd < updates.length) {
28656
28713
  await new Promise((r) => setTimeout(r, 0));
28657
28714
  }
28658
28715
  }
@@ -29287,6 +29344,7 @@ async function restoreFromSnapshot(indexName, data, deps) {
29287
29344
  await deps.durability.manager.persistMetadata(indexName);
29288
29345
  await deps.durability.manager.checkpoint(indexName);
29289
29346
  }
29347
+ await deps.announceIndexCreated(indexName, indexConfig);
29290
29348
  } catch (err) {
29291
29349
  try {
29292
29350
  deps.executor.dropIndex(indexName);
@@ -29358,13 +29416,14 @@ function readProcessMemory() {
29358
29416
  }
29359
29417
  try {
29360
29418
  const usage = proc.memoryUsage();
29419
+ const heap = readHeapStatistics();
29361
29420
  if (!Number.isFinite(usage.heapUsed) || !Number.isFinite(usage.heapTotal) || !Number.isFinite(usage.external) || !Number.isFinite(usage.rss)) {
29362
29421
  return null;
29363
29422
  }
29364
29423
  return {
29365
29424
  heapUsed: usage.heapUsed,
29366
29425
  heapTotal: usage.heapTotal,
29367
- heapLimit: readHeapStatistics()?.limitBytes ?? null,
29426
+ heapLimit: heap === null ? null : heap.configuredLimitBytes ?? heap.limitBytes,
29368
29427
  external: usage.external,
29369
29428
  rss: usage.rss
29370
29429
  };
@@ -29832,21 +29891,19 @@ function findDensestWindow(positions, text, windowSize) {
29832
29891
  }
29833
29892
  function applySnippetTruncation(taggedText, originalText, positions, maxLen, preTag, postTag) {
29834
29893
  if (maxLen <= 0 || originalText.length <= maxLen) {
29835
- return { snippet: taggedText, adjustedPositions: positions };
29894
+ return { snippet: taggedText, visiblePositions: positions };
29836
29895
  }
29837
29896
  const window2 = findDensestWindow(positions, originalText, maxLen);
29838
29897
  const snippetRaw = originalText.substring(window2.start, window2.end);
29839
- const adjustedPositions = [];
29898
+ const visiblePositions = [];
29899
+ const withinSnippet = [];
29840
29900
  for (const pos of positions) {
29841
- if (pos.start >= window2.start && pos.end <= window2.end) {
29842
- adjustedPositions.push({
29843
- start: pos.start - window2.start,
29844
- end: pos.end - window2.start
29845
- });
29846
- }
29901
+ if (pos.start < window2.start || pos.end > window2.end) continue;
29902
+ visiblePositions.push(pos);
29903
+ withinSnippet.push({ start: pos.start - window2.start, end: pos.end - window2.start });
29847
29904
  }
29848
29905
  let snippet = snippetRaw;
29849
- const sortedDesc = [...adjustedPositions].sort((a, b) => b.start - a.start);
29906
+ const sortedDesc = [...withinSnippet].sort((a, b) => b.start - a.start);
29850
29907
  for (const pos of sortedDesc) {
29851
29908
  snippet = snippet.substring(0, pos.start) + preTag + snippet.substring(pos.start, pos.end) + postTag + snippet.substring(pos.end);
29852
29909
  }
@@ -29854,7 +29911,7 @@ function applySnippetTruncation(taggedText, originalText, positions, maxLen, pre
29854
29911
  const needsTrailingEllipsis = window2.end < originalText.length;
29855
29912
  if (needsLeadingEllipsis) snippet = `...${snippet}`;
29856
29913
  if (needsTrailingEllipsis) snippet = `${snippet}...`;
29857
- return { snippet, adjustedPositions };
29914
+ return { snippet, visiblePositions };
29858
29915
  }
29859
29916
  function highlightField(text, queryTokens, language, options) {
29860
29917
  const preTag = options?.preTag ?? "<mark>";
@@ -29901,7 +29958,7 @@ function highlightField(text, queryTokens, language, options) {
29901
29958
  return { snippet: text, positions: [] };
29902
29959
  }
29903
29960
  if (maxSnippetLength > 0 && text.length > maxSnippetLength) {
29904
- const { snippet, adjustedPositions } = applySnippetTruncation(
29961
+ const { snippet, visiblePositions } = applySnippetTruncation(
29905
29962
  text,
29906
29963
  text,
29907
29964
  mergedPositions,
@@ -29909,7 +29966,7 @@ function highlightField(text, queryTokens, language, options) {
29909
29966
  preTag,
29910
29967
  postTag
29911
29968
  );
29912
- return { snippet, positions: adjustedPositions };
29969
+ return { snippet, positions: visiblePositions };
29913
29970
  }
29914
29971
  let result = text;
29915
29972
  const sortedDesc = [...mergedPositions].sort((a, b) => b.start - a.start);
@@ -29920,7 +29977,7 @@ function highlightField(text, queryTokens, language, options) {
29920
29977
  }
29921
29978
 
29922
29979
  // src/engine/query/highlight.ts
29923
- function applyHighlights(hits, params, language, analysis) {
29980
+ function applyHighlights(hits, params, language, analysis, readStoredDocument) {
29924
29981
  if (!params.highlight) return;
29925
29982
  const queryTokenResult = tokenize(params.term ?? "", language, {
29926
29983
  stem: true,
@@ -29946,9 +30003,14 @@ function applyHighlights(hits, params, language, analysis) {
29946
30003
  }
29947
30004
  for (const hit of hits) {
29948
30005
  const highlights = {};
30006
+ const projected = hit.document;
30007
+ let stored;
29949
30008
  for (const field of params.highlight.fields) {
29950
- const doc = hit.document;
29951
- const fieldValue = doc[field];
30009
+ let fieldValue = projected[field];
30010
+ if (typeof fieldValue !== "string" && readStoredDocument !== void 0) {
30011
+ stored ??= readStoredDocument(hit.id);
30012
+ fieldValue = stored?.[field];
30013
+ }
29952
30014
  if (typeof fieldValue === "string") {
29953
30015
  highlights[field] = highlightField(fieldValue, queryTokenResult.tokens, language, {
29954
30016
  preTag: params.highlight.preTag,
@@ -30042,6 +30104,32 @@ function vectorSearchersOf(context) {
30042
30104
  function resolveVectorIndex(context, fieldName) {
30043
30105
  return vectorSearchersOf(context).get(fieldName);
30044
30106
  }
30107
+ var SEARCH_MODES = /* @__PURE__ */ new Set(["fulltext", "vector", "hybrid"]);
30108
+ function requireKnownMode(params) {
30109
+ if (params.mode === void 0 || SEARCH_MODES.has(params.mode)) return;
30110
+ throw new NarsilError(
30111
+ ErrorCodes.SEARCH_INVALID_MODE,
30112
+ `A search takes the mode "fulltext", "vector", or "hybrid", while this query names "${String(params.mode)}"`,
30113
+ { mode: String(params.mode), modes: [...SEARCH_MODES] }
30114
+ );
30115
+ }
30116
+ function requireVectorSearchable(params, context, needsVector) {
30117
+ if (!needsVector) return;
30118
+ const field = params.vector?.field;
30119
+ if (field === void 0) {
30120
+ throw new NarsilError(
30121
+ ErrorCodes.SEARCH_INVALID_MODE,
30122
+ `A "${params.mode}" query needs a vector clause naming the field to search`,
30123
+ { mode: params.mode }
30124
+ );
30125
+ }
30126
+ if (vectorSearchersOf(context).has(field)) return;
30127
+ throw new NarsilError(
30128
+ ErrorCodes.SEARCH_INVALID_FIELD,
30129
+ `Field "${field}" has no vector index, so a vector search cannot use it`,
30130
+ { field, vectorFields: [...vectorSearchersOf(context).keys()] }
30131
+ );
30132
+ }
30045
30133
  function clampAlpha(alpha) {
30046
30134
  if (alpha === void 0) return 0.5;
30047
30135
  if (!Number.isFinite(alpha)) return 0.5;
@@ -30329,6 +30417,7 @@ async function executeQuery(params, context) {
30329
30417
  const offset = clampOffset(params.offset);
30330
30418
  requireWithinResultWindow(limit, offset);
30331
30419
  const sortFields = normalizeSort(params.sort);
30420
+ requireKnownMode(params);
30332
30421
  const sortSignature = sortSignatureOf(params.sort);
30333
30422
  const hasTerm = params.term !== void 0 && params.term.trim().length > 0;
30334
30423
  const hasVector = params.vector !== void 0 && params.vector.value !== void 0;
@@ -30342,6 +30431,7 @@ async function executeQuery(params, context) {
30342
30431
  );
30343
30432
  }
30344
30433
  requireSortableFields(params.sort, config.schema);
30434
+ requireVectorSearchable(params, context, isVectorOnly || isHybridMode);
30345
30435
  let paginated;
30346
30436
  let nextCursor;
30347
30437
  let count;
@@ -30465,7 +30555,9 @@ async function executeQuery(params, context) {
30465
30555
  }
30466
30556
  }
30467
30557
  if (params.highlight) {
30468
- applyHighlights(paginated, params, language, manager.analysis);
30558
+ const dropped = params.highlight.fields.some((field) => !projectionKeepsField(projection, field));
30559
+ const readStoredDocument = dropped ? (docId) => manager.get(docId, { kind: "full" }) : void 0;
30560
+ applyHighlights(paginated, params, language, manager.analysis, readStoredDocument);
30469
30561
  }
30470
30562
  const elapsed = now() - startTime;
30471
30563
  return {
@@ -30481,10 +30573,12 @@ async function executeQuery(params, context) {
30481
30573
  async function executePreflight(params, context) {
30482
30574
  const { manager, language, config, workerSearch, indexName } = context;
30483
30575
  const startTime = now();
30576
+ requireKnownMode(params);
30484
30577
  const hasTerm = params.term !== void 0 && params.term.trim().length > 0;
30485
30578
  const hasVector = params.vector !== void 0 && params.vector.value !== void 0;
30486
30579
  const isHybridMode = params.mode === "hybrid" || hasTerm && hasVector;
30487
30580
  const isVectorOnly = (params.mode === "vector" || hasVector && !hasTerm) && !isHybridMode;
30581
+ requireVectorSearchable(params, context, isVectorOnly || isHybridMode);
30488
30582
  const requestedVectorField = params.vector?.field;
30489
30583
  const hasGlobalVectorIndex = requestedVectorField !== void 0 && vectorSearchersOf(context).has(requestedVectorField);
30490
30584
  let totalMatched;
@@ -30631,13 +30725,14 @@ async function runEngineQuery(core, indexName, params, options) {
30631
30725
  try {
30632
30726
  const entry = core.requireIndex(indexName);
30633
30727
  const manager = core.requireManager(indexName);
30728
+ const scoped = scopedParams(params, options);
30729
+ await core.pluginRegistry.runHook("beforeSearch", { indexName, params: scoped });
30634
30730
  const resolvedParams = await resolveVectorText(
30635
- scopedParams(params, options),
30731
+ scoped,
30636
30732
  entry.embeddingAdapter,
30637
30733
  core.abortController.signal,
30638
30734
  entry.embeddingAdapterName
30639
30735
  );
30640
- await core.pluginRegistry.runHook("beforeSearch", { indexName, params: resolvedParams });
30641
30736
  const workerSearch = core.orchestrator.hasWorkerPool() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
30642
30737
  const result = await executeQuery(resolvedParams, {
30643
30738
  manager,
@@ -30649,14 +30744,13 @@ async function runEngineQuery(core, indexName, params, options) {
30649
30744
  partitionIds: options?.partitionIds,
30650
30745
  cursorBinding: queryBindingOf(params)
30651
30746
  });
30652
- try {
30653
- await core.pluginRegistry.runHook("afterSearch", {
30654
- indexName,
30655
- params: resolvedParams,
30656
- results: result
30657
- });
30658
- } catch (err) {
30659
- console.warn("afterSearch plugin hook error:", err);
30747
+ if (core.pluginRegistry.hasHooks("afterSearch")) {
30748
+ const observed = { ...result, hits: [...result.hits] };
30749
+ try {
30750
+ await core.pluginRegistry.runHook("afterSearch", { indexName, params: resolvedParams, results: observed });
30751
+ } catch (err) {
30752
+ console.warn("afterSearch plugin hook error:", err);
30753
+ }
30660
30754
  }
30661
30755
  if (core.analysisRebuild.isStale(indexName)) {
30662
30756
  result.analysisStale = true;
@@ -30672,8 +30766,10 @@ async function runEnginePreflight(core, indexName, params, options) {
30672
30766
  try {
30673
30767
  const entry = core.requireIndex(indexName);
30674
30768
  const manager = core.requireManager(indexName);
30769
+ const scoped = scopedParams(params, options);
30770
+ await core.pluginRegistry.runHook("beforeSearch", { indexName, params: scoped });
30675
30771
  const resolvedParams = await resolveVectorText(
30676
- scopedParams(params, options),
30772
+ scoped,
30677
30773
  entry.embeddingAdapter,
30678
30774
  core.abortController.signal,
30679
30775
  entry.embeddingAdapterName
@@ -30887,6 +30983,7 @@ function createNarsilFromCore(core, config) {
30887
30983
  indexRegistry,
30888
30984
  getVectorFieldPaths,
30889
30985
  dropIndex: narsil.dropIndex.bind(narsil),
30986
+ announceIndexCreated: (name, restoredConfig) => announceIndexCreated(core, name, restoredConfig),
30890
30987
  requireManager,
30891
30988
  durability,
30892
30989
  filesystemDurability: core.filesystemDurability,
@@ -31043,4 +31140,4 @@ async function createNarsil(config) {
31043
31140
  // src/index.ts
31044
31141
  var VERSION = "0.1.0";
31045
31142
 
31046
- export { ClientErrorCodes, ErrorCodes, NarsilError, ServerErrorCodes, VERSION, clearNormalizationCache, configureNormalizationCache, createNarsil, getLanguage, getNormalizationCacheSize, getStopWords, getTokenizer, hasStopWords, hasTokenizer, isSimdAvailable, registerLanguage, registerStopWords, registerTokenizer, resetNormalizationCache };
31143
+ export { ClientErrorCodes, ErrorCodes, NarsilError, ServerErrorCodes, VERSION, clearNormalizationCache, configureNormalizationCache, createNarsil, getLanguage, getNormalizationCacheSize, getStopWords, getTokenizer, hasStopWords, hasTokenizer, isNarsilError, isSimdAvailable, registerLanguage, registerStopWords, registerTokenizer, resetNormalizationCache };
package/dist/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { b as StopWordOverride, C as CustomTokenizer } from './schema-OSWaaZzx.js';
2
- export { A as AnyDocument, B as BM25Params, E as EmbeddingFieldConfig, F as FieldType, I as IndexConfig, a as InsertOptions, P as PartitionConfig, S as SchemaDefinition, c as ScoringMode, V as VectorIndexConfig, d as VectorQuantizationMode, e as VectorStorageMode, W as WriteOptions } from './schema-OSWaaZzx.js';
1
+ import { b as StopWordOverride, C as CustomTokenizer } from './schema-mLpXZBEh.js';
2
+ export { A as AnyDocument, B as BM25Params, E as EmbeddingFieldConfig, F as FieldType, I as IndexConfig, a as InsertOptions, P as PartitionConfig, S as SchemaDefinition, c as ScoringMode, V as VectorIndexConfig, d as VectorQuantizationMode, e as VectorStorageMode, W as WriteOptions } from './schema-mLpXZBEh.js';
3
3
  import { L as LanguageModule } from './language-DB8KG1z5.js';
4
4
  export { T as TokenizerConfig } from './language-DB8KG1z5.js';
5
- export { A as ArrayFilter, B as BatchResult, C as ClientErrorCode, a as ClientErrorCodes, k as ComparisonFilter, D as DocumentProjection, E as ErrorCode, b as ErrorCodes, F as FacetConfig, l as FacetResult, m as FieldFilter, n as FilterExpression, G as GeoFilter, o as GeoPolygonFilter, p as GeoRadiusFilter, q as GroupConfig, r as GroupReducer, s as GroupResult, H as HighlightConfig, t as HighlightMatch, u as Hit, v as HybridConfig, I as IndexInfo, f as IndexStats, L as ListParams, g as ListResult, w as ListedDocument, M as MemoryStats, N as NarsilError, c as NarsilErrorCode, P as PartitionStatsResult, h as PreflightResult, x as PresenceFilter, y as ProcessMemoryReport, z as QueryCoverage, Q as QueryParams, e as QueryResult, J as ScoreComponents, K as SearchMode, S as ServerErrorCode, d as ServerErrorCodes, O as SortField, R as SortSpec, T as StringFilter, i as SuggestParams, j as SuggestResult, U as TermMatchPolicy, V as VectorMaintenanceResult, W as VectorQueryConfig, X as WorkerCopyReport } from './memory-OmgkX_Z2.js';
6
- import { N as NarsilConfig } from './config-BcYZkdMw.js';
7
- export { A as AnalysisConfig, D as DurabilityConfig, I as IndexContext, a as IndexLifecycleConfig, b as InsertContext, M as MainCopyQueries, c as NarsilPlugin, P as PartitionContext, R as RemoveContext, S as SearchContext, d as StaleAnalysis, U as UpdateContext, W as WorkerConfig, e as WorkerContext } from './config-BcYZkdMw.js';
8
- import { a as Narsil } from './engine-zhms1qId.js';
9
- export { D as DocumentWriteOperations, I as IndexLifecycleOperations, N as NarsilEventMap } from './engine-zhms1qId.js';
5
+ export { A as ArrayFilter, B as BatchResult, C as ClientErrorCode, a as ClientErrorCodes, l as ComparisonFilter, D as DocumentProjection, E as ErrorCode, b as ErrorCodes, F as FacetConfig, m as FacetResult, n as FieldFilter, o as FilterExpression, G as GeoFilter, p as GeoPolygonFilter, q as GeoRadiusFilter, r as GroupConfig, s as GroupReducer, t as GroupResult, H as HighlightConfig, u as HighlightMatch, v as Hit, w as HybridConfig, I as IndexInfo, f as IndexStats, L as ListParams, g as ListResult, x as ListedDocument, M as MemoryStats, N as NarsilError, c as NarsilErrorCode, P as PartitionStatsResult, h as PreflightResult, y as PresenceFilter, z as ProcessMemoryReport, J as QueryCoverage, Q as QueryParams, e as QueryResult, K as ScoreComponents, O as SearchMode, S as ServerErrorCode, d as ServerErrorCodes, R as SortField, T as SortSpec, U as StringFilter, j as SuggestParams, k as SuggestResult, W as TermMatchPolicy, V as VectorMaintenanceResult, X as VectorQueryConfig, Y as WorkerCopyReport, i as isNarsilError } from './memory-xrXOZMbw.js';
6
+ import { N as NarsilConfig } from './config-bXxnJrQc.js';
7
+ export { A as AnalysisConfig, D as DurabilityConfig, I as IndexContext, a as IndexLifecycleConfig, b as InsertContext, M as MainCopyQueries, c as NarsilPlugin, P as PartitionContext, R as RemoveContext, S as SearchContext, d as StaleAnalysis, U as UpdateContext, W as WorkerConfig, e as WorkerContext } from './config-bXxnJrQc.js';
8
+ import { a as Narsil } from './engine-Dj4I91Jv.js';
9
+ export { D as DocumentWriteOperations, I as IndexLifecycleOperations, N as NarsilEventMap } from './engine-Dj4I91Jv.js';
10
10
  export { E as EmbeddingAdapter, I as InvalidationAdapter, a as InvalidationEvent, b as PartitionStatistics, P as PersistenceAdapter } from './adapters-79OIa4xm.js';
11
11
  export { i as isSimdAvailable } from './simd-DW5AtJ6B.js';
12
12