@delali/narsil 0.1.1 → 0.1.2

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.
@@ -1,5 +1,7 @@
1
+ import { createHNSWIndex, magnitude, euclideanDistance, dotProduct, cosineSimilarityWithMagnitudes } from './chunk-GVN2W4VA.mjs';
1
2
  import { english } from './chunk-WYC635GI.mjs';
2
- import { NarsilError, ErrorCodes } from './chunk-JKIBUPAY.mjs';
3
+ import { NarsilError, ErrorCodes } from './chunk-7GKPWGCN.mjs';
4
+ import { decode, encode } from '@msgpack/msgpack';
3
5
 
4
6
  // src/languages/registry.ts
5
7
  var languages = /* @__PURE__ */ new Map();
@@ -95,6 +97,9 @@ var DEFAULT_SPLIT_PATTERN = /[^\p{L}\p{N}_'-]+/u;
95
97
  var DEFAULT_MIN_TOKEN_LENGTH = 1;
96
98
  var normalizationCache = /* @__PURE__ */ new Map();
97
99
  var MAX_CACHE_SIZE = 65536;
100
+ var cachedLangName = "";
101
+ var cachedFlags = "";
102
+ var cachedPrefix = "";
98
103
  function stripDiacritics(text) {
99
104
  return text.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
100
105
  }
@@ -103,9 +108,17 @@ function resolveStopWords(language, override) {
103
108
  if (override instanceof Set) return override;
104
109
  return override(language.stopWords);
105
110
  }
111
+ function getCachePrefix(language, flags) {
112
+ if (language.name === cachedLangName && flags === cachedFlags) return cachedPrefix;
113
+ cachedLangName = language.name;
114
+ cachedFlags = flags;
115
+ cachedPrefix = `${language.name}:${flags}:`;
116
+ return cachedPrefix;
117
+ }
106
118
  function transformToken(raw, language, stem, removeDiacritics) {
107
119
  const flags = (stem ? "s" : "") + (removeDiacritics ? "d" : "");
108
- const cacheKey = `${language.name}:${flags}:${raw}`;
120
+ const prefix = getCachePrefix(language, flags);
121
+ const cacheKey = prefix + raw;
109
122
  const cached = normalizationCache.get(cacheKey);
110
123
  if (cached !== void 0) return cached;
111
124
  let normalized = raw;
@@ -115,20 +128,23 @@ function transformToken(raw, language, stem, removeDiacritics) {
115
128
  if (stem && language.stemmer) {
116
129
  normalized = language.stemmer(normalized);
117
130
  }
118
- cacheAndReturn(cacheKey, normalized);
119
- return normalized;
120
- }
121
- function cacheAndReturn(key, value) {
122
131
  if (normalizationCache.size >= MAX_CACHE_SIZE) {
123
132
  const firstKey = normalizationCache.keys().next().value;
124
133
  normalizationCache.delete(firstKey);
125
134
  }
126
- normalizationCache.set(key, value);
135
+ normalizationCache.set(cacheKey, normalized);
136
+ return normalized;
127
137
  }
128
138
  function splitText(text, language) {
129
139
  const pattern = language.tokenizer?.splitPattern ?? DEFAULT_SPLIT_PATTERN;
130
140
  return text.split(pattern);
131
141
  }
142
+ function isAscii(text) {
143
+ for (let i = 0; i < text.length; i++) {
144
+ if (text.charCodeAt(i) > 127) return false;
145
+ }
146
+ return true;
147
+ }
132
148
  function tokenize(text, language, options) {
133
149
  const {
134
150
  stem = true,
@@ -144,7 +160,7 @@ function tokenize(text, language, options) {
144
160
  originalTokens: customResult.map((t) => t.token)
145
161
  };
146
162
  }
147
- const normalized = text.normalize("NFC").toLowerCase();
163
+ const normalized = isAscii(text) ? text.toLowerCase() : text.normalize("NFC").toLowerCase();
148
164
  const rawParts = splitText(normalized, language);
149
165
  const minLength = language.tokenizer?.minTokenLength ?? DEFAULT_MIN_TOKEN_LENGTH;
150
166
  const effectiveDiacritics = removeDiacritics || (language.tokenizer?.normalizeDiacritics ?? false);
@@ -440,6 +456,31 @@ function validateDocument(document, schema) {
440
456
  }
441
457
  validateDocumentFields(document, schema, "");
442
458
  }
459
+ function collectExtraFields(doc, schema, prefix, extras) {
460
+ for (const key of Object.keys(doc)) {
461
+ if (key === "id") continue;
462
+ const path = prefix ? `${prefix}.${key}` : key;
463
+ if (!(key in schema)) {
464
+ extras.push(path);
465
+ continue;
466
+ }
467
+ const schemaType = schema[key];
468
+ if (isPlainObject(schemaType) && isPlainObject(doc[key])) {
469
+ collectExtraFields(doc[key], schemaType, path, extras);
470
+ }
471
+ }
472
+ }
473
+ function validateDocumentStrict(document, schema) {
474
+ const extras = [];
475
+ collectExtraFields(document, schema, "", extras);
476
+ if (extras.length > 0) {
477
+ throw new NarsilError(
478
+ ErrorCodes.DOC_VALIDATION_FAILED,
479
+ `Document contains fields not defined in schema: ${extras.join(", ")}`,
480
+ { extraFields: extras }
481
+ );
482
+ }
483
+ }
443
484
  function flattenRecursive(schema, prefix, result) {
444
485
  for (const [field, type] of Object.entries(schema)) {
445
486
  const path = prefix ? `${prefix}.${field}` : field;
@@ -478,6 +519,9 @@ function fulltextSearch(partition, params, language, schema, options) {
478
519
  return { scored: [], totalMatched: 0 };
479
520
  }
480
521
  const queryTokens = deduplicateTokens(queryTokenResult.tokens);
522
+ const hasPostFilters = params.filters !== void 0 || params.minScore !== void 0 || params.termMatch !== void 0 && params.termMatch !== "any";
523
+ const requestedLimit = params.limit !== void 0 || params.offset !== void 0 ? (params.limit ?? 10) + (params.offset ?? 0) + 1 : void 0;
524
+ const maxResults = requestedLimit !== void 0 && !hasPostFilters ? requestedLimit : void 0;
481
525
  const rawResult = partition.searchFulltext({
482
526
  queryTokens,
483
527
  fields: params.fields,
@@ -486,7 +530,9 @@ function fulltextSearch(partition, params, language, schema, options) {
486
530
  prefixLength: params.prefixLength ?? 2,
487
531
  exact: params.exact ?? false,
488
532
  bm25Params: options?.bm25Params,
489
- globalStats: options?.globalStats
533
+ globalStats: options?.globalStats,
534
+ maxResults,
535
+ termMatch: params.termMatch
490
536
  });
491
537
  let scored = rawResult.scored;
492
538
  const termMatch = params.termMatch ?? "any";
@@ -501,7 +547,8 @@ function fulltextSearch(partition, params, language, schema, options) {
501
547
  const matchingDocIds = partition.applyFilters(params.filters, schema);
502
548
  scored = scored.filter((doc) => matchingDocIds.has(doc.docId));
503
549
  }
504
- return { scored, totalMatched: scored.length };
550
+ const totalMatched = hasPostFilters ? scored.length : rawResult.totalMatched;
551
+ return { scored, totalMatched };
505
552
  }
506
553
  function validateSearchFields(fields, flatSchema) {
507
554
  for (const field of fields) {
@@ -581,7 +628,8 @@ function hybridSearch(partition, params, language, schema, options) {
581
628
  value: vectorConfig.value,
582
629
  k: params.limit ?? 10,
583
630
  similarity: vectorConfig.similarity,
584
- metric: vectorConfig.metric
631
+ metric: vectorConfig.metric,
632
+ efSearch: vectorConfig.efSearch
585
633
  });
586
634
  const normalizedText = normalizeScores(textResult.scored);
587
635
  const normalizedVector = normalizeScores(vectorResult.scored);
@@ -636,7 +684,8 @@ function runVectorOnly(partition, params) {
636
684
  value: vectorConfig.value,
637
685
  k: params.limit ?? 10,
638
686
  similarity: vectorConfig.similarity,
639
- metric: vectorConfig.metric
687
+ metric: vectorConfig.metric,
688
+ efSearch: vectorConfig.efSearch
640
689
  });
641
690
  return result;
642
691
  }
@@ -680,8 +729,8 @@ function collectGlobalStats(manager) {
680
729
  }
681
730
 
682
731
  // src/partitioning/fan-out.ts
683
- function fanOutQuery(manager, params, language, schema, config, searchOptions) {
684
- const partitions = manager.getAllPartitions();
732
+ async function fanOutQuery(manager, params, language, schema, config, searchOptions) {
733
+ const partitions = config.partitionIds ? config.partitionIds.map((id) => manager.partitionAt(id)).filter((p) => p !== void 0) : manager.getAllPartitions();
685
734
  if (partitions.length === 0) {
686
735
  return { scored: [], totalMatched: 0 };
687
736
  }
@@ -693,7 +742,16 @@ function fanOutQuery(manager, params, language, schema, config, searchOptions) {
693
742
  globalStats = config.globalStats;
694
743
  }
695
744
  const options = buildSearchOptions(searchOptions, globalStats);
696
- const outcomes = dispatchToAllPartitions(partitions, params, language, schema, options);
745
+ if (partitions.length === 1 && !config.dispatcher) {
746
+ const result = dispatchSinglePartition(partitions[0], params, language, schema, options);
747
+ let facets2;
748
+ if (params.facets) {
749
+ const matchingDocIds = new Set(result.scored.map((doc) => doc.docId));
750
+ facets2 = partitions[0].computeFacets(matchingDocIds, params.facets, schema);
751
+ }
752
+ return { scored: result.scored, totalMatched: result.totalMatched, facets: facets2 };
753
+ }
754
+ const outcomes = await dispatchToAllPartitions(partitions, params, language, schema, options, config.dispatcher);
697
755
  const allScoredArrays = outcomes.map((o) => o.result.scored);
698
756
  const merged = kWayMerge(allScoredArrays);
699
757
  let totalMatched = 0;
@@ -718,13 +776,19 @@ function buildSearchOptions(base, globalStats) {
718
776
  globalStats: globalStats ?? base?.globalStats
719
777
  };
720
778
  }
721
- function dispatchToAllPartitions(partitions, params, language, schema, options) {
722
- const outcomes = [];
723
- for (const partition of partitions) {
724
- const result = dispatchSinglePartition(partition, params, language, schema, options);
725
- outcomes.push({ result, partition });
779
+ function dispatchToAllPartitions(partitions, params, language, schema, options, dispatcher) {
780
+ if (!dispatcher) {
781
+ const outcomes = [];
782
+ for (const partition of partitions) {
783
+ const result = dispatchSinglePartition(partition, params, language, schema, options);
784
+ outcomes.push({ result, partition });
785
+ }
786
+ return outcomes;
726
787
  }
727
- return outcomes;
788
+ const promises = partitions.map(
789
+ (partition) => dispatcher.dispatch(partition, params, language, schema, options).then((result) => ({ result, partition }))
790
+ );
791
+ return Promise.all(promises);
728
792
  }
729
793
  function dispatchSinglePartition(partition, params, language, schema, options) {
730
794
  const isHybridMode = params.mode === "hybrid";
@@ -741,7 +805,8 @@ function dispatchSinglePartition(partition, params, language, schema, options) {
741
805
  value: vectorConfig.value,
742
806
  k: params.limit ?? 10,
743
807
  similarity: vectorConfig.similarity,
744
- metric: vectorConfig.metric
808
+ metric: vectorConfig.metric,
809
+ efSearch: vectorConfig.efSearch
745
810
  });
746
811
  }
747
812
  return fulltextSearch(partition, params, language, schema, options);
@@ -871,6 +936,280 @@ function heapPop(heap) {
871
936
  }
872
937
  return top;
873
938
  }
939
+ function encodeRawPayloadV2(wire) {
940
+ return encode(wire);
941
+ }
942
+ var VALID_HNSW_METRICS = /* @__PURE__ */ new Set(["cosine", "dotProduct", "euclidean"]);
943
+ function validateHnswMetric(value) {
944
+ if (value === void 0 || value === null) return void 0;
945
+ if (typeof value === "string" && VALID_HNSW_METRICS.has(value)) {
946
+ return value;
947
+ }
948
+ return void 0;
949
+ }
950
+ function deserializePayloadV2(data) {
951
+ const raw = decode(data);
952
+ const documents = {};
953
+ for (const [docId, doc] of Object.entries(raw.documents ?? {})) {
954
+ documents[docId] = {
955
+ fields: doc.fields,
956
+ fieldLengths: doc.field_lengths ?? {}
957
+ };
958
+ }
959
+ const fieldNames = raw.inverted_index?.field_names ?? [];
960
+ const invertedIndex = {};
961
+ for (const [token, list] of Object.entries(raw.inverted_index?.entries ?? {})) {
962
+ const count = list.ids.length;
963
+ const postings = new Array(count);
964
+ for (let i = 0; i < count; i++) {
965
+ postings[i] = {
966
+ docId: list.ids[i],
967
+ termFrequency: list.tf[i],
968
+ field: fieldNames[list.fi[i]] ?? "",
969
+ positions: list.pos ? list.pos[i] : []
970
+ };
971
+ }
972
+ invertedIndex[token] = {
973
+ docFrequency: list.df,
974
+ postings
975
+ };
976
+ }
977
+ const numeric = {};
978
+ for (const [field, entries] of Object.entries(raw.field_indexes?.numeric ?? {})) {
979
+ numeric[field] = entries.map((e) => ({ value: e.value, docId: e.doc_id }));
980
+ }
981
+ const boolean = {};
982
+ for (const [field, idx] of Object.entries(raw.field_indexes?.boolean ?? {})) {
983
+ boolean[field] = { trueDocs: idx.true_docs ?? [], falseDocs: idx.false_docs ?? [] };
984
+ }
985
+ const geopoint = {};
986
+ for (const [field, entries] of Object.entries(raw.field_indexes?.geopoint ?? {})) {
987
+ geopoint[field] = entries.map((e) => ({ lat: e.lat, lon: e.lon, docId: e.doc_id }));
988
+ }
989
+ const vectorData = {};
990
+ for (const [field, data2] of Object.entries(raw.vector_data ?? {})) {
991
+ vectorData[field] = {
992
+ dimension: data2.dimension,
993
+ vectors: (data2.vectors ?? []).map((v) => ({ docId: v.doc_id, vector: v.vector })),
994
+ hnswGraph: data2.hnsw_graph ? {
995
+ entryPoint: data2.hnsw_graph.entry_point,
996
+ maxLayer: data2.hnsw_graph.max_layer,
997
+ m: data2.hnsw_graph.m,
998
+ efConstruction: data2.hnsw_graph.ef_construction,
999
+ metric: validateHnswMetric(data2.hnsw_graph.metric),
1000
+ nodes: data2.hnsw_graph.nodes
1001
+ } : null
1002
+ };
1003
+ }
1004
+ return {
1005
+ indexName: raw.index_name,
1006
+ partitionId: raw.partition_id,
1007
+ totalPartitions: raw.total_partitions,
1008
+ language: raw.language ?? "english",
1009
+ schema: raw.schema ?? {},
1010
+ docCount: raw.doc_count ?? 0,
1011
+ avgDocLength: raw.avg_doc_length ?? 0,
1012
+ documents,
1013
+ invertedIndex,
1014
+ fieldIndexes: {
1015
+ numeric,
1016
+ boolean,
1017
+ enum: raw.field_indexes?.enum ?? {},
1018
+ geopoint
1019
+ },
1020
+ vectorData,
1021
+ statistics: {
1022
+ totalDocuments: raw.statistics?.total_documents ?? 0,
1023
+ totalFieldLengths: raw.statistics?.total_field_lengths ?? {},
1024
+ averageFieldLengths: raw.statistics?.average_field_lengths ?? {},
1025
+ docFrequencies: raw.statistics?.doc_frequencies ?? {}
1026
+ }
1027
+ };
1028
+ }
1029
+
1030
+ // src/runtime/detect.ts
1031
+ function detectRuntimeName() {
1032
+ if ("Bun" in globalThis) {
1033
+ return "bun";
1034
+ }
1035
+ if ("Deno" in globalThis) {
1036
+ return "deno";
1037
+ }
1038
+ const proc = globalThis.process;
1039
+ if (typeof proc === "object" && proc !== null && typeof proc.versions === "object" && typeof proc.versions?.node === "string") {
1040
+ return "node";
1041
+ }
1042
+ return "browser";
1043
+ }
1044
+ function getCpuCount() {
1045
+ const nav = globalThis.navigator;
1046
+ if (nav && typeof nav.hardwareConcurrency === "number" && nav.hardwareConcurrency >= 1) {
1047
+ return nav.hardwareConcurrency;
1048
+ }
1049
+ return 1;
1050
+ }
1051
+ function detectRuntime() {
1052
+ const runtime = detectRuntimeName();
1053
+ return {
1054
+ runtime,
1055
+ cpuCount: getCpuCount(),
1056
+ supportsWorkerThreads: runtime === "node" || runtime === "bun",
1057
+ supportsWebWorkers: runtime === "browser" || runtime === "deno" || runtime === "bun",
1058
+ supportsFileSystem: runtime === "node" || runtime === "bun" || runtime === "deno",
1059
+ supportsIndexedDB: runtime === "browser" || "indexedDB" in globalThis,
1060
+ supportsBroadcastChannel: "BroadcastChannel" in globalThis
1061
+ };
1062
+ }
1063
+
1064
+ // src/vector/promoter.ts
1065
+ function detectWorkerStrategy() {
1066
+ const runtime = detectRuntime();
1067
+ const url = import.meta.url;
1068
+ const hasBundledWorker = url.endsWith(".mjs") || url.endsWith(".js");
1069
+ if (runtime.supportsWorkerThreads && hasBundledWorker) return "worker-threads";
1070
+ if (runtime.supportsWebWorkers && hasBundledWorker) return "web-worker";
1071
+ return "synchronous";
1072
+ }
1073
+ function collectVectorsForWorker(engine) {
1074
+ const vectors = [];
1075
+ for (const [, entry] of engine.entries()) {
1076
+ vectors.push({ docId: entry.docId, values: Array.from(entry.vector) });
1077
+ }
1078
+ return vectors;
1079
+ }
1080
+ function buildWorkerRequest(engine, hnswConfig) {
1081
+ return {
1082
+ type: "build",
1083
+ vectors: collectVectorsForWorker(engine),
1084
+ dimension: engine.dimension,
1085
+ config: hnswConfig ?? {}
1086
+ };
1087
+ }
1088
+ function resolveWorkerUrl() {
1089
+ const base = import.meta.url;
1090
+ if (base.endsWith(".mjs") || base.endsWith(".js")) {
1091
+ return new URL("./hnsw-build-worker.mjs", base).href;
1092
+ }
1093
+ const replaced = base.replace(/\/src\/vector\/[^/]+$/, "/dist/vector/hnsw-build-worker.mjs");
1094
+ return replaced;
1095
+ }
1096
+ function spawnNodeWorker(request, onComplete, onError) {
1097
+ const entryUrl = resolveWorkerUrl();
1098
+ let worker = null;
1099
+ import('worker_threads').then((wt) => {
1100
+ worker = new wt.Worker(new URL(entryUrl));
1101
+ worker.on("message", (msg) => {
1102
+ const response = msg;
1103
+ worker?.terminate();
1104
+ if (response.type === "success") {
1105
+ onComplete(response.graph);
1106
+ } else {
1107
+ onError(new Error(response.message));
1108
+ }
1109
+ });
1110
+ worker.on("error", (err) => {
1111
+ worker?.terminate();
1112
+ onError(err instanceof Error ? err : new Error(String(err)));
1113
+ });
1114
+ worker.postMessage(request);
1115
+ }).catch(onError);
1116
+ return {
1117
+ terminate() {
1118
+ worker?.terminate();
1119
+ }
1120
+ };
1121
+ }
1122
+ function spawnWebWorker(request, onComplete, onError) {
1123
+ const entryUrl = resolveWorkerUrl();
1124
+ const WorkerCtor = globalThis.Worker;
1125
+ const worker = new WorkerCtor(entryUrl, { type: "module" });
1126
+ worker.onmessage = (event) => {
1127
+ const response = event.data;
1128
+ worker.terminate();
1129
+ if (response.type === "success") {
1130
+ onComplete(response.graph);
1131
+ } else {
1132
+ onError(new Error(response.message));
1133
+ }
1134
+ };
1135
+ worker.onerror = (event) => {
1136
+ worker.terminate();
1137
+ onError(event.error ?? new Error(event.message ?? "Web Worker error"));
1138
+ };
1139
+ worker.postMessage(request);
1140
+ return {
1141
+ terminate() {
1142
+ worker.terminate();
1143
+ }
1144
+ };
1145
+ }
1146
+ function createVectorPromoter(config) {
1147
+ const threshold = config?.promotionThreshold ?? 1e4;
1148
+ const hnswConfig = config?.hnswConfig;
1149
+ const promoting = /* @__PURE__ */ new Set();
1150
+ const strategy = config?.workerStrategy ?? detectWorkerStrategy();
1151
+ const pendingWorkers = /* @__PURE__ */ new Set();
1152
+ return {
1153
+ get strategy() {
1154
+ return strategy;
1155
+ },
1156
+ check(engines) {
1157
+ for (const [field, engine] of engines) {
1158
+ if (engine.isPromoted || promoting.has(field)) continue;
1159
+ if (engine.size < threshold) continue;
1160
+ promoting.add(field);
1161
+ if (strategy === "worker-threads") {
1162
+ const request = buildWorkerRequest(engine, hnswConfig);
1163
+ const handle = spawnNodeWorker(
1164
+ request,
1165
+ (graph) => {
1166
+ pendingWorkers.delete(handle);
1167
+ if (!engine.isPromoted) {
1168
+ engine.deserializeHNSW(graph);
1169
+ }
1170
+ promoting.delete(field);
1171
+ },
1172
+ () => {
1173
+ pendingWorkers.delete(handle);
1174
+ promoting.delete(field);
1175
+ }
1176
+ );
1177
+ pendingWorkers.add(handle);
1178
+ } else if (strategy === "web-worker") {
1179
+ const request = buildWorkerRequest(engine, hnswConfig);
1180
+ const handle = spawnWebWorker(
1181
+ request,
1182
+ (graph) => {
1183
+ pendingWorkers.delete(handle);
1184
+ if (!engine.isPromoted) {
1185
+ engine.deserializeHNSW(graph);
1186
+ }
1187
+ promoting.delete(field);
1188
+ },
1189
+ () => {
1190
+ pendingWorkers.delete(handle);
1191
+ promoting.delete(field);
1192
+ }
1193
+ );
1194
+ pendingWorkers.add(handle);
1195
+ } else {
1196
+ try {
1197
+ engine.promoteToHNSW(hnswConfig);
1198
+ } finally {
1199
+ promoting.delete(field);
1200
+ }
1201
+ }
1202
+ }
1203
+ },
1204
+ shutdown() {
1205
+ for (const handle of pendingWorkers) {
1206
+ handle.terminate();
1207
+ }
1208
+ pendingWorkers.clear();
1209
+ promoting.clear();
1210
+ }
1211
+ };
1212
+ }
874
1213
 
875
1214
  // src/core/document-store.ts
876
1215
  function createDocumentStore() {
@@ -879,6 +1218,9 @@ function createDocumentStore() {
879
1218
  store(docId, document, fieldLengths) {
880
1219
  docs.set(docId, { fields: structuredClone(document), fieldLengths });
881
1220
  },
1221
+ storeRef(docId, document, fieldLengths) {
1222
+ docs.set(docId, { fields: document, fieldLengths });
1223
+ },
882
1224
  get(docId) {
883
1225
  return docs.get(docId);
884
1226
  },
@@ -914,7 +1256,10 @@ function createDocumentStore() {
914
1256
  }
915
1257
 
916
1258
  // src/core/inverted-index.ts
917
- function createInvertedIndex() {
1259
+ var INITIAL_CAPACITY = 8;
1260
+ var MAX_TERM_FREQUENCY = 65535;
1261
+ var COMPACTION_THRESHOLD = 0.3;
1262
+ function createInvertedIndex(fieldNameTable) {
918
1263
  const index = /* @__PURE__ */ new Map();
919
1264
  const charBuckets = /* @__PURE__ */ new Map();
920
1265
  function trackToken(token) {
@@ -953,33 +1298,112 @@ function createInvertedIndex() {
953
1298
  }
954
1299
  return filtered;
955
1300
  }
1301
+ function getOrCreateList(token) {
1302
+ let list = index.get(token);
1303
+ if (!list) {
1304
+ list = {
1305
+ length: 0,
1306
+ docIds: [],
1307
+ termFrequencies: new Uint16Array(INITIAL_CAPACITY),
1308
+ fieldNameIndices: new Uint8Array(INITIAL_CAPACITY),
1309
+ positions: null,
1310
+ docIdSet: /* @__PURE__ */ new Set(),
1311
+ deletedDocs: /* @__PURE__ */ new Set()
1312
+ };
1313
+ index.set(token, list);
1314
+ trackToken(token);
1315
+ }
1316
+ return list;
1317
+ }
1318
+ function growTypedArrays(list) {
1319
+ const newCap = list.termFrequencies.length * 2;
1320
+ const newTF = new Uint16Array(newCap);
1321
+ newTF.set(list.termFrequencies);
1322
+ list.termFrequencies = newTF;
1323
+ const newFNI = new Uint8Array(newCap);
1324
+ newFNI.set(list.fieldNameIndices);
1325
+ list.fieldNameIndices = newFNI;
1326
+ }
1327
+ function compactList(list) {
1328
+ if (list.deletedDocs.size === 0) return;
1329
+ let writeIdx = 0;
1330
+ for (let i = 0; i < list.length; i++) {
1331
+ if (!list.deletedDocs.has(list.docIds[i])) {
1332
+ if (writeIdx !== i) {
1333
+ list.docIds[writeIdx] = list.docIds[i];
1334
+ list.termFrequencies[writeIdx] = list.termFrequencies[i];
1335
+ list.fieldNameIndices[writeIdx] = list.fieldNameIndices[i];
1336
+ if (list.positions) {
1337
+ list.positions[writeIdx] = list.positions[i];
1338
+ }
1339
+ }
1340
+ writeIdx++;
1341
+ }
1342
+ }
1343
+ list.docIds.length = writeIdx;
1344
+ if (list.positions) list.positions.length = writeIdx;
1345
+ list.length = writeIdx;
1346
+ list.deletedDocs.clear();
1347
+ }
1348
+ function compactDocEntries(list, docId) {
1349
+ let writeIdx = 0;
1350
+ for (let i = 0; i < list.length; i++) {
1351
+ if (list.docIds[i] !== docId) {
1352
+ if (writeIdx !== i) {
1353
+ list.docIds[writeIdx] = list.docIds[i];
1354
+ list.termFrequencies[writeIdx] = list.termFrequencies[i];
1355
+ list.fieldNameIndices[writeIdx] = list.fieldNameIndices[i];
1356
+ if (list.positions) {
1357
+ list.positions[writeIdx] = list.positions[i];
1358
+ }
1359
+ }
1360
+ writeIdx++;
1361
+ }
1362
+ }
1363
+ list.docIds.length = writeIdx;
1364
+ if (list.positions) list.positions.length = writeIdx;
1365
+ list.length = writeIdx;
1366
+ }
956
1367
  return {
957
- insert(token, entry) {
958
- let list = index.get(token);
959
- if (!list) {
960
- list = { docFrequency: 0, postings: [] };
961
- index.set(token, list);
962
- trackToken(token);
1368
+ insert(token, docId, termFrequency, fieldNameIndex, positions) {
1369
+ const list = getOrCreateList(token);
1370
+ if (list.deletedDocs.size > 0 && list.deletedDocs.has(docId)) {
1371
+ compactDocEntries(list, docId);
1372
+ list.deletedDocs.delete(docId);
963
1373
  }
964
- const dupeIdx = list.postings.findIndex((p) => p.docId === entry.docId && p.fieldName === entry.fieldName);
965
- if (dupeIdx !== -1) {
966
- list.postings[dupeIdx] = entry;
967
- return;
1374
+ if (list.length >= list.termFrequencies.length) {
1375
+ growTypedArrays(list);
1376
+ }
1377
+ const idx = list.length;
1378
+ list.docIds.push(docId);
1379
+ list.termFrequencies[idx] = termFrequency > MAX_TERM_FREQUENCY ? MAX_TERM_FREQUENCY : termFrequency;
1380
+ list.fieldNameIndices[idx] = fieldNameIndex;
1381
+ if (positions !== null) {
1382
+ if (!list.positions) {
1383
+ list.positions = [];
1384
+ for (let j = 0; j < idx; j++) list.positions.push([]);
1385
+ }
1386
+ list.positions.push(positions);
1387
+ } else if (list.positions !== null) {
1388
+ list.positions.push([]);
1389
+ }
1390
+ if (!list.docIdSet.has(docId)) {
1391
+ list.docIdSet.add(docId);
968
1392
  }
969
- const isNewDoc = !list.postings.some((p) => p.docId === entry.docId);
970
- list.postings.push(entry);
971
- if (isNewDoc) list.docFrequency++;
1393
+ list.length++;
972
1394
  },
973
1395
  remove(token, docId) {
974
1396
  const list = index.get(token);
975
- if (!list) return;
976
- const lengthBefore = list.postings.length;
977
- list.postings = list.postings.filter((p) => p.docId !== docId);
978
- const removed = lengthBefore - list.postings.length;
979
- if (removed > 0) list.docFrequency--;
980
- if (list.postings.length === 0) {
1397
+ if (!list || !list.docIdSet.has(docId)) return;
1398
+ list.docIdSet.delete(docId);
1399
+ list.deletedDocs.add(docId);
1400
+ if (list.docIdSet.size === 0) {
981
1401
  index.delete(token);
982
1402
  untrackToken(token);
1403
+ return;
1404
+ }
1405
+ if (list.deletedDocs.size / list.length > COMPACTION_THRESHOLD) {
1406
+ compactList(list);
983
1407
  }
984
1408
  },
985
1409
  lookup(token) {
@@ -1001,6 +1425,20 @@ function createInvertedIndex() {
1001
1425
  }
1002
1426
  return results;
1003
1427
  },
1428
+ prefixSearch(prefix, limit) {
1429
+ if (prefix.length === 0 || limit <= 0) return [];
1430
+ const candidates = candidatesForPrefix(prefix, prefix.length);
1431
+ const results = [];
1432
+ for (const term of candidates) {
1433
+ if (!term.startsWith(prefix)) continue;
1434
+ const list = index.get(term);
1435
+ if (!list) continue;
1436
+ results.push({ term, documentFrequency: list.docIdSet.size });
1437
+ }
1438
+ results.sort((a, b) => b.documentFrequency - a.documentFrequency);
1439
+ if (results.length > limit) results.length = limit;
1440
+ return results;
1441
+ },
1004
1442
  has(token) {
1005
1443
  return index.has(token);
1006
1444
  },
@@ -1017,7 +1455,19 @@ function createInvertedIndex() {
1017
1455
  serialize() {
1018
1456
  const result = /* @__PURE__ */ Object.create(null);
1019
1457
  for (const [token, list] of index) {
1020
- result[token] = list;
1458
+ const postings = [];
1459
+ for (let i = 0; i < list.length; i++) {
1460
+ if (list.deletedDocs.size > 0 && list.deletedDocs.has(list.docIds[i])) continue;
1461
+ postings.push({
1462
+ docId: list.docIds[i],
1463
+ termFrequency: list.termFrequencies[i],
1464
+ fieldName: fieldNameTable.names[list.fieldNameIndices[i]],
1465
+ positions: list.positions ? list.positions[i] : []
1466
+ });
1467
+ }
1468
+ if (postings.length > 0) {
1469
+ result[token] = { docFrequency: list.docIdSet.size, postings };
1470
+ }
1021
1471
  }
1022
1472
  return result;
1023
1473
  },
@@ -1025,7 +1475,43 @@ function createInvertedIndex() {
1025
1475
  index.clear();
1026
1476
  charBuckets.clear();
1027
1477
  for (const token of Object.keys(data)) {
1028
- index.set(token, data[token]);
1478
+ const src = data[token];
1479
+ const count = src.postings.length;
1480
+ const docIdSet = /* @__PURE__ */ new Set();
1481
+ const docIds = new Array(count);
1482
+ const termFrequencies = new Uint16Array(count);
1483
+ const fieldNameIndices = new Uint8Array(count);
1484
+ let hasPositions = false;
1485
+ for (let i = 0; i < count; i++) {
1486
+ const p = src.postings[i];
1487
+ docIds[i] = p.docId;
1488
+ termFrequencies[i] = p.termFrequency > MAX_TERM_FREQUENCY ? MAX_TERM_FREQUENCY : p.termFrequency;
1489
+ let fnIndex = fieldNameTable.indexMap.get(p.fieldName);
1490
+ if (fnIndex === void 0) {
1491
+ fnIndex = fieldNameTable.names.length;
1492
+ fieldNameTable.names.push(p.fieldName);
1493
+ fieldNameTable.indexMap.set(p.fieldName, fnIndex);
1494
+ }
1495
+ fieldNameIndices[i] = fnIndex;
1496
+ docIdSet.add(p.docId);
1497
+ if (p.positions.length > 0) hasPositions = true;
1498
+ }
1499
+ let positions = null;
1500
+ if (hasPositions) {
1501
+ positions = new Array(count);
1502
+ for (let i = 0; i < count; i++) {
1503
+ positions[i] = src.postings[i].positions;
1504
+ }
1505
+ }
1506
+ index.set(token, {
1507
+ length: count,
1508
+ docIds,
1509
+ termFrequencies,
1510
+ fieldNameIndices,
1511
+ positions,
1512
+ docIdSet,
1513
+ deletedDocs: /* @__PURE__ */ new Set()
1514
+ });
1029
1515
  trackToken(token);
1030
1516
  }
1031
1517
  }
@@ -1047,9 +1533,9 @@ function collectUniqueTokens(tokens) {
1047
1533
  function createPartitionStats() {
1048
1534
  const stats = {
1049
1535
  totalDocuments: 0,
1050
- totalFieldLengths: {},
1051
- averageFieldLengths: {},
1052
- docFrequencies: {},
1536
+ totalFieldLengths: /* @__PURE__ */ Object.create(null),
1537
+ averageFieldLengths: /* @__PURE__ */ Object.create(null),
1538
+ docFrequencies: /* @__PURE__ */ Object.create(null),
1053
1539
  addDocument(fieldLengths, tokens) {
1054
1540
  stats.totalDocuments++;
1055
1541
  const fields = Object.keys(fieldLengths);
@@ -1089,7 +1575,7 @@ function createPartitionStats() {
1089
1575
  },
1090
1576
  recalculateAverages() {
1091
1577
  const fields = Object.keys(stats.totalFieldLengths);
1092
- const newAverages = {};
1578
+ const newAverages = /* @__PURE__ */ Object.create(null);
1093
1579
  for (let i = 0; i < fields.length; i++) {
1094
1580
  const field = fields[i];
1095
1581
  newAverages[field] = stats.totalDocuments > 0 ? stats.totalFieldLengths[field] / stats.totalDocuments : 0;
@@ -1097,18 +1583,42 @@ function createPartitionStats() {
1097
1583
  stats.averageFieldLengths = newAverages;
1098
1584
  },
1099
1585
  serialize() {
1586
+ const tfl = /* @__PURE__ */ Object.create(null);
1587
+ for (const key of Object.keys(stats.totalFieldLengths)) {
1588
+ tfl[key] = stats.totalFieldLengths[key];
1589
+ }
1590
+ const afl = /* @__PURE__ */ Object.create(null);
1591
+ for (const key of Object.keys(stats.averageFieldLengths)) {
1592
+ afl[key] = stats.averageFieldLengths[key];
1593
+ }
1594
+ const df = /* @__PURE__ */ Object.create(null);
1595
+ for (const key of Object.keys(stats.docFrequencies)) {
1596
+ df[key] = stats.docFrequencies[key];
1597
+ }
1100
1598
  return {
1101
1599
  totalDocuments: stats.totalDocuments,
1102
- totalFieldLengths: { ...stats.totalFieldLengths },
1103
- averageFieldLengths: { ...stats.averageFieldLengths },
1104
- docFrequencies: { ...stats.docFrequencies }
1600
+ totalFieldLengths: tfl,
1601
+ averageFieldLengths: afl,
1602
+ docFrequencies: df
1105
1603
  };
1106
1604
  },
1107
1605
  deserialize(data) {
1108
1606
  stats.totalDocuments = data.totalDocuments;
1109
- stats.totalFieldLengths = { ...data.totalFieldLengths };
1110
- stats.averageFieldLengths = { ...data.averageFieldLengths };
1111
- stats.docFrequencies = { ...data.docFrequencies };
1607
+ const tfl = /* @__PURE__ */ Object.create(null);
1608
+ for (const key of Object.keys(data.totalFieldLengths)) {
1609
+ tfl[key] = data.totalFieldLengths[key];
1610
+ }
1611
+ stats.totalFieldLengths = tfl;
1612
+ const afl = /* @__PURE__ */ Object.create(null);
1613
+ for (const key of Object.keys(data.averageFieldLengths)) {
1614
+ afl[key] = data.averageFieldLengths[key];
1615
+ }
1616
+ stats.averageFieldLengths = afl;
1617
+ const df = /* @__PURE__ */ Object.create(null);
1618
+ for (const key of Object.keys(data.docFrequencies)) {
1619
+ df[key] = data.docFrequencies[key];
1620
+ }
1621
+ stats.docFrequencies = df;
1112
1622
  }
1113
1623
  };
1114
1624
  return stats;
@@ -1259,34 +1769,6 @@ function createGeoIndex() {
1259
1769
  };
1260
1770
  }
1261
1771
 
1262
- // src/vector/similarity.ts
1263
- function magnitude(v) {
1264
- let sum = 0;
1265
- for (let i = 0; i < v.length; i++) {
1266
- sum += v[i] * v[i];
1267
- }
1268
- return Math.sqrt(sum);
1269
- }
1270
- function cosineSimilarityWithMagnitudes(a, b, magA, magB) {
1271
- if (magA === 0 || magB === 0) return 0;
1272
- return dotProduct(a, b) / (magA * magB);
1273
- }
1274
- function dotProduct(a, b) {
1275
- let sum = 0;
1276
- for (let i = 0; i < a.length; i++) {
1277
- sum += a[i] * b[i];
1278
- }
1279
- return sum;
1280
- }
1281
- function euclideanDistance(a, b) {
1282
- let sum = 0;
1283
- for (let i = 0; i < a.length; i++) {
1284
- const diff = a[i] - b[i];
1285
- sum += diff * diff;
1286
- }
1287
- return Math.sqrt(sum);
1288
- }
1289
-
1290
1772
  // src/vector/brute-force.ts
1291
1773
  function createBruteForceVectorStore(dimension) {
1292
1774
  const vectors = /* @__PURE__ */ new Map();
@@ -1355,6 +1837,120 @@ function createBruteForceVectorStore(dimension) {
1355
1837
  };
1356
1838
  }
1357
1839
 
1840
+ // src/search/vector-search.ts
1841
+ function createVectorSearchEngine(dimension, defaultHNSWConfig) {
1842
+ const bruteForce = createBruteForceVectorStore(dimension);
1843
+ const hnswConfig = defaultHNSWConfig;
1844
+ let hnsw = null;
1845
+ return {
1846
+ get dimension() {
1847
+ return dimension;
1848
+ },
1849
+ get size() {
1850
+ return bruteForce.size;
1851
+ },
1852
+ get isPromoted() {
1853
+ return hnsw !== null;
1854
+ },
1855
+ insert(docId, vector) {
1856
+ bruteForce.insert(docId, vector);
1857
+ if (hnsw) {
1858
+ hnsw.insert(docId, vector);
1859
+ }
1860
+ },
1861
+ remove(docId) {
1862
+ bruteForce.remove(docId);
1863
+ if (hnsw) {
1864
+ hnsw.remove(docId);
1865
+ }
1866
+ },
1867
+ has(docId) {
1868
+ return bruteForce.has(docId);
1869
+ },
1870
+ search(query, k, metric, minSimilarity, filterDocIds, efSearch) {
1871
+ if (hnsw) {
1872
+ return hnsw.search(query, k, metric, minSimilarity, filterDocIds, efSearch);
1873
+ }
1874
+ return bruteForce.search(query, k, metric, minSimilarity, filterDocIds);
1875
+ },
1876
+ clear() {
1877
+ bruteForce.clear();
1878
+ if (hnsw) {
1879
+ hnsw.clear();
1880
+ hnsw = null;
1881
+ }
1882
+ },
1883
+ entries() {
1884
+ return bruteForce.entries();
1885
+ },
1886
+ promoteToHNSW(overrideConfig) {
1887
+ const cfg = overrideConfig ?? hnswConfig;
1888
+ const newHnsw = createHNSWIndex(dimension, cfg);
1889
+ for (const [, entry] of bruteForce.entries()) {
1890
+ newHnsw.insert(entry.docId, entry.vector);
1891
+ }
1892
+ hnsw = newHnsw;
1893
+ },
1894
+ demoteToLinear() {
1895
+ if (hnsw) {
1896
+ hnsw.clear();
1897
+ hnsw = null;
1898
+ }
1899
+ },
1900
+ getHNSWIndex() {
1901
+ return hnsw;
1902
+ },
1903
+ getBruteForceStore() {
1904
+ return bruteForce;
1905
+ },
1906
+ estimateMemoryBytes() {
1907
+ const count = bruteForce.size;
1908
+ if (count === 0) return 0;
1909
+ const MAP_OVERHEAD = 64;
1910
+ const MAP_ENTRY = 72;
1911
+ const VECTOR_ENTRY_OBJ = 32;
1912
+ const AVG_DOCID_BYTES = 56;
1913
+ const TYPED_ARRAY_HEADER = 64;
1914
+ const MAGNITUDE_BYTES = 8;
1915
+ const perBruteForceEntry = MAP_ENTRY + VECTOR_ENTRY_OBJ + AVG_DOCID_BYTES + TYPED_ARRAY_HEADER + MAGNITUDE_BYTES;
1916
+ let bytes = MAP_OVERHEAD + count * (perBruteForceEntry + dimension * 4);
1917
+ if (hnsw) {
1918
+ const HNSW_NODE_OBJ = 64;
1919
+ const SHARED_REFS = 16;
1920
+ const CONN_ARRAY_HEADER = 32;
1921
+ const SET_OVERHEAD = 64;
1922
+ const SET_ENTRY_COST = 72;
1923
+ const m = hnsw.m;
1924
+ const avgLayers = m / (m - 1);
1925
+ const avgConnsLayer0 = m;
1926
+ const avgConnsUpper = Math.ceil(m / 2);
1927
+ const connMemPerNode = CONN_ARRAY_HEADER + (SET_OVERHEAD + avgConnsLayer0 * SET_ENTRY_COST) + Math.max(0, avgLayers - 1) * (SET_OVERHEAD + avgConnsUpper * SET_ENTRY_COST);
1928
+ const perHnswNode = MAP_ENTRY + HNSW_NODE_OBJ + SHARED_REFS + connMemPerNode;
1929
+ bytes += MAP_OVERHEAD + count * perHnswNode;
1930
+ }
1931
+ return Math.round(bytes);
1932
+ },
1933
+ serializeHNSW() {
1934
+ if (!hnsw) return null;
1935
+ return hnsw.serialize();
1936
+ },
1937
+ deserializeHNSW(data) {
1938
+ const cfg = {};
1939
+ const restoredHnsw = createHNSWIndex(dimension, {
1940
+ m: data.m ?? cfg.m,
1941
+ efConstruction: data.efConstruction ?? cfg.efConstruction,
1942
+ metric: data.metric ?? cfg.metric
1943
+ });
1944
+ const vectorMap = /* @__PURE__ */ new Map();
1945
+ for (const [, entry] of bruteForce.entries()) {
1946
+ vectorMap.set(entry.docId, { vector: entry.vector, mag: entry.magnitude });
1947
+ }
1948
+ restoredHnsw.deserialize(data, vectorMap);
1949
+ hnsw = restoredHnsw;
1950
+ }
1951
+ };
1952
+ }
1953
+
1358
1954
  // src/core/field-index.ts
1359
1955
  function lowerBound(entries, value) {
1360
1956
  let lo = 0;
@@ -1385,12 +1981,20 @@ function collectDocIds(entries, from, to) {
1385
1981
  }
1386
1982
  function createNumericIndex() {
1387
1983
  let entries = [];
1984
+ let sorted = true;
1985
+ function ensureSorted() {
1986
+ if (!sorted) {
1987
+ entries.sort((a, b) => a.value - b.value);
1988
+ sorted = true;
1989
+ }
1990
+ }
1388
1991
  return {
1389
1992
  insert(docId, value) {
1390
- const pos = upperBound(entries, value);
1391
- entries.splice(pos, 0, { value, docId });
1993
+ entries.push({ value, docId });
1994
+ sorted = false;
1392
1995
  },
1393
1996
  remove(docId, value) {
1997
+ ensureSorted();
1394
1998
  const start = lowerBound(entries, value);
1395
1999
  const end = upperBound(entries, value);
1396
2000
  for (let i = start; i < end; i++) {
@@ -1401,9 +2005,11 @@ function createNumericIndex() {
1401
2005
  }
1402
2006
  },
1403
2007
  queryEq(value) {
2008
+ ensureSorted();
1404
2009
  return collectDocIds(entries, lowerBound(entries, value), upperBound(entries, value));
1405
2010
  },
1406
2011
  queryNe(value) {
2012
+ ensureSorted();
1407
2013
  const excluded = this.queryEq(value);
1408
2014
  const result = /* @__PURE__ */ new Set();
1409
2015
  for (const entry of entries) {
@@ -1412,18 +2018,23 @@ function createNumericIndex() {
1412
2018
  return result;
1413
2019
  },
1414
2020
  queryGt(value) {
2021
+ ensureSorted();
1415
2022
  return collectDocIds(entries, upperBound(entries, value), entries.length);
1416
2023
  },
1417
2024
  queryGte(value) {
2025
+ ensureSorted();
1418
2026
  return collectDocIds(entries, lowerBound(entries, value), entries.length);
1419
2027
  },
1420
2028
  queryLt(value) {
2029
+ ensureSorted();
1421
2030
  return collectDocIds(entries, 0, lowerBound(entries, value));
1422
2031
  },
1423
2032
  queryLte(value) {
2033
+ ensureSorted();
1424
2034
  return collectDocIds(entries, 0, upperBound(entries, value));
1425
2035
  },
1426
2036
  queryBetween(min, max) {
2037
+ ensureSorted();
1427
2038
  return collectDocIds(entries, lowerBound(entries, min), upperBound(entries, max));
1428
2039
  },
1429
2040
  getAllDocIds() {
@@ -1434,12 +2045,15 @@ function createNumericIndex() {
1434
2045
  },
1435
2046
  clear() {
1436
2047
  entries = [];
2048
+ sorted = true;
1437
2049
  },
1438
2050
  serialize() {
2051
+ ensureSorted();
1439
2052
  return entries.slice();
1440
2053
  },
1441
2054
  deserialize(data) {
1442
2055
  entries = data.slice();
2056
+ sorted = true;
1443
2057
  }
1444
2058
  };
1445
2059
  }
@@ -1567,6 +2181,14 @@ function createEnumIndex() {
1567
2181
  }
1568
2182
 
1569
2183
  // src/core/partition/utils.ts
2184
+ function getOrCreateFieldNameIndex(table, fieldName) {
2185
+ const existing = table.indexMap.get(fieldName);
2186
+ if (existing !== void 0) return existing;
2187
+ const idx = table.names.length;
2188
+ table.names.push(fieldName);
2189
+ table.indexMap.set(fieldName, idx);
2190
+ return idx;
2191
+ }
1570
2192
  var VECTOR_PATTERN2 = /^vector\[(\d+)]$/;
1571
2193
  function getNestedValue(obj, path) {
1572
2194
  const parts = path.split(".");
@@ -1643,7 +2265,7 @@ function ensureFieldIndex(state, fieldPath, fieldType) {
1643
2265
  } else {
1644
2266
  const dim = parseVectorDimension(fieldType);
1645
2267
  if (dim !== null && !state.vectorStores.has(fieldPath)) {
1646
- state.vectorStores.set(fieldPath, createBruteForceVectorStore(dim));
2268
+ state.vectorStores.set(fieldPath, createVectorSearchEngine(dim));
1647
2269
  }
1648
2270
  }
1649
2271
  }
@@ -1667,53 +2289,71 @@ function indexStringField(state, docId, fieldPath, text, language, options, fiel
1667
2289
  const result = tokenize(text, language, tokenizeOptions(options));
1668
2290
  fieldLengths[fieldPath] = result.tokens.length;
1669
2291
  const fieldTokenList = [];
1670
- const tokenFreqs = /* @__PURE__ */ new Map();
1671
- for (const t of result.tokens) {
1672
- const existing = tokenFreqs.get(t.token);
1673
- if (existing) {
1674
- existing.count++;
1675
- existing.positions.push(t.position);
1676
- } else {
1677
- tokenFreqs.set(t.token, { count: 1, positions: [t.position] });
1678
- }
1679
- fieldTokenList.push(t.token);
1680
- }
1681
- for (const [token, freq] of tokenFreqs) {
1682
- state.invertedIdx.insert(token, {
1683
- docId,
1684
- termFrequency: freq.count,
1685
- fieldName: fieldPath,
1686
- positions: freq.positions
1687
- });
1688
- }
1689
- tokensByField[fieldPath] = fieldTokenList;
1690
- }
1691
- function indexStringArrayField(state, docId, fieldPath, arr, language, options, fieldLengths, tokensByField) {
1692
- const tokenFreqs = /* @__PURE__ */ new Map();
1693
- const fieldTokenList = [];
1694
- let positionOffset = 0;
1695
- for (const item of arr) {
1696
- const result = tokenize(item, language, tokenizeOptions(options));
2292
+ const fieldNameIndex = getOrCreateFieldNameIndex(state.fieldNameTable, fieldPath);
2293
+ if (state.trackPositions) {
2294
+ const tokenFreqs = /* @__PURE__ */ new Map();
1697
2295
  for (const t of result.tokens) {
1698
2296
  const existing = tokenFreqs.get(t.token);
1699
2297
  if (existing) {
1700
2298
  existing.count++;
1701
- existing.positions.push(positionOffset + t.position);
2299
+ existing.positions.push(t.position);
1702
2300
  } else {
1703
- tokenFreqs.set(t.token, { count: 1, positions: [positionOffset + t.position] });
2301
+ tokenFreqs.set(t.token, { count: 1, positions: [t.position] });
1704
2302
  }
1705
2303
  fieldTokenList.push(t.token);
1706
2304
  }
1707
- positionOffset += result.tokens.length;
2305
+ for (const [token, freq] of tokenFreqs) {
2306
+ state.invertedIdx.insert(token, docId, freq.count, fieldNameIndex, freq.positions);
2307
+ }
2308
+ } else {
2309
+ const tokenCounts = /* @__PURE__ */ new Map();
2310
+ for (const t of result.tokens) {
2311
+ tokenCounts.set(t.token, (tokenCounts.get(t.token) ?? 0) + 1);
2312
+ fieldTokenList.push(t.token);
2313
+ }
2314
+ for (const [token, count] of tokenCounts) {
2315
+ state.invertedIdx.insert(token, docId, count, fieldNameIndex, null);
2316
+ }
1708
2317
  }
1709
- fieldLengths[fieldPath] = fieldTokenList.length;
1710
- for (const [token, freq] of tokenFreqs) {
1711
- state.invertedIdx.insert(token, {
1712
- docId,
1713
- termFrequency: freq.count,
1714
- fieldName: fieldPath,
1715
- positions: freq.positions
1716
- });
2318
+ tokensByField[fieldPath] = fieldTokenList;
2319
+ }
2320
+ function indexStringArrayField(state, docId, fieldPath, arr, language, options, fieldLengths, tokensByField) {
2321
+ const fieldTokenList = [];
2322
+ const fieldNameIndex = getOrCreateFieldNameIndex(state.fieldNameTable, fieldPath);
2323
+ if (state.trackPositions) {
2324
+ const tokenFreqs = /* @__PURE__ */ new Map();
2325
+ let positionOffset = 0;
2326
+ for (const item of arr) {
2327
+ const result = tokenize(item, language, tokenizeOptions(options));
2328
+ for (const t of result.tokens) {
2329
+ const existing = tokenFreqs.get(t.token);
2330
+ if (existing) {
2331
+ existing.count++;
2332
+ existing.positions.push(positionOffset + t.position);
2333
+ } else {
2334
+ tokenFreqs.set(t.token, { count: 1, positions: [positionOffset + t.position] });
2335
+ }
2336
+ fieldTokenList.push(t.token);
2337
+ }
2338
+ positionOffset += result.tokens.length;
2339
+ }
2340
+ fieldLengths[fieldPath] = fieldTokenList.length;
2341
+ for (const [token, freq] of tokenFreqs) {
2342
+ state.invertedIdx.insert(token, docId, freq.count, fieldNameIndex, freq.positions);
2343
+ }
2344
+ } else {
2345
+ const tokenCounts = /* @__PURE__ */ new Map();
2346
+ for (const item of arr) {
2347
+ const result = tokenize(item, language, tokenizeOptions(options));
2348
+ for (const t of result.tokens) {
2349
+ tokenCounts.set(t.token, (tokenCounts.get(t.token) ?? 0) + 1);
2350
+ fieldTokenList.push(t.token);
2351
+ }
2352
+ }
2353
+ fieldLengths[fieldPath] = fieldTokenList.length;
2354
+ for (const [token, count] of tokenCounts) {
2355
+ state.invertedIdx.insert(token, docId, count, fieldNameIndex, null);
2356
+ }
1717
2357
  }
1718
2358
  tokensByField[fieldPath] = fieldTokenList;
1719
2359
  }
@@ -2332,66 +2972,188 @@ function computeBM25WithGlobalStats(termFrequency, globalDocFrequency, globalTot
2332
2972
  }
2333
2973
 
2334
2974
  // src/core/partition/search.ts
2975
+ var DEFAULT_MAX_RESULTS = 1e3;
2335
2976
  function searchFulltext(state, params) {
2336
- const { queryTokens, fields, boost, tolerance = 0, prefixLength = 2, exact = false, bm25Params, globalStats } = params;
2977
+ const {
2978
+ queryTokens,
2979
+ fields,
2980
+ boost,
2981
+ tolerance = 0,
2982
+ prefixLength = 2,
2983
+ exact = false,
2984
+ bm25Params,
2985
+ globalStats,
2986
+ maxResults,
2987
+ termMatch
2988
+ } = params;
2337
2989
  if (queryTokens.length === 0) {
2338
2990
  return { scored: [], totalMatched: 0 };
2339
2991
  }
2340
- const docScores = /* @__PURE__ */ new Map();
2341
2992
  const totalDocs = globalStats?.totalDocuments ?? state.stats.totalDocuments;
2342
2993
  const avgFieldLengths = globalStats?.averageFieldLengths ?? state.stats.averageFieldLengths;
2343
2994
  const globalDocFreqs = globalStats?.docFrequencies ?? state.stats.docFrequencies;
2344
- for (const qt of queryTokens) {
2345
- const matchingPostings = exact ? (() => {
2346
- const postingList = state.invertedIdx.lookup(qt.token);
2347
- return postingList ? [{ token: qt.token, postingList }] : [];
2348
- })() : state.invertedIdx.fuzzyLookup(qt.token, tolerance, prefixLength);
2349
- for (const match of matchingPostings) {
2350
- const docFreq = globalStats ? globalDocFreqs[match.token] ?? match.postingList.docFrequency : match.postingList.docFrequency;
2351
- const idf = computeIDF(docFreq, totalDocs);
2352
- for (const posting of match.postingList.postings) {
2353
- if (fields && !fields.includes(posting.fieldName)) continue;
2354
- const fieldBoost = boost?.[posting.fieldName] ?? 1;
2355
- const avgLen = avgFieldLengths[posting.fieldName] ?? 1;
2356
- const storedDoc = state.docStore.get(posting.docId);
2357
- const actualFieldLength = storedDoc?.fieldLengths[posting.fieldName] ?? avgLen;
2358
- const scoreFn = globalStats ? computeBM25WithGlobalStats : computeBM25;
2359
- let termScore = scoreFn(posting.termFrequency, docFreq, totalDocs, actualFieldLength, avgLen, bm25Params);
2360
- termScore *= fieldBoost;
2361
- const existing = docScores.get(posting.docId);
2362
- if (existing) {
2363
- existing.score += termScore;
2364
- existing.termFrequencies[`${posting.fieldName}:${match.token}`] = posting.termFrequency;
2365
- existing.fieldLengths[posting.fieldName] = actualFieldLength;
2366
- existing.idf[match.token] = idf;
2367
- existing.matchedTermCount++;
2368
- } else {
2369
- docScores.set(posting.docId, {
2370
- score: termScore,
2371
- termFrequencies: { [`${posting.fieldName}:${match.token}`]: posting.termFrequency },
2372
- fieldLengths: { [posting.fieldName]: actualFieldLength },
2373
- idf: { [match.token]: idf },
2374
- matchedTermCount: 1
2375
- });
2995
+ const scoreFn = globalStats ? computeBM25WithGlobalStats : computeBM25;
2996
+ const docScores = /* @__PURE__ */ new Map();
2997
+ const fieldLengthCache = /* @__PURE__ */ new Map();
2998
+ const useIntersection = termMatch === "all" && queryTokens.length > 1;
2999
+ const fieldNames = state.fieldNameTable.names;
3000
+ if (useIntersection) {
3001
+ const resolved = [];
3002
+ for (const qt of queryTokens) {
3003
+ const rawMatches = exact ? (() => {
3004
+ const postingList = state.invertedIdx.lookup(qt.token);
3005
+ return postingList ? [{ token: qt.token, postingList }] : [];
3006
+ })() : state.invertedIdx.fuzzyLookup(qt.token, tolerance, prefixLength);
3007
+ let totalPostings = 0;
3008
+ const matches = [];
3009
+ for (const m of rawMatches) {
3010
+ const docFreq = globalStats ? globalDocFreqs[m.token] ?? m.postingList.docIdSet.size : m.postingList.docIdSet.size;
3011
+ const idf = computeIDF(docFreq, totalDocs);
3012
+ totalPostings += m.postingList.length;
3013
+ matches.push({ token: m.token, docFreq, idf, postingList: m.postingList });
3014
+ }
3015
+ resolved.push({ token: qt.token, matches, totalPostings });
3016
+ }
3017
+ resolved.sort((a, b) => a.totalPostings - b.totalPostings);
3018
+ for (let tokenIndex = 0; tokenIndex < resolved.length; tokenIndex++) {
3019
+ for (const match of resolved[tokenIndex].matches) {
3020
+ const list = match.postingList;
3021
+ const hasDeleted = list.deletedDocs.size > 0;
3022
+ for (let pi = 0; pi < list.length; pi++) {
3023
+ const docId = list.docIds[pi];
3024
+ if (hasDeleted && list.deletedDocs.has(docId)) continue;
3025
+ if (tokenIndex > 0 && !docScores.has(docId)) continue;
3026
+ const fieldName = fieldNames[list.fieldNameIndices[pi]];
3027
+ if (fields && !fields.includes(fieldName)) continue;
3028
+ const termFrequency = list.termFrequencies[pi];
3029
+ const fieldBoost = boost?.[fieldName] ?? 1;
3030
+ const avgLen = avgFieldLengths[fieldName] ?? 1;
3031
+ let cachedLengths = fieldLengthCache.get(docId);
3032
+ if (cachedLengths === void 0) {
3033
+ const storedDoc = state.docStore.get(docId);
3034
+ cachedLengths = storedDoc?.fieldLengths ?? null;
3035
+ fieldLengthCache.set(docId, cachedLengths);
3036
+ }
3037
+ const actualFieldLength = cachedLengths?.[fieldName] ?? avgLen;
3038
+ let termScore = scoreFn(termFrequency, match.docFreq, totalDocs, actualFieldLength, avgLen, bm25Params);
3039
+ termScore *= fieldBoost;
3040
+ const existing = docScores.get(docId);
3041
+ if (existing) {
3042
+ existing.score += termScore;
3043
+ existing.termFrequencies[`${fieldName}:${match.token}`] = termFrequency;
3044
+ existing.fieldLengths[fieldName] = actualFieldLength;
3045
+ existing.idf[match.token] = match.idf;
3046
+ } else {
3047
+ docScores.set(docId, {
3048
+ score: termScore,
3049
+ termFrequencies: { [`${fieldName}:${match.token}`]: termFrequency },
3050
+ fieldLengths: { [fieldName]: actualFieldLength },
3051
+ idf: { [match.token]: match.idf }
3052
+ });
3053
+ }
3054
+ }
3055
+ }
3056
+ }
3057
+ } else {
3058
+ for (const qt of queryTokens) {
3059
+ const matchingPostings = exact ? (() => {
3060
+ const postingList = state.invertedIdx.lookup(qt.token);
3061
+ return postingList ? [{ token: qt.token, postingList }] : [];
3062
+ })() : state.invertedIdx.fuzzyLookup(qt.token, tolerance, prefixLength);
3063
+ for (const match of matchingPostings) {
3064
+ const docFreq = globalStats ? globalDocFreqs[match.token] ?? match.postingList.docIdSet.size : match.postingList.docIdSet.size;
3065
+ const idf = computeIDF(docFreq, totalDocs);
3066
+ const list = match.postingList;
3067
+ const hasDeleted = list.deletedDocs.size > 0;
3068
+ for (let pi = 0; pi < list.length; pi++) {
3069
+ const docId = list.docIds[pi];
3070
+ if (hasDeleted && list.deletedDocs.has(docId)) continue;
3071
+ const fieldName = fieldNames[list.fieldNameIndices[pi]];
3072
+ if (fields && !fields.includes(fieldName)) continue;
3073
+ const termFrequency = list.termFrequencies[pi];
3074
+ const fieldBoost = boost?.[fieldName] ?? 1;
3075
+ const avgLen = avgFieldLengths[fieldName] ?? 1;
3076
+ let cachedLengths = fieldLengthCache.get(docId);
3077
+ if (cachedLengths === void 0) {
3078
+ const storedDoc = state.docStore.get(docId);
3079
+ cachedLengths = storedDoc?.fieldLengths ?? null;
3080
+ fieldLengthCache.set(docId, cachedLengths);
3081
+ }
3082
+ const actualFieldLength = cachedLengths?.[fieldName] ?? avgLen;
3083
+ let termScore = scoreFn(termFrequency, docFreq, totalDocs, actualFieldLength, avgLen, bm25Params);
3084
+ termScore *= fieldBoost;
3085
+ const existing = docScores.get(docId);
3086
+ if (existing) {
3087
+ existing.score += termScore;
3088
+ existing.termFrequencies[`${fieldName}:${match.token}`] = termFrequency;
3089
+ existing.fieldLengths[fieldName] = actualFieldLength;
3090
+ existing.idf[match.token] = idf;
3091
+ } else {
3092
+ docScores.set(docId, {
3093
+ score: termScore,
3094
+ termFrequencies: { [`${fieldName}:${match.token}`]: termFrequency },
3095
+ fieldLengths: { [fieldName]: actualFieldLength },
3096
+ idf: { [match.token]: idf }
3097
+ });
3098
+ }
2376
3099
  }
2377
3100
  }
2378
3101
  }
2379
3102
  }
2380
- const scored = [];
3103
+ const totalMatched = docScores.size;
3104
+ const k = maxResults !== void 0 && maxResults > 0 ? maxResults : DEFAULT_MAX_RESULTS;
3105
+ const scored = topKFromMap(docScores, Math.min(k, totalMatched));
3106
+ return { scored, totalMatched };
3107
+ }
3108
+ function topKFromMap(docScores, k) {
3109
+ if (k <= 0) return [];
3110
+ const heap = [];
2381
3111
  for (const [docId, data] of docScores) {
2382
- scored.push({
2383
- docId,
3112
+ if (heap.length < k) {
3113
+ heap.push({ docId, score: data.score });
3114
+ if (heap.length === k) buildMinHeap(heap);
3115
+ } else if (data.score > heap[0].score) {
3116
+ heap[0] = { docId, score: data.score };
3117
+ siftDown(heap, 0);
3118
+ }
3119
+ }
3120
+ heap.sort((a, b) => b.score - a.score);
3121
+ const result = new Array(heap.length);
3122
+ for (let i = 0; i < heap.length; i++) {
3123
+ const data = docScores.get(heap[i].docId);
3124
+ if (!data) continue;
3125
+ result[i] = {
3126
+ docId: heap[i].docId,
2384
3127
  score: data.score,
2385
3128
  termFrequencies: data.termFrequencies,
2386
3129
  fieldLengths: data.fieldLengths,
2387
3130
  idf: data.idf
2388
- });
3131
+ };
3132
+ }
3133
+ return result;
3134
+ }
3135
+ function buildMinHeap(heap) {
3136
+ for (let i = (heap.length >> 1) - 1; i >= 0; i--) {
3137
+ siftDown(heap, i);
3138
+ }
3139
+ }
3140
+ function siftDown(heap, idx) {
3141
+ const len = heap.length;
3142
+ while (true) {
3143
+ let smallest = idx;
3144
+ const left = 2 * idx + 1;
3145
+ const right = 2 * idx + 2;
3146
+ if (left < len && heap[left].score < heap[smallest].score) smallest = left;
3147
+ if (right < len && heap[right].score < heap[smallest].score) smallest = right;
3148
+ if (smallest === idx) break;
3149
+ const tmp = heap[idx];
3150
+ heap[idx] = heap[smallest];
3151
+ heap[smallest] = tmp;
3152
+ idx = smallest;
2389
3153
  }
2390
- scored.sort((a, b) => b.score - a.score);
2391
- return { scored, totalMatched: scored.length };
2392
3154
  }
2393
3155
  function searchVector(state, params) {
2394
- const { field, value, k, similarity = 0, metric = "cosine", filterDocIds } = params;
3156
+ const { field, value, k, similarity = 0, metric = "cosine", filterDocIds, efSearch } = params;
2395
3157
  const vecStore = state.vectorStores.get(field);
2396
3158
  if (!vecStore) {
2397
3159
  return { scored: [], totalMatched: 0 };
@@ -2404,7 +3166,7 @@ function searchVector(state, params) {
2404
3166
  );
2405
3167
  }
2406
3168
  const queryVec = new Float32Array(value);
2407
- const results = vecStore.search(queryVec, k, metric, similarity, filterDocIds);
3169
+ const results = vecStore.search(queryVec, k, metric, similarity, filterDocIds, efSearch);
2408
3170
  return { scored: results, totalMatched: results.length };
2409
3171
  }
2410
3172
  function buildFilterContext(state, schema) {
@@ -2587,7 +3349,7 @@ function serializePartition(state, partitionId, indexName, totalPartitions, lang
2587
3349
  serializedVectors[path] = {
2588
3350
  dimension: store.dimension,
2589
3351
  vectors,
2590
- hnswGraph: null
3352
+ hnswGraph: store.serializeHNSW()
2591
3353
  };
2592
3354
  }
2593
3355
  const serializedStats = state.stats.serialize();
@@ -2611,6 +3373,109 @@ function serializePartition(state, partitionId, indexName, totalPartitions, lang
2611
3373
  statistics: serializedStats
2612
3374
  };
2613
3375
  }
3376
+ function serializePartitionToWirePayloadV2(state, partitionId, indexName, totalPartitions, language, schema) {
3377
+ const flatSchema = getFlatSchema(state, schema);
3378
+ const flatSchemaStrings = {};
3379
+ for (const [k, v] of Object.entries(flatSchema)) {
3380
+ flatSchemaStrings[k] = v;
3381
+ }
3382
+ const wireDocs = /* @__PURE__ */ Object.create(null);
3383
+ for (const [docId, stored] of state.docStore.all()) {
3384
+ wireDocs[docId] = {
3385
+ fields: structuredClone(stored.fields),
3386
+ field_lengths: { ...stored.fieldLengths }
3387
+ };
3388
+ }
3389
+ const fieldNames = [...state.fieldNameTable.names];
3390
+ const wireEntries = /* @__PURE__ */ Object.create(null);
3391
+ for (const token of state.invertedIdx.tokens()) {
3392
+ const list = state.invertedIdx.lookup(token);
3393
+ if (!list) continue;
3394
+ const hasDeleted = list.deletedDocs.size > 0;
3395
+ const ids = [];
3396
+ const tf = [];
3397
+ const fi = [];
3398
+ const pos = list.positions ? [] : null;
3399
+ for (let i = 0; i < list.length; i++) {
3400
+ if (hasDeleted && list.deletedDocs.has(list.docIds[i])) continue;
3401
+ ids.push(list.docIds[i]);
3402
+ tf.push(list.termFrequencies[i]);
3403
+ fi.push(list.fieldNameIndices[i]);
3404
+ if (pos && list.positions) {
3405
+ pos.push(list.positions[i]);
3406
+ }
3407
+ }
3408
+ if (ids.length > 0) {
3409
+ wireEntries[token] = { df: list.docIdSet.size, ids, tf, fi, pos };
3410
+ }
3411
+ }
3412
+ const wireNumeric = {};
3413
+ for (const [path, idx] of state.numericIndexes) {
3414
+ wireNumeric[path] = idx.serialize().map((e) => ({ value: e.value, doc_id: e.docId }));
3415
+ }
3416
+ const wireBoolean = {};
3417
+ for (const [path, idx] of state.booleanIndexes) {
3418
+ const s = idx.serialize();
3419
+ wireBoolean[path] = { true_docs: s.trueDocs, false_docs: s.falseDocs };
3420
+ }
3421
+ const wireEnum = {};
3422
+ for (const [path, idx] of state.enumIndexes) {
3423
+ wireEnum[path] = idx.serialize();
3424
+ }
3425
+ const wireGeo = {};
3426
+ for (const [path, idx] of state.geoIndexes) {
3427
+ wireGeo[path] = idx.serialize().map((e) => ({ lat: e.lat, lon: e.lon, doc_id: e.docId }));
3428
+ }
3429
+ const wireVectors = {};
3430
+ for (const [path, store] of state.vectorStores) {
3431
+ const vectors = [];
3432
+ for (const [, entry] of store.entries()) {
3433
+ vectors.push({ doc_id: entry.docId, vector: Array.from(entry.vector) });
3434
+ }
3435
+ const hnswData = store.serializeHNSW();
3436
+ wireVectors[path] = {
3437
+ dimension: store.dimension,
3438
+ vectors,
3439
+ hnsw_graph: hnswData ? {
3440
+ entry_point: hnswData.entryPoint,
3441
+ max_layer: hnswData.maxLayer,
3442
+ m: hnswData.m,
3443
+ ef_construction: hnswData.efConstruction,
3444
+ metric: hnswData.metric,
3445
+ nodes: hnswData.nodes
3446
+ } : null
3447
+ };
3448
+ }
3449
+ const serializedStats = state.stats.serialize();
3450
+ return {
3451
+ v: 2,
3452
+ index_name: indexName,
3453
+ partition_id: partitionId,
3454
+ total_partitions: totalPartitions,
3455
+ language,
3456
+ schema: flatSchemaStrings,
3457
+ doc_count: state.docStore.count(),
3458
+ avg_doc_length: Object.values(serializedStats.averageFieldLengths).reduce((sum, v) => sum + v, 0),
3459
+ documents: wireDocs,
3460
+ inverted_index: {
3461
+ field_names: fieldNames,
3462
+ entries: wireEntries
3463
+ },
3464
+ field_indexes: {
3465
+ numeric: wireNumeric,
3466
+ boolean: wireBoolean,
3467
+ enum: wireEnum,
3468
+ geopoint: wireGeo
3469
+ },
3470
+ vector_data: wireVectors,
3471
+ statistics: {
3472
+ total_documents: serializedStats.totalDocuments,
3473
+ total_field_lengths: serializedStats.totalFieldLengths,
3474
+ average_field_lengths: serializedStats.averageFieldLengths,
3475
+ doc_frequencies: serializedStats.docFrequencies
3476
+ }
3477
+ };
3478
+ }
2614
3479
  function deserializePartition(state, data, clearFn, schema) {
2615
3480
  clearFn();
2616
3481
  const invertedData = /* @__PURE__ */ Object.create(null);
@@ -2621,7 +3486,7 @@ function deserializePartition(state, data, clearFn, schema) {
2621
3486
  docId: p.docId,
2622
3487
  termFrequency: p.termFrequency,
2623
3488
  fieldName: p.field,
2624
- positions: [...p.positions]
3489
+ positions: state.trackPositions ? [...p.positions] : []
2625
3490
  }))
2626
3491
  };
2627
3492
  }
@@ -2652,10 +3517,13 @@ function deserializePartition(state, data, clearFn, schema) {
2652
3517
  state.geoIndexes.set(path, geoIdx);
2653
3518
  }
2654
3519
  for (const [path, vecData] of Object.entries(data.vectorData)) {
2655
- const store = createBruteForceVectorStore(vecData.dimension);
3520
+ const store = createVectorSearchEngine(vecData.dimension);
2656
3521
  for (const entry of vecData.vectors) {
2657
3522
  store.insert(entry.docId, new Float32Array(entry.vector));
2658
3523
  }
3524
+ if (vecData.hnswGraph) {
3525
+ store.deserializeHNSW(vecData.hnswGraph);
3526
+ }
2659
3527
  state.vectorStores.set(path, store);
2660
3528
  }
2661
3529
  state.stats.deserialize(data.statistics);
@@ -2676,9 +3544,15 @@ function deserializePartition(state, data, clearFn, schema) {
2676
3544
  }
2677
3545
 
2678
3546
  // src/core/partition/index.ts
2679
- function createPartitionIndex(partitionId) {
3547
+ function createPartitionIndex(partitionId, trackPositions = true, vectorPromotionConfig) {
3548
+ const vectorPromoter = vectorPromotionConfig ? createVectorPromoter({
3549
+ promotionThreshold: vectorPromotionConfig.threshold,
3550
+ hnswConfig: vectorPromotionConfig.hnswConfig,
3551
+ workerStrategy: vectorPromotionConfig.workerStrategy
3552
+ }) : null;
3553
+ const fieldNameTable = { names: [], indexMap: /* @__PURE__ */ new Map() };
2680
3554
  const state = {
2681
- invertedIdx: createInvertedIndex(),
3555
+ invertedIdx: createInvertedIndex(fieldNameTable),
2682
3556
  docStore: createDocumentStore(),
2683
3557
  stats: createPartitionStats(),
2684
3558
  numericIndexes: /* @__PURE__ */ new Map(),
@@ -2686,10 +3560,13 @@ function createPartitionIndex(partitionId) {
2686
3560
  enumIndexes: /* @__PURE__ */ new Map(),
2687
3561
  geoIndexes: /* @__PURE__ */ new Map(),
2688
3562
  vectorStores: /* @__PURE__ */ new Map(),
3563
+ fieldNameTable,
2689
3564
  flatSchemaCache: null,
2690
- lastSchemaRef: null
3565
+ lastSchemaRef: null,
3566
+ trackPositions
2691
3567
  };
2692
3568
  function clearAll() {
3569
+ vectorPromoter?.shutdown();
2693
3570
  state.invertedIdx.clear();
2694
3571
  state.docStore.clear();
2695
3572
  for (const idx of state.numericIndexes.values()) idx.clear();
@@ -2723,6 +3600,9 @@ function createPartitionIndex(partitionId) {
2723
3600
  }
2724
3601
  if (options?.validate !== false) {
2725
3602
  validateDocument(document, schema);
3603
+ if (options?.strict) {
3604
+ validateDocumentStrict(document, schema);
3605
+ }
2726
3606
  }
2727
3607
  const flatSchema = getFlatSchema(state, schema);
2728
3608
  const { fieldLengths, tokensByField } = indexDocument(
@@ -2733,8 +3613,13 @@ function createPartitionIndex(partitionId) {
2733
3613
  language,
2734
3614
  options
2735
3615
  );
2736
- state.docStore.store(docId, document, fieldLengths);
3616
+ if (options?.skipClone) {
3617
+ state.docStore.storeRef(docId, document, fieldLengths);
3618
+ } else {
3619
+ state.docStore.store(docId, document, fieldLengths);
3620
+ }
2737
3621
  state.stats.addDocument(fieldLengths, tokensByField);
3622
+ vectorPromoter?.check(state.vectorStores);
2738
3623
  },
2739
3624
  remove(docId, schema, language, options) {
2740
3625
  const stored = state.docStore.get(docId);
@@ -2759,6 +3644,9 @@ function createPartitionIndex(partitionId) {
2759
3644
  }
2760
3645
  if (options?.validate !== false) {
2761
3646
  validateDocument(document, schema);
3647
+ if (options?.strict) {
3648
+ validateDocumentStrict(document, schema);
3649
+ }
2762
3650
  }
2763
3651
  const flatSchema = getFlatSchema(state, schema);
2764
3652
  const needsTextReindex = textFieldsChanged(stored.fields, document, flatSchema);
@@ -2793,6 +3681,11 @@ function createPartitionIndex(partitionId) {
2793
3681
  if (!stored) return void 0;
2794
3682
  return structuredClone(stored.fields);
2795
3683
  },
3684
+ getRef(docId) {
3685
+ const stored = state.docStore.get(docId);
3686
+ if (!stored) return void 0;
3687
+ return stored.fields;
3688
+ },
2796
3689
  has(docId) {
2797
3690
  return state.docStore.has(docId);
2798
3691
  },
@@ -2805,6 +3698,41 @@ function createPartitionIndex(partitionId) {
2805
3698
  }
2806
3699
  },
2807
3700
  clear: clearAll,
3701
+ estimateMemoryBytes() {
3702
+ const docCount = state.docStore.count();
3703
+ if (docCount === 0) return 0;
3704
+ const AVG_DOC_OVERHEAD = 350;
3705
+ let bytes = docCount * AVG_DOC_OVERHEAD;
3706
+ const docFreqs = state.stats.docFrequencies;
3707
+ const POSTING_ENTRY_SIZE = 24;
3708
+ const PER_TERM_OVERHEAD = 180;
3709
+ let totalPostings = 0;
3710
+ let termCount = 0;
3711
+ for (const term in docFreqs) {
3712
+ totalPostings += docFreqs[term];
3713
+ termCount++;
3714
+ }
3715
+ bytes += totalPostings * POSTING_ENTRY_SIZE;
3716
+ bytes += termCount * PER_TERM_OVERHEAD;
3717
+ const FIELD_ENTRY_OVERHEAD = 42;
3718
+ bytes += docCount * state.numericIndexes.size * FIELD_ENTRY_OVERHEAD;
3719
+ bytes += docCount * state.booleanIndexes.size * FIELD_ENTRY_OVERHEAD;
3720
+ bytes += docCount * state.enumIndexes.size * FIELD_ENTRY_OVERHEAD;
3721
+ bytes += docCount * state.geoIndexes.size * FIELD_ENTRY_OVERHEAD;
3722
+ for (const store of state.vectorStores.values()) {
3723
+ bytes += store.estimateMemoryBytes();
3724
+ }
3725
+ return bytes;
3726
+ },
3727
+ vectorFieldCount() {
3728
+ return state.vectorStores.size;
3729
+ },
3730
+ hasPromotedHnsw() {
3731
+ for (const store of state.vectorStores.values()) {
3732
+ if (store.isPromoted) return true;
3733
+ }
3734
+ return false;
3735
+ },
2808
3736
  searchFulltext(params) {
2809
3737
  return searchFulltext(state, params);
2810
3738
  },
@@ -2817,9 +3745,16 @@ function createPartitionIndex(partitionId) {
2817
3745
  computeFacets(docIds, config, schema) {
2818
3746
  return computeFacets(state, docIds, config, schema);
2819
3747
  },
3748
+ suggestTerms(prefix, limit) {
3749
+ return state.invertedIdx.prefixSearch(prefix, limit);
3750
+ },
2820
3751
  serialize(indexName, totalPartitions, language, schema) {
2821
3752
  return serializePartition(state, partitionId, indexName, totalPartitions, language, schema);
2822
3753
  },
3754
+ serializeToBytes(indexName, totalPartitions, language, schema) {
3755
+ const wire = serializePartitionToWirePayloadV2(state, partitionId, indexName, totalPartitions, language, schema);
3756
+ return encodeRawPayloadV2(wire);
3757
+ },
2823
3758
  deserialize(data, schema) {
2824
3759
  deserializePartition(state, data, clearAll, schema);
2825
3760
  }
@@ -2830,10 +3765,12 @@ function createPartitionIndex(partitionId) {
2830
3765
  // src/partitioning/manager.ts
2831
3766
  function createPartitionManager(indexName, config, language, router, initialPartitionCount) {
2832
3767
  const count = initialPartitionCount ?? 1;
3768
+ const trackPositions = config.trackPositions ?? true;
3769
+ const vectorPromotionConfig = config.vectorPromotion;
2833
3770
  let partitions = [];
2834
3771
  const docPartitionMap = /* @__PURE__ */ new Map();
2835
3772
  for (let i = 0; i < count; i++) {
2836
- partitions.push(createPartitionIndex(i));
3773
+ partitions.push(createPartitionIndex(i, trackPositions, vectorPromotionConfig));
2837
3774
  }
2838
3775
  function validatePartitionId(partitionId) {
2839
3776
  if (partitionId < 0 || partitionId >= partitions.length) {
@@ -2882,6 +3819,9 @@ function createPartitionManager(indexName, config, language, router, initialPart
2882
3819
  validatePartitionId(partitionId);
2883
3820
  return partitions[partitionId];
2884
3821
  },
3822
+ partitionAt(index) {
3823
+ return partitions[index];
3824
+ },
2885
3825
  getAllPartitions() {
2886
3826
  return [...partitions];
2887
3827
  },
@@ -2890,7 +3830,15 @@ function createPartitionManager(indexName, config, language, router, initialPart
2890
3830
  rebuildDocPartitionMap();
2891
3831
  },
2892
3832
  addPartition() {
2893
- const partition = createPartitionIndex(partitions.length);
3833
+ const currentMaxPartitions = config.partitions?.maxPartitions;
3834
+ if (currentMaxPartitions !== void 0 && partitions.length >= currentMaxPartitions) {
3835
+ throw new NarsilError(
3836
+ ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
3837
+ `Cannot add partition: maximum of ${currentMaxPartitions} partitions reached`,
3838
+ { maxPartitions: currentMaxPartitions, currentCount: partitions.length }
3839
+ );
3840
+ }
3841
+ const partition = createPartitionIndex(partitions.length, trackPositions, vectorPromotionConfig);
2894
3842
  partitions.push(partition);
2895
3843
  return partition;
2896
3844
  },
@@ -2900,8 +3848,26 @@ function createPartitionManager(indexName, config, language, router, initialPart
2900
3848
  rebuildDocPartitionMap();
2901
3849
  },
2902
3850
  insert(docId, document, options) {
3851
+ const currentMaxDocs = config.partitions?.maxDocsPerPartition;
3852
+ if (currentMaxDocs !== void 0) {
3853
+ const totalCapacity = currentMaxDocs * partitions.length;
3854
+ if (docPartitionMap.size >= totalCapacity) {
3855
+ throw new NarsilError(
3856
+ ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
3857
+ `Index "${indexName}" has reached its capacity of ${totalCapacity} documents (${currentMaxDocs} per partition \xD7 ${partitions.length} partitions)`,
3858
+ {
3859
+ indexName,
3860
+ currentCount: docPartitionMap.size,
3861
+ totalCapacity,
3862
+ maxDocsPerPartition: currentMaxDocs,
3863
+ partitionCount: partitions.length
3864
+ }
3865
+ );
3866
+ }
3867
+ }
2903
3868
  const pid = router.route(docId, partitions.length);
2904
- partitions[pid].insert(docId, document, config.schema, language, options);
3869
+ const insertOpts = config.strict ? { ...options, strict: true } : options;
3870
+ partitions[pid].insert(docId, document, config.schema, language, insertOpts);
2905
3871
  docPartitionMap.set(docId, pid);
2906
3872
  },
2907
3873
  remove(docId) {
@@ -2917,13 +3883,19 @@ function createPartitionManager(indexName, config, language, router, initialPart
2917
3883
  if (pid === void 0) {
2918
3884
  throw new NarsilError(ErrorCodes.DOC_NOT_FOUND, `Document "${docId}" not found in any partition`, { docId });
2919
3885
  }
2920
- partitions[pid].update(docId, document, config.schema, language, options);
3886
+ const updateOpts = config.strict ? { ...options, strict: true } : options;
3887
+ partitions[pid].update(docId, document, config.schema, language, updateOpts);
2921
3888
  },
2922
3889
  get(docId) {
2923
3890
  const pid = docPartitionMap.get(docId);
2924
3891
  if (pid === void 0) return void 0;
2925
3892
  return partitions[pid].get(docId);
2926
3893
  },
3894
+ getRef(docId) {
3895
+ const pid = docPartitionMap.get(docId);
3896
+ if (pid === void 0) return void 0;
3897
+ return partitions[pid].getRef(docId);
3898
+ },
2927
3899
  has(docId) {
2928
3900
  return docPartitionMap.has(docId);
2929
3901
  },
@@ -2934,6 +3906,10 @@ function createPartitionManager(indexName, config, language, router, initialPart
2934
3906
  validatePartitionId(partitionId);
2935
3907
  return partitions[partitionId].serialize(indexName, partitions.length, language.name, config.schema);
2936
3908
  },
3909
+ serializePartitionToBytes(partitionId) {
3910
+ validatePartitionId(partitionId);
3911
+ return partitions[partitionId].serializeToBytes(indexName, partitions.length, language.name, config.schema);
3912
+ },
2937
3913
  deserializePartition(partitionId, data) {
2938
3914
  validatePartitionId(partitionId);
2939
3915
  partitions[partitionId].deserialize(data, config.schema);
@@ -2954,6 +3930,22 @@ function createPartitionManager(indexName, config, language, router, initialPart
2954
3930
  }
2955
3931
  }
2956
3932
  return { totalDocuments, docFrequencies, totalFieldLengths };
3933
+ },
3934
+ estimateMemoryBytes() {
3935
+ let total = 0;
3936
+ for (let i = 0; i < partitions.length; i++) {
3937
+ total += partitions[i].estimateMemoryBytes();
3938
+ }
3939
+ return total;
3940
+ },
3941
+ getPartitionStats() {
3942
+ return partitions.map((p, i) => ({
3943
+ partitionId: i,
3944
+ documentCount: p.count(),
3945
+ estimatedMemoryBytes: p.estimateMemoryBytes(),
3946
+ vectorFieldCount: p.vectorFieldCount(),
3947
+ isHnswPromoted: p.hasPromotedHnsw()
3948
+ }));
2957
3949
  }
2958
3950
  };
2959
3951
  return manager;
@@ -3050,7 +4042,7 @@ function createDirectExecutor() {
3050
4042
  }
3051
4043
  case "insert": {
3052
4044
  const entry = requireIndex(action.indexName);
3053
- entry.manager.insert(action.docId, action.document);
4045
+ entry.manager.insert(action.docId, action.document, action.skipClone ? { skipClone: true } : void 0);
3054
4046
  return void 0;
3055
4047
  }
3056
4048
  case "remove": {
@@ -3065,14 +4057,15 @@ function createDirectExecutor() {
3065
4057
  }
3066
4058
  case "query": {
3067
4059
  const entry = requireIndex(action.indexName);
3068
- const result = fanOutQuery(entry.manager, action.params, entry.language, entry.config.schema, {
3069
- scoringMode: entry.config.defaultScoring ?? "local"
4060
+ const result = await fanOutQuery(entry.manager, action.params, entry.language, entry.config.schema, {
4061
+ scoringMode: entry.config.defaultScoring ?? "local",
4062
+ partitionIds: action.partitionIds
3070
4063
  });
3071
4064
  return result;
3072
4065
  }
3073
4066
  case "preflight": {
3074
4067
  const entry = requireIndex(action.indexName);
3075
- const result = fanOutQuery(entry.manager, action.params, entry.language, entry.config.schema, {
4068
+ const result = await fanOutQuery(entry.manager, action.params, entry.language, entry.config.schema, {
3076
4069
  scoringMode: entry.config.defaultScoring ?? "local"
3077
4070
  });
3078
4071
  return { count: result.totalMatched };
@@ -3144,4 +4137,4 @@ function createDirectExecutor() {
3144
4137
  };
3145
4138
  }
3146
4139
 
3147
- export { createDirectExecutor, fanOutQuery, getLanguage, registerLanguage, tokenize, validateSchema };
4140
+ export { createDirectExecutor, createPartitionIndex, createPartitionRouter, deserializePayloadV2, detectRuntime, fanOutQuery, fnv1a, getLanguage, kWayMerge, registerLanguage, tokenize, validateSchema };