@delali/narsil 0.1.1 → 0.1.3

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);
@@ -872,6 +937,283 @@ function heapPop(heap) {
872
937
  return top;
873
938
  }
874
939
 
940
+ // src/runtime/detect.ts
941
+ function detectRuntimeName() {
942
+ if ("Bun" in globalThis) {
943
+ return "bun";
944
+ }
945
+ if ("Deno" in globalThis) {
946
+ return "deno";
947
+ }
948
+ const proc = globalThis.process;
949
+ if (typeof proc === "object" && proc !== null && typeof proc.versions === "object" && typeof proc.versions?.node === "string") {
950
+ return "node";
951
+ }
952
+ return "browser";
953
+ }
954
+ function getCpuCount() {
955
+ const nav = globalThis.navigator;
956
+ if (nav && typeof nav.hardwareConcurrency === "number" && nav.hardwareConcurrency >= 1) {
957
+ return nav.hardwareConcurrency;
958
+ }
959
+ return 1;
960
+ }
961
+ function detectRuntime() {
962
+ const runtime = detectRuntimeName();
963
+ return {
964
+ runtime,
965
+ cpuCount: getCpuCount(),
966
+ supportsWorkerThreads: runtime === "node" || runtime === "bun",
967
+ supportsWebWorkers: runtime === "browser" || runtime === "deno" || runtime === "bun",
968
+ supportsFileSystem: runtime === "node" || runtime === "bun" || runtime === "deno",
969
+ supportsIndexedDB: runtime === "browser" || "indexedDB" in globalThis,
970
+ supportsBroadcastChannel: "BroadcastChannel" in globalThis
971
+ };
972
+ }
973
+
974
+ // src/vector/promoter.ts
975
+ function detectWorkerStrategy() {
976
+ const runtime = detectRuntime();
977
+ const url = import.meta.url;
978
+ const hasBundledWorker = url.endsWith(".mjs") || url.endsWith(".js");
979
+ if (runtime.supportsWorkerThreads && hasBundledWorker) return "worker-threads";
980
+ if (runtime.supportsWebWorkers && hasBundledWorker) return "web-worker";
981
+ return "synchronous";
982
+ }
983
+ function collectVectorsForWorker(engine) {
984
+ const vectors = [];
985
+ for (const [, entry] of engine.entries()) {
986
+ vectors.push({ docId: entry.docId, values: Array.from(entry.vector) });
987
+ }
988
+ return vectors;
989
+ }
990
+ function buildWorkerRequest(engine, hnswConfig) {
991
+ return {
992
+ type: "build",
993
+ vectors: collectVectorsForWorker(engine),
994
+ dimension: engine.dimension,
995
+ config: hnswConfig ?? {}
996
+ };
997
+ }
998
+ function resolveWorkerUrl() {
999
+ const base = import.meta.url;
1000
+ if (base.endsWith(".mjs") || base.endsWith(".js")) {
1001
+ const distIndex = base.lastIndexOf("/dist/");
1002
+ if (distIndex !== -1) {
1003
+ return new URL("vector/hnsw-build-worker.mjs", base.slice(0, distIndex + 6)).href;
1004
+ }
1005
+ return new URL("./hnsw-build-worker.mjs", base).href;
1006
+ }
1007
+ return base.replace(/\/src\/vector\/[^/]+$/, "/dist/vector/hnsw-build-worker.mjs");
1008
+ }
1009
+ function spawnNodeWorker(request, onComplete, onError) {
1010
+ const entryUrl = resolveWorkerUrl();
1011
+ let worker = null;
1012
+ import('worker_threads').then((wt) => {
1013
+ worker = new wt.Worker(new URL(entryUrl));
1014
+ worker.on("message", (msg) => {
1015
+ const response = msg;
1016
+ worker?.terminate();
1017
+ if (response.type === "success") {
1018
+ onComplete(response.graph);
1019
+ } else {
1020
+ onError(new Error(response.message));
1021
+ }
1022
+ });
1023
+ worker.on("error", (err) => {
1024
+ worker?.terminate();
1025
+ onError(err instanceof Error ? err : new Error(String(err)));
1026
+ });
1027
+ worker.postMessage(request);
1028
+ }).catch(onError);
1029
+ return {
1030
+ terminate() {
1031
+ worker?.terminate();
1032
+ }
1033
+ };
1034
+ }
1035
+ function spawnWebWorker(request, onComplete, onError) {
1036
+ const entryUrl = resolveWorkerUrl();
1037
+ const WorkerCtor = globalThis.Worker;
1038
+ const worker = new WorkerCtor(entryUrl, { type: "module" });
1039
+ worker.onmessage = (event) => {
1040
+ const response = event.data;
1041
+ worker.terminate();
1042
+ if (response.type === "success") {
1043
+ onComplete(response.graph);
1044
+ } else {
1045
+ onError(new Error(response.message));
1046
+ }
1047
+ };
1048
+ worker.onerror = (event) => {
1049
+ worker.terminate();
1050
+ onError(event.error ?? new Error(event.message ?? "Web Worker error"));
1051
+ };
1052
+ worker.postMessage(request);
1053
+ return {
1054
+ terminate() {
1055
+ worker.terminate();
1056
+ }
1057
+ };
1058
+ }
1059
+ function createVectorPromoter(config) {
1060
+ const threshold = config?.promotionThreshold ?? 1e4;
1061
+ const hnswConfig = config?.hnswConfig;
1062
+ const promoting = /* @__PURE__ */ new Set();
1063
+ const strategy = config?.workerStrategy ?? detectWorkerStrategy();
1064
+ const pendingWorkers = /* @__PURE__ */ new Set();
1065
+ return {
1066
+ get strategy() {
1067
+ return strategy;
1068
+ },
1069
+ check(engines) {
1070
+ for (const [field, engine] of engines) {
1071
+ if (engine.isPromoted || promoting.has(field)) continue;
1072
+ if (engine.size < threshold) continue;
1073
+ promoting.add(field);
1074
+ if (strategy === "worker-threads") {
1075
+ const request = buildWorkerRequest(engine, hnswConfig);
1076
+ const handle = spawnNodeWorker(
1077
+ request,
1078
+ (graph) => {
1079
+ pendingWorkers.delete(handle);
1080
+ if (!engine.isPromoted) {
1081
+ engine.deserializeHNSW(graph);
1082
+ }
1083
+ promoting.delete(field);
1084
+ },
1085
+ () => {
1086
+ pendingWorkers.delete(handle);
1087
+ promoting.delete(field);
1088
+ }
1089
+ );
1090
+ pendingWorkers.add(handle);
1091
+ } else if (strategy === "web-worker") {
1092
+ const request = buildWorkerRequest(engine, hnswConfig);
1093
+ const handle = spawnWebWorker(
1094
+ request,
1095
+ (graph) => {
1096
+ pendingWorkers.delete(handle);
1097
+ if (!engine.isPromoted) {
1098
+ engine.deserializeHNSW(graph);
1099
+ }
1100
+ promoting.delete(field);
1101
+ },
1102
+ () => {
1103
+ pendingWorkers.delete(handle);
1104
+ promoting.delete(field);
1105
+ }
1106
+ );
1107
+ pendingWorkers.add(handle);
1108
+ } else {
1109
+ try {
1110
+ engine.promoteToHNSW(hnswConfig);
1111
+ } finally {
1112
+ promoting.delete(field);
1113
+ }
1114
+ }
1115
+ }
1116
+ },
1117
+ shutdown() {
1118
+ for (const handle of pendingWorkers) {
1119
+ handle.terminate();
1120
+ }
1121
+ pendingWorkers.clear();
1122
+ promoting.clear();
1123
+ }
1124
+ };
1125
+ }
1126
+ function encodeRawPayloadV2(wire) {
1127
+ return encode(wire);
1128
+ }
1129
+ var VALID_HNSW_METRICS = /* @__PURE__ */ new Set(["cosine", "dotProduct", "euclidean"]);
1130
+ function validateHnswMetric(value) {
1131
+ if (value === void 0 || value === null) return void 0;
1132
+ if (typeof value === "string" && VALID_HNSW_METRICS.has(value)) {
1133
+ return value;
1134
+ }
1135
+ return void 0;
1136
+ }
1137
+ function deserializePayloadV2(data) {
1138
+ const raw = decode(data);
1139
+ const documents = {};
1140
+ for (const [docId, doc] of Object.entries(raw.documents ?? {})) {
1141
+ documents[docId] = {
1142
+ fields: doc.fields,
1143
+ fieldLengths: doc.field_lengths ?? {}
1144
+ };
1145
+ }
1146
+ const fieldNames = raw.inverted_index?.field_names ?? [];
1147
+ const invertedIndex = {};
1148
+ for (const [token, list] of Object.entries(raw.inverted_index?.entries ?? {})) {
1149
+ const count = list.ids.length;
1150
+ const postings = new Array(count);
1151
+ for (let i = 0; i < count; i++) {
1152
+ postings[i] = {
1153
+ docId: list.ids[i],
1154
+ termFrequency: list.tf[i],
1155
+ field: fieldNames[list.fi[i]] ?? "",
1156
+ positions: list.pos ? list.pos[i] : []
1157
+ };
1158
+ }
1159
+ invertedIndex[token] = {
1160
+ docFrequency: list.df,
1161
+ postings
1162
+ };
1163
+ }
1164
+ const numeric = {};
1165
+ for (const [field, entries] of Object.entries(raw.field_indexes?.numeric ?? {})) {
1166
+ numeric[field] = entries.map((e) => ({ value: e.value, docId: e.doc_id }));
1167
+ }
1168
+ const boolean = {};
1169
+ for (const [field, idx] of Object.entries(raw.field_indexes?.boolean ?? {})) {
1170
+ boolean[field] = { trueDocs: idx.true_docs ?? [], falseDocs: idx.false_docs ?? [] };
1171
+ }
1172
+ const geopoint = {};
1173
+ for (const [field, entries] of Object.entries(raw.field_indexes?.geopoint ?? {})) {
1174
+ geopoint[field] = entries.map((e) => ({ lat: e.lat, lon: e.lon, docId: e.doc_id }));
1175
+ }
1176
+ const vectorData = {};
1177
+ for (const [field, data2] of Object.entries(raw.vector_data ?? {})) {
1178
+ vectorData[field] = {
1179
+ dimension: data2.dimension,
1180
+ vectors: (data2.vectors ?? []).map((v) => ({ docId: v.doc_id, vector: v.vector })),
1181
+ hnswGraph: data2.hnsw_graph ? {
1182
+ entryPoint: data2.hnsw_graph.entry_point,
1183
+ maxLayer: data2.hnsw_graph.max_layer,
1184
+ m: data2.hnsw_graph.m,
1185
+ efConstruction: data2.hnsw_graph.ef_construction,
1186
+ metric: validateHnswMetric(data2.hnsw_graph.metric),
1187
+ nodes: data2.hnsw_graph.nodes
1188
+ } : null
1189
+ };
1190
+ }
1191
+ return {
1192
+ indexName: raw.index_name,
1193
+ partitionId: raw.partition_id,
1194
+ totalPartitions: raw.total_partitions,
1195
+ language: raw.language ?? "english",
1196
+ schema: raw.schema ?? {},
1197
+ docCount: raw.doc_count ?? 0,
1198
+ avgDocLength: raw.avg_doc_length ?? 0,
1199
+ documents,
1200
+ invertedIndex,
1201
+ fieldIndexes: {
1202
+ numeric,
1203
+ boolean,
1204
+ enum: raw.field_indexes?.enum ?? {},
1205
+ geopoint
1206
+ },
1207
+ vectorData,
1208
+ statistics: {
1209
+ totalDocuments: raw.statistics?.total_documents ?? 0,
1210
+ totalFieldLengths: raw.statistics?.total_field_lengths ?? {},
1211
+ averageFieldLengths: raw.statistics?.average_field_lengths ?? {},
1212
+ docFrequencies: raw.statistics?.doc_frequencies ?? {}
1213
+ }
1214
+ };
1215
+ }
1216
+
875
1217
  // src/core/document-store.ts
876
1218
  function createDocumentStore() {
877
1219
  const docs = /* @__PURE__ */ new Map();
@@ -879,6 +1221,9 @@ function createDocumentStore() {
879
1221
  store(docId, document, fieldLengths) {
880
1222
  docs.set(docId, { fields: structuredClone(document), fieldLengths });
881
1223
  },
1224
+ storeRef(docId, document, fieldLengths) {
1225
+ docs.set(docId, { fields: document, fieldLengths });
1226
+ },
882
1227
  get(docId) {
883
1228
  return docs.get(docId);
884
1229
  },
@@ -914,7 +1259,10 @@ function createDocumentStore() {
914
1259
  }
915
1260
 
916
1261
  // src/core/inverted-index.ts
917
- function createInvertedIndex() {
1262
+ var INITIAL_CAPACITY = 8;
1263
+ var MAX_TERM_FREQUENCY = 65535;
1264
+ var COMPACTION_THRESHOLD = 0.3;
1265
+ function createInvertedIndex(fieldNameTable) {
918
1266
  const index = /* @__PURE__ */ new Map();
919
1267
  const charBuckets = /* @__PURE__ */ new Map();
920
1268
  function trackToken(token) {
@@ -953,33 +1301,112 @@ function createInvertedIndex() {
953
1301
  }
954
1302
  return filtered;
955
1303
  }
1304
+ function getOrCreateList(token) {
1305
+ let list = index.get(token);
1306
+ if (!list) {
1307
+ list = {
1308
+ length: 0,
1309
+ docIds: [],
1310
+ termFrequencies: new Uint16Array(INITIAL_CAPACITY),
1311
+ fieldNameIndices: new Uint8Array(INITIAL_CAPACITY),
1312
+ positions: null,
1313
+ docIdSet: /* @__PURE__ */ new Set(),
1314
+ deletedDocs: /* @__PURE__ */ new Set()
1315
+ };
1316
+ index.set(token, list);
1317
+ trackToken(token);
1318
+ }
1319
+ return list;
1320
+ }
1321
+ function growTypedArrays(list) {
1322
+ const newCap = list.termFrequencies.length * 2;
1323
+ const newTF = new Uint16Array(newCap);
1324
+ newTF.set(list.termFrequencies);
1325
+ list.termFrequencies = newTF;
1326
+ const newFNI = new Uint8Array(newCap);
1327
+ newFNI.set(list.fieldNameIndices);
1328
+ list.fieldNameIndices = newFNI;
1329
+ }
1330
+ function compactList(list) {
1331
+ if (list.deletedDocs.size === 0) return;
1332
+ let writeIdx = 0;
1333
+ for (let i = 0; i < list.length; i++) {
1334
+ if (!list.deletedDocs.has(list.docIds[i])) {
1335
+ if (writeIdx !== i) {
1336
+ list.docIds[writeIdx] = list.docIds[i];
1337
+ list.termFrequencies[writeIdx] = list.termFrequencies[i];
1338
+ list.fieldNameIndices[writeIdx] = list.fieldNameIndices[i];
1339
+ if (list.positions) {
1340
+ list.positions[writeIdx] = list.positions[i];
1341
+ }
1342
+ }
1343
+ writeIdx++;
1344
+ }
1345
+ }
1346
+ list.docIds.length = writeIdx;
1347
+ if (list.positions) list.positions.length = writeIdx;
1348
+ list.length = writeIdx;
1349
+ list.deletedDocs.clear();
1350
+ }
1351
+ function compactDocEntries(list, docId) {
1352
+ let writeIdx = 0;
1353
+ for (let i = 0; i < list.length; i++) {
1354
+ if (list.docIds[i] !== docId) {
1355
+ if (writeIdx !== i) {
1356
+ list.docIds[writeIdx] = list.docIds[i];
1357
+ list.termFrequencies[writeIdx] = list.termFrequencies[i];
1358
+ list.fieldNameIndices[writeIdx] = list.fieldNameIndices[i];
1359
+ if (list.positions) {
1360
+ list.positions[writeIdx] = list.positions[i];
1361
+ }
1362
+ }
1363
+ writeIdx++;
1364
+ }
1365
+ }
1366
+ list.docIds.length = writeIdx;
1367
+ if (list.positions) list.positions.length = writeIdx;
1368
+ list.length = writeIdx;
1369
+ }
956
1370
  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);
1371
+ insert(token, docId, termFrequency, fieldNameIndex, positions) {
1372
+ const list = getOrCreateList(token);
1373
+ if (list.deletedDocs.size > 0 && list.deletedDocs.has(docId)) {
1374
+ compactDocEntries(list, docId);
1375
+ list.deletedDocs.delete(docId);
963
1376
  }
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;
1377
+ if (list.length >= list.termFrequencies.length) {
1378
+ growTypedArrays(list);
1379
+ }
1380
+ const idx = list.length;
1381
+ list.docIds.push(docId);
1382
+ list.termFrequencies[idx] = termFrequency > MAX_TERM_FREQUENCY ? MAX_TERM_FREQUENCY : termFrequency;
1383
+ list.fieldNameIndices[idx] = fieldNameIndex;
1384
+ if (positions !== null) {
1385
+ if (!list.positions) {
1386
+ list.positions = [];
1387
+ for (let j = 0; j < idx; j++) list.positions.push([]);
1388
+ }
1389
+ list.positions.push(positions);
1390
+ } else if (list.positions !== null) {
1391
+ list.positions.push([]);
1392
+ }
1393
+ if (!list.docIdSet.has(docId)) {
1394
+ list.docIdSet.add(docId);
968
1395
  }
969
- const isNewDoc = !list.postings.some((p) => p.docId === entry.docId);
970
- list.postings.push(entry);
971
- if (isNewDoc) list.docFrequency++;
1396
+ list.length++;
972
1397
  },
973
1398
  remove(token, docId) {
974
1399
  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) {
1400
+ if (!list || !list.docIdSet.has(docId)) return;
1401
+ list.docIdSet.delete(docId);
1402
+ list.deletedDocs.add(docId);
1403
+ if (list.docIdSet.size === 0) {
981
1404
  index.delete(token);
982
1405
  untrackToken(token);
1406
+ return;
1407
+ }
1408
+ if (list.deletedDocs.size / list.length > COMPACTION_THRESHOLD) {
1409
+ compactList(list);
983
1410
  }
984
1411
  },
985
1412
  lookup(token) {
@@ -1001,6 +1428,20 @@ function createInvertedIndex() {
1001
1428
  }
1002
1429
  return results;
1003
1430
  },
1431
+ prefixSearch(prefix, limit) {
1432
+ if (prefix.length === 0 || limit <= 0) return [];
1433
+ const candidates = candidatesForPrefix(prefix, prefix.length);
1434
+ const results = [];
1435
+ for (const term of candidates) {
1436
+ if (!term.startsWith(prefix)) continue;
1437
+ const list = index.get(term);
1438
+ if (!list) continue;
1439
+ results.push({ term, documentFrequency: list.docIdSet.size });
1440
+ }
1441
+ results.sort((a, b) => b.documentFrequency - a.documentFrequency);
1442
+ if (results.length > limit) results.length = limit;
1443
+ return results;
1444
+ },
1004
1445
  has(token) {
1005
1446
  return index.has(token);
1006
1447
  },
@@ -1017,7 +1458,19 @@ function createInvertedIndex() {
1017
1458
  serialize() {
1018
1459
  const result = /* @__PURE__ */ Object.create(null);
1019
1460
  for (const [token, list] of index) {
1020
- result[token] = list;
1461
+ const postings = [];
1462
+ for (let i = 0; i < list.length; i++) {
1463
+ if (list.deletedDocs.size > 0 && list.deletedDocs.has(list.docIds[i])) continue;
1464
+ postings.push({
1465
+ docId: list.docIds[i],
1466
+ termFrequency: list.termFrequencies[i],
1467
+ fieldName: fieldNameTable.names[list.fieldNameIndices[i]],
1468
+ positions: list.positions ? list.positions[i] : []
1469
+ });
1470
+ }
1471
+ if (postings.length > 0) {
1472
+ result[token] = { docFrequency: list.docIdSet.size, postings };
1473
+ }
1021
1474
  }
1022
1475
  return result;
1023
1476
  },
@@ -1025,7 +1478,43 @@ function createInvertedIndex() {
1025
1478
  index.clear();
1026
1479
  charBuckets.clear();
1027
1480
  for (const token of Object.keys(data)) {
1028
- index.set(token, data[token]);
1481
+ const src = data[token];
1482
+ const count = src.postings.length;
1483
+ const docIdSet = /* @__PURE__ */ new Set();
1484
+ const docIds = new Array(count);
1485
+ const termFrequencies = new Uint16Array(count);
1486
+ const fieldNameIndices = new Uint8Array(count);
1487
+ let hasPositions = false;
1488
+ for (let i = 0; i < count; i++) {
1489
+ const p = src.postings[i];
1490
+ docIds[i] = p.docId;
1491
+ termFrequencies[i] = p.termFrequency > MAX_TERM_FREQUENCY ? MAX_TERM_FREQUENCY : p.termFrequency;
1492
+ let fnIndex = fieldNameTable.indexMap.get(p.fieldName);
1493
+ if (fnIndex === void 0) {
1494
+ fnIndex = fieldNameTable.names.length;
1495
+ fieldNameTable.names.push(p.fieldName);
1496
+ fieldNameTable.indexMap.set(p.fieldName, fnIndex);
1497
+ }
1498
+ fieldNameIndices[i] = fnIndex;
1499
+ docIdSet.add(p.docId);
1500
+ if (p.positions.length > 0) hasPositions = true;
1501
+ }
1502
+ let positions = null;
1503
+ if (hasPositions) {
1504
+ positions = new Array(count);
1505
+ for (let i = 0; i < count; i++) {
1506
+ positions[i] = src.postings[i].positions;
1507
+ }
1508
+ }
1509
+ index.set(token, {
1510
+ length: count,
1511
+ docIds,
1512
+ termFrequencies,
1513
+ fieldNameIndices,
1514
+ positions,
1515
+ docIdSet,
1516
+ deletedDocs: /* @__PURE__ */ new Set()
1517
+ });
1029
1518
  trackToken(token);
1030
1519
  }
1031
1520
  }
@@ -1047,9 +1536,9 @@ function collectUniqueTokens(tokens) {
1047
1536
  function createPartitionStats() {
1048
1537
  const stats = {
1049
1538
  totalDocuments: 0,
1050
- totalFieldLengths: {},
1051
- averageFieldLengths: {},
1052
- docFrequencies: {},
1539
+ totalFieldLengths: /* @__PURE__ */ Object.create(null),
1540
+ averageFieldLengths: /* @__PURE__ */ Object.create(null),
1541
+ docFrequencies: /* @__PURE__ */ Object.create(null),
1053
1542
  addDocument(fieldLengths, tokens) {
1054
1543
  stats.totalDocuments++;
1055
1544
  const fields = Object.keys(fieldLengths);
@@ -1089,7 +1578,7 @@ function createPartitionStats() {
1089
1578
  },
1090
1579
  recalculateAverages() {
1091
1580
  const fields = Object.keys(stats.totalFieldLengths);
1092
- const newAverages = {};
1581
+ const newAverages = /* @__PURE__ */ Object.create(null);
1093
1582
  for (let i = 0; i < fields.length; i++) {
1094
1583
  const field = fields[i];
1095
1584
  newAverages[field] = stats.totalDocuments > 0 ? stats.totalFieldLengths[field] / stats.totalDocuments : 0;
@@ -1097,18 +1586,42 @@ function createPartitionStats() {
1097
1586
  stats.averageFieldLengths = newAverages;
1098
1587
  },
1099
1588
  serialize() {
1589
+ const tfl = /* @__PURE__ */ Object.create(null);
1590
+ for (const key of Object.keys(stats.totalFieldLengths)) {
1591
+ tfl[key] = stats.totalFieldLengths[key];
1592
+ }
1593
+ const afl = /* @__PURE__ */ Object.create(null);
1594
+ for (const key of Object.keys(stats.averageFieldLengths)) {
1595
+ afl[key] = stats.averageFieldLengths[key];
1596
+ }
1597
+ const df = /* @__PURE__ */ Object.create(null);
1598
+ for (const key of Object.keys(stats.docFrequencies)) {
1599
+ df[key] = stats.docFrequencies[key];
1600
+ }
1100
1601
  return {
1101
1602
  totalDocuments: stats.totalDocuments,
1102
- totalFieldLengths: { ...stats.totalFieldLengths },
1103
- averageFieldLengths: { ...stats.averageFieldLengths },
1104
- docFrequencies: { ...stats.docFrequencies }
1603
+ totalFieldLengths: tfl,
1604
+ averageFieldLengths: afl,
1605
+ docFrequencies: df
1105
1606
  };
1106
1607
  },
1107
1608
  deserialize(data) {
1108
1609
  stats.totalDocuments = data.totalDocuments;
1109
- stats.totalFieldLengths = { ...data.totalFieldLengths };
1110
- stats.averageFieldLengths = { ...data.averageFieldLengths };
1111
- stats.docFrequencies = { ...data.docFrequencies };
1610
+ const tfl = /* @__PURE__ */ Object.create(null);
1611
+ for (const key of Object.keys(data.totalFieldLengths)) {
1612
+ tfl[key] = data.totalFieldLengths[key];
1613
+ }
1614
+ stats.totalFieldLengths = tfl;
1615
+ const afl = /* @__PURE__ */ Object.create(null);
1616
+ for (const key of Object.keys(data.averageFieldLengths)) {
1617
+ afl[key] = data.averageFieldLengths[key];
1618
+ }
1619
+ stats.averageFieldLengths = afl;
1620
+ const df = /* @__PURE__ */ Object.create(null);
1621
+ for (const key of Object.keys(data.docFrequencies)) {
1622
+ df[key] = data.docFrequencies[key];
1623
+ }
1624
+ stats.docFrequencies = df;
1112
1625
  }
1113
1626
  };
1114
1627
  return stats;
@@ -1259,34 +1772,6 @@ function createGeoIndex() {
1259
1772
  };
1260
1773
  }
1261
1774
 
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
1775
  // src/vector/brute-force.ts
1291
1776
  function createBruteForceVectorStore(dimension) {
1292
1777
  const vectors = /* @__PURE__ */ new Map();
@@ -1355,6 +1840,120 @@ function createBruteForceVectorStore(dimension) {
1355
1840
  };
1356
1841
  }
1357
1842
 
1843
+ // src/search/vector-search.ts
1844
+ function createVectorSearchEngine(dimension, defaultHNSWConfig) {
1845
+ const bruteForce = createBruteForceVectorStore(dimension);
1846
+ const hnswConfig = defaultHNSWConfig;
1847
+ let hnsw = null;
1848
+ return {
1849
+ get dimension() {
1850
+ return dimension;
1851
+ },
1852
+ get size() {
1853
+ return bruteForce.size;
1854
+ },
1855
+ get isPromoted() {
1856
+ return hnsw !== null;
1857
+ },
1858
+ insert(docId, vector) {
1859
+ bruteForce.insert(docId, vector);
1860
+ if (hnsw) {
1861
+ hnsw.insert(docId, vector);
1862
+ }
1863
+ },
1864
+ remove(docId) {
1865
+ bruteForce.remove(docId);
1866
+ if (hnsw) {
1867
+ hnsw.remove(docId);
1868
+ }
1869
+ },
1870
+ has(docId) {
1871
+ return bruteForce.has(docId);
1872
+ },
1873
+ search(query, k, metric, minSimilarity, filterDocIds, efSearch) {
1874
+ if (hnsw) {
1875
+ return hnsw.search(query, k, metric, minSimilarity, filterDocIds, efSearch);
1876
+ }
1877
+ return bruteForce.search(query, k, metric, minSimilarity, filterDocIds);
1878
+ },
1879
+ clear() {
1880
+ bruteForce.clear();
1881
+ if (hnsw) {
1882
+ hnsw.clear();
1883
+ hnsw = null;
1884
+ }
1885
+ },
1886
+ entries() {
1887
+ return bruteForce.entries();
1888
+ },
1889
+ promoteToHNSW(overrideConfig) {
1890
+ const cfg = overrideConfig ?? hnswConfig;
1891
+ const newHnsw = createHNSWIndex(dimension, cfg);
1892
+ for (const [, entry] of bruteForce.entries()) {
1893
+ newHnsw.insert(entry.docId, entry.vector);
1894
+ }
1895
+ hnsw = newHnsw;
1896
+ },
1897
+ demoteToLinear() {
1898
+ if (hnsw) {
1899
+ hnsw.clear();
1900
+ hnsw = null;
1901
+ }
1902
+ },
1903
+ getHNSWIndex() {
1904
+ return hnsw;
1905
+ },
1906
+ getBruteForceStore() {
1907
+ return bruteForce;
1908
+ },
1909
+ estimateMemoryBytes() {
1910
+ const count = bruteForce.size;
1911
+ if (count === 0) return 0;
1912
+ const MAP_OVERHEAD = 64;
1913
+ const MAP_ENTRY = 72;
1914
+ const VECTOR_ENTRY_OBJ = 32;
1915
+ const AVG_DOCID_BYTES = 56;
1916
+ const TYPED_ARRAY_HEADER = 64;
1917
+ const MAGNITUDE_BYTES = 8;
1918
+ const perBruteForceEntry = MAP_ENTRY + VECTOR_ENTRY_OBJ + AVG_DOCID_BYTES + TYPED_ARRAY_HEADER + MAGNITUDE_BYTES;
1919
+ let bytes = MAP_OVERHEAD + count * (perBruteForceEntry + dimension * 4);
1920
+ if (hnsw) {
1921
+ const HNSW_NODE_OBJ = 64;
1922
+ const SHARED_REFS = 16;
1923
+ const CONN_ARRAY_HEADER = 32;
1924
+ const SET_OVERHEAD = 64;
1925
+ const SET_ENTRY_COST = 72;
1926
+ const m = hnsw.m;
1927
+ const avgLayers = m / (m - 1);
1928
+ const avgConnsLayer0 = m;
1929
+ const avgConnsUpper = Math.ceil(m / 2);
1930
+ const connMemPerNode = CONN_ARRAY_HEADER + (SET_OVERHEAD + avgConnsLayer0 * SET_ENTRY_COST) + Math.max(0, avgLayers - 1) * (SET_OVERHEAD + avgConnsUpper * SET_ENTRY_COST);
1931
+ const perHnswNode = MAP_ENTRY + HNSW_NODE_OBJ + SHARED_REFS + connMemPerNode;
1932
+ bytes += MAP_OVERHEAD + count * perHnswNode;
1933
+ }
1934
+ return Math.round(bytes);
1935
+ },
1936
+ serializeHNSW() {
1937
+ if (!hnsw) return null;
1938
+ return hnsw.serialize();
1939
+ },
1940
+ deserializeHNSW(data) {
1941
+ const cfg = {};
1942
+ const restoredHnsw = createHNSWIndex(dimension, {
1943
+ m: data.m ?? cfg.m,
1944
+ efConstruction: data.efConstruction ?? cfg.efConstruction,
1945
+ metric: data.metric ?? cfg.metric
1946
+ });
1947
+ const vectorMap = /* @__PURE__ */ new Map();
1948
+ for (const [, entry] of bruteForce.entries()) {
1949
+ vectorMap.set(entry.docId, { vector: entry.vector, mag: entry.magnitude });
1950
+ }
1951
+ restoredHnsw.deserialize(data, vectorMap);
1952
+ hnsw = restoredHnsw;
1953
+ }
1954
+ };
1955
+ }
1956
+
1358
1957
  // src/core/field-index.ts
1359
1958
  function lowerBound(entries, value) {
1360
1959
  let lo = 0;
@@ -1385,12 +1984,20 @@ function collectDocIds(entries, from, to) {
1385
1984
  }
1386
1985
  function createNumericIndex() {
1387
1986
  let entries = [];
1987
+ let sorted = true;
1988
+ function ensureSorted() {
1989
+ if (!sorted) {
1990
+ entries.sort((a, b) => a.value - b.value);
1991
+ sorted = true;
1992
+ }
1993
+ }
1388
1994
  return {
1389
1995
  insert(docId, value) {
1390
- const pos = upperBound(entries, value);
1391
- entries.splice(pos, 0, { value, docId });
1996
+ entries.push({ value, docId });
1997
+ sorted = false;
1392
1998
  },
1393
1999
  remove(docId, value) {
2000
+ ensureSorted();
1394
2001
  const start = lowerBound(entries, value);
1395
2002
  const end = upperBound(entries, value);
1396
2003
  for (let i = start; i < end; i++) {
@@ -1401,9 +2008,11 @@ function createNumericIndex() {
1401
2008
  }
1402
2009
  },
1403
2010
  queryEq(value) {
2011
+ ensureSorted();
1404
2012
  return collectDocIds(entries, lowerBound(entries, value), upperBound(entries, value));
1405
2013
  },
1406
2014
  queryNe(value) {
2015
+ ensureSorted();
1407
2016
  const excluded = this.queryEq(value);
1408
2017
  const result = /* @__PURE__ */ new Set();
1409
2018
  for (const entry of entries) {
@@ -1412,18 +2021,23 @@ function createNumericIndex() {
1412
2021
  return result;
1413
2022
  },
1414
2023
  queryGt(value) {
2024
+ ensureSorted();
1415
2025
  return collectDocIds(entries, upperBound(entries, value), entries.length);
1416
2026
  },
1417
2027
  queryGte(value) {
2028
+ ensureSorted();
1418
2029
  return collectDocIds(entries, lowerBound(entries, value), entries.length);
1419
2030
  },
1420
2031
  queryLt(value) {
2032
+ ensureSorted();
1421
2033
  return collectDocIds(entries, 0, lowerBound(entries, value));
1422
2034
  },
1423
2035
  queryLte(value) {
2036
+ ensureSorted();
1424
2037
  return collectDocIds(entries, 0, upperBound(entries, value));
1425
2038
  },
1426
2039
  queryBetween(min, max) {
2040
+ ensureSorted();
1427
2041
  return collectDocIds(entries, lowerBound(entries, min), upperBound(entries, max));
1428
2042
  },
1429
2043
  getAllDocIds() {
@@ -1434,12 +2048,15 @@ function createNumericIndex() {
1434
2048
  },
1435
2049
  clear() {
1436
2050
  entries = [];
2051
+ sorted = true;
1437
2052
  },
1438
2053
  serialize() {
2054
+ ensureSorted();
1439
2055
  return entries.slice();
1440
2056
  },
1441
2057
  deserialize(data) {
1442
2058
  entries = data.slice();
2059
+ sorted = true;
1443
2060
  }
1444
2061
  };
1445
2062
  }
@@ -1567,6 +2184,14 @@ function createEnumIndex() {
1567
2184
  }
1568
2185
 
1569
2186
  // src/core/partition/utils.ts
2187
+ function getOrCreateFieldNameIndex(table, fieldName) {
2188
+ const existing = table.indexMap.get(fieldName);
2189
+ if (existing !== void 0) return existing;
2190
+ const idx = table.names.length;
2191
+ table.names.push(fieldName);
2192
+ table.indexMap.set(fieldName, idx);
2193
+ return idx;
2194
+ }
1570
2195
  var VECTOR_PATTERN2 = /^vector\[(\d+)]$/;
1571
2196
  function getNestedValue(obj, path) {
1572
2197
  const parts = path.split(".");
@@ -1643,7 +2268,7 @@ function ensureFieldIndex(state, fieldPath, fieldType) {
1643
2268
  } else {
1644
2269
  const dim = parseVectorDimension(fieldType);
1645
2270
  if (dim !== null && !state.vectorStores.has(fieldPath)) {
1646
- state.vectorStores.set(fieldPath, createBruteForceVectorStore(dim));
2271
+ state.vectorStores.set(fieldPath, createVectorSearchEngine(dim));
1647
2272
  }
1648
2273
  }
1649
2274
  }
@@ -1667,53 +2292,71 @@ function indexStringField(state, docId, fieldPath, text, language, options, fiel
1667
2292
  const result = tokenize(text, language, tokenizeOptions(options));
1668
2293
  fieldLengths[fieldPath] = result.tokens.length;
1669
2294
  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));
2295
+ const fieldNameIndex = getOrCreateFieldNameIndex(state.fieldNameTable, fieldPath);
2296
+ if (state.trackPositions) {
2297
+ const tokenFreqs = /* @__PURE__ */ new Map();
1697
2298
  for (const t of result.tokens) {
1698
2299
  const existing = tokenFreqs.get(t.token);
1699
2300
  if (existing) {
1700
2301
  existing.count++;
1701
- existing.positions.push(positionOffset + t.position);
2302
+ existing.positions.push(t.position);
1702
2303
  } else {
1703
- tokenFreqs.set(t.token, { count: 1, positions: [positionOffset + t.position] });
2304
+ tokenFreqs.set(t.token, { count: 1, positions: [t.position] });
1704
2305
  }
1705
2306
  fieldTokenList.push(t.token);
1706
2307
  }
1707
- positionOffset += result.tokens.length;
2308
+ for (const [token, freq] of tokenFreqs) {
2309
+ state.invertedIdx.insert(token, docId, freq.count, fieldNameIndex, freq.positions);
2310
+ }
2311
+ } else {
2312
+ const tokenCounts = /* @__PURE__ */ new Map();
2313
+ for (const t of result.tokens) {
2314
+ tokenCounts.set(t.token, (tokenCounts.get(t.token) ?? 0) + 1);
2315
+ fieldTokenList.push(t.token);
2316
+ }
2317
+ for (const [token, count] of tokenCounts) {
2318
+ state.invertedIdx.insert(token, docId, count, fieldNameIndex, null);
2319
+ }
1708
2320
  }
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
- });
2321
+ tokensByField[fieldPath] = fieldTokenList;
2322
+ }
2323
+ function indexStringArrayField(state, docId, fieldPath, arr, language, options, fieldLengths, tokensByField) {
2324
+ const fieldTokenList = [];
2325
+ const fieldNameIndex = getOrCreateFieldNameIndex(state.fieldNameTable, fieldPath);
2326
+ if (state.trackPositions) {
2327
+ const tokenFreqs = /* @__PURE__ */ new Map();
2328
+ let positionOffset = 0;
2329
+ for (const item of arr) {
2330
+ const result = tokenize(item, language, tokenizeOptions(options));
2331
+ for (const t of result.tokens) {
2332
+ const existing = tokenFreqs.get(t.token);
2333
+ if (existing) {
2334
+ existing.count++;
2335
+ existing.positions.push(positionOffset + t.position);
2336
+ } else {
2337
+ tokenFreqs.set(t.token, { count: 1, positions: [positionOffset + t.position] });
2338
+ }
2339
+ fieldTokenList.push(t.token);
2340
+ }
2341
+ positionOffset += result.tokens.length;
2342
+ }
2343
+ fieldLengths[fieldPath] = fieldTokenList.length;
2344
+ for (const [token, freq] of tokenFreqs) {
2345
+ state.invertedIdx.insert(token, docId, freq.count, fieldNameIndex, freq.positions);
2346
+ }
2347
+ } else {
2348
+ const tokenCounts = /* @__PURE__ */ new Map();
2349
+ for (const item of arr) {
2350
+ const result = tokenize(item, language, tokenizeOptions(options));
2351
+ for (const t of result.tokens) {
2352
+ tokenCounts.set(t.token, (tokenCounts.get(t.token) ?? 0) + 1);
2353
+ fieldTokenList.push(t.token);
2354
+ }
2355
+ }
2356
+ fieldLengths[fieldPath] = fieldTokenList.length;
2357
+ for (const [token, count] of tokenCounts) {
2358
+ state.invertedIdx.insert(token, docId, count, fieldNameIndex, null);
2359
+ }
1717
2360
  }
1718
2361
  tokensByField[fieldPath] = fieldTokenList;
1719
2362
  }
@@ -2332,66 +2975,188 @@ function computeBM25WithGlobalStats(termFrequency, globalDocFrequency, globalTot
2332
2975
  }
2333
2976
 
2334
2977
  // src/core/partition/search.ts
2978
+ var DEFAULT_MAX_RESULTS = 1e3;
2335
2979
  function searchFulltext(state, params) {
2336
- const { queryTokens, fields, boost, tolerance = 0, prefixLength = 2, exact = false, bm25Params, globalStats } = params;
2980
+ const {
2981
+ queryTokens,
2982
+ fields,
2983
+ boost,
2984
+ tolerance = 0,
2985
+ prefixLength = 2,
2986
+ exact = false,
2987
+ bm25Params,
2988
+ globalStats,
2989
+ maxResults,
2990
+ termMatch
2991
+ } = params;
2337
2992
  if (queryTokens.length === 0) {
2338
2993
  return { scored: [], totalMatched: 0 };
2339
2994
  }
2340
- const docScores = /* @__PURE__ */ new Map();
2341
2995
  const totalDocs = globalStats?.totalDocuments ?? state.stats.totalDocuments;
2342
2996
  const avgFieldLengths = globalStats?.averageFieldLengths ?? state.stats.averageFieldLengths;
2343
2997
  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
- });
2998
+ const scoreFn = globalStats ? computeBM25WithGlobalStats : computeBM25;
2999
+ const docScores = /* @__PURE__ */ new Map();
3000
+ const fieldLengthCache = /* @__PURE__ */ new Map();
3001
+ const useIntersection = termMatch === "all" && queryTokens.length > 1;
3002
+ const fieldNames = state.fieldNameTable.names;
3003
+ if (useIntersection) {
3004
+ const resolved = [];
3005
+ for (const qt of queryTokens) {
3006
+ const rawMatches = exact ? (() => {
3007
+ const postingList = state.invertedIdx.lookup(qt.token);
3008
+ return postingList ? [{ token: qt.token, postingList }] : [];
3009
+ })() : state.invertedIdx.fuzzyLookup(qt.token, tolerance, prefixLength);
3010
+ let totalPostings = 0;
3011
+ const matches = [];
3012
+ for (const m of rawMatches) {
3013
+ const docFreq = globalStats ? globalDocFreqs[m.token] ?? m.postingList.docIdSet.size : m.postingList.docIdSet.size;
3014
+ const idf = computeIDF(docFreq, totalDocs);
3015
+ totalPostings += m.postingList.length;
3016
+ matches.push({ token: m.token, docFreq, idf, postingList: m.postingList });
3017
+ }
3018
+ resolved.push({ token: qt.token, matches, totalPostings });
3019
+ }
3020
+ resolved.sort((a, b) => a.totalPostings - b.totalPostings);
3021
+ for (let tokenIndex = 0; tokenIndex < resolved.length; tokenIndex++) {
3022
+ for (const match of resolved[tokenIndex].matches) {
3023
+ const list = match.postingList;
3024
+ const hasDeleted = list.deletedDocs.size > 0;
3025
+ for (let pi = 0; pi < list.length; pi++) {
3026
+ const docId = list.docIds[pi];
3027
+ if (hasDeleted && list.deletedDocs.has(docId)) continue;
3028
+ if (tokenIndex > 0 && !docScores.has(docId)) continue;
3029
+ const fieldName = fieldNames[list.fieldNameIndices[pi]];
3030
+ if (fields && !fields.includes(fieldName)) continue;
3031
+ const termFrequency = list.termFrequencies[pi];
3032
+ const fieldBoost = boost?.[fieldName] ?? 1;
3033
+ const avgLen = avgFieldLengths[fieldName] ?? 1;
3034
+ let cachedLengths = fieldLengthCache.get(docId);
3035
+ if (cachedLengths === void 0) {
3036
+ const storedDoc = state.docStore.get(docId);
3037
+ cachedLengths = storedDoc?.fieldLengths ?? null;
3038
+ fieldLengthCache.set(docId, cachedLengths);
3039
+ }
3040
+ const actualFieldLength = cachedLengths?.[fieldName] ?? avgLen;
3041
+ let termScore = scoreFn(termFrequency, match.docFreq, totalDocs, actualFieldLength, avgLen, bm25Params);
3042
+ termScore *= fieldBoost;
3043
+ const existing = docScores.get(docId);
3044
+ if (existing) {
3045
+ existing.score += termScore;
3046
+ existing.termFrequencies[`${fieldName}:${match.token}`] = termFrequency;
3047
+ existing.fieldLengths[fieldName] = actualFieldLength;
3048
+ existing.idf[match.token] = match.idf;
3049
+ } else {
3050
+ docScores.set(docId, {
3051
+ score: termScore,
3052
+ termFrequencies: { [`${fieldName}:${match.token}`]: termFrequency },
3053
+ fieldLengths: { [fieldName]: actualFieldLength },
3054
+ idf: { [match.token]: match.idf }
3055
+ });
3056
+ }
3057
+ }
3058
+ }
3059
+ }
3060
+ } else {
3061
+ for (const qt of queryTokens) {
3062
+ const matchingPostings = exact ? (() => {
3063
+ const postingList = state.invertedIdx.lookup(qt.token);
3064
+ return postingList ? [{ token: qt.token, postingList }] : [];
3065
+ })() : state.invertedIdx.fuzzyLookup(qt.token, tolerance, prefixLength);
3066
+ for (const match of matchingPostings) {
3067
+ const docFreq = globalStats ? globalDocFreqs[match.token] ?? match.postingList.docIdSet.size : match.postingList.docIdSet.size;
3068
+ const idf = computeIDF(docFreq, totalDocs);
3069
+ const list = match.postingList;
3070
+ const hasDeleted = list.deletedDocs.size > 0;
3071
+ for (let pi = 0; pi < list.length; pi++) {
3072
+ const docId = list.docIds[pi];
3073
+ if (hasDeleted && list.deletedDocs.has(docId)) continue;
3074
+ const fieldName = fieldNames[list.fieldNameIndices[pi]];
3075
+ if (fields && !fields.includes(fieldName)) continue;
3076
+ const termFrequency = list.termFrequencies[pi];
3077
+ const fieldBoost = boost?.[fieldName] ?? 1;
3078
+ const avgLen = avgFieldLengths[fieldName] ?? 1;
3079
+ let cachedLengths = fieldLengthCache.get(docId);
3080
+ if (cachedLengths === void 0) {
3081
+ const storedDoc = state.docStore.get(docId);
3082
+ cachedLengths = storedDoc?.fieldLengths ?? null;
3083
+ fieldLengthCache.set(docId, cachedLengths);
3084
+ }
3085
+ const actualFieldLength = cachedLengths?.[fieldName] ?? avgLen;
3086
+ let termScore = scoreFn(termFrequency, docFreq, totalDocs, actualFieldLength, avgLen, bm25Params);
3087
+ termScore *= fieldBoost;
3088
+ const existing = docScores.get(docId);
3089
+ if (existing) {
3090
+ existing.score += termScore;
3091
+ existing.termFrequencies[`${fieldName}:${match.token}`] = termFrequency;
3092
+ existing.fieldLengths[fieldName] = actualFieldLength;
3093
+ existing.idf[match.token] = idf;
3094
+ } else {
3095
+ docScores.set(docId, {
3096
+ score: termScore,
3097
+ termFrequencies: { [`${fieldName}:${match.token}`]: termFrequency },
3098
+ fieldLengths: { [fieldName]: actualFieldLength },
3099
+ idf: { [match.token]: idf }
3100
+ });
3101
+ }
2376
3102
  }
2377
3103
  }
2378
3104
  }
2379
3105
  }
2380
- const scored = [];
3106
+ const totalMatched = docScores.size;
3107
+ const k = maxResults !== void 0 && maxResults > 0 ? maxResults : DEFAULT_MAX_RESULTS;
3108
+ const scored = topKFromMap(docScores, Math.min(k, totalMatched));
3109
+ return { scored, totalMatched };
3110
+ }
3111
+ function topKFromMap(docScores, k) {
3112
+ if (k <= 0) return [];
3113
+ const heap = [];
2381
3114
  for (const [docId, data] of docScores) {
2382
- scored.push({
2383
- docId,
3115
+ if (heap.length < k) {
3116
+ heap.push({ docId, score: data.score });
3117
+ if (heap.length === k) buildMinHeap(heap);
3118
+ } else if (data.score > heap[0].score) {
3119
+ heap[0] = { docId, score: data.score };
3120
+ siftDown(heap, 0);
3121
+ }
3122
+ }
3123
+ heap.sort((a, b) => b.score - a.score);
3124
+ const result = new Array(heap.length);
3125
+ for (let i = 0; i < heap.length; i++) {
3126
+ const data = docScores.get(heap[i].docId);
3127
+ if (!data) continue;
3128
+ result[i] = {
3129
+ docId: heap[i].docId,
2384
3130
  score: data.score,
2385
3131
  termFrequencies: data.termFrequencies,
2386
3132
  fieldLengths: data.fieldLengths,
2387
3133
  idf: data.idf
2388
- });
3134
+ };
3135
+ }
3136
+ return result;
3137
+ }
3138
+ function buildMinHeap(heap) {
3139
+ for (let i = (heap.length >> 1) - 1; i >= 0; i--) {
3140
+ siftDown(heap, i);
3141
+ }
3142
+ }
3143
+ function siftDown(heap, idx) {
3144
+ const len = heap.length;
3145
+ while (true) {
3146
+ let smallest = idx;
3147
+ const left = 2 * idx + 1;
3148
+ const right = 2 * idx + 2;
3149
+ if (left < len && heap[left].score < heap[smallest].score) smallest = left;
3150
+ if (right < len && heap[right].score < heap[smallest].score) smallest = right;
3151
+ if (smallest === idx) break;
3152
+ const tmp = heap[idx];
3153
+ heap[idx] = heap[smallest];
3154
+ heap[smallest] = tmp;
3155
+ idx = smallest;
2389
3156
  }
2390
- scored.sort((a, b) => b.score - a.score);
2391
- return { scored, totalMatched: scored.length };
2392
3157
  }
2393
3158
  function searchVector(state, params) {
2394
- const { field, value, k, similarity = 0, metric = "cosine", filterDocIds } = params;
3159
+ const { field, value, k, similarity = 0, metric = "cosine", filterDocIds, efSearch } = params;
2395
3160
  const vecStore = state.vectorStores.get(field);
2396
3161
  if (!vecStore) {
2397
3162
  return { scored: [], totalMatched: 0 };
@@ -2404,7 +3169,7 @@ function searchVector(state, params) {
2404
3169
  );
2405
3170
  }
2406
3171
  const queryVec = new Float32Array(value);
2407
- const results = vecStore.search(queryVec, k, metric, similarity, filterDocIds);
3172
+ const results = vecStore.search(queryVec, k, metric, similarity, filterDocIds, efSearch);
2408
3173
  return { scored: results, totalMatched: results.length };
2409
3174
  }
2410
3175
  function buildFilterContext(state, schema) {
@@ -2587,7 +3352,7 @@ function serializePartition(state, partitionId, indexName, totalPartitions, lang
2587
3352
  serializedVectors[path] = {
2588
3353
  dimension: store.dimension,
2589
3354
  vectors,
2590
- hnswGraph: null
3355
+ hnswGraph: store.serializeHNSW()
2591
3356
  };
2592
3357
  }
2593
3358
  const serializedStats = state.stats.serialize();
@@ -2611,6 +3376,109 @@ function serializePartition(state, partitionId, indexName, totalPartitions, lang
2611
3376
  statistics: serializedStats
2612
3377
  };
2613
3378
  }
3379
+ function serializePartitionToWirePayloadV2(state, partitionId, indexName, totalPartitions, language, schema) {
3380
+ const flatSchema = getFlatSchema(state, schema);
3381
+ const flatSchemaStrings = {};
3382
+ for (const [k, v] of Object.entries(flatSchema)) {
3383
+ flatSchemaStrings[k] = v;
3384
+ }
3385
+ const wireDocs = /* @__PURE__ */ Object.create(null);
3386
+ for (const [docId, stored] of state.docStore.all()) {
3387
+ wireDocs[docId] = {
3388
+ fields: structuredClone(stored.fields),
3389
+ field_lengths: { ...stored.fieldLengths }
3390
+ };
3391
+ }
3392
+ const fieldNames = [...state.fieldNameTable.names];
3393
+ const wireEntries = /* @__PURE__ */ Object.create(null);
3394
+ for (const token of state.invertedIdx.tokens()) {
3395
+ const list = state.invertedIdx.lookup(token);
3396
+ if (!list) continue;
3397
+ const hasDeleted = list.deletedDocs.size > 0;
3398
+ const ids = [];
3399
+ const tf = [];
3400
+ const fi = [];
3401
+ const pos = list.positions ? [] : null;
3402
+ for (let i = 0; i < list.length; i++) {
3403
+ if (hasDeleted && list.deletedDocs.has(list.docIds[i])) continue;
3404
+ ids.push(list.docIds[i]);
3405
+ tf.push(list.termFrequencies[i]);
3406
+ fi.push(list.fieldNameIndices[i]);
3407
+ if (pos && list.positions) {
3408
+ pos.push(list.positions[i]);
3409
+ }
3410
+ }
3411
+ if (ids.length > 0) {
3412
+ wireEntries[token] = { df: list.docIdSet.size, ids, tf, fi, pos };
3413
+ }
3414
+ }
3415
+ const wireNumeric = {};
3416
+ for (const [path, idx] of state.numericIndexes) {
3417
+ wireNumeric[path] = idx.serialize().map((e) => ({ value: e.value, doc_id: e.docId }));
3418
+ }
3419
+ const wireBoolean = {};
3420
+ for (const [path, idx] of state.booleanIndexes) {
3421
+ const s = idx.serialize();
3422
+ wireBoolean[path] = { true_docs: s.trueDocs, false_docs: s.falseDocs };
3423
+ }
3424
+ const wireEnum = {};
3425
+ for (const [path, idx] of state.enumIndexes) {
3426
+ wireEnum[path] = idx.serialize();
3427
+ }
3428
+ const wireGeo = {};
3429
+ for (const [path, idx] of state.geoIndexes) {
3430
+ wireGeo[path] = idx.serialize().map((e) => ({ lat: e.lat, lon: e.lon, doc_id: e.docId }));
3431
+ }
3432
+ const wireVectors = {};
3433
+ for (const [path, store] of state.vectorStores) {
3434
+ const vectors = [];
3435
+ for (const [, entry] of store.entries()) {
3436
+ vectors.push({ doc_id: entry.docId, vector: Array.from(entry.vector) });
3437
+ }
3438
+ const hnswData = store.serializeHNSW();
3439
+ wireVectors[path] = {
3440
+ dimension: store.dimension,
3441
+ vectors,
3442
+ hnsw_graph: hnswData ? {
3443
+ entry_point: hnswData.entryPoint,
3444
+ max_layer: hnswData.maxLayer,
3445
+ m: hnswData.m,
3446
+ ef_construction: hnswData.efConstruction,
3447
+ metric: hnswData.metric,
3448
+ nodes: hnswData.nodes
3449
+ } : null
3450
+ };
3451
+ }
3452
+ const serializedStats = state.stats.serialize();
3453
+ return {
3454
+ v: 2,
3455
+ index_name: indexName,
3456
+ partition_id: partitionId,
3457
+ total_partitions: totalPartitions,
3458
+ language,
3459
+ schema: flatSchemaStrings,
3460
+ doc_count: state.docStore.count(),
3461
+ avg_doc_length: Object.values(serializedStats.averageFieldLengths).reduce((sum, v) => sum + v, 0),
3462
+ documents: wireDocs,
3463
+ inverted_index: {
3464
+ field_names: fieldNames,
3465
+ entries: wireEntries
3466
+ },
3467
+ field_indexes: {
3468
+ numeric: wireNumeric,
3469
+ boolean: wireBoolean,
3470
+ enum: wireEnum,
3471
+ geopoint: wireGeo
3472
+ },
3473
+ vector_data: wireVectors,
3474
+ statistics: {
3475
+ total_documents: serializedStats.totalDocuments,
3476
+ total_field_lengths: serializedStats.totalFieldLengths,
3477
+ average_field_lengths: serializedStats.averageFieldLengths,
3478
+ doc_frequencies: serializedStats.docFrequencies
3479
+ }
3480
+ };
3481
+ }
2614
3482
  function deserializePartition(state, data, clearFn, schema) {
2615
3483
  clearFn();
2616
3484
  const invertedData = /* @__PURE__ */ Object.create(null);
@@ -2621,7 +3489,7 @@ function deserializePartition(state, data, clearFn, schema) {
2621
3489
  docId: p.docId,
2622
3490
  termFrequency: p.termFrequency,
2623
3491
  fieldName: p.field,
2624
- positions: [...p.positions]
3492
+ positions: state.trackPositions ? [...p.positions] : []
2625
3493
  }))
2626
3494
  };
2627
3495
  }
@@ -2652,10 +3520,13 @@ function deserializePartition(state, data, clearFn, schema) {
2652
3520
  state.geoIndexes.set(path, geoIdx);
2653
3521
  }
2654
3522
  for (const [path, vecData] of Object.entries(data.vectorData)) {
2655
- const store = createBruteForceVectorStore(vecData.dimension);
3523
+ const store = createVectorSearchEngine(vecData.dimension);
2656
3524
  for (const entry of vecData.vectors) {
2657
3525
  store.insert(entry.docId, new Float32Array(entry.vector));
2658
3526
  }
3527
+ if (vecData.hnswGraph) {
3528
+ store.deserializeHNSW(vecData.hnswGraph);
3529
+ }
2659
3530
  state.vectorStores.set(path, store);
2660
3531
  }
2661
3532
  state.stats.deserialize(data.statistics);
@@ -2676,9 +3547,15 @@ function deserializePartition(state, data, clearFn, schema) {
2676
3547
  }
2677
3548
 
2678
3549
  // src/core/partition/index.ts
2679
- function createPartitionIndex(partitionId) {
3550
+ function createPartitionIndex(partitionId, trackPositions = true, vectorPromotionConfig) {
3551
+ const vectorPromoter = vectorPromotionConfig ? createVectorPromoter({
3552
+ promotionThreshold: vectorPromotionConfig.threshold,
3553
+ hnswConfig: vectorPromotionConfig.hnswConfig,
3554
+ workerStrategy: vectorPromotionConfig.workerStrategy
3555
+ }) : null;
3556
+ const fieldNameTable = { names: [], indexMap: /* @__PURE__ */ new Map() };
2680
3557
  const state = {
2681
- invertedIdx: createInvertedIndex(),
3558
+ invertedIdx: createInvertedIndex(fieldNameTable),
2682
3559
  docStore: createDocumentStore(),
2683
3560
  stats: createPartitionStats(),
2684
3561
  numericIndexes: /* @__PURE__ */ new Map(),
@@ -2686,10 +3563,13 @@ function createPartitionIndex(partitionId) {
2686
3563
  enumIndexes: /* @__PURE__ */ new Map(),
2687
3564
  geoIndexes: /* @__PURE__ */ new Map(),
2688
3565
  vectorStores: /* @__PURE__ */ new Map(),
3566
+ fieldNameTable,
2689
3567
  flatSchemaCache: null,
2690
- lastSchemaRef: null
3568
+ lastSchemaRef: null,
3569
+ trackPositions
2691
3570
  };
2692
3571
  function clearAll() {
3572
+ vectorPromoter?.shutdown();
2693
3573
  state.invertedIdx.clear();
2694
3574
  state.docStore.clear();
2695
3575
  for (const idx of state.numericIndexes.values()) idx.clear();
@@ -2723,6 +3603,9 @@ function createPartitionIndex(partitionId) {
2723
3603
  }
2724
3604
  if (options?.validate !== false) {
2725
3605
  validateDocument(document, schema);
3606
+ if (options?.strict) {
3607
+ validateDocumentStrict(document, schema);
3608
+ }
2726
3609
  }
2727
3610
  const flatSchema = getFlatSchema(state, schema);
2728
3611
  const { fieldLengths, tokensByField } = indexDocument(
@@ -2733,8 +3616,13 @@ function createPartitionIndex(partitionId) {
2733
3616
  language,
2734
3617
  options
2735
3618
  );
2736
- state.docStore.store(docId, document, fieldLengths);
3619
+ if (options?.skipClone) {
3620
+ state.docStore.storeRef(docId, document, fieldLengths);
3621
+ } else {
3622
+ state.docStore.store(docId, document, fieldLengths);
3623
+ }
2737
3624
  state.stats.addDocument(fieldLengths, tokensByField);
3625
+ vectorPromoter?.check(state.vectorStores);
2738
3626
  },
2739
3627
  remove(docId, schema, language, options) {
2740
3628
  const stored = state.docStore.get(docId);
@@ -2759,6 +3647,9 @@ function createPartitionIndex(partitionId) {
2759
3647
  }
2760
3648
  if (options?.validate !== false) {
2761
3649
  validateDocument(document, schema);
3650
+ if (options?.strict) {
3651
+ validateDocumentStrict(document, schema);
3652
+ }
2762
3653
  }
2763
3654
  const flatSchema = getFlatSchema(state, schema);
2764
3655
  const needsTextReindex = textFieldsChanged(stored.fields, document, flatSchema);
@@ -2793,6 +3684,11 @@ function createPartitionIndex(partitionId) {
2793
3684
  if (!stored) return void 0;
2794
3685
  return structuredClone(stored.fields);
2795
3686
  },
3687
+ getRef(docId) {
3688
+ const stored = state.docStore.get(docId);
3689
+ if (!stored) return void 0;
3690
+ return stored.fields;
3691
+ },
2796
3692
  has(docId) {
2797
3693
  return state.docStore.has(docId);
2798
3694
  },
@@ -2805,6 +3701,41 @@ function createPartitionIndex(partitionId) {
2805
3701
  }
2806
3702
  },
2807
3703
  clear: clearAll,
3704
+ estimateMemoryBytes() {
3705
+ const docCount = state.docStore.count();
3706
+ if (docCount === 0) return 0;
3707
+ const AVG_DOC_OVERHEAD = 350;
3708
+ let bytes = docCount * AVG_DOC_OVERHEAD;
3709
+ const docFreqs = state.stats.docFrequencies;
3710
+ const POSTING_ENTRY_SIZE = 24;
3711
+ const PER_TERM_OVERHEAD = 180;
3712
+ let totalPostings = 0;
3713
+ let termCount = 0;
3714
+ for (const term in docFreqs) {
3715
+ totalPostings += docFreqs[term];
3716
+ termCount++;
3717
+ }
3718
+ bytes += totalPostings * POSTING_ENTRY_SIZE;
3719
+ bytes += termCount * PER_TERM_OVERHEAD;
3720
+ const FIELD_ENTRY_OVERHEAD = 42;
3721
+ bytes += docCount * state.numericIndexes.size * FIELD_ENTRY_OVERHEAD;
3722
+ bytes += docCount * state.booleanIndexes.size * FIELD_ENTRY_OVERHEAD;
3723
+ bytes += docCount * state.enumIndexes.size * FIELD_ENTRY_OVERHEAD;
3724
+ bytes += docCount * state.geoIndexes.size * FIELD_ENTRY_OVERHEAD;
3725
+ for (const store of state.vectorStores.values()) {
3726
+ bytes += store.estimateMemoryBytes();
3727
+ }
3728
+ return bytes;
3729
+ },
3730
+ vectorFieldCount() {
3731
+ return state.vectorStores.size;
3732
+ },
3733
+ hasPromotedHnsw() {
3734
+ for (const store of state.vectorStores.values()) {
3735
+ if (store.isPromoted) return true;
3736
+ }
3737
+ return false;
3738
+ },
2808
3739
  searchFulltext(params) {
2809
3740
  return searchFulltext(state, params);
2810
3741
  },
@@ -2817,9 +3748,16 @@ function createPartitionIndex(partitionId) {
2817
3748
  computeFacets(docIds, config, schema) {
2818
3749
  return computeFacets(state, docIds, config, schema);
2819
3750
  },
3751
+ suggestTerms(prefix, limit) {
3752
+ return state.invertedIdx.prefixSearch(prefix, limit);
3753
+ },
2820
3754
  serialize(indexName, totalPartitions, language, schema) {
2821
3755
  return serializePartition(state, partitionId, indexName, totalPartitions, language, schema);
2822
3756
  },
3757
+ serializeToBytes(indexName, totalPartitions, language, schema) {
3758
+ const wire = serializePartitionToWirePayloadV2(state, partitionId, indexName, totalPartitions, language, schema);
3759
+ return encodeRawPayloadV2(wire);
3760
+ },
2823
3761
  deserialize(data, schema) {
2824
3762
  deserializePartition(state, data, clearAll, schema);
2825
3763
  }
@@ -2830,10 +3768,12 @@ function createPartitionIndex(partitionId) {
2830
3768
  // src/partitioning/manager.ts
2831
3769
  function createPartitionManager(indexName, config, language, router, initialPartitionCount) {
2832
3770
  const count = initialPartitionCount ?? 1;
3771
+ const trackPositions = config.trackPositions ?? true;
3772
+ const vectorPromotionConfig = config.vectorPromotion;
2833
3773
  let partitions = [];
2834
3774
  const docPartitionMap = /* @__PURE__ */ new Map();
2835
3775
  for (let i = 0; i < count; i++) {
2836
- partitions.push(createPartitionIndex(i));
3776
+ partitions.push(createPartitionIndex(i, trackPositions, vectorPromotionConfig));
2837
3777
  }
2838
3778
  function validatePartitionId(partitionId) {
2839
3779
  if (partitionId < 0 || partitionId >= partitions.length) {
@@ -2882,6 +3822,9 @@ function createPartitionManager(indexName, config, language, router, initialPart
2882
3822
  validatePartitionId(partitionId);
2883
3823
  return partitions[partitionId];
2884
3824
  },
3825
+ partitionAt(index) {
3826
+ return partitions[index];
3827
+ },
2885
3828
  getAllPartitions() {
2886
3829
  return [...partitions];
2887
3830
  },
@@ -2890,7 +3833,15 @@ function createPartitionManager(indexName, config, language, router, initialPart
2890
3833
  rebuildDocPartitionMap();
2891
3834
  },
2892
3835
  addPartition() {
2893
- const partition = createPartitionIndex(partitions.length);
3836
+ const currentMaxPartitions = config.partitions?.maxPartitions;
3837
+ if (currentMaxPartitions !== void 0 && partitions.length >= currentMaxPartitions) {
3838
+ throw new NarsilError(
3839
+ ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
3840
+ `Cannot add partition: maximum of ${currentMaxPartitions} partitions reached`,
3841
+ { maxPartitions: currentMaxPartitions, currentCount: partitions.length }
3842
+ );
3843
+ }
3844
+ const partition = createPartitionIndex(partitions.length, trackPositions, vectorPromotionConfig);
2894
3845
  partitions.push(partition);
2895
3846
  return partition;
2896
3847
  },
@@ -2900,8 +3851,26 @@ function createPartitionManager(indexName, config, language, router, initialPart
2900
3851
  rebuildDocPartitionMap();
2901
3852
  },
2902
3853
  insert(docId, document, options) {
3854
+ const currentMaxDocs = config.partitions?.maxDocsPerPartition;
3855
+ if (currentMaxDocs !== void 0) {
3856
+ const totalCapacity = currentMaxDocs * partitions.length;
3857
+ if (docPartitionMap.size >= totalCapacity) {
3858
+ throw new NarsilError(
3859
+ ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
3860
+ `Index "${indexName}" has reached its capacity of ${totalCapacity} documents (${currentMaxDocs} per partition \xD7 ${partitions.length} partitions)`,
3861
+ {
3862
+ indexName,
3863
+ currentCount: docPartitionMap.size,
3864
+ totalCapacity,
3865
+ maxDocsPerPartition: currentMaxDocs,
3866
+ partitionCount: partitions.length
3867
+ }
3868
+ );
3869
+ }
3870
+ }
2903
3871
  const pid = router.route(docId, partitions.length);
2904
- partitions[pid].insert(docId, document, config.schema, language, options);
3872
+ const insertOpts = config.strict ? { ...options, strict: true } : options;
3873
+ partitions[pid].insert(docId, document, config.schema, language, insertOpts);
2905
3874
  docPartitionMap.set(docId, pid);
2906
3875
  },
2907
3876
  remove(docId) {
@@ -2917,13 +3886,19 @@ function createPartitionManager(indexName, config, language, router, initialPart
2917
3886
  if (pid === void 0) {
2918
3887
  throw new NarsilError(ErrorCodes.DOC_NOT_FOUND, `Document "${docId}" not found in any partition`, { docId });
2919
3888
  }
2920
- partitions[pid].update(docId, document, config.schema, language, options);
3889
+ const updateOpts = config.strict ? { ...options, strict: true } : options;
3890
+ partitions[pid].update(docId, document, config.schema, language, updateOpts);
2921
3891
  },
2922
3892
  get(docId) {
2923
3893
  const pid = docPartitionMap.get(docId);
2924
3894
  if (pid === void 0) return void 0;
2925
3895
  return partitions[pid].get(docId);
2926
3896
  },
3897
+ getRef(docId) {
3898
+ const pid = docPartitionMap.get(docId);
3899
+ if (pid === void 0) return void 0;
3900
+ return partitions[pid].getRef(docId);
3901
+ },
2927
3902
  has(docId) {
2928
3903
  return docPartitionMap.has(docId);
2929
3904
  },
@@ -2934,6 +3909,10 @@ function createPartitionManager(indexName, config, language, router, initialPart
2934
3909
  validatePartitionId(partitionId);
2935
3910
  return partitions[partitionId].serialize(indexName, partitions.length, language.name, config.schema);
2936
3911
  },
3912
+ serializePartitionToBytes(partitionId) {
3913
+ validatePartitionId(partitionId);
3914
+ return partitions[partitionId].serializeToBytes(indexName, partitions.length, language.name, config.schema);
3915
+ },
2937
3916
  deserializePartition(partitionId, data) {
2938
3917
  validatePartitionId(partitionId);
2939
3918
  partitions[partitionId].deserialize(data, config.schema);
@@ -2954,6 +3933,22 @@ function createPartitionManager(indexName, config, language, router, initialPart
2954
3933
  }
2955
3934
  }
2956
3935
  return { totalDocuments, docFrequencies, totalFieldLengths };
3936
+ },
3937
+ estimateMemoryBytes() {
3938
+ let total = 0;
3939
+ for (let i = 0; i < partitions.length; i++) {
3940
+ total += partitions[i].estimateMemoryBytes();
3941
+ }
3942
+ return total;
3943
+ },
3944
+ getPartitionStats() {
3945
+ return partitions.map((p, i) => ({
3946
+ partitionId: i,
3947
+ documentCount: p.count(),
3948
+ estimatedMemoryBytes: p.estimateMemoryBytes(),
3949
+ vectorFieldCount: p.vectorFieldCount(),
3950
+ isHnswPromoted: p.hasPromotedHnsw()
3951
+ }));
2957
3952
  }
2958
3953
  };
2959
3954
  return manager;
@@ -3050,7 +4045,7 @@ function createDirectExecutor() {
3050
4045
  }
3051
4046
  case "insert": {
3052
4047
  const entry = requireIndex(action.indexName);
3053
- entry.manager.insert(action.docId, action.document);
4048
+ entry.manager.insert(action.docId, action.document, action.skipClone ? { skipClone: true } : void 0);
3054
4049
  return void 0;
3055
4050
  }
3056
4051
  case "remove": {
@@ -3065,14 +4060,15 @@ function createDirectExecutor() {
3065
4060
  }
3066
4061
  case "query": {
3067
4062
  const entry = requireIndex(action.indexName);
3068
- const result = fanOutQuery(entry.manager, action.params, entry.language, entry.config.schema, {
3069
- scoringMode: entry.config.defaultScoring ?? "local"
4063
+ const result = await fanOutQuery(entry.manager, action.params, entry.language, entry.config.schema, {
4064
+ scoringMode: entry.config.defaultScoring ?? "local",
4065
+ partitionIds: action.partitionIds
3070
4066
  });
3071
4067
  return result;
3072
4068
  }
3073
4069
  case "preflight": {
3074
4070
  const entry = requireIndex(action.indexName);
3075
- const result = fanOutQuery(entry.manager, action.params, entry.language, entry.config.schema, {
4071
+ const result = await fanOutQuery(entry.manager, action.params, entry.language, entry.config.schema, {
3076
4072
  scoringMode: entry.config.defaultScoring ?? "local"
3077
4073
  });
3078
4074
  return { count: result.totalMatched };
@@ -3144,4 +4140,4 @@ function createDirectExecutor() {
3144
4140
  };
3145
4141
  }
3146
4142
 
3147
- export { createDirectExecutor, fanOutQuery, getLanguage, registerLanguage, tokenize, validateSchema };
4143
+ export { createDirectExecutor, createPartitionIndex, createPartitionRouter, deserializePayloadV2, detectRuntime, fanOutQuery, fnv1a, getLanguage, kWayMerge, registerLanguage, tokenize, validateSchema };