@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,9 +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-3GWP2TO6.mjs';
2
- import { clampRowCount, sortSignatureOf, requireSortableFields, decodePageCursor, requireMatchingCursor, resolveProjection, encodePageCursor, requireWithinResultWindow, normalizeSort, fanOutQuery, applySorting, applyPagination, countsWithoutScores, fanOutMatchCount, tokenize, fnv1aBytes, fulltextMatches, mergeFacets, pruneStatsToQueryTerms, partitionsIn } from './chunk-WM7IS45M.mjs';
3
- import { flattenSchema } from './chunk-ZT5BRDFW.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
4
  import { MIN_OVERSAMPLE } from './chunk-ZJ4CSAB2.mjs';
5
5
  import { compareCodePoints, compareComparableKeys } from './chunk-2GWVQWGA.mjs';
6
- import { NarsilError, ErrorCodes } from './chunk-P4ELXVJQ.mjs';
6
+ import { NarsilError, ErrorCodes } from './chunk-FRQVMFY7.mjs';
7
7
 
8
8
  // src/search/cursor-binding.ts
9
9
  var ABSENT_TAG = 0;
@@ -171,32 +171,37 @@ function now() {
171
171
  return Date.now();
172
172
  }
173
173
  function validateIndexName(name) {
174
- if (!name || name.length === 0) {
175
- 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
+ });
176
179
  }
177
180
  if (name.length > MAX_INDEX_NAME_LENGTH) {
178
- throw new NarsilError(
179
- ErrorCodes.INDEX_NOT_FOUND,
180
- `Index name must not exceed ${MAX_INDEX_NAME_LENGTH} characters`,
181
- {
182
- indexName: name,
183
- length: name.length
184
- }
185
- );
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
+ });
186
185
  }
187
186
  if (!INDEX_NAME_PATTERN.test(name)) {
188
187
  throw new NarsilError(
189
- ErrorCodes.INDEX_NOT_FOUND,
188
+ ErrorCodes.CONFIG_INVALID,
190
189
  `Index name "${name}" contains invalid characters; use alphanumeric, dots, hyphens, and underscores only`,
191
190
  { indexName: name }
192
191
  );
193
192
  }
194
193
  if (name.includes("..")) {
195
- 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 });
196
195
  }
197
196
  }
198
197
  function validateDocId(docId) {
199
- 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) {
200
205
  throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not be empty", { docId });
201
206
  }
202
207
  if (docId.length > MAX_DOC_ID_LENGTH) {
@@ -237,6 +242,16 @@ function requirePositiveSafeInteger(field, value) {
237
242
  throw new NarsilError(ErrorCodes.CONFIG_INVALID, `${field} must be a positive safe integer`, { field, value });
238
243
  }
239
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
+ }
240
255
  function validateWorkerConfig(workers, lifecycle) {
241
256
  if (workers === void 0) return;
242
257
  requirePositiveSafeInteger("workers.count", workers.count);
@@ -247,6 +262,7 @@ function validateWorkerConfig(workers, lifecycle) {
247
262
  mainCopyQueries: workers.mainCopyQueries
248
263
  });
249
264
  }
265
+ validateBootstrapModule(workers.bootstrapModule);
250
266
  const closeAfter = lifecycle?.idleTimeoutMs;
251
267
  if (workers.idleTimeoutMs !== void 0 && closeAfter !== void 0 && workers.idleTimeoutMs > closeAfter) {
252
268
  throw new NarsilError(
@@ -568,21 +584,19 @@ function findDensestWindow(positions, text, windowSize) {
568
584
  }
569
585
  function applySnippetTruncation(taggedText, originalText, positions, maxLen, preTag, postTag) {
570
586
  if (maxLen <= 0 || originalText.length <= maxLen) {
571
- return { snippet: taggedText, adjustedPositions: positions };
587
+ return { snippet: taggedText, visiblePositions: positions };
572
588
  }
573
589
  const window = findDensestWindow(positions, originalText, maxLen);
574
590
  const snippetRaw = originalText.substring(window.start, window.end);
575
- const adjustedPositions = [];
591
+ const visiblePositions = [];
592
+ const withinSnippet = [];
576
593
  for (const pos of positions) {
577
- if (pos.start >= window.start && pos.end <= window.end) {
578
- adjustedPositions.push({
579
- start: pos.start - window.start,
580
- end: pos.end - window.start
581
- });
582
- }
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 });
583
597
  }
584
598
  let snippet = snippetRaw;
585
- const sortedDesc = [...adjustedPositions].sort((a, b) => b.start - a.start);
599
+ const sortedDesc = [...withinSnippet].sort((a, b) => b.start - a.start);
586
600
  for (const pos of sortedDesc) {
587
601
  snippet = snippet.substring(0, pos.start) + preTag + snippet.substring(pos.start, pos.end) + postTag + snippet.substring(pos.end);
588
602
  }
@@ -590,7 +604,7 @@ function applySnippetTruncation(taggedText, originalText, positions, maxLen, pre
590
604
  const needsTrailingEllipsis = window.end < originalText.length;
591
605
  if (needsLeadingEllipsis) snippet = `...${snippet}`;
592
606
  if (needsTrailingEllipsis) snippet = `${snippet}...`;
593
- return { snippet, adjustedPositions };
607
+ return { snippet, visiblePositions };
594
608
  }
595
609
  function highlightField(text, queryTokens, language, options) {
596
610
  const preTag = options?.preTag ?? "<mark>";
@@ -637,7 +651,7 @@ function highlightField(text, queryTokens, language, options) {
637
651
  return { snippet: text, positions: [] };
638
652
  }
639
653
  if (maxSnippetLength > 0 && text.length > maxSnippetLength) {
640
- const { snippet, adjustedPositions } = applySnippetTruncation(
654
+ const { snippet, visiblePositions } = applySnippetTruncation(
641
655
  text,
642
656
  text,
643
657
  mergedPositions,
@@ -645,7 +659,7 @@ function highlightField(text, queryTokens, language, options) {
645
659
  preTag,
646
660
  postTag
647
661
  );
648
- return { snippet, positions: adjustedPositions };
662
+ return { snippet, positions: visiblePositions };
649
663
  }
650
664
  let result = text;
651
665
  const sortedDesc = [...mergedPositions].sort((a, b) => b.start - a.start);
@@ -656,7 +670,7 @@ function highlightField(text, queryTokens, language, options) {
656
670
  }
657
671
 
658
672
  // src/engine/query/highlight.ts
659
- function applyHighlights(hits, params, language, analysis) {
673
+ function applyHighlights(hits, params, language, analysis, readStoredDocument) {
660
674
  if (!params.highlight) return;
661
675
  const queryTokenResult = tokenize(params.term ?? "", language, {
662
676
  stem: true,
@@ -682,9 +696,14 @@ function applyHighlights(hits, params, language, analysis) {
682
696
  }
683
697
  for (const hit of hits) {
684
698
  const highlights = {};
699
+ const projected = hit.document;
700
+ let stored;
685
701
  for (const field of params.highlight.fields) {
686
- const doc = hit.document;
687
- 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
+ }
688
707
  if (typeof fieldValue === "string") {
689
708
  highlights[field] = highlightField(fieldValue, queryTokenResult.tokens, language, {
690
709
  preTag: params.highlight.preTag,
@@ -778,6 +797,32 @@ function vectorSearchersOf(context) {
778
797
  function resolveVectorIndex(context, fieldName) {
779
798
  return vectorSearchersOf(context).get(fieldName);
780
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
+ }
781
826
  function clampAlpha(alpha) {
782
827
  if (alpha === void 0) return 0.5;
783
828
  if (!Number.isFinite(alpha)) return 0.5;
@@ -1065,6 +1110,7 @@ async function executeQuery(params, context) {
1065
1110
  const offset = clampOffset(params.offset);
1066
1111
  requireWithinResultWindow(limit, offset);
1067
1112
  const sortFields = normalizeSort(params.sort);
1113
+ requireKnownMode(params);
1068
1114
  const sortSignature = sortSignatureOf(params.sort);
1069
1115
  const hasTerm = params.term !== void 0 && params.term.trim().length > 0;
1070
1116
  const hasVector = params.vector !== void 0 && params.vector.value !== void 0;
@@ -1078,6 +1124,7 @@ async function executeQuery(params, context) {
1078
1124
  );
1079
1125
  }
1080
1126
  requireSortableFields(params.sort, config.schema);
1127
+ requireVectorSearchable(params, context, isVectorOnly || isHybridMode);
1081
1128
  let paginated;
1082
1129
  let nextCursor;
1083
1130
  let count;
@@ -1201,7 +1248,9 @@ async function executeQuery(params, context) {
1201
1248
  }
1202
1249
  }
1203
1250
  if (params.highlight) {
1204
- 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);
1205
1254
  }
1206
1255
  const elapsed = now() - startTime;
1207
1256
  return {
@@ -1217,10 +1266,12 @@ async function executeQuery(params, context) {
1217
1266
  async function executePreflight(params, context) {
1218
1267
  const { manager, language, config, workerSearch, indexName } = context;
1219
1268
  const startTime = now();
1269
+ requireKnownMode(params);
1220
1270
  const hasTerm = params.term !== void 0 && params.term.trim().length > 0;
1221
1271
  const hasVector = params.vector !== void 0 && params.vector.value !== void 0;
1222
1272
  const isHybridMode = params.mode === "hybrid" || hasTerm && hasVector;
1223
1273
  const isVectorOnly = (params.mode === "vector" || hasVector && !hasTerm) && !isHybridMode;
1274
+ requireVectorSearchable(params, context, isVectorOnly || isHybridMode);
1224
1275
  const requestedVectorField = params.vector?.field;
1225
1276
  const hasGlobalVectorIndex = requestedVectorField !== void 0 && vectorSearchersOf(context).has(requestedVectorField);
1226
1277
  let totalMatched;
@@ -1,10 +1,8 @@
1
1
  import { OSQ_STEPS, OSQ_GRID, OSQ_REFINE_ROUNDS, OSQ_REFINE_TOLERANCE, OSQ4_NARROW_DIMENSION_LIMIT, DEFAULT_OSQ4_NARROW_OVERSAMPLE, DEFAULT_OVERSAMPLE_BY_BITS, OSQ_QUERY_BITS, OSQ_LAMBDA } from './chunk-ZJ4CSAB2.mjs';
2
- import { COMPACTION_TOMBSTONE_RATIO, COMPACTION_ABSOLUTE_THRESHOLD, MAX_LAYER_CAP, PLACEMENT_ROOM_REQUESTS, INITIAL_LIST_CAPACITY, DEFAULT_EF_SEARCH, INITIAL_UPPER_SLOTS } from './chunk-WFKIGEBY.mjs';
2
+ import { COMPACTION_TOMBSTONE_RATIO, COMPACTION_ABSOLUTE_THRESHOLD, nativeSearchCoreIsEnabled, loadNativeCore, stopUsingTheNativeSearchCore, noteFallback, MAX_LAYER_CAP, PLACEMENT_ROOM_REQUESTS, INITIAL_LIST_CAPACITY, DEFAULT_EF_SEARCH, INITIAL_UPPER_SLOTS } from './chunk-UTOFOJOR.mjs';
3
3
  import { compareCodePoints } from './chunk-2GWVQWGA.mjs';
4
- import { NarsilError, ErrorCodes } from './chunk-P4ELXVJQ.mjs';
5
- import { existsSync, readSync, closeSync, openSync } from 'fs';
6
- import { createRequire } from 'module';
7
- import { join, dirname } from 'path';
4
+ import { NarsilError, ErrorCodes } from './chunk-FRQVMFY7.mjs';
5
+ import { readSync, closeSync, openSync } from 'fs';
8
6
 
9
7
  // src/vector/constants.ts
10
8
  var WASM_PAGE_BYTES = 65536;
@@ -185,97 +183,6 @@ function arenaFloat32Distance(simd, byteA, byteB, dimension, metric, magnitudeA,
185
183
  return 1 - dot / (magnitudeA * magnitudeB);
186
184
  }
187
185
 
188
- // src/vector/native/backend.ts
189
- var nativeSearchCoreEnabled = true;
190
- function nativeSearchCoreIsEnabled() {
191
- return nativeSearchCoreEnabled;
192
- }
193
- function stopUsingTheNativeSearchCore(error) {
194
- if (!nativeSearchCoreEnabled) return;
195
- nativeSearchCoreEnabled = false;
196
- console.warn(
197
- "The native search core raised an error, so this thread searches through WebAssembly from now on:",
198
- error instanceof Error ? error.message : String(error)
199
- );
200
- }
201
- function noteFallback(operation) {
202
- }
203
- var NATIVE_CORE_ABI_VERSION = 2;
204
- var BINARY_FILE_NAME = "narsil-core.node";
205
- var MUSL_DIRECTORY = "musl";
206
- var PLATFORMS_WITH_A_BINARY = /* @__PURE__ */ new Set([
207
- "darwin-arm64",
208
- "darwin-x64",
209
- "linux-arm64",
210
- "linux-x64",
211
- "win32-arm64",
212
- "win32-x64"
213
- ]);
214
- var loaded;
215
- function usesMuslLibc() {
216
- try {
217
- const report = process.report.getReport();
218
- return report.header?.glibcVersionRuntime === void 0;
219
- } catch {
220
- return false;
221
- }
222
- }
223
- function publishedBinaryPath(require2) {
224
- const target = `${process.platform}-${process.arch}`;
225
- if (!PLATFORMS_WITH_A_BINARY.has(target)) return null;
226
- try {
227
- const manifest = require2.resolve(`@delali/narsil-native-${target}/package.json`);
228
- const segments = process.platform === "linux" && usesMuslLibc() ? [MUSL_DIRECTORY, BINARY_FILE_NAME] : [BINARY_FILE_NAME];
229
- return join(dirname(manifest), ...segments);
230
- } catch {
231
- return null;
232
- }
233
- }
234
- function namedBinaryPath() {
235
- const named = process.env.NARSIL_NATIVE_CORE_PATH;
236
- return named !== void 0 && named.length > 0 ? named : null;
237
- }
238
- function refuseToSearchWithoutTheCore(reason) {
239
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, `NARSIL_REQUIRE_NATIVE_CORE is 1, and ${reason}`, {
240
- platform: `${process.platform}-${process.arch}`
241
- });
242
- }
243
- function reportMissingCore(reason, named) {
244
- if (process.env.NARSIL_REQUIRE_NATIVE_CORE === "1") refuseToSearchWithoutTheCore(reason);
245
- if (named) console.warn(`Narsil searches through WebAssembly in this process, because ${reason}.`);
246
- return null;
247
- }
248
- function loadedCore(path) {
249
- const require2 = createRequire(import.meta.url);
250
- const core = require2(path);
251
- if (typeof core.abiVersion !== "function" || core.abiVersion() !== NATIVE_CORE_ABI_VERSION) return null;
252
- return core;
253
- }
254
- function loadNativeCore() {
255
- if (loaded !== void 0) return loaded;
256
- loaded = null;
257
- const named = namedBinaryPath();
258
- if (process.env.NARSIL_SEARCH_BACKEND === "wasm") {
259
- return reportMissingCore("NARSIL_SEARCH_BACKEND asks for the WebAssembly search", false);
260
- }
261
- const path = named ?? publishedBinaryPath(createRequire(import.meta.url));
262
- if (path === null || !existsSync(path)) {
263
- return reportMissingCore(`this platform has no native search core at ${path ?? "any known path"}`, named !== null);
264
- }
265
- try {
266
- loaded = loadedCore(path);
267
- } catch (error) {
268
- return reportMissingCore(
269
- `loading the native search core at ${path} failed with "${error instanceof Error ? error.message : String(error)}"`,
270
- named !== null
271
- );
272
- }
273
- if (loaded === null) {
274
- return reportMissingCore(`the native search core at ${path} reports another ABI version`, named !== null);
275
- }
276
- return loaded;
277
- }
278
-
279
186
  // src/vector/shared-buffers/growable.ts
280
187
  function sharedMemoryAvailable() {
281
188
  return typeof SharedArrayBuffer === "function";
@@ -1998,6 +1905,7 @@ function insertNode(state, ord, holdsGraphLock = false) {
1998
1905
  writeRecordBeforePlacement(state, ord);
1999
1906
  if (placeThroughTheCore(state, ord, holdsGraphLock)) return true;
2000
1907
  if (nodeExists(state, ord)) return false;
1908
+ noteFallback("place");
2001
1909
  if (!holdsGraphLock) lockGraphShared(state.locks);
2002
1910
  try {
2003
1911
  clearTombstone(state, ord);
@@ -2068,6 +1976,7 @@ function markTombstone(state, ord) {
2068
1976
  if (!nodeExists(state, ord) || !reachTombstone(state, ord)) return;
2069
1977
  const field = nativeFieldFor(state);
2070
1978
  if (field !== null && nativeRemove(state, field, ord)) return;
1979
+ noteFallback("remove");
2071
1980
  if (Atomics.compareExchange(state.tombstones, ord, 0, 1) === 0) {
2072
1981
  Atomics.add(state.header, GRAPH_TOMBSTONE_COUNT, 1);
2073
1982
  }
@@ -2083,6 +1992,7 @@ function compactTombstones(state) {
2083
1992
  if (Atomics.load(state.header, GRAPH_TOMBSTONE_COUNT) === 0) return;
2084
1993
  const field = nativeFieldFor(state);
2085
1994
  if (field !== null && nativeCompact(state, field, state.buildMetric)) return;
1995
+ noteFallback("compact");
2086
1996
  const tombstonedOrds = [];
2087
1997
  const slots = adjacencySlots(state.adjacency);
2088
1998
  for (let ord = 0; ord < slots; ord++) {
@@ -2236,6 +2146,7 @@ function traverse(state, query, k, searchMetric, options) {
2236
2146
  return { candidates, depth: useQuantized ? depth : 0, arenaQuery: null, qMag: 0, native };
2237
2147
  }
2238
2148
  }
2149
+ noteFallback("search");
2239
2150
  const store = state.store;
2240
2151
  const arenaQuery = store.prepareQueryArena(query);
2241
2152
  const qMag = arenaQuery ? arenaQuery.magnitude : magnitude(query);
@@ -2361,6 +2272,7 @@ function collectHits(state, query, k, searchMetric, minSimilarity, options) {
2361
2272
  return hits;
2362
2273
  }
2363
2274
  function rescoreWithFullPrecision(state, candidates, query, qMag, arenaQuery, depth, metric, minSimilarity, filter) {
2275
+ noteFallback("score");
2364
2276
  const rescored = [];
2365
2277
  const nearest = Math.min(candidates.size, depth);
2366
2278
  for (let i = 0; i < nearest; i++) {
@@ -2632,6 +2544,7 @@ function openSharedQuantizer(store, layout, metric) {
2632
2544
  function writeCodesOf(ordinals) {
2633
2545
  if (!isCalibrated() || ordinals.length === 0) return;
2634
2546
  if (nativeQuantise(store.handles, metric, ordinals)) return;
2547
+ noteFallback("quantise");
2635
2548
  for (const ordinal of ordinals) writeRecordOf(ordinal);
2636
2549
  }
2637
2550
  function stageQuery(block, query) {
@@ -2728,6 +2641,7 @@ function openSharedQuantizer(store, layout, metric) {
2728
2641
  writeCodesOf,
2729
2642
  calibrateFrom(ordinals) {
2730
2643
  if (nativeCalibrate(store.handles, metric, ordinals)) return isCalibrated();
2644
+ noteFallback("calibrate");
2731
2645
  const centroid = osqCentroid(storedVectors(ordinals), dimension, metric);
2732
2646
  if (centroid === null) return false;
2733
2647
  writeCentroid(centroid);
@@ -4189,7 +4103,11 @@ function createHNSWIndex(dimension, store, config, quantizer, handles) {
4189
4103
  throw new Error(`Cannot insert HNSW node: vector for "${docId}" not found in VectorStore`);
4190
4104
  }
4191
4105
  if (entry.vector.length !== dimension) {
4192
- throw new Error(`Vector dimension mismatch: expected ${dimension}, got ${entry.vector.length}`);
4106
+ throw new NarsilError(
4107
+ ErrorCodes.VECTOR_DIMENSION_MISMATCH,
4108
+ `Vector dimension mismatch: expected ${dimension}, got ${entry.vector.length}`,
4109
+ { docId, expected: dimension, received: entry.vector.length }
4110
+ );
4193
4111
  }
4194
4112
  insertNode(state, ord);
4195
4113
  }
@@ -4277,4 +4195,4 @@ function createHNSWIndex(dimension, store, config, quantizer, handles) {
4277
4195
  };
4278
4196
  }
4279
4197
 
4280
- export { DISK_STORAGE_BLOCK_BYTES, GRAPH_BUILD_CHUNKS_IN_FLIGHT_PER_WORKER, GRAPH_BUILD_DISPATCH_CHUNK, MAX_WORKER_COPIES, SCRATCH_SLOTS_PER_THREAD_POOL, STORE_CALIBRATION_GENERATION, VECTOR_INSERT_TIMEOUT_MS, VECTOR_SCRATCH_SLOTS, VECTOR_SEARCH_LOAD_TIMEOUT_MS, VECTOR_SEARCH_TIMEOUT_MS, addToOrdinalFilter, cosineSimilarityWithMagnitudes, createHNSWIndex, createOrdinalFilter, createOsqQuantizer, createVectorStore, dotProduct, entryForOrd, euclideanDistance, isSimdAvailable, isTombstoned, magnitude, nativeScoresOf, nativeStoreScratchBytes, noteFallback, openSharedVectorField, ordinalFilterHas, ordinalFilterValues, osqBitsOf, osqRecordBytes, releaseLocksHeldBy, removeFromOrdinalFilter, requireWholeNeighbourLists, sharedMemoryAvailable, toDistance, toScore };
4198
+ export { DISK_STORAGE_BLOCK_BYTES, GRAPH_BUILD_CHUNKS_IN_FLIGHT_PER_WORKER, GRAPH_BUILD_DISPATCH_CHUNK, MAX_WORKER_COPIES, SCRATCH_SLOTS_PER_THREAD_POOL, STORE_CALIBRATION_GENERATION, VECTOR_INSERT_TIMEOUT_MS, VECTOR_SCRATCH_SLOTS, VECTOR_SEARCH_LOAD_TIMEOUT_MS, VECTOR_SEARCH_TIMEOUT_MS, addToOrdinalFilter, cosineSimilarityWithMagnitudes, createHNSWIndex, createOrdinalFilter, createOsqQuantizer, createVectorStore, dotProduct, entryForOrd, euclideanDistance, isSimdAvailable, isTombstoned, magnitude, nativeScoresOf, nativeStoreScratchBytes, openSharedVectorField, ordinalFilterHas, ordinalFilterValues, osqBitsOf, osqRecordBytes, releaseLocksHeldBy, removeFromOrdinalFilter, requireWholeNeighbourLists, sharedMemoryAvailable, toDistance, toScore };
@@ -0,0 +1,135 @@
1
+ import { NarsilError, ErrorCodes } from './chunk-FRQVMFY7.mjs';
2
+ import { existsSync } from 'fs';
3
+ import { createRequire } from 'module';
4
+ import { join, dirname } from 'path';
5
+
6
+ // src/vector/hnsw/constants.ts
7
+ var MAX_LAYER_CAP = 32;
8
+ var MAX_M = 512;
9
+ var INITIAL_UPPER_SLOTS = 64;
10
+ var INITIAL_LIST_CAPACITY = 64;
11
+ var PLACEMENT_ROOM_REQUESTS = 8;
12
+ var COMPACTION_TOMBSTONE_RATIO = 0.1;
13
+ var COMPACTION_ABSOLUTE_THRESHOLD = 1e3;
14
+ var DEFAULT_EF_SEARCH = 50;
15
+
16
+ // src/vector/native/backend.ts
17
+ var nativeSearchCoreEnabled = true;
18
+ function nativeSearchCoreIsEnabled() {
19
+ return nativeSearchCoreEnabled;
20
+ }
21
+ function stopUsingTheNativeSearchCore(error) {
22
+ if (!nativeSearchCoreEnabled) return;
23
+ nativeSearchCoreEnabled = false;
24
+ console.warn(
25
+ "The native search core raised an error, so this thread searches through WebAssembly from now on:",
26
+ error instanceof Error ? error.message : String(error)
27
+ );
28
+ }
29
+ function noteFallback(operation) {
30
+ }
31
+ var NATIVE_CORE_ABI_VERSION = 2;
32
+ var BINARY_FILE_NAME = "narsil-core.node";
33
+ var MUSL_DIRECTORY = "musl";
34
+ var PLATFORMS_WITH_A_BINARY = /* @__PURE__ */ new Set([
35
+ "darwin-arm64",
36
+ "darwin-x64",
37
+ "linux-arm64",
38
+ "linux-x64",
39
+ "win32-arm64",
40
+ "win32-x64"
41
+ ]);
42
+ var loaded;
43
+ function usesMuslLibc() {
44
+ try {
45
+ const report = process.report.getReport();
46
+ return report.header?.glibcVersionRuntime === void 0;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+ function isUnavailable(value) {
52
+ return "missing" in value;
53
+ }
54
+ function binaryFileSegments() {
55
+ return process.platform === "linux" && usesMuslLibc() ? [MUSL_DIRECTORY, BINARY_FILE_NAME] : [BINARY_FILE_NAME];
56
+ }
57
+ function publishedCoreLocation(target) {
58
+ const packageName = `@delali/narsil-native-${target}`;
59
+ let manifest;
60
+ try {
61
+ manifest = createRequire(import.meta.url).resolve(`${packageName}/package.json`);
62
+ } catch {
63
+ return { missing: `this installation has no "${packageName}" package` };
64
+ }
65
+ const path = join(dirname(manifest), ...binaryFileSegments());
66
+ if (!existsSync(path)) return { missing: `the "${packageName}" package has no search core at ${path}` };
67
+ return { path };
68
+ }
69
+ function coreLocation() {
70
+ const named = process.env.NARSIL_NATIVE_CORE_PATH;
71
+ if (named !== void 0 && named.length > 0) {
72
+ if (!existsSync(named)) {
73
+ return { missing: `this process sets NARSIL_NATIVE_CORE_PATH to ${named}, where no file exists` };
74
+ }
75
+ return { path: named };
76
+ }
77
+ const target = `${process.platform}-${process.arch}`;
78
+ if (!PLATFORMS_WITH_A_BINARY.has(target)) {
79
+ return { missing: `the Narsil release includes no search core for ${target}` };
80
+ }
81
+ return publishedCoreLocation(target);
82
+ }
83
+ function refuseToSearchWithoutTheCore(reason) {
84
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, `NARSIL_REQUIRE_NATIVE_CORE is 1, yet ${reason}`, {
85
+ platform: `${process.platform}-${process.arch}`
86
+ });
87
+ }
88
+ function requestedBackend() {
89
+ const raw = process.env.NARSIL_SEARCH_BACKEND;
90
+ if (raw === void 0 || raw.trim().length === 0) return null;
91
+ const named = raw.trim().toLowerCase();
92
+ if (named === "native" || named === "wasm") return named;
93
+ throw new NarsilError(
94
+ ErrorCodes.CONFIG_INVALID,
95
+ `NARSIL_SEARCH_BACKEND takes either "native" or "wasm", while this environment sets it to "${raw}"`,
96
+ { value: raw, backends: ["native", "wasm"] }
97
+ );
98
+ }
99
+ function reportMissingCore(reason, announce) {
100
+ if (process.env.NARSIL_REQUIRE_NATIVE_CORE === "1") refuseToSearchWithoutTheCore(reason);
101
+ loaded = null;
102
+ if (announce) console.warn(`Narsil searches through WebAssembly on this thread, because ${reason}.`);
103
+ return null;
104
+ }
105
+ function coreAt(path) {
106
+ let core;
107
+ try {
108
+ core = createRequire(import.meta.url)(path);
109
+ } catch (error) {
110
+ const message = error instanceof Error ? error.message : String(error);
111
+ return { missing: `node raised "${message}" while loading the search core at ${path}` };
112
+ }
113
+ if (typeof core.abiVersion !== "function") return { missing: `the file at ${path} is no Narsil search core` };
114
+ const reported = core.abiVersion();
115
+ if (reported !== NATIVE_CORE_ABI_VERSION) {
116
+ return {
117
+ missing: `the search core at ${path} reports ABI version ${reported}, while this build of Narsil needs ${NATIVE_CORE_ABI_VERSION}`
118
+ };
119
+ }
120
+ return core;
121
+ }
122
+ function loadNativeCore() {
123
+ if (loaded !== void 0) return loaded;
124
+ if (requestedBackend() === "wasm") {
125
+ return reportMissingCore("this process sets NARSIL_SEARCH_BACKEND to the WebAssembly search", false);
126
+ }
127
+ const location = coreLocation();
128
+ if (isUnavailable(location)) return reportMissingCore(location.missing, true);
129
+ const core = coreAt(location.path);
130
+ if (isUnavailable(core)) return reportMissingCore(core.missing, true);
131
+ loaded = core;
132
+ return loaded;
133
+ }
134
+
135
+ export { COMPACTION_ABSOLUTE_THRESHOLD, COMPACTION_TOMBSTONE_RATIO, DEFAULT_EF_SEARCH, INITIAL_LIST_CAPACITY, INITIAL_UPPER_SLOTS, MAX_LAYER_CAP, MAX_M, PLACEMENT_ROOM_REQUESTS, loadNativeCore, nativeSearchCoreIsEnabled, noteFallback, stopUsingTheNativeSearchCore };
@@ -1,21 +1,59 @@
1
1
  import { detectRuntime } from './chunk-AV4IJKD2.mjs';
2
- import { cloneProjected, mergeFacets, visitTokens, countsWithoutScores, fanOutMatchCount, fanOutQuery, sanitizeGlobalStats, projectionKeepsField, setNestedValue, fnv1a, bitsetHas, createBitSet, kWayMerge, boundedLevenshtein, bitsetSet, tokenize, bitsetFromSet, tokenizeIterator, bitsetAnd, bitsetOr, bitsetNot } from './chunk-WM7IS45M.mjs';
3
- import { flattenSchema, validateDocument, validateDocumentStrict, isQuantizationMode, isStorageMode, extractVectorFieldsFromSchema, isTextFieldType, VECTOR_PATTERN } from './chunk-ZT5BRDFW.mjs';
2
+ import { cloneProjected, mergeFacets, visitTokens, countsWithoutScores, fanOutMatchCount, fanOutQuery, sanitizeGlobalStats, projectionKeepsField, setNestedValue, fnv1a, bitsetHas, createBitSet, kWayMerge, boundedLevenshtein, bitsetSet, tokenize, bitsetFromSet, tokenizeIterator, bitsetAnd, bitsetOr, bitsetNot } from './chunk-5T6AOR37.mjs';
3
+ import { flattenSchema, validateDocument, validateDocumentStrict, isQuantizationMode, isStorageMode, extractVectorFieldsFromSchema, isTextFieldType, VECTOR_PATTERN } from './chunk-L225SHNB.mjs';
4
4
  import { english } from './chunk-TGP5M5R7.mjs';
5
- import { SCRATCH_SLOTS_PER_THREAD_POOL, osqRecordBytes, openSharedVectorField, osqBitsOf, createVectorStore, DISK_STORAGE_BLOCK_BYTES, createOsqQuantizer, magnitude, createHNSWIndex, nativeStoreScratchBytes, createOrdinalFilter, addToOrdinalFilter, ordinalFilterValues, isTombstoned, nativeScoresOf, toScore, removeFromOrdinalFilter, noteFallback, euclideanDistance, dotProduct, cosineSimilarityWithMagnitudes, ordinalFilterHas, requireWholeNeighbourLists, STORE_CALIBRATION_GENERATION, entryForOrd, toDistance, sharedMemoryAvailable, GRAPH_BUILD_CHUNKS_IN_FLIGHT_PER_WORKER, GRAPH_BUILD_DISPATCH_CHUNK, releaseLocksHeldBy, VECTOR_SEARCH_TIMEOUT_MS, VECTOR_SEARCH_LOAD_TIMEOUT_MS, VECTOR_INSERT_TIMEOUT_MS, VECTOR_SCRATCH_SLOTS } from './chunk-T62YOSGS.mjs';
5
+ import { SCRATCH_SLOTS_PER_THREAD_POOL, osqRecordBytes, openSharedVectorField, osqBitsOf, createVectorStore, DISK_STORAGE_BLOCK_BYTES, createOsqQuantizer, magnitude, createHNSWIndex, nativeStoreScratchBytes, createOrdinalFilter, addToOrdinalFilter, ordinalFilterValues, isTombstoned, nativeScoresOf, toScore, removeFromOrdinalFilter, euclideanDistance, dotProduct, cosineSimilarityWithMagnitudes, ordinalFilterHas, requireWholeNeighbourLists, STORE_CALIBRATION_GENERATION, entryForOrd, toDistance, sharedMemoryAvailable, GRAPH_BUILD_CHUNKS_IN_FLIGHT_PER_WORKER, GRAPH_BUILD_DISPATCH_CHUNK, releaseLocksHeldBy, VECTOR_SEARCH_TIMEOUT_MS, VECTOR_SEARCH_LOAD_TIMEOUT_MS, VECTOR_INSERT_TIMEOUT_MS, VECTOR_SCRATCH_SLOTS } from './chunk-U2AL3SON.mjs';
6
+ import { noteFallback } from './chunk-UTOFOJOR.mjs';
6
7
  import { VALID_HNSW_METRICS } from './chunk-IFBKLBGB.mjs';
7
8
  import { compareCodePoints, toComparableSortValue, readSortField, compareComparableValues, SORT_VALUE_MAX_CODE_POINTS } from './chunk-2GWVQWGA.mjs';
8
- import { NarsilError, ErrorCodes } from './chunk-P4ELXVJQ.mjs';
9
+ import { NarsilError, ErrorCodes } from './chunk-FRQVMFY7.mjs';
10
+ import { totalmem } from 'os';
9
11
  import { getHeapStatistics } from 'v8';
10
12
  import { encode, decode } from '@msgpack/msgpack';
11
13
 
14
+ var MEGABYTE_BYTES = 1048576;
15
+ var IN_MEGABYTES = /^--max-old-space-size=(\d+)$/;
16
+ var AS_PERCENTAGE = /^--max-old-space-size-percentage=(\d+(?:\.\d+)?)$/;
17
+ function flagEntries() {
18
+ const fromNodeOptions = (process.env.NODE_OPTIONS ?? "").split(/\s+/).filter((entry) => entry.length > 0);
19
+ return [...process.execArgv, ...fromNodeOptions];
20
+ }
21
+ function memoryTheHostAllows() {
22
+ const constrained = process.constrainedMemory?.() ?? 0;
23
+ return constrained > 0 ? constrained : totalmem();
24
+ }
25
+ function readConfiguredLimit() {
26
+ let megabytes = null;
27
+ let percentage = null;
28
+ for (const entry of flagEntries()) {
29
+ const inMegabytes = IN_MEGABYTES.exec(entry);
30
+ if (inMegabytes !== null) {
31
+ megabytes = Number(inMegabytes[1]);
32
+ percentage = null;
33
+ }
34
+ const asPercentage = AS_PERCENTAGE.exec(entry);
35
+ if (asPercentage !== null) {
36
+ percentage = Number(asPercentage[1]);
37
+ megabytes = null;
38
+ }
39
+ }
40
+ const bytes = megabytes !== null && megabytes > 0 ? megabytes * MEGABYTE_BYTES : percentage !== null && percentage > 0 && percentage <= 100 ? Math.floor(memoryTheHostAllows() * percentage / 100) : null;
41
+ return bytes !== null && Number.isFinite(bytes) && bytes > 0 ? bytes : null;
42
+ }
43
+ var configuredLimit;
44
+ function configuredLimitBytes() {
45
+ if (configuredLimit === void 0) configuredLimit = readConfiguredLimit();
46
+ return configuredLimit;
47
+ }
12
48
  function readHeapStatistics() {
13
49
  try {
14
50
  const stats = getHeapStatistics();
15
51
  const usedBytes = stats.used_heap_size;
16
52
  const limitBytes = stats.heap_size_limit;
17
53
  if (!Number.isFinite(usedBytes) || !Number.isFinite(limitBytes) || limitBytes <= 0) return null;
18
- return { usedBytes, limitBytes };
54
+ const available = stats.total_available_size;
55
+ const availableBytes = Number.isFinite(available) && available >= 0 ? available : null;
56
+ return { usedBytes, limitBytes, availableBytes, configuredLimitBytes: configuredLimitBytes() };
19
57
  } catch {
20
58
  return null;
21
59
  }
@@ -8421,8 +8459,9 @@ function completeGraph(state) {
8421
8459
  }
8422
8460
  function maintenanceStatus(state) {
8423
8461
  const storeSize = state.store.size;
8424
- const tombstoneRatio = storeSize > 0 ? state.tombstones.size / storeSize : 0;
8425
8462
  const graphCount = state.hnsw ? 1 : 0;
8463
+ const graphNodes = state.hnsw === null ? storeSize : state.hnsw.size + state.hnsw.tombstoneCount;
8464
+ const tombstoneRatio = graphNodes > 0 ? state.tombstones.size / graphNodes : 0;
8426
8465
  const estimatedCompactMs = Math.round(state.tombstones.size * ESTIMATED_MS_PER_TOMBSTONE * state.dimensionScale);
8427
8466
  const optimizeVectorCount = state.hnsw === null || graphNeedsRebuild(state) ? storeSize : state.buffer.size;
8428
8467
  const estimatedOptimizeMs = Math.round(optimizeVectorCount * ESTIMATED_MS_PER_VECTOR_REBUILD * state.dimensionScale);
@@ -8642,7 +8681,11 @@ function search(state, query, k, options) {
8642
8681
  }
8643
8682
  function searchWithFilter(state, query, k, options, filter) {
8644
8683
  if (query.length !== state.dimension) {
8645
- throw new Error(`Vector dimension mismatch: expected ${state.dimension}, got ${query.length}`);
8684
+ throw new NarsilError(
8685
+ ErrorCodes.VECTOR_DIMENSION_MISMATCH,
8686
+ `Vector dimension mismatch: expected ${state.dimension}, got ${query.length}`,
8687
+ { expected: state.dimension, received: query.length }
8688
+ );
8646
8689
  }
8647
8690
  const currentLiveSize = liveSize(state);
8648
8691
  if (currentLiveSize === 0) return [];
@@ -11217,7 +11260,9 @@ function createDirectExecutor(options) {
11217
11260
  }
11218
11261
  case "memoryReport": {
11219
11262
  const usage = typeof process !== "undefined" && process.memoryUsage ? process.memoryUsage() : {};
11220
- return { ...usage, heapLimit: readHeapStatistics()?.limitBytes ?? null };
11263
+ const heap = readHeapStatistics();
11264
+ const heapLimit = heap === null ? null : heap.configuredLimitBytes ?? heap.limitBytes;
11265
+ return { ...usage, heapLimit };
11221
11266
  }
11222
11267
  case "shutdown": {
11223
11268
  for (const [, entry] of indexes) {