@delali/narsil 0.2.4-next.51 → 0.2.4-next.54

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 (64) hide show
  1. package/README.md +33 -27
  2. package/dist/adapters/filesystem.mjs +1 -1
  3. package/dist/adapters/indexeddb.mjs +1 -1
  4. package/dist/{capabilities-BWm4IITU.d.ts → capabilities-DlYw0Ov0.d.ts} +13 -2
  5. package/dist/{chunk-KMMJMDUA.mjs → chunk-3L4U6EJQ.mjs} +15 -10
  6. package/dist/chunk-4NVPQ6S5.mjs +366 -0
  7. package/dist/{chunk-CMVG2UEF.mjs → chunk-5T6AOR37.mjs} +56 -518
  8. package/dist/{chunk-ND34I3SV.mjs → chunk-BH5SYB5W.mjs} +3295 -2149
  9. package/dist/{chunk-XE6XR2OE.mjs → chunk-D2S642IV.mjs} +4 -2
  10. package/dist/{chunk-P4ELXVJQ.mjs → chunk-FRQVMFY7.mjs} +7 -1
  11. package/dist/chunk-IFBKLBGB.mjs +7 -0
  12. package/dist/chunk-L225SHNB.mjs +513 -0
  13. package/dist/{chunk-MHE7YWAU.mjs → chunk-NTZYA3SJ.mjs} +3 -3
  14. package/dist/{chunk-WYJXSLER.mjs → chunk-POWOTUKJ.mjs} +7 -1
  15. package/dist/{chunk-CV5H7XL7.mjs → chunk-RGS7FMCB.mjs} +53 -10
  16. package/dist/{chunk-ZHUQKZBE.mjs → chunk-TVT5V4E7.mjs} +101 -34
  17. package/dist/chunk-U2AL3SON.mjs +4198 -0
  18. package/dist/chunk-UTOFOJOR.mjs +135 -0
  19. package/dist/{chunk-BEIK3EOZ.mjs → chunk-XNQAIO74.mjs} +5523 -3847
  20. package/dist/chunk-Y3L5MDRV.mjs +96 -0
  21. package/dist/chunk-ZJ4CSAB2.mjs +13 -0
  22. package/dist/{client-B_X6_AbT.d.ts → client-CQUqt9Ir.d.ts} +13 -4
  23. package/dist/client.d.ts +8 -8
  24. package/dist/client.mjs +20 -14
  25. package/dist/{config-Czh2YNDi.d.ts → config-CNas6wJH.d.ts} +2 -2
  26. package/dist/{config-OON7IJ_B.d.ts → config-bXxnJrQc.d.ts} +80 -35
  27. package/dist/direct-executor-RBA5O7EN.mjs +12 -0
  28. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  29. package/dist/distribution/coordinator/etcd.mjs +1 -1
  30. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  31. package/dist/distribution/transport/grpc.mjs +1 -1
  32. package/dist/distribution.d.ts +8 -8
  33. package/dist/distribution.mjs +32 -17
  34. package/dist/embeddings/openai.mjs +1 -1
  35. package/dist/{engine-9PtFY50q.d.ts → engine-Dj4I91Jv.d.ts} +21 -8
  36. package/dist/index.browser.mjs +12962 -7786
  37. package/dist/index.d.ts +9 -21
  38. package/dist/index.mjs +14 -13
  39. package/dist/{memory-Bzd5xcEW.d.ts → memory-xrXOZMbw.d.ts} +68 -13
  40. package/dist/persistence/durability/checkpoint-worker.d.ts +138 -3
  41. package/dist/persistence/durability/checkpoint-worker.mjs +32 -18
  42. package/dist/react.d.ts +11 -11
  43. package/dist/react.mjs +16 -4
  44. package/dist/{schema-BqRx9t4E.d.ts → schema-mLpXZBEh.d.ts} +43 -12
  45. package/dist/serialization/crc32-worker.mjs +2 -1
  46. package/dist/server.d.ts +13 -10
  47. package/dist/server.mjs +136 -47
  48. package/dist/simd-DW5AtJ6B.d.ts +41 -0
  49. package/dist/{tasks-DT7L1p6w.d.ts → tasks-Df73Tjgq.d.ts} +9 -5
  50. package/dist/{thread-PZLEO6JL.mjs → thread-5UP6MGED.mjs} +131 -43
  51. package/dist/{types-DXPxaXPO.d.ts → types-BTIVO8DI.d.ts} +1 -1
  52. package/dist/vector/search-worker.d.ts +292 -128
  53. package/dist/vector/search-worker.mjs +70 -54
  54. package/dist/workers/entry.mjs +30 -14
  55. package/package.json +52 -31
  56. package/dist/brute-force-DbHYCTc3.d.ts +0 -8
  57. package/dist/chunk-CBLS46IE.mjs +0 -10
  58. package/dist/chunk-PIWKZPP7.mjs +0 -64
  59. package/dist/chunk-PMBHH44R.mjs +0 -492
  60. package/dist/chunk-RP23PWMV.mjs +0 -11
  61. package/dist/chunk-X6YZO4MQ.mjs +0 -1572
  62. package/dist/direct-executor-OREXU72C.mjs +0 -12
  63. package/dist/vector/hnsw-build-worker.d.ts +0 -105
  64. package/dist/vector/hnsw-build-worker.mjs +0 -123
@@ -1,5 +1,6 @@
1
- import { MAX_SORT_FIELDS } from './chunk-CBLS46IE.mjs';
2
- import { ServerErrorCodes, NarsilError, ErrorCodes } from './chunk-P4ELXVJQ.mjs';
1
+ import { MAX_SORT_FIELDS } from './chunk-L225SHNB.mjs';
2
+ import { nativeSearchCoreIsEnabled, loadNativeCore } from './chunk-UTOFOJOR.mjs';
3
+ import { NarsilError, ServerErrorCodes, ErrorCodes } from './chunk-FRQVMFY7.mjs';
3
4
 
4
5
  // src/server/capabilities.ts
5
6
  var ASYNC_IMPORT_CAPABILITY = "documents.import.async";
@@ -122,6 +123,7 @@ var STATUS_BY_CODE = {
122
123
  PARTITION_CORRUPTED: 503,
123
124
  WORKER_CRASHED: 503,
124
125
  WORKER_TIMEOUT: 503,
126
+ WORKER_ACTION_FAILED: 500,
125
127
  PERSISTENCE_SAVE_FAILED: 503,
126
128
  PERSISTENCE_LOAD_FAILED: 503,
127
129
  PERSISTENCE_DELETE_FAILED: 503,
@@ -217,12 +219,15 @@ var DEFAULT_MAX_BODY_BYTES = 16 * BYTES_PER_MB;
217
219
  var DEFAULT_MAX_IMPORT_BYTES = 100 * BYTES_PER_MB;
218
220
  var DEFAULT_MAX_LINE_BYTES = 4 * BYTES_PER_MB;
219
221
  var DEFAULT_IMPORT_BATCH_SIZE = 1e3;
222
+ var IMPORT_FLUSHES_IN_FLIGHT = 4;
220
223
  var DEFAULT_MAX_CONCURRENT_REQUESTS = 0;
221
224
  var DEFAULT_MAX_RESULT_WINDOW = 1e4;
222
225
  var DEFAULT_MAX_FETCH_DOCUMENTS = 1e4;
223
226
  var DEFAULT_MAX_IMPORT_ERRORS = 100;
224
227
  var DEFAULT_MAX_TASK_PAGE_SIZE = 1e3;
225
228
  var DEFAULT_MAX_CONCURRENT_TASKS = 4;
229
+ var MIN_COUNTED_SERVER_LIMIT = 1;
230
+ var MIN_UNBOUNDED_SERVER_LIMIT = 0;
226
231
  var RESPONSE_STREAM_THRESHOLD_BYTES = 64 * 1024;
227
232
  var MAX_INDEX_TOUCH_INTERVAL_MS = 1e3;
228
233
  var INDEX_TOUCH_INTERVALS_PER_IDLE_TIMEOUT = 4;
@@ -483,7 +488,7 @@ function validateRebalance(body) {
483
488
  return null;
484
489
  }
485
490
  var TASK_TYPES = ["optimizeVectors", "rebalance", "restore", "import", "rebuildAnalysis"];
486
- var TASK_STATUSES = ["queued", "running", "succeeded", "failed", "cancelled"];
491
+ var TASK_STATUSES = ["running", "succeeded", "failed", "cancelled"];
487
492
  function parseCountParam(raw, field, minimum) {
488
493
  if (raw === null) return minimum;
489
494
  const value = Number(raw);
@@ -602,6 +607,22 @@ function indexDocNotFound(docId) {
602
607
  return new NarsilError(ErrorCodes.DOC_NOT_FOUND, `Document "${docId}" not found`, { docId });
603
608
  }
604
609
 
610
+ // src/vector/native/search-path.ts
611
+ function vectorSearchPath() {
612
+ return nativeSearchCoreIsEnabled() && loadNativeCore() !== null ? "native" : "wasm";
613
+ }
614
+
615
+ // src/server/handlers/version.ts
616
+ function versionReport(build) {
617
+ return { name: "narsil", ...build, vectorSearch: vectorSearchPath() };
618
+ }
619
+ function createVersionHandler(deps) {
620
+ function report(ctx) {
621
+ respondJson(ctx, versionReport(deps.build));
622
+ }
623
+ return { report };
624
+ }
625
+
605
626
  // src/server/request.ts
606
627
  function initAbortHandler(res) {
607
628
  const listeners = [];
@@ -620,7 +641,22 @@ function initAbortHandler(res) {
620
641
  }
621
642
  };
622
643
  }
623
- function readBody(res, maxBytes, abort) {
644
+ function declaredBodyBytes(header) {
645
+ return /^\d{1,15}$/.test(header) ? Number(header) : null;
646
+ }
647
+ function bodyWithRoomFor(body, filled, needed, maxBytes) {
648
+ if (needed <= body.length) return body;
649
+ const grown = Buffer.allocUnsafeSlow(Math.min(maxBytes, Math.max(needed, body.length * 2)));
650
+ grown.set(body.subarray(0, filled));
651
+ return grown;
652
+ }
653
+ function bodyOfExactly(body, filled) {
654
+ if (filled === body.length) return body;
655
+ const exact = Buffer.allocUnsafeSlow(filled);
656
+ exact.set(body.subarray(0, filled));
657
+ return exact;
658
+ }
659
+ function readBody(res, maxBytes, abort, declaredBytes = null) {
624
660
  return new Promise((resolve, reject) => {
625
661
  if (abort.aborted) {
626
662
  reject(new Error("aborted"));
@@ -628,7 +664,9 @@ function readBody(res, maxBytes, abort) {
628
664
  }
629
665
  let done = false;
630
666
  let total = 0;
631
- const chunks = [];
667
+ let filled = 0;
668
+ let body = null;
669
+ const reserved = declaredBytes !== null && declaredBytes <= maxBytes ? declaredBytes : 0;
632
670
  abort.onAbort(() => {
633
671
  if (done) return;
634
672
  done = true;
@@ -645,10 +683,14 @@ function readBody(res, maxBytes, abort) {
645
683
  reject(new Error("payload too large"));
646
684
  return;
647
685
  }
648
- chunks.push(Buffer.from(new Uint8Array(chunk)));
686
+ const arrived = new Uint8Array(chunk);
687
+ const firstSize = isLast ? arrived.length : Math.max(reserved, arrived.length);
688
+ body = bodyWithRoomFor(body ?? Buffer.allocUnsafeSlow(firstSize), filled, total, maxBytes);
689
+ body.set(arrived, filled);
690
+ filled = total;
649
691
  if (isLast) {
650
692
  done = true;
651
- resolve(Buffer.concat(chunks));
693
+ resolve(bodyOfExactly(body, filled));
652
694
  }
653
695
  });
654
696
  });
@@ -703,6 +745,7 @@ function createRouteRunner(deps) {
703
745
  }
704
746
  const query = new URLSearchParams(req.getQuery() ?? "");
705
747
  const contentType = req.getHeader("content-type");
748
+ const declaredBytes = needsBody ? declaredBodyBytes(req.getHeader("content-length")) : null;
706
749
  let hookContext = null;
707
750
  if (useHook) {
708
751
  const headers = {};
@@ -728,7 +771,7 @@ function createRouteRunner(deps) {
728
771
  );
729
772
  return;
730
773
  }
731
- const bodyPromise = needsBody ? readBody(res, opts.maxBytes, abort) : Promise.resolve(null);
774
+ const bodyPromise = needsBody ? readBody(res, opts.maxBytes, abort, declaredBytes) : Promise.resolve(null);
732
775
  const hookPromise = hookContext !== null && authorize !== void 0 ? authorize(hookContext) : Promise.resolve({ allowed: true });
733
776
  Promise.all([bodyPromise, hookPromise]).then(async ([rawBody, authorization]) => {
734
777
  if (abort.aborted) return;
@@ -767,7 +810,7 @@ function serverRoutes(limits) {
767
810
  return [
768
811
  { method: "get", path: "/livez", handler: "livez", options: probe },
769
812
  { method: "get", path: "/readyz", handler: "readyz", options: probe },
770
- { method: "get", path: "/health", handler: "livez", options: probe },
813
+ { method: "get", path: "/health", handler: "readyz", options: probe },
771
814
  { method: "get", path: "/version", handler: "version", options: probe },
772
815
  { method: "get", path: "/capabilities", handler: "capabilities", options: probe },
773
816
  { method: "get", path: "/cluster", handler: "clusterTopology", options: bare },
@@ -873,4 +916,4 @@ function childAppOf(app) {
873
916
  return candidate;
874
917
  }
875
918
 
876
- export { ASYNC_IMPORT_CAPABILITY, DEFAULT_IMPORT_BATCH_SIZE, DEFAULT_MAX_BODY_BYTES, DEFAULT_MAX_CONCURRENT_REQUESTS, DEFAULT_MAX_CONCURRENT_TASKS, DEFAULT_MAX_FETCH_DOCUMENTS, DEFAULT_MAX_IMPORT_BYTES, DEFAULT_MAX_IMPORT_ERRORS, DEFAULT_MAX_LINE_BYTES, DEFAULT_MAX_RESULT_WINDOW, DEFAULT_MAX_RETAINED_TASKS, DEFAULT_MAX_TASK_PAGE_SIZE, DEFAULT_TASK_PAGE_SIZE, INDEX_LIFECYCLE_CAPABILITY, INDEX_TOUCH_INTERVALS_PER_IDLE_TIMEOUT, MAIN_THREAD_GATE_SLOT, MAX_INDEX_TOUCH_INTERVAL_MS, REBUILD_ANALYSIS_CAPABILITY, RUNNING_TASK_TTL_MS, SERVER_CAPABILITIES, TASK_CANCEL_CAPABILITY, TASK_FILTER_CAPABILITY, TASK_PROGRESS_WRITE_INTERVAL_MS, TERMINAL_TASK_TTL_MS, acceptorOf, authorizerFor, badRequest, childAppOf, clearGateSlot, corsWriter, createDocumentReadHandlers, createRouteRunner, createSharedGateBuffer, createSharedRequestGate, gateSlotOfWorker, httpStatusForNarsilError, loadUWebSockets, parseJson, parseJsonOptional, parseTaskListQuery, registerServerRoutes, rejectInvalid, resolveCors, respondError, respondJson, sendBinary, sendError, sendRelayed, serializeBatchResult, serializeNarsilError, serverRoutes, validateBatch, validateCreateIndex, validateDocumentBody, validateQuery, validateRebalance, validateSuggest };
919
+ export { ASYNC_IMPORT_CAPABILITY, DEFAULT_IMPORT_BATCH_SIZE, DEFAULT_MAX_BODY_BYTES, DEFAULT_MAX_CONCURRENT_REQUESTS, DEFAULT_MAX_CONCURRENT_TASKS, DEFAULT_MAX_FETCH_DOCUMENTS, DEFAULT_MAX_IMPORT_BYTES, DEFAULT_MAX_IMPORT_ERRORS, DEFAULT_MAX_LINE_BYTES, DEFAULT_MAX_RESULT_WINDOW, DEFAULT_MAX_RETAINED_TASKS, DEFAULT_MAX_TASK_PAGE_SIZE, DEFAULT_TASK_PAGE_SIZE, IMPORT_FLUSHES_IN_FLIGHT, INDEX_LIFECYCLE_CAPABILITY, INDEX_TOUCH_INTERVALS_PER_IDLE_TIMEOUT, MAIN_THREAD_GATE_SLOT, MAX_INDEX_TOUCH_INTERVAL_MS, MIN_COUNTED_SERVER_LIMIT, MIN_UNBOUNDED_SERVER_LIMIT, REBUILD_ANALYSIS_CAPABILITY, RUNNING_TASK_TTL_MS, SERVER_CAPABILITIES, TASK_CANCEL_CAPABILITY, TASK_FILTER_CAPABILITY, TASK_PROGRESS_WRITE_INTERVAL_MS, TERMINAL_TASK_TTL_MS, acceptorOf, authorizerFor, badRequest, childAppOf, clearGateSlot, corsWriter, createDocumentReadHandlers, createRouteRunner, createSharedGateBuffer, createSharedRequestGate, createVersionHandler, gateSlotOfWorker, httpStatusForNarsilError, loadUWebSockets, parseJson, parseJsonOptional, parseTaskListQuery, registerServerRoutes, rejectInvalid, resolveCors, respondError, respondJson, sendBinary, sendError, sendRelayed, serializeBatchResult, serializeNarsilError, serverRoutes, toHttpError, validateBatch, validateCreateIndex, validateDocumentBody, validateQuery, validateRebalance, validateSuggest, versionReport };
@@ -1,7 +1,9 @@
1
- import { MAX_INDEX_NAME_LENGTH, INDEX_NAME_PATTERN, MAX_DOC_ID_LENGTH, DEFAULT_LIMIT, DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, MAX_SUGGEST_SCATTER_LIMIT, DEFAULT_OFFSET } from './chunk-XE6XR2OE.mjs';
2
- import { clampRowCount, sortSignatureOf, requireSortableFields, decodePageCursor, requireMatchingCursor, resolveProjection, encodePageCursor, requireWithinResultWindow, normalizeSort, fanOutQuery, flattenSchema, applySorting, applyPagination, countsWithoutScores, fanOutMatchCount, tokenize, fnv1aBytes, fulltextMatches, mergeFacets, pruneStatsToQueryTerms, partitionsIn } from './chunk-CMVG2UEF.mjs';
1
+ import { MAX_INDEX_NAME_LENGTH, INDEX_NAME_PATTERN, MAX_DOC_ID_LENGTH, DEFAULT_LIMIT, DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, MAX_SUGGEST_SCATTER_LIMIT, ABSOLUTE_WINDOWS_PATH_PATTERN, MODULE_URL_SCHEME_PATTERN, DEFAULT_OFFSET } from './chunk-D2S642IV.mjs';
2
+ import { clampRowCount, sortSignatureOf, requireSortableFields, decodePageCursor, requireMatchingCursor, resolveProjection, encodePageCursor, requireWithinResultWindow, normalizeSort, fanOutQuery, applySorting, applyPagination, projectionKeepsField, countsWithoutScores, fanOutMatchCount, tokenize, fnv1aBytes, fulltextMatches, mergeFacets, pruneStatsToQueryTerms, partitionsIn } from './chunk-5T6AOR37.mjs';
3
+ import { flattenSchema } from './chunk-L225SHNB.mjs';
4
+ import { MIN_OVERSAMPLE } from './chunk-ZJ4CSAB2.mjs';
3
5
  import { compareCodePoints, compareComparableKeys } from './chunk-2GWVQWGA.mjs';
4
- import { NarsilError, ErrorCodes } from './chunk-P4ELXVJQ.mjs';
6
+ import { NarsilError, ErrorCodes } from './chunk-FRQVMFY7.mjs';
5
7
 
6
8
  // src/search/cursor-binding.ts
7
9
  var ABSENT_TAG = 0;
@@ -169,32 +171,37 @@ function now() {
169
171
  return Date.now();
170
172
  }
171
173
  function validateIndexName(name) {
172
- if (!name || name.length === 0) {
173
- throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, "Index name must not be empty", { indexName: name });
174
+ const given = name;
175
+ if (typeof given !== "string" || name.length === 0) {
176
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "Index name must be a string of at least one character", {
177
+ received: given === null ? "null" : typeof given
178
+ });
174
179
  }
175
180
  if (name.length > MAX_INDEX_NAME_LENGTH) {
176
- throw new NarsilError(
177
- ErrorCodes.INDEX_NOT_FOUND,
178
- `Index name must not exceed ${MAX_INDEX_NAME_LENGTH} characters`,
179
- {
180
- indexName: name,
181
- length: name.length
182
- }
183
- );
181
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, `Index name must not exceed ${MAX_INDEX_NAME_LENGTH} characters`, {
182
+ indexName: name,
183
+ length: name.length
184
+ });
184
185
  }
185
186
  if (!INDEX_NAME_PATTERN.test(name)) {
186
187
  throw new NarsilError(
187
- ErrorCodes.INDEX_NOT_FOUND,
188
+ ErrorCodes.CONFIG_INVALID,
188
189
  `Index name "${name}" contains invalid characters; use alphanumeric, dots, hyphens, and underscores only`,
189
190
  { indexName: name }
190
191
  );
191
192
  }
192
193
  if (name.includes("..")) {
193
- throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index name "${name}" must not contain ".."`, { indexName: name });
194
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, `Index name "${name}" must not contain ".."`, { indexName: name });
194
195
  }
195
196
  }
196
197
  function validateDocId(docId) {
197
- if (!docId || docId.length === 0) {
198
+ const given = docId;
199
+ if (typeof given !== "string") {
200
+ throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must be a string", {
201
+ received: given === null ? "null" : Array.isArray(given) ? "array" : typeof given
202
+ });
203
+ }
204
+ if (docId.length === 0) {
198
205
  throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not be empty", { docId });
199
206
  }
200
207
  if (docId.length > MAX_DOC_ID_LENGTH) {
@@ -235,6 +242,16 @@ function requirePositiveSafeInteger(field, value) {
235
242
  throw new NarsilError(ErrorCodes.CONFIG_INVALID, `${field} must be a positive safe integer`, { field, value });
236
243
  }
237
244
  }
245
+ function validateBootstrapModule(bootstrapModule) {
246
+ if (bootstrapModule === void 0) return;
247
+ const absolute = bootstrapModule.startsWith("/") || ABSOLUTE_WINDOWS_PATH_PATTERN.test(bootstrapModule) || MODULE_URL_SCHEME_PATTERN.test(bootstrapModule);
248
+ if (absolute) return;
249
+ throw new NarsilError(
250
+ ErrorCodes.CONFIG_INVALID,
251
+ `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`,
252
+ { bootstrapModule }
253
+ );
254
+ }
238
255
  function validateWorkerConfig(workers, lifecycle) {
239
256
  if (workers === void 0) return;
240
257
  requirePositiveSafeInteger("workers.count", workers.count);
@@ -245,6 +262,7 @@ function validateWorkerConfig(workers, lifecycle) {
245
262
  mainCopyQueries: workers.mainCopyQueries
246
263
  });
247
264
  }
265
+ validateBootstrapModule(workers.bootstrapModule);
248
266
  const closeAfter = lifecycle?.idleTimeoutMs;
249
267
  if (workers.idleTimeoutMs !== void 0 && closeAfter !== void 0 && workers.idleTimeoutMs > closeAfter) {
250
268
  throw new NarsilError(
@@ -566,21 +584,19 @@ function findDensestWindow(positions, text, windowSize) {
566
584
  }
567
585
  function applySnippetTruncation(taggedText, originalText, positions, maxLen, preTag, postTag) {
568
586
  if (maxLen <= 0 || originalText.length <= maxLen) {
569
- return { snippet: taggedText, adjustedPositions: positions };
587
+ return { snippet: taggedText, visiblePositions: positions };
570
588
  }
571
589
  const window = findDensestWindow(positions, originalText, maxLen);
572
590
  const snippetRaw = originalText.substring(window.start, window.end);
573
- const adjustedPositions = [];
591
+ const visiblePositions = [];
592
+ const withinSnippet = [];
574
593
  for (const pos of positions) {
575
- if (pos.start >= window.start && pos.end <= window.end) {
576
- adjustedPositions.push({
577
- start: pos.start - window.start,
578
- end: pos.end - window.start
579
- });
580
- }
594
+ if (pos.start < window.start || pos.end > window.end) continue;
595
+ visiblePositions.push(pos);
596
+ withinSnippet.push({ start: pos.start - window.start, end: pos.end - window.start });
581
597
  }
582
598
  let snippet = snippetRaw;
583
- const sortedDesc = [...adjustedPositions].sort((a, b) => b.start - a.start);
599
+ const sortedDesc = [...withinSnippet].sort((a, b) => b.start - a.start);
584
600
  for (const pos of sortedDesc) {
585
601
  snippet = snippet.substring(0, pos.start) + preTag + snippet.substring(pos.start, pos.end) + postTag + snippet.substring(pos.end);
586
602
  }
@@ -588,7 +604,7 @@ function applySnippetTruncation(taggedText, originalText, positions, maxLen, pre
588
604
  const needsTrailingEllipsis = window.end < originalText.length;
589
605
  if (needsLeadingEllipsis) snippet = `...${snippet}`;
590
606
  if (needsTrailingEllipsis) snippet = `${snippet}...`;
591
- return { snippet, adjustedPositions };
607
+ return { snippet, visiblePositions };
592
608
  }
593
609
  function highlightField(text, queryTokens, language, options) {
594
610
  const preTag = options?.preTag ?? "<mark>";
@@ -635,7 +651,7 @@ function highlightField(text, queryTokens, language, options) {
635
651
  return { snippet: text, positions: [] };
636
652
  }
637
653
  if (maxSnippetLength > 0 && text.length > maxSnippetLength) {
638
- const { snippet, adjustedPositions } = applySnippetTruncation(
654
+ const { snippet, visiblePositions } = applySnippetTruncation(
639
655
  text,
640
656
  text,
641
657
  mergedPositions,
@@ -643,7 +659,7 @@ function highlightField(text, queryTokens, language, options) {
643
659
  preTag,
644
660
  postTag
645
661
  );
646
- return { snippet, positions: adjustedPositions };
662
+ return { snippet, positions: visiblePositions };
647
663
  }
648
664
  let result = text;
649
665
  const sortedDesc = [...mergedPositions].sort((a, b) => b.start - a.start);
@@ -654,7 +670,7 @@ function highlightField(text, queryTokens, language, options) {
654
670
  }
655
671
 
656
672
  // src/engine/query/highlight.ts
657
- function applyHighlights(hits, params, language, analysis) {
673
+ function applyHighlights(hits, params, language, analysis, readStoredDocument) {
658
674
  if (!params.highlight) return;
659
675
  const queryTokenResult = tokenize(params.term ?? "", language, {
660
676
  stem: true,
@@ -680,9 +696,14 @@ function applyHighlights(hits, params, language, analysis) {
680
696
  }
681
697
  for (const hit of hits) {
682
698
  const highlights = {};
699
+ const projected = hit.document;
700
+ let stored;
683
701
  for (const field of params.highlight.fields) {
684
- const doc = hit.document;
685
- const fieldValue = doc[field];
702
+ let fieldValue = projected[field];
703
+ if (typeof fieldValue !== "string" && readStoredDocument !== void 0) {
704
+ stored ??= readStoredDocument(hit.id);
705
+ fieldValue = stored?.[field];
706
+ }
686
707
  if (typeof fieldValue === "string") {
687
708
  highlights[field] = highlightField(fieldValue, queryTokenResult.tokens, language, {
688
709
  preTag: params.highlight.preTag,
@@ -776,6 +797,32 @@ function vectorSearchersOf(context) {
776
797
  function resolveVectorIndex(context, fieldName) {
777
798
  return vectorSearchersOf(context).get(fieldName);
778
799
  }
800
+ var SEARCH_MODES = /* @__PURE__ */ new Set(["fulltext", "vector", "hybrid"]);
801
+ function requireKnownMode(params) {
802
+ if (params.mode === void 0 || SEARCH_MODES.has(params.mode)) return;
803
+ throw new NarsilError(
804
+ ErrorCodes.SEARCH_INVALID_MODE,
805
+ `A search takes the mode "fulltext", "vector", or "hybrid", while this query names "${String(params.mode)}"`,
806
+ { mode: String(params.mode), modes: [...SEARCH_MODES] }
807
+ );
808
+ }
809
+ function requireVectorSearchable(params, context, needsVector) {
810
+ if (!needsVector) return;
811
+ const field = params.vector?.field;
812
+ if (field === void 0) {
813
+ throw new NarsilError(
814
+ ErrorCodes.SEARCH_INVALID_MODE,
815
+ `A "${params.mode}" query needs a vector clause naming the field to search`,
816
+ { mode: params.mode }
817
+ );
818
+ }
819
+ if (vectorSearchersOf(context).has(field)) return;
820
+ throw new NarsilError(
821
+ ErrorCodes.SEARCH_INVALID_FIELD,
822
+ `Field "${field}" has no vector index, so a vector search cannot use it`,
823
+ { field, vectorFields: [...vectorSearchersOf(context).keys()] }
824
+ );
825
+ }
779
826
  function clampAlpha(alpha) {
780
827
  if (alpha === void 0) return 0.5;
781
828
  if (!Number.isFinite(alpha)) return 0.5;
@@ -938,6 +985,18 @@ function minMaxNormalize(docs) {
938
985
  }
939
986
 
940
987
  // src/engine/query/vector.ts
988
+ function oversampleOf(vectorConfig) {
989
+ const oversample = vectorConfig.oversample;
990
+ if (oversample === void 0) return void 0;
991
+ if (typeof oversample !== "number" || !Number.isFinite(oversample) || oversample < MIN_OVERSAMPLE) {
992
+ throw new NarsilError(
993
+ ErrorCodes.CONFIG_INVALID,
994
+ `vector.oversample must be a finite number of at least ${MIN_OVERSAMPLE}`,
995
+ { oversample }
996
+ );
997
+ }
998
+ return oversample;
999
+ }
941
1000
  async function executeVectorSearch(params, context, limit, offset) {
942
1001
  const { manager, config, partitionIds } = context;
943
1002
  const vectorConfig = params.vector;
@@ -965,7 +1024,8 @@ async function executeVectorSearch(params, context, limit, offset) {
965
1024
  minSimilarity: vectorConfig.similarity ?? -Infinity,
966
1025
  ...filterDocIds !== void 0 ? { filterDocIds } : {},
967
1026
  ...filterPartitions !== void 0 ? { filterPartitions } : {},
968
- efSearch: vectorConfig.efSearch
1027
+ efSearch: vectorConfig.efSearch,
1028
+ oversample: oversampleOf(vectorConfig)
969
1029
  });
970
1030
  const scored = vectorResultsToScored(results);
971
1031
  return { scored, totalMatched: scored.length };
@@ -994,7 +1054,8 @@ async function vectorLeg(vectorConfig, queryVector, context, filterDocIds, k) {
994
1054
  minSimilarity: vectorConfig.similarity ?? -Infinity,
995
1055
  ...filterDocIds !== void 0 ? { filterDocIds } : {},
996
1056
  ...filterPartitions !== void 0 ? { filterPartitions } : {},
997
- efSearch: vectorConfig.efSearch
1057
+ efSearch: vectorConfig.efSearch,
1058
+ oversample: oversampleOf(vectorConfig)
998
1059
  });
999
1060
  return vectorResultsToScored(results);
1000
1061
  }
@@ -1049,6 +1110,7 @@ async function executeQuery(params, context) {
1049
1110
  const offset = clampOffset(params.offset);
1050
1111
  requireWithinResultWindow(limit, offset);
1051
1112
  const sortFields = normalizeSort(params.sort);
1113
+ requireKnownMode(params);
1052
1114
  const sortSignature = sortSignatureOf(params.sort);
1053
1115
  const hasTerm = params.term !== void 0 && params.term.trim().length > 0;
1054
1116
  const hasVector = params.vector !== void 0 && params.vector.value !== void 0;
@@ -1062,6 +1124,7 @@ async function executeQuery(params, context) {
1062
1124
  );
1063
1125
  }
1064
1126
  requireSortableFields(params.sort, config.schema);
1127
+ requireVectorSearchable(params, context, isVectorOnly || isHybridMode);
1065
1128
  let paginated;
1066
1129
  let nextCursor;
1067
1130
  let count;
@@ -1185,7 +1248,9 @@ async function executeQuery(params, context) {
1185
1248
  }
1186
1249
  }
1187
1250
  if (params.highlight) {
1188
- applyHighlights(paginated, params, language, manager.analysis);
1251
+ const dropped = params.highlight.fields.some((field) => !projectionKeepsField(projection, field));
1252
+ const readStoredDocument = dropped ? (docId) => manager.get(docId, { kind: "full" }) : void 0;
1253
+ applyHighlights(paginated, params, language, manager.analysis, readStoredDocument);
1189
1254
  }
1190
1255
  const elapsed = now() - startTime;
1191
1256
  return {
@@ -1201,10 +1266,12 @@ async function executeQuery(params, context) {
1201
1266
  async function executePreflight(params, context) {
1202
1267
  const { manager, language, config, workerSearch, indexName } = context;
1203
1268
  const startTime = now();
1269
+ requireKnownMode(params);
1204
1270
  const hasTerm = params.term !== void 0 && params.term.trim().length > 0;
1205
1271
  const hasVector = params.vector !== void 0 && params.vector.value !== void 0;
1206
1272
  const isHybridMode = params.mode === "hybrid" || hasTerm && hasVector;
1207
1273
  const isVectorOnly = (params.mode === "vector" || hasVector && !hasTerm) && !isHybridMode;
1274
+ requireVectorSearchable(params, context, isVectorOnly || isHybridMode);
1208
1275
  const requestedVectorField = params.vector?.field;
1209
1276
  const hasGlobalVectorIndex = requestedVectorField !== void 0 && vectorSearchersOf(context).has(requestedVectorField);
1210
1277
  let totalMatched;