@delali/narsil 0.1.8 → 0.1.10

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.
@@ -1049,6 +1049,38 @@ function extractVectorFieldsFromSchema(schema) {
1049
1049
  }
1050
1050
  return result;
1051
1051
  }
1052
+
1053
+ // src/schema/validator/vector-promotion.ts
1054
+ function fail(message, details) {
1055
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, message, details);
1056
+ }
1057
+ function isPositiveInteger(value) {
1058
+ return Number.isInteger(value) && value >= 1;
1059
+ }
1060
+ function validateVectorPromotion(config) {
1061
+ if (config === void 0) return;
1062
+ if (config.threshold !== void 0 && !isPositiveInteger(config.threshold)) {
1063
+ fail("vectorPromotion.threshold must be a positive integer", { threshold: config.threshold });
1064
+ }
1065
+ const quantization = config.quantization;
1066
+ if (quantization !== void 0 && quantization !== "sq8" && quantization !== "none") {
1067
+ fail("vectorPromotion.quantization must be 'sq8' or 'none'", { quantization });
1068
+ }
1069
+ const hnsw = config.hnswConfig;
1070
+ if (hnsw === void 0) return;
1071
+ if (hnsw.m !== void 0 && !isPositiveInteger(hnsw.m)) {
1072
+ fail("vectorPromotion.hnswConfig.m must be a positive integer", { m: hnsw.m });
1073
+ }
1074
+ if (hnsw.efConstruction !== void 0 && !isPositiveInteger(hnsw.efConstruction)) {
1075
+ fail("vectorPromotion.hnswConfig.efConstruction must be a positive integer", {
1076
+ efConstruction: hnsw.efConstruction
1077
+ });
1078
+ }
1079
+ const metric = hnsw.metric;
1080
+ if (metric !== void 0 && metric !== "cosine" && metric !== "dotProduct" && metric !== "euclidean") {
1081
+ fail("vectorPromotion.hnswConfig.metric must be 'cosine', 'dotProduct', or 'euclidean'", { metric });
1082
+ }
1083
+ }
1052
1084
  function encodeRawPayloadV2(wire) {
1053
1085
  return encode(wire);
1054
1086
  }
@@ -3992,6 +4024,123 @@ function createPluginRegistry() {
3992
4024
  };
3993
4025
  }
3994
4026
 
4027
+ // src/schema/embedding-validator.ts
4028
+ var VECTOR_DIMENSION_PATTERN = /^vector\[(\d+)]$/;
4029
+ function validateEmbeddingConfig(embeddingConfig, schema, instanceAdapter) {
4030
+ const fieldEntries = Object.keys(embeddingConfig.fields);
4031
+ if (fieldEntries.length === 0) {
4032
+ throw new NarsilError(
4033
+ ErrorCodes.EMBEDDING_CONFIG_INVALID,
4034
+ "Embedding config must define at least one field mapping"
4035
+ );
4036
+ }
4037
+ if (typeof embeddingConfig.adapter === "string") {
4038
+ throw new NarsilError(
4039
+ ErrorCodes.EMBEDDING_CONFIG_INVALID,
4040
+ `Embedding adapter "${embeddingConfig.adapter}" must be resolved against the engine registry before validation`,
4041
+ { adapter: embeddingConfig.adapter }
4042
+ );
4043
+ }
4044
+ const resolvedAdapter = embeddingConfig.adapter ?? instanceAdapter;
4045
+ if (!resolvedAdapter) {
4046
+ throw new NarsilError(
4047
+ ErrorCodes.EMBEDDING_CONFIG_INVALID,
4048
+ "Embedding config requires an adapter (set on the index or the instance)"
4049
+ );
4050
+ }
4051
+ if (typeof resolvedAdapter.dimensions !== "number" || !Number.isInteger(resolvedAdapter.dimensions) || resolvedAdapter.dimensions <= 0) {
4052
+ throw new NarsilError(
4053
+ ErrorCodes.EMBEDDING_CONFIG_INVALID,
4054
+ `Adapter dimensions must be a positive integer, got ${resolvedAdapter.dimensions}`
4055
+ );
4056
+ }
4057
+ const flatFields = flattenSchema(schema);
4058
+ for (const [targetField, sourceFields] of Object.entries(embeddingConfig.fields)) {
4059
+ const targetType = flatFields[targetField];
4060
+ if (targetType === void 0) {
4061
+ throw new NarsilError(
4062
+ ErrorCodes.EMBEDDING_CONFIG_INVALID,
4063
+ `Embedding target field "${targetField}" does not exist in the schema`,
4064
+ { field: targetField }
4065
+ );
4066
+ }
4067
+ const vectorMatch = VECTOR_DIMENSION_PATTERN.exec(targetType);
4068
+ if (!vectorMatch) {
4069
+ throw new NarsilError(
4070
+ ErrorCodes.EMBEDDING_CONFIG_INVALID,
4071
+ `Embedding target field "${targetField}" must be a vector type, got "${targetType}"`,
4072
+ { field: targetField, type: targetType }
4073
+ );
4074
+ }
4075
+ const schemaDimension = Number.parseInt(vectorMatch[1], 10);
4076
+ if (schemaDimension !== resolvedAdapter.dimensions) {
4077
+ throw new NarsilError(
4078
+ ErrorCodes.EMBEDDING_DIMENSION_MISMATCH,
4079
+ `Embedding target field "${targetField}" has dimension ${schemaDimension} but the adapter produces ${resolvedAdapter.dimensions}-dimensional vectors`,
4080
+ { field: targetField, expected: schemaDimension, actual: resolvedAdapter.dimensions }
4081
+ );
4082
+ }
4083
+ const sources = Array.isArray(sourceFields) ? sourceFields : [sourceFields];
4084
+ if (sources.length === 0) {
4085
+ throw new NarsilError(
4086
+ ErrorCodes.EMBEDDING_CONFIG_INVALID,
4087
+ `Embedding target field "${targetField}" must have at least one source field`,
4088
+ { field: targetField }
4089
+ );
4090
+ }
4091
+ for (const source of sources) {
4092
+ const sourceType = flatFields[source];
4093
+ if (sourceType === void 0) {
4094
+ throw new NarsilError(
4095
+ ErrorCodes.EMBEDDING_CONFIG_INVALID,
4096
+ `Embedding source field "${source}" does not exist in the schema`,
4097
+ { field: source, targetField }
4098
+ );
4099
+ }
4100
+ if (sourceType !== "string") {
4101
+ throw new NarsilError(
4102
+ ErrorCodes.EMBEDDING_CONFIG_INVALID,
4103
+ `Embedding source field "${source}" must be a string type, got "${sourceType}"`,
4104
+ { field: source, type: sourceType, targetField }
4105
+ );
4106
+ }
4107
+ }
4108
+ }
4109
+ return resolvedAdapter;
4110
+ }
4111
+ function validateRegisteredAdapter(name, adapter) {
4112
+ if (typeof name !== "string" || name.trim().length === 0) {
4113
+ throw new NarsilError(ErrorCodes.EMBEDDING_CONFIG_INVALID, "Embedding adapter names must be non-empty strings");
4114
+ }
4115
+ if (typeof adapter?.embed !== "function") {
4116
+ throw new NarsilError(
4117
+ ErrorCodes.EMBEDDING_CONFIG_INVALID,
4118
+ `Embedding adapter "${name}" must provide an embed function`,
4119
+ { adapter: name }
4120
+ );
4121
+ }
4122
+ if (typeof adapter.dimensions !== "number" || !Number.isInteger(adapter.dimensions) || adapter.dimensions <= 0) {
4123
+ throw new NarsilError(
4124
+ ErrorCodes.EMBEDDING_CONFIG_INVALID,
4125
+ `Embedding adapter "${name}" dimensions must be a positive integer, got ${adapter.dimensions}`,
4126
+ { adapter: name, dimensions: adapter.dimensions }
4127
+ );
4128
+ }
4129
+ }
4130
+ function validateRequiredFieldsInSchema(required, schema) {
4131
+ if (required.length === 0) return;
4132
+ const flatFields = flattenSchema(schema);
4133
+ for (const field of required) {
4134
+ if (flatFields[field] === void 0) {
4135
+ throw new NarsilError(
4136
+ ErrorCodes.EMBEDDING_CONFIG_INVALID,
4137
+ `Required field "${field}" does not exist in the schema`,
4138
+ { field }
4139
+ );
4140
+ }
4141
+ }
4142
+ }
4143
+
3995
4144
  // src/search/facets.ts
3996
4145
  function mergeFacets(partitionFacets) {
3997
4146
  const merged = /* @__PURE__ */ new Map();
@@ -6027,6 +6176,14 @@ function createHNSWIndex(dimension, store, config, quantizer) {
6027
6176
  };
6028
6177
  }
6029
6178
 
6179
+ // src/workers/node-worker.browser.ts
6180
+ async function spawnNodeWorker(_entryPoint) {
6181
+ return null;
6182
+ }
6183
+ async function isNodeMainThread() {
6184
+ return true;
6185
+ }
6186
+
6030
6187
  // src/runtime/detect.ts
6031
6188
  function detectRuntimeName() {
6032
6189
  if ("Bun" in globalThis) {
@@ -6076,9 +6233,7 @@ async function spawnWorker() {
6076
6233
  const entryPoint = resolveWorkerEntryPoint();
6077
6234
  if (runtime.supportsWorkerThreads) {
6078
6235
  try {
6079
- const workerThreads = await import('worker_threads');
6080
- const instance = new workerThreads.Worker(new URL(entryPoint));
6081
- return instance;
6236
+ return await spawnNodeWorker(new URL(entryPoint));
6082
6237
  } catch {
6083
6238
  return null;
6084
6239
  }
@@ -6966,21 +7121,21 @@ function createExecutionPromoter(config) {
6966
7121
  return { check, markPromoted, isPromoted };
6967
7122
  }
6968
7123
 
6969
- // src/persistence/durability/durable-filesystem.ts
6970
- var cachedFs = null;
6971
- var cachedPath = null;
7124
+ // src/persistence/durability/node-fs.browser.ts
7125
+ function filesystemUnavailable() {
7126
+ throw new NarsilError(
7127
+ ErrorCodes.CONFIG_INVALID,
7128
+ "Filesystem durability requires a Node.js runtime; use the memory or IndexedDB persistence adapters in the browser"
7129
+ );
7130
+ }
6972
7131
  async function getFs() {
6973
- if (cachedFs === null) {
6974
- cachedFs = await import('fs/promises');
6975
- }
6976
- return cachedFs;
7132
+ return filesystemUnavailable();
6977
7133
  }
6978
7134
  async function getPath() {
6979
- if (cachedPath === null) {
6980
- cachedPath = await import('path');
6981
- }
6982
- return cachedPath;
7135
+ return filesystemUnavailable();
6983
7136
  }
7137
+
7138
+ // src/persistence/durability/durable-filesystem.ts
6984
7139
  function validateRelativeKey(key) {
6985
7140
  if (key.length === 0) {
6986
7141
  throw new NarsilError(ErrorCodes.PERSISTENCE_SAVE_FAILED, "Invalid key: empty");
@@ -7293,12 +7448,7 @@ var mainThreadResolved = false;
7293
7448
  var onMainThread = true;
7294
7449
  async function isOnMainThread() {
7295
7450
  if (!mainThreadResolved) {
7296
- try {
7297
- const workerThreads = await import('worker_threads');
7298
- onMainThread = workerThreads.isMainThread;
7299
- } catch {
7300
- onMainThread = true;
7301
- }
7451
+ onMainThread = await isNodeMainThread();
7302
7452
  mainThreadResolved = true;
7303
7453
  }
7304
7454
  return onMainThread;
@@ -7313,8 +7463,7 @@ function resolveWorkerEntryPoint2() {
7313
7463
  }
7314
7464
  async function spawnWorker2() {
7315
7465
  try {
7316
- const workerThreads = await import('worker_threads');
7317
- return new workerThreads.Worker(new URL(resolveWorkerEntryPoint2()));
7466
+ return await spawnNodeWorker(new URL(resolveWorkerEntryPoint2()));
7318
7467
  } catch {
7319
7468
  return null;
7320
7469
  }
@@ -7580,6 +7729,9 @@ function metadataToWire(meta) {
7580
7729
  if (meta.vectorFields) {
7581
7730
  wire.vector_fields = meta.vectorFields;
7582
7731
  }
7732
+ if (meta.embedding) {
7733
+ wire.embedding = meta.embedding.adapter !== void 0 ? { adapter: meta.embedding.adapter, fields: meta.embedding.fields } : { fields: meta.embedding.fields };
7734
+ }
7583
7735
  return wire;
7584
7736
  }
7585
7737
  function wireToMetadata(raw) {
@@ -7595,6 +7747,9 @@ function wireToMetadata(raw) {
7595
7747
  if (raw.vector_fields) {
7596
7748
  meta.vectorFields = raw.vector_fields;
7597
7749
  }
7750
+ if (raw.embedding && typeof raw.embedding === "object" && typeof raw.embedding.fields === "object") {
7751
+ meta.embedding = typeof raw.embedding.adapter === "string" ? { adapter: raw.embedding.adapter, fields: raw.embedding.fields } : { fields: raw.embedding.fields };
7752
+ }
7598
7753
  return meta;
7599
7754
  }
7600
7755
  function serializeMetadata(meta) {
@@ -7987,8 +8142,10 @@ function resolveWorkerEntryPoint3() {
7987
8142
  }
7988
8143
  async function spawnWorker3() {
7989
8144
  try {
7990
- const workerThreads = await import('worker_threads');
7991
- const worker = new workerThreads.Worker(new URL(resolveWorkerEntryPoint3()));
8145
+ const worker = await spawnNodeWorker(new URL(resolveWorkerEntryPoint3()));
8146
+ if (worker === null) {
8147
+ return null;
8148
+ }
7992
8149
  if (typeof worker.unref === "function") {
7993
8150
  worker.unref();
7994
8151
  }
@@ -8947,11 +9104,15 @@ function unflattenSchema(flat) {
8947
9104
  return root;
8948
9105
  }
8949
9106
  function reconstructSchemaFromMetadata(metadata) {
8950
- return {
9107
+ const config = {
8951
9108
  schema: unflattenSchema(metadata.schema),
8952
9109
  language: metadata.language,
8953
9110
  bm25: { k1: metadata.bm25Params.k1, b: metadata.bm25Params.b }
8954
9111
  };
9112
+ if (metadata.embedding) {
9113
+ config.embedding = metadata.embedding.adapter !== void 0 ? { adapter: metadata.embedding.adapter, fields: metadata.embedding.fields } : { fields: metadata.embedding.fields };
9114
+ }
9115
+ return config;
8955
9116
  }
8956
9117
 
8957
9118
  // src/persistence/durability/segment/build-segment.ts
@@ -10399,7 +10560,7 @@ function buildMetadata(indexName, hooks) {
10399
10560
  if (config === void 0 || manager === void 0) {
10400
10561
  return void 0;
10401
10562
  }
10402
- return {
10563
+ const metadata = {
10403
10564
  indexName,
10404
10565
  schema: config.schema,
10405
10566
  language: config.language,
@@ -10408,6 +10569,10 @@ function buildMetadata(indexName, hooks) {
10408
10569
  createdAt: Date.now(),
10409
10570
  engineVersion: VERSION
10410
10571
  };
10572
+ if (config.embedding) {
10573
+ metadata.embedding = config.embedding;
10574
+ }
10575
+ return metadata;
10411
10576
  }
10412
10577
  function createDurabilityIntegration(tier, hooks) {
10413
10578
  const managerHooks = {
@@ -10928,11 +11093,16 @@ function createDurabilityFromConfig(config, wiring) {
10928
11093
  if (entry === void 0) {
10929
11094
  return void 0;
10930
11095
  }
11096
+ const embedding = entry.config.embedding ? {
11097
+ fields: entry.config.embedding.fields,
11098
+ ...entry.embeddingAdapterName !== null ? { adapter: entry.embeddingAdapterName } : {}
11099
+ } : void 0;
10931
11100
  return {
10932
11101
  schema: flattenSchema(entry.config.schema),
10933
11102
  language: entry.language.name,
10934
11103
  k1: entry.config.bm25?.k1 ?? 1.2,
10935
- b: entry.config.bm25?.b ?? 0.75
11104
+ b: entry.config.bm25?.b ?? 0.75,
11105
+ ...embedding !== void 0 ? { embedding } : {}
10936
11106
  };
10937
11107
  },
10938
11108
  createIndexFromMetadata: wiring.createIndexFromMetadata,
@@ -10951,6 +11121,13 @@ function createEngineCore(config) {
10951
11121
  }
10952
11122
  const idGenerator = config?.idGenerator ?? generateId;
10953
11123
  const indexRegistry = /* @__PURE__ */ new Map();
11124
+ const embeddingAdapters = /* @__PURE__ */ new Map();
11125
+ if (config?.embeddingAdapters) {
11126
+ for (const [name, adapter] of Object.entries(config.embeddingAdapters)) {
11127
+ validateRegisteredAdapter(name, adapter);
11128
+ embeddingAdapters.set(name, adapter);
11129
+ }
11130
+ }
10954
11131
  const eventHandlers = /* @__PURE__ */ new Map();
10955
11132
  const shutdownState = { isShutdown: false };
10956
11133
  const abortController = new AbortController();
@@ -11000,11 +11177,35 @@ function createEngineCore(config) {
11000
11177
  }
11001
11178
  const indexConfig = reconstructSchemaFromMetadata(metadata);
11002
11179
  const language = getLanguage(indexConfig.language ?? "english");
11180
+ const adapterName = metadata.embedding?.adapter ?? null;
11181
+ let embeddingAdapter = null;
11182
+ if (metadata.embedding) {
11183
+ const candidate = adapterName !== null ? embeddingAdapters.get(adapterName) ?? null : config?.embedding ?? null;
11184
+ if (candidate) {
11185
+ try {
11186
+ validateEmbeddingConfig(
11187
+ { fields: metadata.embedding.fields, adapter: candidate },
11188
+ indexConfig.schema,
11189
+ void 0
11190
+ );
11191
+ } catch (err) {
11192
+ if (err instanceof NarsilError) {
11193
+ throw new NarsilError(err.code, `Recovery of index "${metadata.indexName}" failed: ${err.message}`, {
11194
+ indexName: metadata.indexName,
11195
+ adapter: adapterName ?? void 0
11196
+ });
11197
+ }
11198
+ throw err;
11199
+ }
11200
+ embeddingAdapter = candidate;
11201
+ }
11202
+ }
11003
11203
  executor.createIndex(metadata.indexName, indexConfig, language);
11004
11204
  indexRegistry.set(metadata.indexName, {
11005
11205
  config: indexConfig,
11006
11206
  language,
11007
- embeddingAdapter: null,
11207
+ embeddingAdapter,
11208
+ embeddingAdapterName: adapterName,
11008
11209
  vectorFieldPaths: getVectorFieldPaths(indexConfig.schema)
11009
11210
  });
11010
11211
  }
@@ -11047,6 +11248,7 @@ function createEngineCore(config) {
11047
11248
  durability,
11048
11249
  idGenerator,
11049
11250
  indexRegistry,
11251
+ embeddingAdapters,
11050
11252
  eventHandlers,
11051
11253
  shutdownState,
11052
11254
  abortController,
@@ -11064,6 +11266,160 @@ function createEngineCore(config) {
11064
11266
  rebalanceCtx
11065
11267
  };
11066
11268
  }
11269
+ var MAX_INDEX_NAME_LENGTH = 255;
11270
+
11271
+ // src/engine/validation.ts
11272
+ var INDEX_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
11273
+ var MAX_DOC_ID_LENGTH = 512;
11274
+ var BATCH_CHUNK_SIZE = 1e3;
11275
+ var MAX_LIMIT = 1e4;
11276
+ var MAX_OFFSET = 1e5;
11277
+ var DEFAULT_LIMIT = 10;
11278
+ var DEFAULT_OFFSET = 0;
11279
+ function now() {
11280
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
11281
+ return performance.now();
11282
+ }
11283
+ return Date.now();
11284
+ }
11285
+ function validateIndexName(name) {
11286
+ if (!name || name.length === 0) {
11287
+ throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, "Index name must not be empty", { indexName: name });
11288
+ }
11289
+ if (name.length > MAX_INDEX_NAME_LENGTH) {
11290
+ throw new NarsilError(
11291
+ ErrorCodes.INDEX_NOT_FOUND,
11292
+ `Index name must not exceed ${MAX_INDEX_NAME_LENGTH} characters`,
11293
+ {
11294
+ indexName: name,
11295
+ length: name.length
11296
+ }
11297
+ );
11298
+ }
11299
+ if (!INDEX_NAME_PATTERN.test(name)) {
11300
+ throw new NarsilError(
11301
+ ErrorCodes.INDEX_NOT_FOUND,
11302
+ `Index name "${name}" contains invalid characters; use alphanumeric, dots, hyphens, and underscores only`,
11303
+ { indexName: name }
11304
+ );
11305
+ }
11306
+ if (name.includes("..")) {
11307
+ throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index name "${name}" must not contain ".."`, { indexName: name });
11308
+ }
11309
+ }
11310
+ function validateDocId(docId) {
11311
+ if (!docId || docId.length === 0) {
11312
+ throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not be empty", { docId });
11313
+ }
11314
+ if (docId.length > MAX_DOC_ID_LENGTH) {
11315
+ throw new NarsilError(
11316
+ ErrorCodes.DOC_VALIDATION_FAILED,
11317
+ `Document ID must not exceed ${MAX_DOC_ID_LENGTH} characters`,
11318
+ {
11319
+ docId,
11320
+ length: docId.length
11321
+ }
11322
+ );
11323
+ }
11324
+ if (docId.includes("\0")) {
11325
+ throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not contain null bytes", { docId });
11326
+ }
11327
+ }
11328
+ function clampLimit(limit) {
11329
+ if (limit === void 0) return DEFAULT_LIMIT;
11330
+ return Math.max(0, Math.min(limit, MAX_LIMIT));
11331
+ }
11332
+ function clampOffset(offset) {
11333
+ if (offset === void 0) return DEFAULT_OFFSET;
11334
+ return Math.max(0, Math.min(offset, MAX_OFFSET));
11335
+ }
11336
+
11337
+ // src/engine/index-lifecycle.ts
11338
+ async function createEngineIndex(core, config, name, indexConfig) {
11339
+ core.guardShutdown();
11340
+ validateIndexName(name);
11341
+ if (core.indexRegistry.has(name)) {
11342
+ throw new NarsilError(ErrorCodes.INDEX_ALREADY_EXISTS, `Index "${name}" already exists`, { indexName: name });
11343
+ }
11344
+ validateSchema(indexConfig.schema);
11345
+ validateVectorPromotion(indexConfig.vectorPromotion);
11346
+ let resolvedEmbeddingAdapter = null;
11347
+ let embeddingAdapterName = null;
11348
+ if (indexConfig.embedding) {
11349
+ let configuredAdapter = indexConfig.embedding.adapter;
11350
+ if (typeof configuredAdapter === "string") {
11351
+ embeddingAdapterName = configuredAdapter;
11352
+ const registered = core.embeddingAdapters.get(configuredAdapter);
11353
+ if (!registered) {
11354
+ throw new NarsilError(
11355
+ ErrorCodes.EMBEDDING_CONFIG_INVALID,
11356
+ `Embedding adapter "${configuredAdapter}" is not registered on this engine`,
11357
+ { adapter: configuredAdapter, available: [...core.embeddingAdapters.keys()] }
11358
+ );
11359
+ }
11360
+ configuredAdapter = registered;
11361
+ }
11362
+ resolvedEmbeddingAdapter = validateEmbeddingConfig(
11363
+ { fields: indexConfig.embedding.fields, adapter: configuredAdapter },
11364
+ indexConfig.schema,
11365
+ config?.embedding
11366
+ );
11367
+ }
11368
+ if (indexConfig.required && indexConfig.required.length > 0) {
11369
+ validateRequiredFieldsInSchema(indexConfig.required, indexConfig.schema);
11370
+ }
11371
+ const language = getLanguage(indexConfig.language ?? "english");
11372
+ core.executor.createIndex(name, indexConfig, language);
11373
+ core.indexRegistry.set(name, {
11374
+ config: indexConfig,
11375
+ language,
11376
+ embeddingAdapter: resolvedEmbeddingAdapter,
11377
+ embeddingAdapterName,
11378
+ vectorFieldPaths: getVectorFieldPaths(indexConfig.schema)
11379
+ });
11380
+ if (core.durability) {
11381
+ await core.durability.manager.persistMetadata(name);
11382
+ }
11383
+ await core.pluginRegistry.runHook("onIndexCreate", { indexName: name, config: indexConfig });
11384
+ }
11385
+ async function dropEngineIndex(core, name) {
11386
+ core.guardShutdown();
11387
+ const entry = core.requireIndex(name);
11388
+ core.executor.dropIndex(name);
11389
+ core.indexRegistry.delete(name);
11390
+ if (core.durability) {
11391
+ await core.durability.manager.removeIndex(name);
11392
+ }
11393
+ await core.pluginRegistry.runHook("onIndexDrop", { indexName: name, config: entry.config });
11394
+ }
11395
+ function registerEngineEmbeddingAdapter(core, name, adapter) {
11396
+ core.guardShutdown();
11397
+ validateRegisteredAdapter(name, adapter);
11398
+ const affected = [];
11399
+ for (const [indexName, entry] of core.indexRegistry) {
11400
+ if (entry.embeddingAdapterName !== name || !entry.config.embedding) continue;
11401
+ try {
11402
+ validateEmbeddingConfig({ fields: entry.config.embedding.fields, adapter }, entry.config.schema, void 0);
11403
+ } catch (err) {
11404
+ if (err instanceof NarsilError) {
11405
+ throw new NarsilError(
11406
+ err.code,
11407
+ `Cannot bind embedding adapter "${name}" to index "${indexName}": ${err.message}`,
11408
+ {
11409
+ indexName,
11410
+ adapter: name
11411
+ }
11412
+ );
11413
+ }
11414
+ throw err;
11415
+ }
11416
+ affected.push(entry);
11417
+ }
11418
+ core.embeddingAdapters.set(name, adapter);
11419
+ for (const entry of affected) {
11420
+ entry.embeddingAdapter = adapter;
11421
+ }
11422
+ }
11067
11423
 
11068
11424
  // src/engine/lifecycle.ts
11069
11425
  async function shutdownEngine(core) {
@@ -11098,6 +11454,18 @@ async function shutdownEngine(core) {
11098
11454
  }
11099
11455
 
11100
11456
  // src/engine/embed.ts
11457
+ function assertDocumentCarriesMappedVectors(document, embeddingConfig, adapterName) {
11458
+ for (const targetField of Object.keys(embeddingConfig.fields)) {
11459
+ const existing = resolveFieldValue(document, targetField);
11460
+ if (existing === void 0 || existing === null) {
11461
+ throw new NarsilError(
11462
+ ErrorCodes.EMBEDDING_CONFIG_INVALID,
11463
+ adapterName ? `The index embeds "${targetField}" via adapter "${adapterName}", which is not registered on this engine. Register the adapter or supply the vector on the document.` : `The index embeds "${targetField}" automatically but no embedding adapter is bound. Bind an adapter or supply the vector on the document.`,
11464
+ adapterName ? { field: targetField, adapter: adapterName } : { field: targetField }
11465
+ );
11466
+ }
11467
+ }
11468
+ }
11101
11469
  function validateVector2(vector, targetField, expectedDimensions) {
11102
11470
  if (!(vector instanceof Float32Array)) {
11103
11471
  throw new NarsilError(
@@ -11299,73 +11667,6 @@ async function embedBatchDocumentFields(documents, embeddingConfig, adapter, sig
11299
11667
  }
11300
11668
  return { embedded, failed };
11301
11669
  }
11302
- var MAX_INDEX_NAME_LENGTH = 255;
11303
-
11304
- // src/engine/validation.ts
11305
- var INDEX_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
11306
- var MAX_DOC_ID_LENGTH = 512;
11307
- var BATCH_CHUNK_SIZE = 1e3;
11308
- var MAX_LIMIT = 1e4;
11309
- var MAX_OFFSET = 1e5;
11310
- var DEFAULT_LIMIT = 10;
11311
- var DEFAULT_OFFSET = 0;
11312
- function now() {
11313
- if (typeof performance !== "undefined" && typeof performance.now === "function") {
11314
- return performance.now();
11315
- }
11316
- return Date.now();
11317
- }
11318
- function validateIndexName(name) {
11319
- if (!name || name.length === 0) {
11320
- throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, "Index name must not be empty", { indexName: name });
11321
- }
11322
- if (name.length > MAX_INDEX_NAME_LENGTH) {
11323
- throw new NarsilError(
11324
- ErrorCodes.INDEX_NOT_FOUND,
11325
- `Index name must not exceed ${MAX_INDEX_NAME_LENGTH} characters`,
11326
- {
11327
- indexName: name,
11328
- length: name.length
11329
- }
11330
- );
11331
- }
11332
- if (!INDEX_NAME_PATTERN.test(name)) {
11333
- throw new NarsilError(
11334
- ErrorCodes.INDEX_NOT_FOUND,
11335
- `Index name "${name}" contains invalid characters; use alphanumeric, dots, hyphens, and underscores only`,
11336
- { indexName: name }
11337
- );
11338
- }
11339
- if (name.includes("..")) {
11340
- throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index name "${name}" must not contain ".."`, { indexName: name });
11341
- }
11342
- }
11343
- function validateDocId(docId) {
11344
- if (!docId || docId.length === 0) {
11345
- throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not be empty", { docId });
11346
- }
11347
- if (docId.length > MAX_DOC_ID_LENGTH) {
11348
- throw new NarsilError(
11349
- ErrorCodes.DOC_VALIDATION_FAILED,
11350
- `Document ID must not exceed ${MAX_DOC_ID_LENGTH} characters`,
11351
- {
11352
- docId,
11353
- length: docId.length
11354
- }
11355
- );
11356
- }
11357
- if (docId.includes("\0")) {
11358
- throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not contain null bytes", { docId });
11359
- }
11360
- }
11361
- function clampLimit(limit) {
11362
- if (limit === void 0) return DEFAULT_LIMIT;
11363
- return Math.max(0, Math.min(limit, MAX_LIMIT));
11364
- }
11365
- function clampOffset(offset) {
11366
- if (offset === void 0) return DEFAULT_OFFSET;
11367
- return Math.max(0, Math.min(offset, MAX_OFFSET));
11368
- }
11369
11670
 
11370
11671
  // src/engine/mutations/durable-rollback.ts
11371
11672
  function rollbackFailed(docId, originalError, rollbackError) {
@@ -11428,13 +11729,21 @@ async function insertDocument(ctx, indexName, document, docId, options) {
11428
11729
  if (entry.config.required && entry.config.required.length > 0) {
11429
11730
  validateRequiredFields(document, entry.config.required);
11430
11731
  }
11431
- if (entry.embeddingAdapter && entry.config.embedding) {
11432
- await embedDocumentFields(
11433
- document,
11434
- entry.config.embedding,
11435
- entry.embeddingAdapter,
11436
- ctx.abortController.signal
11437
- );
11732
+ if (entry.config.embedding) {
11733
+ if (entry.embeddingAdapter) {
11734
+ await embedDocumentFields(
11735
+ document,
11736
+ entry.config.embedding,
11737
+ entry.embeddingAdapter,
11738
+ ctx.abortController.signal
11739
+ );
11740
+ } else {
11741
+ assertDocumentCarriesMappedVectors(
11742
+ document,
11743
+ entry.config.embedding,
11744
+ entry.embeddingAdapterName
11745
+ );
11746
+ }
11438
11747
  }
11439
11748
  const insertManager = ctx.requireManager(indexName);
11440
11749
  const insertVecIndexes = insertManager.getVectorIndexes();
@@ -11514,6 +11823,7 @@ async function insertDocumentBatch(ctx, indexName, documents, options) {
11514
11823
  const hasAfterHook = ctx.pluginRegistry.hasHooks("afterInsert");
11515
11824
  const hasRequired = entry.config.required && entry.config.required.length > 0;
11516
11825
  const hasEmbedding = entry.embeddingAdapter && entry.config.embedding;
11826
+ const embeddingUnbound = !entry.embeddingAdapter && entry.config.embedding;
11517
11827
  const batchManager = ctx.requireManager(indexName);
11518
11828
  const batchVecIndexes = batchManager.getVectorIndexes();
11519
11829
  const batchVectorFieldPaths = batchVecIndexes.size > 0 ? entry.vectorFieldPaths : /* @__PURE__ */ new Set();
@@ -11529,7 +11839,7 @@ async function insertDocumentBatch(ctx, indexName, documents, options) {
11529
11839
  } catch (err) {
11530
11840
  chunkFailedIndexes.add(i);
11531
11841
  failed.push({
11532
- docId: "",
11842
+ docId: providedDocId(documents[i]) ?? "",
11533
11843
  error: err instanceof NarsilError ? err : new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, String(err))
11534
11844
  });
11535
11845
  }
@@ -11554,16 +11864,33 @@ async function insertDocumentBatch(ctx, indexName, documents, options) {
11554
11864
  for (const [sliceIndex, error] of embedResult.failed) {
11555
11865
  const originalIdx = embeddableOriginalIndexes[sliceIndex];
11556
11866
  chunkFailedIndexes.add(originalIdx);
11557
- failed.push({ docId: "", error });
11867
+ failed.push({ docId: providedDocId(documents[originalIdx]) ?? "", error });
11558
11868
  }
11559
11869
  } catch (err) {
11560
11870
  const embeddingError = err instanceof NarsilError ? err : new NarsilError(ErrorCodes.EMBEDDING_FAILED, String(err));
11561
11871
  for (const originalIdx of embeddableOriginalIndexes) {
11562
11872
  chunkFailedIndexes.add(originalIdx);
11563
- failed.push({ docId: "", error: embeddingError });
11873
+ failed.push({ docId: providedDocId(documents[originalIdx]) ?? "", error: embeddingError });
11564
11874
  }
11565
11875
  }
11566
11876
  }
11877
+ } else if (embeddingUnbound) {
11878
+ for (let i = chunkStart; i < chunkEnd; i++) {
11879
+ if (chunkFailedIndexes.has(i)) continue;
11880
+ try {
11881
+ assertDocumentCarriesMappedVectors(
11882
+ documents[i],
11883
+ entry.config.embedding,
11884
+ entry.embeddingAdapterName
11885
+ );
11886
+ } catch (err) {
11887
+ chunkFailedIndexes.add(i);
11888
+ failed.push({
11889
+ docId: providedDocId(documents[i]) ?? "",
11890
+ error: err instanceof NarsilError ? err : new NarsilError(ErrorCodes.EMBEDDING_FAILED, String(err))
11891
+ });
11892
+ }
11893
+ }
11567
11894
  }
11568
11895
  for (let i = chunkStart; i < chunkEnd; i++) {
11569
11896
  if (ctx.abortController.signal.aborted) break;
@@ -11757,13 +12084,21 @@ async function updateDocument(ctx, indexName, docId, document) {
11757
12084
  if (ctx.bufferIfRebalancing(indexName, { action: "update", docId, document, indexName })) {
11758
12085
  return;
11759
12086
  }
11760
- if (entry.embeddingAdapter && entry.config.embedding) {
11761
- await embedDocumentFields(
11762
- document,
11763
- entry.config.embedding,
11764
- entry.embeddingAdapter,
11765
- ctx.abortController.signal
11766
- );
12087
+ if (entry.config.embedding) {
12088
+ if (entry.embeddingAdapter) {
12089
+ await embedDocumentFields(
12090
+ document,
12091
+ entry.config.embedding,
12092
+ entry.embeddingAdapter,
12093
+ ctx.abortController.signal
12094
+ );
12095
+ } else {
12096
+ assertDocumentCarriesMappedVectors(
12097
+ document,
12098
+ entry.config.embedding,
12099
+ entry.embeddingAdapterName
12100
+ );
12101
+ }
11767
12102
  }
11768
12103
  const updateManager = ctx.requireManager(indexName);
11769
12104
  const oldDocument = updateManager.get(docId);
@@ -12813,7 +13148,7 @@ async function executeRebalance(manager, indexName, targetPartitionCount, ctx) {
12813
13148
  }
12814
13149
 
12815
13150
  // src/engine/resolve-vector-text.ts
12816
- async function resolveVectorText(params, embeddingAdapter, signal) {
13151
+ async function resolveVectorText(params, embeddingAdapter, signal, embeddingAdapterName) {
12817
13152
  if (!params.vector) return params;
12818
13153
  if (params.vector.text === void 0 && params.vector.value === void 0) return params;
12819
13154
  if (params.vector.value !== void 0 && params.vector.text === void 0) return params;
@@ -12823,7 +13158,8 @@ async function resolveVectorText(params, embeddingAdapter, signal) {
12823
13158
  if (!embeddingAdapter) {
12824
13159
  throw new NarsilError(
12825
13160
  ErrorCodes.EMBEDDING_CONFIG_INVALID,
12826
- "Vector query with 'text' requires an embedding adapter on the index or instance"
13161
+ embeddingAdapterName ? `Vector query with 'text' needs embedding adapter "${embeddingAdapterName}", which is not registered on this engine. Register it with registerEmbeddingAdapter or supply a raw vector.` : "Vector query with 'text' requires an embedding adapter on the index or instance",
13162
+ embeddingAdapterName ? { adapter: embeddingAdapterName } : void 0
12827
13163
  );
12828
13164
  }
12829
13165
  if (typeof params.vector.text !== "string") {
@@ -12905,7 +13241,13 @@ async function restoreFromSnapshot(indexName, data, executor, indexRegistry, get
12905
13241
  const indexConfig = { schema, language: envelope.language };
12906
13242
  executor.createIndex(indexName, indexConfig, language);
12907
13243
  const vectorFieldPaths = getVectorFieldPaths2(schema);
12908
- indexRegistry.set(indexName, { config: indexConfig, language, embeddingAdapter: null, vectorFieldPaths });
13244
+ indexRegistry.set(indexName, {
13245
+ config: indexConfig,
13246
+ language,
13247
+ embeddingAdapter: null,
13248
+ embeddingAdapterName: null,
13249
+ vectorFieldPaths
13250
+ });
12909
13251
  try {
12910
13252
  const manager = requireManager(indexName);
12911
13253
  while (manager.partitionCount < envelope.partitions.length) {
@@ -13036,97 +13378,6 @@ function readProcessMemory() {
13036
13378
  }
13037
13379
  }
13038
13380
 
13039
- // src/schema/embedding-validator.ts
13040
- var VECTOR_DIMENSION_PATTERN = /^vector\[(\d+)]$/;
13041
- function validateEmbeddingConfig(embeddingConfig, schema, instanceAdapter) {
13042
- const fieldEntries = Object.keys(embeddingConfig.fields);
13043
- if (fieldEntries.length === 0) {
13044
- throw new NarsilError(
13045
- ErrorCodes.EMBEDDING_CONFIG_INVALID,
13046
- "Embedding config must define at least one field mapping"
13047
- );
13048
- }
13049
- const resolvedAdapter = embeddingConfig.adapter ?? instanceAdapter;
13050
- if (!resolvedAdapter) {
13051
- throw new NarsilError(
13052
- ErrorCodes.EMBEDDING_CONFIG_INVALID,
13053
- "Embedding config requires an adapter (set on the index or the instance)"
13054
- );
13055
- }
13056
- if (typeof resolvedAdapter.dimensions !== "number" || !Number.isInteger(resolvedAdapter.dimensions) || resolvedAdapter.dimensions <= 0) {
13057
- throw new NarsilError(
13058
- ErrorCodes.EMBEDDING_CONFIG_INVALID,
13059
- `Adapter dimensions must be a positive integer, got ${resolvedAdapter.dimensions}`
13060
- );
13061
- }
13062
- const flatFields = flattenSchema(schema);
13063
- for (const [targetField, sourceFields] of Object.entries(embeddingConfig.fields)) {
13064
- const targetType = flatFields[targetField];
13065
- if (targetType === void 0) {
13066
- throw new NarsilError(
13067
- ErrorCodes.EMBEDDING_CONFIG_INVALID,
13068
- `Embedding target field "${targetField}" does not exist in the schema`,
13069
- { field: targetField }
13070
- );
13071
- }
13072
- const vectorMatch = VECTOR_DIMENSION_PATTERN.exec(targetType);
13073
- if (!vectorMatch) {
13074
- throw new NarsilError(
13075
- ErrorCodes.EMBEDDING_CONFIG_INVALID,
13076
- `Embedding target field "${targetField}" must be a vector type, got "${targetType}"`,
13077
- { field: targetField, type: targetType }
13078
- );
13079
- }
13080
- const schemaDimension = Number.parseInt(vectorMatch[1], 10);
13081
- if (schemaDimension !== resolvedAdapter.dimensions) {
13082
- throw new NarsilError(
13083
- ErrorCodes.EMBEDDING_DIMENSION_MISMATCH,
13084
- `Embedding target field "${targetField}" has dimension ${schemaDimension} but the adapter produces ${resolvedAdapter.dimensions}-dimensional vectors`,
13085
- { field: targetField, expected: schemaDimension, actual: resolvedAdapter.dimensions }
13086
- );
13087
- }
13088
- const sources = Array.isArray(sourceFields) ? sourceFields : [sourceFields];
13089
- if (sources.length === 0) {
13090
- throw new NarsilError(
13091
- ErrorCodes.EMBEDDING_CONFIG_INVALID,
13092
- `Embedding target field "${targetField}" must have at least one source field`,
13093
- { field: targetField }
13094
- );
13095
- }
13096
- for (const source of sources) {
13097
- const sourceType = flatFields[source];
13098
- if (sourceType === void 0) {
13099
- throw new NarsilError(
13100
- ErrorCodes.EMBEDDING_CONFIG_INVALID,
13101
- `Embedding source field "${source}" does not exist in the schema`,
13102
- { field: source, targetField }
13103
- );
13104
- }
13105
- if (sourceType !== "string") {
13106
- throw new NarsilError(
13107
- ErrorCodes.EMBEDDING_CONFIG_INVALID,
13108
- `Embedding source field "${source}" must be a string type, got "${sourceType}"`,
13109
- { field: source, type: sourceType, targetField }
13110
- );
13111
- }
13112
- }
13113
- }
13114
- return resolvedAdapter;
13115
- }
13116
- function validateRequiredFieldsInSchema(required, schema) {
13117
- if (required.length === 0) return;
13118
- const flatFields = flattenSchema(schema);
13119
- for (const field of required) {
13120
- if (flatFields[field] === void 0) {
13121
- throw new NarsilError(
13122
- ErrorCodes.EMBEDDING_CONFIG_INVALID,
13123
- `Required field "${field}" does not exist in the schema`,
13124
- { field }
13125
- );
13126
- }
13127
- }
13128
- }
13129
-
13130
13381
  // src/narsil.ts
13131
13382
  async function createNarsil(config) {
13132
13383
  const core = createEngineCore(config);
@@ -13153,43 +13404,14 @@ function createNarsilFromCore(core, config) {
13153
13404
  rebalanceCtx
13154
13405
  } = core;
13155
13406
  const narsil = {
13156
- async createIndex(name, indexConfig) {
13157
- guardShutdown();
13158
- validateIndexName(name);
13159
- if (indexRegistry.has(name)) {
13160
- throw new NarsilError(ErrorCodes.INDEX_ALREADY_EXISTS, `Index "${name}" already exists`, { indexName: name });
13161
- }
13162
- validateSchema(indexConfig.schema);
13163
- let resolvedEmbeddingAdapter = null;
13164
- if (indexConfig.embedding) {
13165
- resolvedEmbeddingAdapter = validateEmbeddingConfig(indexConfig.embedding, indexConfig.schema, config?.embedding);
13166
- }
13167
- if (indexConfig.required && indexConfig.required.length > 0) {
13168
- validateRequiredFieldsInSchema(indexConfig.required, indexConfig.schema);
13169
- }
13170
- const language = getLanguage(indexConfig.language ?? "english");
13171
- executor.createIndex(name, indexConfig, language);
13172
- const vectorFieldPaths = getVectorFieldPaths(indexConfig.schema);
13173
- indexRegistry.set(name, {
13174
- config: indexConfig,
13175
- language,
13176
- embeddingAdapter: resolvedEmbeddingAdapter,
13177
- vectorFieldPaths
13178
- });
13179
- if (durability) {
13180
- await durability.manager.persistMetadata(name);
13181
- }
13182
- await pluginRegistry.runHook("onIndexCreate", { indexName: name, config: indexConfig });
13407
+ createIndex(name, indexConfig) {
13408
+ return createEngineIndex(core, config, name, indexConfig);
13183
13409
  },
13184
- async dropIndex(name) {
13185
- guardShutdown();
13186
- const entry = requireIndex(name);
13187
- executor.dropIndex(name);
13188
- indexRegistry.delete(name);
13189
- if (durability) {
13190
- await durability.manager.removeIndex(name);
13191
- }
13192
- await pluginRegistry.runHook("onIndexDrop", { indexName: name, config: entry.config });
13410
+ registerEmbeddingAdapter(name, adapter) {
13411
+ registerEngineEmbeddingAdapter(core, name, adapter);
13412
+ },
13413
+ dropIndex(name) {
13414
+ return dropEngineIndex(core, name);
13193
13415
  },
13194
13416
  listIndexes() {
13195
13417
  const infos = [];
@@ -13268,7 +13490,12 @@ function createNarsilFromCore(core, config) {
13268
13490
  guardShutdown();
13269
13491
  const entry = requireIndex(indexName);
13270
13492
  const manager = requireManager(indexName);
13271
- const resolvedParams = await resolveVectorText(params, entry.embeddingAdapter, abortController.signal);
13493
+ const resolvedParams = await resolveVectorText(
13494
+ params,
13495
+ entry.embeddingAdapter,
13496
+ abortController.signal,
13497
+ entry.embeddingAdapterName
13498
+ );
13272
13499
  await pluginRegistry.runHook("beforeSearch", { indexName, params: resolvedParams });
13273
13500
  const workerSearch = orchestrator.isPromoted() ? orchestrator.searchViaWorker.bind(orchestrator) : void 0;
13274
13501
  const result = await executeQuery(resolvedParams, {
@@ -13293,7 +13520,12 @@ function createNarsilFromCore(core, config) {
13293
13520
  guardShutdown();
13294
13521
  const entry = requireIndex(indexName);
13295
13522
  const manager = requireManager(indexName);
13296
- const resolvedParams = await resolveVectorText(params, entry.embeddingAdapter, abortController.signal);
13523
+ const resolvedParams = await resolveVectorText(
13524
+ params,
13525
+ entry.embeddingAdapter,
13526
+ abortController.signal,
13527
+ entry.embeddingAdapterName
13528
+ );
13297
13529
  const workerSearch = orchestrator.isPromoted() ? orchestrator.searchViaWorker.bind(orchestrator) : void 0;
13298
13530
  return executePreflight(resolvedParams, {
13299
13531
  manager,