@delali/narsil 0.1.11 → 0.1.12

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.
@@ -129,6 +129,7 @@ function computeDefaultCacheSize() {
129
129
  return Math.max(CACHE_SIZE_FLOOR, Math.min(2e5, CACHE_SIZE_CEILING));
130
130
  }
131
131
  var normalizationCache = /* @__PURE__ */ new Map();
132
+ var normalizationCacheSize = 0;
132
133
  var maxCacheSize = computeDefaultCacheSize();
133
134
  function configureNormalizationCache(maxSize) {
134
135
  if (!Number.isFinite(maxSize)) {
@@ -151,19 +152,13 @@ function configureNormalizationCache(maxSize) {
151
152
  );
152
153
  }
153
154
  maxCacheSize = Math.max(CACHE_SIZE_FLOOR, Math.min(Math.floor(maxSize), CACHE_SIZE_CEILING));
154
- if (normalizationCache.size > maxCacheSize) {
155
- const excess = normalizationCache.size - maxCacheSize;
156
- let count = 0;
157
- for (const key of normalizationCache.keys()) {
158
- if (count >= excess) break;
159
- normalizationCache.delete(key);
160
- count++;
161
- }
155
+ if (normalizationCacheSize > maxCacheSize) {
156
+ evictOldestEntries(normalizationCacheSize - maxCacheSize);
162
157
  }
163
158
  }
164
159
  var cachedLangName = "";
165
160
  var cachedFlags = "";
166
- var cachedPrefix = "";
161
+ var cachedBucket = null;
167
162
  function stripDiacritics(text) {
168
163
  return text.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
169
164
  }
@@ -172,18 +167,44 @@ function resolveStopWords(language, override) {
172
167
  if (override instanceof Set) return override;
173
168
  return override(language.stopWords);
174
169
  }
175
- function getCachePrefix(language, flags) {
176
- if (language.name === cachedLangName && flags === cachedFlags) return cachedPrefix;
170
+ function getCacheBucket(language, flags) {
171
+ if (cachedBucket && language.name === cachedLangName && flags === cachedFlags) return cachedBucket;
172
+ const key = `${language.name}:${flags}`;
173
+ let bucket = normalizationCache.get(key);
174
+ if (!bucket) {
175
+ bucket = /* @__PURE__ */ new Map();
176
+ normalizationCache.set(key, bucket);
177
+ }
177
178
  cachedLangName = language.name;
178
179
  cachedFlags = flags;
179
- cachedPrefix = `${language.name}:${flags}:`;
180
- return cachedPrefix;
180
+ cachedBucket = bucket;
181
+ return bucket;
182
+ }
183
+ function evictOldestEntries(count) {
184
+ let remaining = count;
185
+ for (const [key, bucket] of normalizationCache) {
186
+ if (remaining <= 0) break;
187
+ if (bucket.size <= remaining) {
188
+ remaining -= bucket.size;
189
+ normalizationCacheSize -= bucket.size;
190
+ normalizationCache.delete(key);
191
+ if (bucket === cachedBucket) cachedBucket = null;
192
+ } else {
193
+ let deleted = 0;
194
+ for (const raw of bucket.keys()) {
195
+ if (deleted >= remaining) break;
196
+ bucket.delete(raw);
197
+ deleted++;
198
+ }
199
+ normalizationCacheSize -= deleted;
200
+ remaining = 0;
201
+ }
202
+ }
181
203
  }
182
204
  function transformToken(raw, language, stem, removeDiacritics) {
183
205
  const flags = (stem ? "s" : "") + (removeDiacritics ? "d" : "");
184
- const prefix = getCachePrefix(language, flags);
185
- const cacheKey = prefix + raw;
186
- const cached = normalizationCache.get(cacheKey);
206
+ let bucket = getCacheBucket(language, flags);
207
+ const cached = bucket.get(raw);
187
208
  if (cached !== void 0) return cached;
188
209
  let normalized = raw;
189
210
  if (removeDiacritics) {
@@ -192,16 +213,12 @@ function transformToken(raw, language, stem, removeDiacritics) {
192
213
  if (stem && language.stemmer) {
193
214
  normalized = language.stemmer(normalized);
194
215
  }
195
- if (normalizationCache.size >= maxCacheSize) {
196
- const evictCount = Math.max(1, maxCacheSize >>> 2);
197
- let count = 0;
198
- for (const key of normalizationCache.keys()) {
199
- if (count >= evictCount) break;
200
- normalizationCache.delete(key);
201
- count++;
202
- }
216
+ if (normalizationCacheSize >= maxCacheSize) {
217
+ evictOldestEntries(Math.max(1, maxCacheSize >>> 2));
218
+ bucket = getCacheBucket(language, flags);
203
219
  }
204
- normalizationCache.set(cacheKey, normalized);
220
+ bucket.set(raw, normalized);
221
+ normalizationCacheSize++;
205
222
  return normalized;
206
223
  }
207
224
  function splitText(text, language) {
@@ -231,24 +248,28 @@ function tokenize(text, language, options) {
231
248
  stem = true,
232
249
  removeStopWords = true,
233
250
  removeDiacritics = false,
251
+ collectSurfaces = false,
234
252
  customTokenizer,
235
253
  stopWordOverride
236
254
  } = options ?? {};
237
255
  if (customTokenizer) {
238
256
  const customResult = customTokenizer.tokenize(text);
257
+ const originals = customResult.map((t) => t.token);
239
258
  return {
240
259
  tokens: customResult,
241
- originalTokens: customResult.map((t) => t.token)
260
+ originalTokens: originals
242
261
  };
243
262
  }
244
263
  const normalized = isAscii(text) ? text.toLowerCase() : text.normalize("NFC").toLowerCase();
245
264
  const rawParts = splitText(normalized, language);
246
265
  const minLength = language.tokenizer?.minTokenLength ?? DEFAULT_MIN_TOKEN_LENGTH;
247
266
  const effectiveDiacritics = removeDiacritics || (language.tokenizer?.normalizeDiacritics ?? false);
267
+ const stemsTokens = stem && language.stemmer !== void 0;
248
268
  const stopWords2 = removeStopWords ? resolveStopWords(language, stopWordOverride) : /* @__PURE__ */ new Set();
249
269
  const stripPossessives = language.tokenizer?.stripPossessive ?? false;
250
270
  const tokens = [];
251
271
  const originalTokens = [];
272
+ const surfaces = collectSurfaces && stemsTokens ? [] : void 0;
252
273
  let position = 0;
253
274
  for (const part of rawParts) {
254
275
  const candidate = stripPossessives ? stripPossessive(part) : part;
@@ -264,16 +285,21 @@ function tokenize(text, language, options) {
264
285
  if (processed.length > 0) {
265
286
  tokens.push({ token: processed, position });
266
287
  originalTokens.push(part);
288
+ if (surfaces) {
289
+ const surface = effectiveDiacritics ? transformToken(candidate, language, false, true) : candidate;
290
+ surfaces.push(surface === processed ? void 0 : surface);
291
+ }
267
292
  }
268
293
  position++;
269
294
  }
270
- return { tokens, originalTokens };
295
+ return { tokens, originalTokens, surfaces };
271
296
  }
272
297
  function* tokenizeIterator(text, language, options) {
273
298
  const {
274
299
  stem = true,
275
300
  removeStopWords = true,
276
301
  removeDiacritics = false,
302
+ collectSurfaces = false,
277
303
  customTokenizer,
278
304
  stopWordOverride
279
305
  } = options ?? {};
@@ -288,6 +314,8 @@ function* tokenizeIterator(text, language, options) {
288
314
  const rawParts = splitText(normalized, language);
289
315
  const minLength = language.tokenizer?.minTokenLength ?? DEFAULT_MIN_TOKEN_LENGTH;
290
316
  const effectiveDiacritics = removeDiacritics || (language.tokenizer?.normalizeDiacritics ?? false);
317
+ const stemsTokens = stem && language.stemmer !== void 0;
318
+ const wantSurfaces = collectSurfaces && stemsTokens;
291
319
  const stopWords2 = removeStopWords ? resolveStopWords(language, stopWordOverride) : /* @__PURE__ */ new Set();
292
320
  const stripPossessives = language.tokenizer?.stripPossessive ?? false;
293
321
  let position = 0;
@@ -303,20 +331,27 @@ function* tokenizeIterator(text, language, options) {
303
331
  }
304
332
  const processed = transformToken(candidate, language, stem, effectiveDiacritics);
305
333
  if (processed.length > 0) {
306
- yield { token: processed, position };
334
+ if (wantSurfaces) {
335
+ const surface = effectiveDiacritics ? transformToken(candidate, language, false, true) : candidate;
336
+ yield { token: processed, position, surface: surface === processed ? void 0 : surface };
337
+ } else {
338
+ yield { token: processed, position };
339
+ }
307
340
  }
308
341
  position++;
309
342
  }
310
343
  }
311
344
  function clearNormalizationCache() {
312
345
  normalizationCache.clear();
346
+ normalizationCacheSize = 0;
347
+ cachedBucket = null;
313
348
  }
314
349
  function resetNormalizationCache() {
315
- normalizationCache.clear();
350
+ clearNormalizationCache();
316
351
  maxCacheSize = computeDefaultCacheSize();
317
352
  }
318
353
  function getNormalizationCacheSize() {
319
- return normalizationCache.size;
354
+ return normalizationCacheSize;
320
355
  }
321
356
 
322
357
  // src/languages/english.ts
@@ -1081,8 +1116,20 @@ function validateVectorPromotion(config) {
1081
1116
  fail("vectorPromotion.hnswConfig.metric must be 'cosine', 'dotProduct', or 'euclidean'", { metric });
1082
1117
  }
1083
1118
  }
1084
- function encodeRawPayloadV2(wire) {
1085
- return encode(wire);
1119
+ function sanitizeSurfaceForms(raw) {
1120
+ if (raw === void 0 || raw === null || typeof raw !== "object" || Array.isArray(raw)) return void 0;
1121
+ const result = /* @__PURE__ */ Object.create(null);
1122
+ let count = 0;
1123
+ for (const [surface, value] of Object.entries(raw)) {
1124
+ if (typeof value === "number") {
1125
+ result[surface] = value;
1126
+ count++;
1127
+ } else if (Array.isArray(value) && typeof value[0] === "number" && typeof value[1] === "string") {
1128
+ result[surface] = [value[0], value[1]];
1129
+ count++;
1130
+ }
1131
+ }
1132
+ return count > 0 ? result : void 0;
1086
1133
  }
1087
1134
  var VALID_HNSW_METRICS = /* @__PURE__ */ new Set(["cosine", "dotProduct", "euclidean"]);
1088
1135
  function validateHnswMetric(value) {
@@ -1092,6 +1139,153 @@ function validateHnswMetric(value) {
1092
1139
  }
1093
1140
  return void 0;
1094
1141
  }
1142
+ function wireToPartition(raw) {
1143
+ const documents = {};
1144
+ for (const [docId, doc] of Object.entries(raw.documents ?? {})) {
1145
+ documents[docId] = {
1146
+ fields: doc.fields,
1147
+ fieldLengths: doc.field_lengths ?? {}
1148
+ };
1149
+ }
1150
+ const invertedIndex = {};
1151
+ for (const [token, list] of Object.entries(raw.inverted_index ?? {})) {
1152
+ invertedIndex[token] = {
1153
+ docFrequency: list.doc_freq,
1154
+ postings: (list.postings ?? []).map((p) => ({
1155
+ docId: p.doc_id,
1156
+ termFrequency: p.term_freq,
1157
+ field: p.field,
1158
+ positions: p.positions ?? []
1159
+ }))
1160
+ };
1161
+ }
1162
+ const numeric = {};
1163
+ for (const [field, entries] of Object.entries(raw.field_indexes?.numeric ?? {})) {
1164
+ numeric[field] = entries.map((e) => ({ value: e.value, docId: e.doc_id }));
1165
+ }
1166
+ const boolean = {};
1167
+ for (const [field, idx] of Object.entries(raw.field_indexes?.boolean ?? {})) {
1168
+ boolean[field] = { trueDocs: idx.true_docs ?? [], falseDocs: idx.false_docs ?? [] };
1169
+ }
1170
+ const geopoint = {};
1171
+ for (const [field, entries] of Object.entries(raw.field_indexes?.geopoint ?? {})) {
1172
+ geopoint[field] = entries.map((e) => ({ lat: e.lat, lon: e.lon, docId: e.doc_id }));
1173
+ }
1174
+ const vectorData = {};
1175
+ for (const [field, data] of Object.entries(raw.vector_data ?? {})) {
1176
+ vectorData[field] = {
1177
+ dimension: data.dimension,
1178
+ vectors: (data.vectors ?? []).map((v) => ({ docId: v.doc_id, vector: v.vector })),
1179
+ hnswGraph: data.hnsw_graph ? {
1180
+ entryPoint: data.hnsw_graph.entry_point,
1181
+ maxLayer: data.hnsw_graph.max_layer,
1182
+ m: data.hnsw_graph.m,
1183
+ efConstruction: data.hnsw_graph.ef_construction,
1184
+ metric: validateHnswMetric(data.hnsw_graph.metric),
1185
+ nodes: data.hnsw_graph.nodes
1186
+ } : null,
1187
+ sq8: data.sq8 ? {
1188
+ alpha: data.sq8.alpha,
1189
+ offset: data.sq8.offset,
1190
+ quantizedVectors: data.sq8.quantized_vectors,
1191
+ vectorSums: data.sq8.vector_sums,
1192
+ vectorSumSqs: data.sq8.vector_sum_sqs
1193
+ } : null
1194
+ };
1195
+ }
1196
+ return {
1197
+ indexName: raw.index_name,
1198
+ partitionId: raw.partition_id,
1199
+ totalPartitions: raw.total_partitions,
1200
+ language: raw.language ?? "english",
1201
+ schema: raw.schema ?? {},
1202
+ docCount: raw.doc_count ?? 0,
1203
+ avgDocLength: raw.avg_doc_length ?? 0,
1204
+ documents,
1205
+ invertedIndex,
1206
+ fieldIndexes: {
1207
+ numeric,
1208
+ boolean,
1209
+ enum: raw.field_indexes?.enum ?? {},
1210
+ geopoint
1211
+ },
1212
+ surfaceForms: sanitizeSurfaceForms(raw.surface_forms),
1213
+ vectorData,
1214
+ statistics: {
1215
+ totalDocuments: raw.statistics?.total_documents ?? 0,
1216
+ totalFieldLengths: raw.statistics?.total_field_lengths ?? {},
1217
+ averageFieldLengths: raw.statistics?.average_field_lengths ?? {},
1218
+ docFrequencies: raw.statistics?.doc_frequencies ?? {}
1219
+ }
1220
+ };
1221
+ }
1222
+ function deserializePayloadV1(data) {
1223
+ const raw = decode(data);
1224
+ return wireToPartition(raw);
1225
+ }
1226
+ function metadataToWire(meta) {
1227
+ const wire = {
1228
+ index_name: meta.indexName,
1229
+ schema: meta.schema,
1230
+ language: meta.language,
1231
+ partition_count: meta.partitionCount,
1232
+ bm25_params: meta.bm25Params,
1233
+ created_at: meta.createdAt,
1234
+ engine_version: meta.engineVersion
1235
+ };
1236
+ if (meta.vectorFields) {
1237
+ wire.vector_fields = meta.vectorFields;
1238
+ }
1239
+ if (meta.embedding) {
1240
+ wire.embedding = meta.embedding.adapter !== void 0 ? { adapter: meta.embedding.adapter, fields: meta.embedding.fields } : { fields: meta.embedding.fields };
1241
+ }
1242
+ if (meta.surfaceForms === true) {
1243
+ wire.surface_forms_enabled = true;
1244
+ }
1245
+ return wire;
1246
+ }
1247
+ function wireToMetadata(raw) {
1248
+ const meta = {
1249
+ indexName: raw.index_name,
1250
+ schema: raw.schema ?? {},
1251
+ language: raw.language ?? "english",
1252
+ partitionCount: raw.partition_count ?? 1,
1253
+ bm25Params: raw.bm25_params ?? { k1: 1.2, b: 0.75 },
1254
+ createdAt: raw.created_at ?? 0,
1255
+ engineVersion: raw.engine_version ?? "0.0.0"
1256
+ };
1257
+ if (raw.vector_fields) {
1258
+ meta.vectorFields = raw.vector_fields;
1259
+ }
1260
+ if (raw.embedding && typeof raw.embedding === "object" && typeof raw.embedding.fields === "object") {
1261
+ meta.embedding = typeof raw.embedding.adapter === "string" ? { adapter: raw.embedding.adapter, fields: raw.embedding.fields } : { fields: raw.embedding.fields };
1262
+ }
1263
+ if (raw.surface_forms_enabled === true) {
1264
+ meta.surfaceForms = true;
1265
+ }
1266
+ return meta;
1267
+ }
1268
+ function serializeMetadata(meta) {
1269
+ const wire = metadataToWire(meta);
1270
+ return encode(wire);
1271
+ }
1272
+ function deserializeMetadata(data) {
1273
+ const raw = decode(data);
1274
+ return wireToMetadata(raw);
1275
+ }
1276
+
1277
+ // src/serialization/payload-v2.ts
1278
+ function encodeRawPayloadV2(wire) {
1279
+ return encode(wire);
1280
+ }
1281
+ var VALID_HNSW_METRICS2 = /* @__PURE__ */ new Set(["cosine", "dotProduct", "euclidean"]);
1282
+ function validateHnswMetric2(value) {
1283
+ if (value === void 0 || value === null) return void 0;
1284
+ if (typeof value === "string" && VALID_HNSW_METRICS2.has(value)) {
1285
+ return value;
1286
+ }
1287
+ return void 0;
1288
+ }
1095
1289
  function deserializePayloadV2(data) {
1096
1290
  const raw = decode(data);
1097
1291
  const documents = {};
@@ -1141,7 +1335,7 @@ function deserializePayloadV2(data) {
1141
1335
  maxLayer: data2.hnsw_graph.max_layer,
1142
1336
  m: data2.hnsw_graph.m,
1143
1337
  efConstruction: data2.hnsw_graph.ef_construction,
1144
- metric: validateHnswMetric(data2.hnsw_graph.metric),
1338
+ metric: validateHnswMetric2(data2.hnsw_graph.metric),
1145
1339
  nodes: data2.hnsw_graph.nodes
1146
1340
  } : null,
1147
1341
  sq8: data2.sq8 ? {
@@ -1169,6 +1363,7 @@ function deserializePayloadV2(data) {
1169
1363
  enum: raw.field_indexes?.enum ?? {},
1170
1364
  geopoint
1171
1365
  },
1366
+ surfaceForms: sanitizeSurfaceForms(raw.surface_forms),
1172
1367
  vectorData,
1173
1368
  statistics: {
1174
1369
  totalDocuments: raw.statistics?.total_documents ?? 0,
@@ -1384,7 +1579,8 @@ function createInvertedIndex(fieldNameTable) {
1384
1579
  fieldNameIndices: new Uint8Array(INITIAL_CAPACITY),
1385
1580
  positions: null,
1386
1581
  docIdSet: /* @__PURE__ */ new Set(),
1387
- deletedDocs: /* @__PURE__ */ new Set()
1582
+ deletedDocs: /* @__PURE__ */ new Set(),
1583
+ totalTermFrequency: 0
1388
1584
  };
1389
1585
  index.set(token, list);
1390
1586
  trackToken(token);
@@ -1414,6 +1610,8 @@ function createInvertedIndex(fieldNameTable) {
1414
1610
  }
1415
1611
  }
1416
1612
  writeIdx++;
1613
+ } else {
1614
+ list.totalTermFrequency -= list.termFrequencies[i];
1417
1615
  }
1418
1616
  }
1419
1617
  list.docIds.length = writeIdx;
@@ -1434,6 +1632,8 @@ function createInvertedIndex(fieldNameTable) {
1434
1632
  }
1435
1633
  }
1436
1634
  writeIdx++;
1635
+ } else {
1636
+ list.totalTermFrequency -= list.termFrequencies[i];
1437
1637
  }
1438
1638
  }
1439
1639
  list.docIds.length = writeIdx;
@@ -1454,6 +1654,7 @@ function createInvertedIndex(fieldNameTable) {
1454
1654
  list.docIds.push(internalId);
1455
1655
  list.termFrequencies[idx] = termFrequency > MAX_TERM_FREQUENCY ? MAX_TERM_FREQUENCY : termFrequency;
1456
1656
  list.fieldNameIndices[idx] = fieldNameIndex;
1657
+ list.totalTermFrequency += list.termFrequencies[idx];
1457
1658
  if (positions !== null) {
1458
1659
  if (!list.positions) {
1459
1660
  list.positions = [];
@@ -1581,12 +1782,14 @@ function createInvertedIndex(fieldNameTable) {
1581
1782
  const fieldNameIndices = new Uint8Array(count);
1582
1783
  let hasPositions = false;
1583
1784
  let validCount = 0;
1785
+ let totalTermFrequency = 0;
1584
1786
  for (let i = 0; i < count; i++) {
1585
1787
  const p = src.postings[i];
1586
1788
  const internalId = resolver.toInternal(p.docId);
1587
1789
  if (internalId === void 0) continue;
1588
1790
  docIds[validCount] = internalId;
1589
1791
  termFrequencies[validCount] = p.termFrequency > MAX_TERM_FREQUENCY ? MAX_TERM_FREQUENCY : p.termFrequency;
1792
+ totalTermFrequency += termFrequencies[validCount];
1590
1793
  let fnIndex = fieldNameTable.indexMap.get(p.fieldName);
1591
1794
  if (fnIndex === void 0) {
1592
1795
  fnIndex = fieldNameTable.names.length;
@@ -1620,7 +1823,8 @@ function createInvertedIndex(fieldNameTable) {
1620
1823
  fieldNameIndices: finalFNI,
1621
1824
  positions,
1622
1825
  docIdSet,
1623
- deletedDocs: /* @__PURE__ */ new Set()
1826
+ deletedDocs: /* @__PURE__ */ new Set(),
1827
+ totalTermFrequency
1624
1828
  });
1625
1829
  trackToken(token);
1626
1830
  }
@@ -1762,6 +1966,115 @@ function createPartitionStats() {
1762
1966
  return stats;
1763
1967
  }
1764
1968
 
1969
+ // src/core/surface-registry.ts
1970
+ function createSurfaceRegistry() {
1971
+ const entries = /* @__PURE__ */ new Map();
1972
+ const buckets = /* @__PURE__ */ new Map();
1973
+ let totalsByToken = null;
1974
+ function track(surface) {
1975
+ const ch = surface[0];
1976
+ let bucket = buckets.get(ch);
1977
+ if (!bucket) {
1978
+ bucket = /* @__PURE__ */ new Set();
1979
+ buckets.set(ch, bucket);
1980
+ }
1981
+ bucket.add(surface);
1982
+ }
1983
+ function untrack(surface) {
1984
+ const ch = surface[0];
1985
+ const bucket = buckets.get(ch);
1986
+ if (bucket) {
1987
+ bucket.delete(surface);
1988
+ if (bucket.size === 0) buckets.delete(ch);
1989
+ }
1990
+ }
1991
+ function adjustTotals(token, delta) {
1992
+ if (!totalsByToken || delta === 0) return;
1993
+ const next = (totalsByToken.get(token) ?? 0) + delta;
1994
+ if (next <= 0) totalsByToken.delete(token);
1995
+ else totalsByToken.set(token, next);
1996
+ }
1997
+ return {
1998
+ add(surface, token, occurrences) {
1999
+ if (surface.length === 0 || token.length === 0 || occurrences <= 0) return;
2000
+ if (surface === token) return;
2001
+ const existing = entries.get(surface);
2002
+ if (existing) {
2003
+ existing.count += occurrences;
2004
+ adjustTotals(existing.token, occurrences);
2005
+ return;
2006
+ }
2007
+ entries.set(surface, { token, count: occurrences });
2008
+ track(surface);
2009
+ adjustTotals(token, occurrences);
2010
+ },
2011
+ subtract(surface, occurrences) {
2012
+ if (occurrences <= 0) return;
2013
+ const existing = entries.get(surface);
2014
+ if (!existing) return;
2015
+ const removed = Math.min(occurrences, existing.count);
2016
+ existing.count -= occurrences;
2017
+ adjustTotals(existing.token, -removed);
2018
+ if (existing.count <= 0) {
2019
+ entries.delete(surface);
2020
+ untrack(surface);
2021
+ }
2022
+ },
2023
+ candidatesForPrefix(prefix) {
2024
+ if (prefix.length === 0) return [];
2025
+ const bucket = buckets.get(prefix[0]);
2026
+ if (!bucket) return [];
2027
+ const results = [];
2028
+ for (const surface of bucket) {
2029
+ if (surface.length < prefix.length || !surface.startsWith(prefix)) continue;
2030
+ const entry = entries.get(surface);
2031
+ if (!entry) continue;
2032
+ results.push({ surface, token: entry.token, occurrences: entry.count });
2033
+ }
2034
+ return results;
2035
+ },
2036
+ stemChangedTotalFor(token) {
2037
+ if (!totalsByToken) {
2038
+ totalsByToken = /* @__PURE__ */ new Map();
2039
+ for (const entry of entries.values()) {
2040
+ totalsByToken.set(entry.token, (totalsByToken.get(entry.token) ?? 0) + entry.count);
2041
+ }
2042
+ }
2043
+ return totalsByToken.get(token) ?? 0;
2044
+ },
2045
+ size() {
2046
+ return entries.size;
2047
+ },
2048
+ clear() {
2049
+ entries.clear();
2050
+ buckets.clear();
2051
+ totalsByToken = null;
2052
+ },
2053
+ serialize() {
2054
+ const result = /* @__PURE__ */ Object.create(null);
2055
+ for (const [surface, entry] of entries) {
2056
+ result[surface] = [entry.count, entry.token];
2057
+ }
2058
+ return result;
2059
+ },
2060
+ deserialize(data) {
2061
+ entries.clear();
2062
+ buckets.clear();
2063
+ totalsByToken = null;
2064
+ for (const surface of Object.keys(data)) {
2065
+ if (surface.length === 0) continue;
2066
+ const value = data[surface];
2067
+ if (!Array.isArray(value) || typeof value[0] !== "number" || typeof value[1] !== "string") continue;
2068
+ const count = value[0];
2069
+ const token = value[1];
2070
+ if (!Number.isFinite(count) || count <= 0 || token.length === 0 || token === surface) continue;
2071
+ entries.set(surface, { token, count: Math.floor(count) });
2072
+ track(surface);
2073
+ }
2074
+ }
2075
+ };
2076
+ }
2077
+
1765
2078
  // src/core/partition/utils.ts
1766
2079
  function getOrCreateFieldNameIndex(table, fieldName) {
1767
2080
  const existing = table.indexMap.get(fieldName);
@@ -1806,6 +2119,7 @@ function tokenizeOptions(options) {
1806
2119
  return {
1807
2120
  stem: true,
1808
2121
  removeStopWords: true,
2122
+ collectSurfaces: options?.collectSurfaces === true,
1809
2123
  stopWordOverride: options?.stopWordOverride,
1810
2124
  customTokenizer: options?.customTokenizer
1811
2125
  };
@@ -1891,32 +2205,177 @@ function computeFacets(state, docIds, config, schema) {
1891
2205
  return result;
1892
2206
  }
1893
2207
 
1894
- // src/core/bitset.ts
1895
- function createBitSet(capacity) {
1896
- return new Uint32Array(capacity + 31 >>> 5 || 1);
1897
- }
1898
- function bitsetSet(bs, index) {
1899
- const word = index >>> 5;
1900
- if (word < bs.length) {
1901
- bs[word] |= 1 << (index & 31);
1902
- }
1903
- }
1904
- function bitsetHas(bs, index) {
1905
- const word = index >>> 5;
1906
- if (word >= bs.length) return false;
1907
- return (bs[word] & 1 << (index & 31)) !== 0;
2208
+ // src/geo/haversine.ts
2209
+ var EARTH_RADIUS_METERS = 63710088e-1;
2210
+ var DEG_TO_RAD = Math.PI / 180;
2211
+ function haversineDistance(lat1, lon1, lat2, lon2) {
2212
+ const phi1 = lat1 * DEG_TO_RAD;
2213
+ const phi2 = lat2 * DEG_TO_RAD;
2214
+ const deltaPhi = (lat2 - lat1) * DEG_TO_RAD;
2215
+ const deltaLambda = (lon2 - lon1) * DEG_TO_RAD;
2216
+ const halfDeltaPhi = Math.sin(deltaPhi / 2);
2217
+ const halfDeltaLambda = Math.sin(deltaLambda / 2);
2218
+ const a = halfDeltaPhi * halfDeltaPhi + Math.cos(phi1) * Math.cos(phi2) * halfDeltaLambda * halfDeltaLambda;
2219
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
2220
+ return EARTH_RADIUS_METERS * c;
1908
2221
  }
1909
- function bitsetAnd(a, b) {
1910
- const len = Math.min(a.length, b.length);
1911
- const result = new Uint32Array(len);
1912
- for (let i = 0; i < len; i++) {
1913
- result[i] = a[i] & b[i];
2222
+
2223
+ // src/geo/polygon.ts
2224
+ function isPointInPolygon(lat, lon, polygon) {
2225
+ if (polygon.length < 3) return false;
2226
+ let inside = false;
2227
+ const n = polygon.length;
2228
+ for (let i = 0, j = n - 1; i < n; j = i++) {
2229
+ const lonI = polygon[i].lon;
2230
+ const latI = polygon[i].lat;
2231
+ const lonJ = polygon[j].lon;
2232
+ const latJ = polygon[j].lat;
2233
+ if (lonI > lon !== lonJ > lon) {
2234
+ const slope = (latJ - latI) / (lonJ - lonI);
2235
+ const intersectLat = latI + slope * (lon - lonI);
2236
+ if (lat < intersectLat) {
2237
+ inside = !inside;
2238
+ }
2239
+ }
1914
2240
  }
1915
- return result;
2241
+ return inside;
1916
2242
  }
1917
- function bitsetOr(a, b) {
1918
- const longer = a.length >= b.length ? a : b;
1919
- const shorter = a.length >= b.length ? b : a;
2243
+
2244
+ // src/geo/vincenty.ts
2245
+ var WGS84_A = 6378137;
2246
+ var WGS84_F = 1 / 298.257223563;
2247
+ var WGS84_B = WGS84_A * (1 - WGS84_F);
2248
+ var DEG_TO_RAD2 = Math.PI / 180;
2249
+ var MAX_ITERATIONS = 200;
2250
+ var CONVERGENCE_THRESHOLD = 1e-12;
2251
+ function vincentyDistance(lat1, lon1, lat2, lon2) {
2252
+ const phi1 = lat1 * DEG_TO_RAD2;
2253
+ const phi2 = lat2 * DEG_TO_RAD2;
2254
+ const L = (lon2 - lon1) * DEG_TO_RAD2;
2255
+ const U1 = Math.atan((1 - WGS84_F) * Math.tan(phi1));
2256
+ const U2 = Math.atan((1 - WGS84_F) * Math.tan(phi2));
2257
+ const sinU1 = Math.sin(U1);
2258
+ const cosU1 = Math.cos(U1);
2259
+ const sinU2 = Math.sin(U2);
2260
+ const cosU2 = Math.cos(U2);
2261
+ let lambda = L;
2262
+ let prevLambda;
2263
+ let sinSigma = 0;
2264
+ let cosSigma = 0;
2265
+ let sigma = 0;
2266
+ let sinAlpha = 0;
2267
+ let cos2Alpha = 0;
2268
+ let cos2SigmaM = 0;
2269
+ let iterations = MAX_ITERATIONS;
2270
+ do {
2271
+ const sinLambda = Math.sin(lambda);
2272
+ const cosLambda = Math.cos(lambda);
2273
+ const term1 = cosU2 * sinLambda;
2274
+ const term2 = cosU1 * sinU2 - sinU1 * cosU2 * cosLambda;
2275
+ sinSigma = Math.sqrt(term1 * term1 + term2 * term2);
2276
+ if (sinSigma === 0) return 0;
2277
+ cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;
2278
+ sigma = Math.atan2(sinSigma, cosSigma);
2279
+ sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma;
2280
+ cos2Alpha = 1 - sinAlpha * sinAlpha;
2281
+ cos2SigmaM = cos2Alpha === 0 ? 0 : cosSigma - 2 * sinU1 * sinU2 / cos2Alpha;
2282
+ const C = WGS84_F / 16 * cos2Alpha * (4 + WGS84_F * (4 - 3 * cos2Alpha));
2283
+ prevLambda = lambda;
2284
+ lambda = L + (1 - C) * WGS84_F * sinAlpha * (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM)));
2285
+ } while (Math.abs(lambda - prevLambda) > CONVERGENCE_THRESHOLD && --iterations > 0);
2286
+ if (iterations === 0) {
2287
+ return haversineDistance(lat1, lon1, lat2, lon2);
2288
+ }
2289
+ const uSq = cos2Alpha * (WGS84_A * WGS84_A - WGS84_B * WGS84_B) / (WGS84_B * WGS84_B);
2290
+ const A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
2291
+ const B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
2292
+ const deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) - B / 6 * cos2SigmaM * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM * cos2SigmaM)));
2293
+ return WGS84_B * A * (sigma - deltaSigma);
2294
+ }
2295
+
2296
+ // src/geo/geo-index.ts
2297
+ function createGeoIndex() {
2298
+ const entries = [];
2299
+ return {
2300
+ get entries() {
2301
+ return entries;
2302
+ },
2303
+ insert(internalId, lat, lon) {
2304
+ entries.push({ lat, lon, docId: internalId });
2305
+ },
2306
+ remove(internalId) {
2307
+ for (let i = entries.length - 1; i >= 0; i--) {
2308
+ if (entries[i].docId === internalId) {
2309
+ entries.splice(i, 1);
2310
+ return;
2311
+ }
2312
+ }
2313
+ },
2314
+ radiusQuery(lat, lon, distanceMeters, inside, highPrecision) {
2315
+ const result = /* @__PURE__ */ new Set();
2316
+ const distanceFn = highPrecision ? vincentyDistance : haversineDistance;
2317
+ for (let i = 0; i < entries.length; i++) {
2318
+ const entry = entries[i];
2319
+ const dist = distanceFn(lat, lon, entry.lat, entry.lon);
2320
+ const withinRadius = dist <= distanceMeters;
2321
+ if (inside ? withinRadius : !withinRadius) {
2322
+ result.add(entry.docId);
2323
+ }
2324
+ }
2325
+ return result;
2326
+ },
2327
+ polygonQuery(points, inside) {
2328
+ const result = /* @__PURE__ */ new Set();
2329
+ for (let i = 0; i < entries.length; i++) {
2330
+ const entry = entries[i];
2331
+ const withinPolygon = isPointInPolygon(entry.lat, entry.lon, points);
2332
+ if (inside ? withinPolygon : !withinPolygon) {
2333
+ result.add(entry.docId);
2334
+ }
2335
+ }
2336
+ return result;
2337
+ },
2338
+ clear() {
2339
+ entries.length = 0;
2340
+ },
2341
+ serialize() {
2342
+ return entries.map((e) => ({ ...e }));
2343
+ },
2344
+ deserialize(data) {
2345
+ entries.length = 0;
2346
+ for (const entry of data) {
2347
+ entries.push({ lat: entry.lat, lon: entry.lon, docId: entry.docId });
2348
+ }
2349
+ }
2350
+ };
2351
+ }
2352
+
2353
+ // src/core/bitset.ts
2354
+ function createBitSet(capacity) {
2355
+ return new Uint32Array(capacity + 31 >>> 5 || 1);
2356
+ }
2357
+ function bitsetSet(bs, index) {
2358
+ const word = index >>> 5;
2359
+ if (word < bs.length) {
2360
+ bs[word] |= 1 << (index & 31);
2361
+ }
2362
+ }
2363
+ function bitsetHas(bs, index) {
2364
+ const word = index >>> 5;
2365
+ if (word >= bs.length) return false;
2366
+ return (bs[word] & 1 << (index & 31)) !== 0;
2367
+ }
2368
+ function bitsetAnd(a, b) {
2369
+ const len = Math.min(a.length, b.length);
2370
+ const result = new Uint32Array(len);
2371
+ for (let i = 0; i < len; i++) {
2372
+ result[i] = a[i] & b[i];
2373
+ }
2374
+ return result;
2375
+ }
2376
+ function bitsetOr(a, b) {
2377
+ const longer = a.length >= b.length ? a : b;
2378
+ const shorter = a.length >= b.length ? b : a;
1920
2379
  const result = new Uint32Array(longer.length);
1921
2380
  for (let i = 0; i < shorter.length; i++) {
1922
2381
  result[i] = longer[i] | shorter[i];
@@ -1952,1197 +2411,1096 @@ function bitsetFromSet(set, capacity) {
1952
2411
  return bs;
1953
2412
  }
1954
2413
 
1955
- // src/filters/combinators.ts
1956
- function applyAndBitset(bitsets) {
1957
- if (bitsets.length === 0) return new Uint32Array(0);
1958
- if (bitsets.length === 1) return bitsets[0];
1959
- let result = bitsets[0];
1960
- for (let i = 1; i < bitsets.length; i++) {
1961
- result = bitsetAnd(result, bitsets[i]);
1962
- }
1963
- return result;
1964
- }
1965
- function applyOrBitset(bitsets) {
1966
- if (bitsets.length === 0) return new Uint32Array(0);
1967
- if (bitsets.length === 1) return bitsets[0];
1968
- let result = bitsets[0];
1969
- for (let i = 1; i < bitsets.length; i++) {
1970
- result = bitsetOr(result, bitsets[i]);
1971
- }
1972
- return result;
1973
- }
1974
- function applyNotBitset(excluded, capacity) {
1975
- return bitsetNot(excluded, capacity);
1976
- }
1977
-
1978
- // src/filters/operators/shared.ts
1979
- var METERS_PER_KM = 1e3;
1980
- var METERS_PER_MI = 1609.344;
1981
- function convertToMeters(distance, unit) {
1982
- if (unit === "km") return distance * METERS_PER_KM;
1983
- if (unit === "mi") return distance * METERS_PER_MI;
1984
- return distance;
1985
- }
1986
- function matchesNumericComparison(value, filter) {
1987
- if (filter.eq !== void 0 && value !== filter.eq) return false;
1988
- if (filter.ne !== void 0 && value === filter.ne) return false;
1989
- if (filter.gt !== void 0 && !(value > filter.gt)) return false;
1990
- if (filter.lt !== void 0 && !(value < filter.lt)) return false;
1991
- if (filter.gte !== void 0 && !(value >= filter.gte)) return false;
1992
- if (filter.lte !== void 0 && !(value <= filter.lte)) return false;
1993
- if (filter.between !== void 0 && !(value >= filter.between[0] && value <= filter.between[1])) return false;
1994
- return true;
1995
- }
1996
-
1997
- // src/filters/operators/bitset.ts
1998
- function resolveBitset(bs) {
1999
- return typeof bs === "function" ? bs() : bs;
2000
- }
2001
- function scanToBitset(docIds, capacity, getValue, predicate) {
2002
- const resolved = resolveBitset(docIds);
2003
- const result = createBitSet(capacity);
2004
- for (let wi = 0; wi < resolved.length; wi++) {
2005
- let word = resolved[wi];
2006
- if (word === 0) continue;
2007
- const base = wi << 5;
2008
- while (word !== 0) {
2009
- const tz = Math.clz32(word & -word) ^ 31;
2010
- const id = base + tz;
2011
- if (predicate(getValue(id))) {
2012
- bitsetSet(result, id);
2414
+ // src/core/field-index/boolean.ts
2415
+ function createBooleanIndex() {
2416
+ const trueDocs = /* @__PURE__ */ new Set();
2417
+ const falseDocs = /* @__PURE__ */ new Set();
2418
+ return {
2419
+ insert(internalId, value) {
2420
+ if (value) trueDocs.add(internalId);
2421
+ else falseDocs.add(internalId);
2422
+ },
2423
+ remove(internalId, value) {
2424
+ if (value) trueDocs.delete(internalId);
2425
+ else falseDocs.delete(internalId);
2426
+ },
2427
+ queryEq(value) {
2428
+ return new Set(value ? trueDocs : falseDocs);
2429
+ },
2430
+ queryNe(value) {
2431
+ return new Set(value ? falseDocs : trueDocs);
2432
+ },
2433
+ getAllDocIds() {
2434
+ const all = new Set(trueDocs);
2435
+ for (const id of falseDocs) all.add(id);
2436
+ return all;
2437
+ },
2438
+ queryEqBitset(value, capacity) {
2439
+ const source = value ? trueDocs : falseDocs;
2440
+ const bs = createBitSet(capacity);
2441
+ for (const id of source) {
2442
+ bitsetSet(bs, id);
2013
2443
  }
2014
- word &= word - 1;
2444
+ return bs;
2445
+ },
2446
+ getAllDocIdsBitset(capacity) {
2447
+ const bs = createBitSet(capacity);
2448
+ for (const id of trueDocs) bitsetSet(bs, id);
2449
+ for (const id of falseDocs) bitsetSet(bs, id);
2450
+ return bs;
2451
+ },
2452
+ count() {
2453
+ return trueDocs.size + falseDocs.size;
2454
+ },
2455
+ clear() {
2456
+ trueDocs.clear();
2457
+ falseDocs.clear();
2458
+ },
2459
+ serialize() {
2460
+ return {
2461
+ trueDocs: Array.from(trueDocs),
2462
+ falseDocs: Array.from(falseDocs)
2463
+ };
2464
+ },
2465
+ deserialize(data) {
2466
+ trueDocs.clear();
2467
+ falseDocs.clear();
2468
+ for (const id of data.trueDocs) trueDocs.add(id);
2469
+ for (const id of data.falseDocs) falseDocs.add(id);
2015
2470
  }
2016
- }
2017
- return result;
2471
+ };
2018
2472
  }
2019
- function applyEqBitset(value, allDocsBitset, capacity, getValue, fieldIndex) {
2020
- if (fieldIndex?.type === "numeric" && typeof value === "number") {
2021
- return fieldIndex.index.eqBitset(value, capacity);
2022
- }
2023
- if (fieldIndex?.type === "boolean" && typeof value === "boolean") {
2024
- return value ? fieldIndex.index.getTrueBitset(capacity) : fieldIndex.index.getFalseBitset(capacity);
2025
- }
2026
- if (fieldIndex?.type === "enum" && typeof value === "string") {
2027
- return fieldIndex.index.getDocIdsBitset(value, capacity);
2028
- }
2029
- return scanToBitset(allDocsBitset, capacity, getValue, (v) => v === value);
2030
- }
2031
- function applyNeBitset(value, allDocsBitset, capacity, getValue, fieldIndex) {
2032
- if (fieldIndex?.type === "numeric" && typeof value === "number") {
2033
- const all = fieldIndex.index.allDocIdsBitset(capacity);
2034
- const eq = fieldIndex.index.eqBitset(value, capacity);
2035
- return bitsetAnd(all, bitsetNot(eq, capacity));
2036
- }
2037
- if (fieldIndex?.type === "boolean" && typeof value === "boolean") {
2038
- return value ? fieldIndex.index.getFalseBitset(capacity) : fieldIndex.index.getTrueBitset(capacity);
2039
- }
2040
- if (fieldIndex?.type === "enum" && typeof value === "string") {
2041
- const all = fieldIndex.index.allDocIdsBitset(capacity);
2042
- const eq = fieldIndex.index.getDocIdsBitset(value, capacity);
2043
- return bitsetAnd(all, bitsetNot(eq, capacity));
2044
- }
2045
- return scanToBitset(allDocsBitset, capacity, getValue, (v) => v !== void 0 && v !== null && v !== value);
2046
- }
2047
- function applyGtBitset(value, allDocsBitset, capacity, getValue, fieldIndex) {
2048
- if (fieldIndex?.type === "numeric") {
2049
- return fieldIndex.index.gtBitset(value, capacity);
2050
- }
2051
- return scanToBitset(allDocsBitset, capacity, getValue, (v) => typeof v === "number" && v > value);
2052
- }
2053
- function applyLtBitset(value, allDocsBitset, capacity, getValue, fieldIndex) {
2054
- if (fieldIndex?.type === "numeric") {
2055
- return fieldIndex.index.ltBitset(value, capacity);
2056
- }
2057
- return scanToBitset(allDocsBitset, capacity, getValue, (v) => typeof v === "number" && v < value);
2058
- }
2059
- function applyGteBitset(value, allDocsBitset, capacity, getValue, fieldIndex) {
2060
- if (fieldIndex?.type === "numeric") {
2061
- return fieldIndex.index.gteBitset(value, capacity);
2473
+
2474
+ // src/core/field-index/enum.ts
2475
+ function createEnumIndex() {
2476
+ const valueMap = /* @__PURE__ */ new Map();
2477
+ function allDocs() {
2478
+ const result = /* @__PURE__ */ new Set();
2479
+ for (const docSet of valueMap.values()) {
2480
+ for (const id of docSet) result.add(id);
2481
+ }
2482
+ return result;
2062
2483
  }
2063
- return scanToBitset(allDocsBitset, capacity, getValue, (v) => typeof v === "number" && v >= value);
2484
+ return {
2485
+ insert(internalId, value) {
2486
+ let docSet = valueMap.get(value);
2487
+ if (!docSet) {
2488
+ docSet = /* @__PURE__ */ new Set();
2489
+ valueMap.set(value, docSet);
2490
+ }
2491
+ docSet.add(internalId);
2492
+ },
2493
+ remove(internalId, value) {
2494
+ const docSet = valueMap.get(value);
2495
+ if (!docSet) return;
2496
+ docSet.delete(internalId);
2497
+ if (docSet.size === 0) valueMap.delete(value);
2498
+ },
2499
+ queryEq(value) {
2500
+ const docSet = valueMap.get(value);
2501
+ return docSet ? new Set(docSet) : /* @__PURE__ */ new Set();
2502
+ },
2503
+ queryNe(value) {
2504
+ const excluded = valueMap.get(value);
2505
+ const result = allDocs();
2506
+ if (excluded) {
2507
+ for (const id of excluded) result.delete(id);
2508
+ }
2509
+ return result;
2510
+ },
2511
+ queryIn(values) {
2512
+ const result = /* @__PURE__ */ new Set();
2513
+ for (const val of values) {
2514
+ const docSet = valueMap.get(val);
2515
+ if (docSet) {
2516
+ for (const id of docSet) result.add(id);
2517
+ }
2518
+ }
2519
+ return result;
2520
+ },
2521
+ queryNin(values) {
2522
+ const excluded = this.queryIn(values);
2523
+ const result = allDocs();
2524
+ for (const id of excluded) result.delete(id);
2525
+ return result;
2526
+ },
2527
+ getAllDocIds() {
2528
+ return allDocs();
2529
+ },
2530
+ queryEqBitset(value, capacity) {
2531
+ const bs = createBitSet(capacity);
2532
+ const docSet = valueMap.get(value);
2533
+ if (docSet) {
2534
+ for (const id of docSet) bitsetSet(bs, id);
2535
+ }
2536
+ return bs;
2537
+ },
2538
+ queryInBitset(values, capacity) {
2539
+ const bs = createBitSet(capacity);
2540
+ for (const val of values) {
2541
+ const docSet = valueMap.get(val);
2542
+ if (docSet) {
2543
+ for (const id of docSet) bitsetSet(bs, id);
2544
+ }
2545
+ }
2546
+ return bs;
2547
+ },
2548
+ getAllDocIdsBitset(capacity) {
2549
+ const bs = createBitSet(capacity);
2550
+ for (const docSet of valueMap.values()) {
2551
+ for (const id of docSet) bitsetSet(bs, id);
2552
+ }
2553
+ return bs;
2554
+ },
2555
+ count() {
2556
+ let total = 0;
2557
+ for (const docSet of valueMap.values()) total += docSet.size;
2558
+ return total;
2559
+ },
2560
+ clear() {
2561
+ valueMap.clear();
2562
+ },
2563
+ serialize() {
2564
+ const result = /* @__PURE__ */ Object.create(null);
2565
+ for (const [value, docSet] of valueMap) {
2566
+ result[value] = Array.from(docSet);
2567
+ }
2568
+ return result;
2569
+ },
2570
+ deserialize(data) {
2571
+ valueMap.clear();
2572
+ for (const value of Object.keys(data)) {
2573
+ valueMap.set(value, new Set(data[value]));
2574
+ }
2575
+ }
2576
+ };
2064
2577
  }
2065
- function applyLteBitset(value, allDocsBitset, capacity, getValue, fieldIndex) {
2066
- if (fieldIndex?.type === "numeric") {
2067
- return fieldIndex.index.lteBitset(value, capacity);
2578
+
2579
+ // src/core/field-index/numeric.ts
2580
+ function lowerBound(entries, value) {
2581
+ let lo = 0;
2582
+ let hi = entries.length;
2583
+ while (lo < hi) {
2584
+ const mid = lo + hi >>> 1;
2585
+ if (entries[mid].value < value) lo = mid + 1;
2586
+ else hi = mid;
2068
2587
  }
2069
- return scanToBitset(allDocsBitset, capacity, getValue, (v) => typeof v === "number" && v <= value);
2588
+ return lo;
2070
2589
  }
2071
- function applyBetweenBitset(range, allDocsBitset, capacity, getValue, fieldIndex) {
2072
- if (fieldIndex?.type === "numeric") {
2073
- return fieldIndex.index.betweenBitset(range[0], range[1], capacity);
2590
+ function upperBound(entries, value) {
2591
+ let lo = 0;
2592
+ let hi = entries.length;
2593
+ while (lo < hi) {
2594
+ const mid = lo + hi >>> 1;
2595
+ if (entries[mid].value <= value) lo = mid + 1;
2596
+ else hi = mid;
2074
2597
  }
2075
- return scanToBitset(allDocsBitset, capacity, getValue, (v) => typeof v === "number" && v >= range[0] && v <= range[1]);
2598
+ return lo;
2076
2599
  }
2077
- function applyInBitset(values, allDocsBitset, capacity, getValue, fieldIndex) {
2078
- if (fieldIndex?.type === "enum") {
2079
- return fieldIndex.index.getDocIdsInBitset(values, capacity);
2600
+ function collectDocIds(entries, from, to) {
2601
+ const result = /* @__PURE__ */ new Set();
2602
+ for (let i = from; i < to; i++) {
2603
+ result.add(entries[i].docId);
2080
2604
  }
2081
- const valSet = new Set(values);
2082
- return scanToBitset(allDocsBitset, capacity, getValue, (v) => typeof v === "string" && valSet.has(v));
2605
+ return result;
2083
2606
  }
2084
- function applyNinBitset(values, allDocsBitset, capacity, getValue, fieldIndex) {
2085
- if (fieldIndex?.type === "enum") {
2086
- const all = fieldIndex.index.allDocIdsBitset(capacity);
2087
- const matched = fieldIndex.index.getDocIdsInBitset(values, capacity);
2088
- return bitsetAnd(all, bitsetNot(matched, capacity));
2607
+ function collectDocIdsBitset(entries, from, to, capacity) {
2608
+ const bs = createBitSet(capacity);
2609
+ for (let i = from; i < to; i++) {
2610
+ bitsetSet(bs, entries[i].docId);
2089
2611
  }
2090
- const valSet = new Set(values);
2091
- return scanToBitset(
2092
- allDocsBitset,
2093
- capacity,
2094
- getValue,
2095
- (v) => v !== void 0 && v !== null && typeof v === "string" && !valSet.has(v)
2096
- );
2097
- }
2098
- function applyStartsWithBitset(prefix, allDocsBitset, capacity, getValue) {
2099
- return scanToBitset(allDocsBitset, capacity, getValue, (v) => typeof v === "string" && v.startsWith(prefix));
2100
- }
2101
- function applyEndsWithBitset(suffix, allDocsBitset, capacity, getValue) {
2102
- return scanToBitset(allDocsBitset, capacity, getValue, (v) => typeof v === "string" && v.endsWith(suffix));
2103
- }
2104
- function applyContainsAllBitset(values, allDocsBitset, capacity, getValue) {
2105
- return scanToBitset(allDocsBitset, capacity, getValue, (v) => {
2106
- if (!Array.isArray(v)) return false;
2107
- const arr = v;
2108
- for (const val of values) {
2109
- if (!arr.includes(val)) return false;
2110
- }
2111
- return true;
2112
- });
2612
+ return bs;
2113
2613
  }
2114
- function applyMatchesAnyBitset(values, allDocsBitset, capacity, getValue) {
2115
- const valSet = new Set(values);
2116
- return scanToBitset(allDocsBitset, capacity, getValue, (v) => {
2117
- if (!Array.isArray(v)) return false;
2118
- for (const item of v) {
2119
- if (valSet.has(item)) return true;
2614
+ function createNumericIndex() {
2615
+ let entries = [];
2616
+ let sorted = true;
2617
+ function ensureSorted() {
2618
+ if (!sorted) {
2619
+ entries.sort((a, b) => a.value - b.value);
2620
+ sorted = true;
2120
2621
  }
2121
- return false;
2122
- });
2123
- }
2124
- function applySizeBitset(sizeFilter, allDocsBitset, capacity, getValue) {
2125
- return scanToBitset(
2126
- allDocsBitset,
2127
- capacity,
2128
- getValue,
2129
- (v) => Array.isArray(v) && matchesNumericComparison(v.length, sizeFilter)
2130
- );
2131
- }
2132
- function applyExistsBitset(allDocsBitset, capacity, getValue) {
2133
- return scanToBitset(allDocsBitset, capacity, getValue, (v) => v !== void 0 && v !== null);
2134
- }
2135
- function applyNotExistsBitset(allDocsBitset, capacity, getValue) {
2136
- return scanToBitset(allDocsBitset, capacity, getValue, (v) => v === void 0 || v === null);
2137
- }
2138
- function applyIsEmptyBitset(allDocsBitset, capacity, getValue) {
2139
- return scanToBitset(allDocsBitset, capacity, getValue, (v) => {
2140
- if (v === void 0 || v === null) return true;
2141
- if (typeof v === "string" && v === "") return true;
2142
- if (Array.isArray(v) && v.length === 0) return true;
2143
- return false;
2144
- });
2145
- }
2146
- function applyIsNotEmptyBitset(allDocsBitset, capacity, getValue) {
2147
- return scanToBitset(allDocsBitset, capacity, getValue, (v) => {
2148
- if (v === void 0 || v === null) return false;
2149
- if (typeof v === "string" && v === "") return false;
2150
- if (Array.isArray(v) && v.length === 0) return false;
2151
- return true;
2152
- });
2153
- }
2154
- function applyGeoRadiusBitset(filter, geoIndex, capacity) {
2155
- const distanceMeters = convertToMeters(filter.distance, filter.unit);
2156
- const setResult = geoIndex.radiusQuery(
2157
- filter.lat,
2158
- filter.lon,
2159
- distanceMeters,
2160
- filter.inside ?? true,
2161
- filter.highPrecision ?? false
2162
- );
2163
- return bitsetFromSet(setResult, capacity);
2164
- }
2165
- function applyGeoPolygonBitset(filter, geoIndex, capacity) {
2166
- const setResult = geoIndex.polygonQuery(filter.points, filter.inside ?? true);
2167
- return bitsetFromSet(setResult, capacity);
2622
+ }
2623
+ return {
2624
+ insert(internalId, value) {
2625
+ entries.push({ value, docId: internalId });
2626
+ sorted = false;
2627
+ },
2628
+ remove(internalId, value) {
2629
+ ensureSorted();
2630
+ const start = lowerBound(entries, value);
2631
+ const end = upperBound(entries, value);
2632
+ for (let i = start; i < end; i++) {
2633
+ if (entries[i].docId === internalId) {
2634
+ entries.splice(i, 1);
2635
+ return;
2636
+ }
2637
+ }
2638
+ },
2639
+ queryEq(value) {
2640
+ ensureSorted();
2641
+ return collectDocIds(entries, lowerBound(entries, value), upperBound(entries, value));
2642
+ },
2643
+ queryNe(value) {
2644
+ ensureSorted();
2645
+ const excluded = this.queryEq(value);
2646
+ const result = /* @__PURE__ */ new Set();
2647
+ for (const entry of entries) {
2648
+ if (!excluded.has(entry.docId)) result.add(entry.docId);
2649
+ }
2650
+ return result;
2651
+ },
2652
+ queryGt(value) {
2653
+ ensureSorted();
2654
+ return collectDocIds(entries, upperBound(entries, value), entries.length);
2655
+ },
2656
+ queryGte(value) {
2657
+ ensureSorted();
2658
+ return collectDocIds(entries, lowerBound(entries, value), entries.length);
2659
+ },
2660
+ queryLt(value) {
2661
+ ensureSorted();
2662
+ return collectDocIds(entries, 0, lowerBound(entries, value));
2663
+ },
2664
+ queryLte(value) {
2665
+ ensureSorted();
2666
+ return collectDocIds(entries, 0, upperBound(entries, value));
2667
+ },
2668
+ queryBetween(min, max) {
2669
+ ensureSorted();
2670
+ return collectDocIds(entries, lowerBound(entries, min), upperBound(entries, max));
2671
+ },
2672
+ getAllDocIds() {
2673
+ return collectDocIds(entries, 0, entries.length);
2674
+ },
2675
+ queryEqBitset(value, capacity) {
2676
+ ensureSorted();
2677
+ return collectDocIdsBitset(entries, lowerBound(entries, value), upperBound(entries, value), capacity);
2678
+ },
2679
+ queryGtBitset(value, capacity) {
2680
+ ensureSorted();
2681
+ return collectDocIdsBitset(entries, upperBound(entries, value), entries.length, capacity);
2682
+ },
2683
+ queryGteBitset(value, capacity) {
2684
+ ensureSorted();
2685
+ return collectDocIdsBitset(entries, lowerBound(entries, value), entries.length, capacity);
2686
+ },
2687
+ queryLtBitset(value, capacity) {
2688
+ ensureSorted();
2689
+ return collectDocIdsBitset(entries, 0, lowerBound(entries, value), capacity);
2690
+ },
2691
+ queryLteBitset(value, capacity) {
2692
+ ensureSorted();
2693
+ return collectDocIdsBitset(entries, 0, upperBound(entries, value), capacity);
2694
+ },
2695
+ queryBetweenBitset(min, max, capacity) {
2696
+ ensureSorted();
2697
+ return collectDocIdsBitset(entries, lowerBound(entries, min), upperBound(entries, max), capacity);
2698
+ },
2699
+ getAllDocIdsBitset(capacity) {
2700
+ return collectDocIdsBitset(entries, 0, entries.length, capacity);
2701
+ },
2702
+ count() {
2703
+ return entries.length;
2704
+ },
2705
+ clear() {
2706
+ entries = [];
2707
+ sorted = true;
2708
+ },
2709
+ serialize() {
2710
+ ensureSorted();
2711
+ return entries.slice();
2712
+ },
2713
+ deserialize(data) {
2714
+ entries = data.slice();
2715
+ sorted = true;
2716
+ }
2717
+ };
2168
2718
  }
2169
2719
 
2170
- // src/filters/evaluator.ts
2171
- function evaluateFilters(expression, context) {
2172
- const bitsets = [];
2173
- if (expression.fields) {
2174
- for (const [fieldPath, filter] of Object.entries(expression.fields)) {
2175
- bitsets.push(evaluateFieldFilter(fieldPath, filter, context));
2176
- }
2177
- }
2178
- if (expression.and?.length) {
2179
- const andBitsets = expression.and.map((expr) => evaluateFilters(expr, context));
2180
- bitsets.push(applyAndBitset(andBitsets));
2181
- }
2182
- if (expression.or?.length) {
2183
- const orBitsets = expression.or.map((expr) => evaluateFilters(expr, context));
2184
- bitsets.push(applyOrBitset(orBitsets));
2185
- }
2186
- if (expression.not) {
2187
- const excluded = evaluateFilters(expression.not, context);
2188
- const universe = context.allDocIdsBitset;
2189
- bitsets.push(applyAndBitset([universe, applyNotBitset(excluded, context.capacity)]));
2720
+ // src/core/partition/indexing.ts
2721
+ function ensureFieldIndex(state, fieldPath, fieldType) {
2722
+ if ((fieldType === "number" || fieldType === "number[]") && !state.numericIndexes.has(fieldPath)) {
2723
+ state.numericIndexes.set(fieldPath, createNumericIndex());
2724
+ } else if ((fieldType === "boolean" || fieldType === "boolean[]") && !state.booleanIndexes.has(fieldPath)) {
2725
+ state.booleanIndexes.set(fieldPath, createBooleanIndex());
2726
+ } else if ((fieldType === "enum" || fieldType === "enum[]") && !state.enumIndexes.has(fieldPath)) {
2727
+ state.enumIndexes.set(fieldPath, createEnumIndex());
2728
+ } else if (fieldType === "geopoint" && !state.geoIndexes.has(fieldPath)) {
2729
+ state.geoIndexes.set(fieldPath, createGeoIndex());
2190
2730
  }
2191
- if (bitsets.length === 0) return context.allDocIdsBitset;
2192
- if (bitsets.length === 1) return bitsets[0];
2193
- return applyAndBitset(bitsets);
2194
2731
  }
2195
- function evaluateFieldFilter(fieldPath, filter, context) {
2196
- const fieldIndex = context.fieldIndexes[fieldPath];
2197
- const getValue = (internalId) => context.getFieldValue(internalId, fieldPath);
2198
- const f = filter;
2199
- const bitsets = [];
2200
- const { capacity } = context;
2201
- let allDocsBitset = null;
2202
- const getAllDocsBitset = () => {
2203
- if (allDocsBitset === null) {
2204
- allDocsBitset = context.allDocIdsBitset;
2205
- }
2206
- return allDocsBitset;
2207
- };
2208
- if ("radius" in f && f.radius) {
2209
- const geoIndex = resolveGeoIndex(fieldPath, fieldIndex);
2210
- bitsets.push(applyGeoRadiusBitset(filter.radius, geoIndex, capacity));
2211
- }
2212
- if ("polygon" in f && f.polygon) {
2213
- const geoIndex = resolveGeoIndex(fieldPath, fieldIndex);
2214
- bitsets.push(applyGeoPolygonBitset(filter.polygon, geoIndex, capacity));
2215
- }
2216
- if ("eq" in f && f.eq !== void 0) {
2217
- bitsets.push(applyEqBitset(f.eq, getAllDocsBitset, capacity, getValue, fieldIndex));
2218
- }
2219
- if ("ne" in f && f.ne !== void 0) {
2220
- bitsets.push(applyNeBitset(f.ne, getAllDocsBitset, capacity, getValue, fieldIndex));
2221
- }
2222
- if ("gt" in f && f.gt !== void 0) {
2223
- bitsets.push(applyGtBitset(f.gt, getAllDocsBitset, capacity, getValue, fieldIndex));
2224
- }
2225
- if ("lt" in f && f.lt !== void 0) {
2226
- bitsets.push(applyLtBitset(f.lt, getAllDocsBitset, capacity, getValue, fieldIndex));
2227
- }
2228
- if ("gte" in f && f.gte !== void 0) {
2229
- bitsets.push(applyGteBitset(f.gte, getAllDocsBitset, capacity, getValue, fieldIndex));
2230
- }
2231
- if ("lte" in f && f.lte !== void 0) {
2232
- bitsets.push(applyLteBitset(f.lte, getAllDocsBitset, capacity, getValue, fieldIndex));
2233
- }
2234
- if ("between" in f && f.between !== void 0) {
2235
- bitsets.push(applyBetweenBitset(f.between, getAllDocsBitset, capacity, getValue, fieldIndex));
2236
- }
2237
- if ("in" in f && f.in !== void 0) {
2238
- bitsets.push(applyInBitset(f.in, getAllDocsBitset, capacity, getValue, fieldIndex));
2239
- }
2240
- if ("nin" in f && f.nin !== void 0) {
2241
- bitsets.push(applyNinBitset(f.nin, getAllDocsBitset, capacity, getValue, fieldIndex));
2242
- }
2243
- if ("startsWith" in f && f.startsWith !== void 0) {
2244
- bitsets.push(applyStartsWithBitset(f.startsWith, getAllDocsBitset, capacity, getValue));
2245
- }
2246
- if ("endsWith" in f && f.endsWith !== void 0) {
2247
- bitsets.push(applyEndsWithBitset(f.endsWith, getAllDocsBitset, capacity, getValue));
2248
- }
2249
- if ("containsAll" in f && f.containsAll !== void 0) {
2250
- bitsets.push(
2251
- applyContainsAllBitset(f.containsAll, getAllDocsBitset, capacity, getValue)
2252
- );
2253
- }
2254
- if ("matchesAny" in f && f.matchesAny !== void 0) {
2255
- bitsets.push(
2256
- applyMatchesAnyBitset(f.matchesAny, getAllDocsBitset, capacity, getValue)
2257
- );
2258
- }
2259
- if ("size" in f && f.size !== void 0) {
2260
- bitsets.push(applySizeBitset(f.size, getAllDocsBitset, capacity, getValue));
2261
- }
2262
- if ("exists" in f && f.exists !== void 0) {
2263
- bitsets.push(
2264
- f.exists ? applyExistsBitset(getAllDocsBitset, capacity, getValue) : applyNotExistsBitset(getAllDocsBitset, capacity, getValue)
2265
- );
2732
+ function getOrCreateNumericIndex(state, fieldPath) {
2733
+ let idx = state.numericIndexes.get(fieldPath);
2734
+ if (!idx) {
2735
+ idx = createNumericIndex();
2736
+ state.numericIndexes.set(fieldPath, idx);
2266
2737
  }
2267
- if ("notExists" in f && f.notExists !== void 0) {
2268
- bitsets.push(
2269
- f.notExists ? applyNotExistsBitset(getAllDocsBitset, capacity, getValue) : applyExistsBitset(getAllDocsBitset, capacity, getValue)
2270
- );
2738
+ return idx;
2739
+ }
2740
+ function getOrCreateBooleanIndex(state, fieldPath) {
2741
+ let idx = state.booleanIndexes.get(fieldPath);
2742
+ if (!idx) {
2743
+ idx = createBooleanIndex();
2744
+ state.booleanIndexes.set(fieldPath, idx);
2271
2745
  }
2272
- if ("isEmpty" in f && f.isEmpty !== void 0) {
2273
- bitsets.push(
2274
- f.isEmpty ? applyIsEmptyBitset(getAllDocsBitset, capacity, getValue) : applyIsNotEmptyBitset(getAllDocsBitset, capacity, getValue)
2275
- );
2746
+ return idx;
2747
+ }
2748
+ function resolveInternalId(state, docId) {
2749
+ const internalId = state.docStore.getInternalId(docId);
2750
+ if (internalId === void 0) {
2751
+ throw new Error(`Internal ID not found for document "${docId}"`);
2276
2752
  }
2277
- if ("isNotEmpty" in f && f.isNotEmpty !== void 0) {
2278
- bitsets.push(
2279
- f.isNotEmpty ? applyIsNotEmptyBitset(getAllDocsBitset, capacity, getValue) : applyIsEmptyBitset(getAllDocsBitset, capacity, getValue)
2280
- );
2753
+ return internalId;
2754
+ }
2755
+ function countSurface(counts, surface, token) {
2756
+ if (surface === void 0 || surface.length === 0) return;
2757
+ const existing = counts.get(surface);
2758
+ if (existing) {
2759
+ existing.count++;
2760
+ } else {
2761
+ counts.set(surface, { token, count: 1 });
2281
2762
  }
2282
- if (bitsets.length === 0) return context.allDocIdsBitset;
2283
- if (bitsets.length === 1) return bitsets[0];
2284
- return applyAndBitset(bitsets);
2285
2763
  }
2286
- function resolveGeoIndex(fieldPath, fieldIndex) {
2287
- if (fieldIndex?.type !== "geopoint") {
2288
- throw new NarsilError(
2289
- ErrorCodes.SEARCH_INVALID_FILTER,
2290
- `Field "${fieldPath}" requires a geopoint index for geo filters`,
2291
- { fieldPath }
2292
- );
2764
+ function applySurfaceCounts(state, counts, direction) {
2765
+ for (const [surface, entry] of counts) {
2766
+ if (direction === 1) {
2767
+ state.surfaceRegistry.add(surface, entry.token, entry.count);
2768
+ } else {
2769
+ state.surfaceRegistry.subtract(surface, entry.count);
2770
+ }
2293
2771
  }
2294
- return fieldIndex.index;
2295
2772
  }
2296
-
2297
- // src/core/partition/filters.ts
2298
- function buildFilterContext(state, schema) {
2299
- const flat = getFlatSchema(state, schema);
2300
- const fieldIndexes = {};
2301
- const capacity = state.docStore.internalIdCapacity();
2302
- for (const [fieldPath, fieldType] of Object.entries(flat)) {
2303
- if (fieldType === "number" || fieldType === "number[]") {
2304
- const numIdx = state.numericIndexes.get(fieldPath);
2305
- if (numIdx) {
2306
- fieldIndexes[fieldPath] = {
2307
- type: "numeric",
2308
- index: {
2309
- eq: (v) => numIdx.queryEq(v),
2310
- gt: (v) => numIdx.queryGt(v),
2311
- gte: (v) => numIdx.queryGte(v),
2312
- lt: (v) => numIdx.queryLt(v),
2313
- lte: (v) => numIdx.queryLte(v),
2314
- between: (min, max) => numIdx.queryBetween(min, max),
2315
- allDocIds: () => numIdx.getAllDocIds(),
2316
- eqBitset: (v, cap) => numIdx.queryEqBitset(v, cap),
2317
- gtBitset: (v, cap) => numIdx.queryGtBitset(v, cap),
2318
- gteBitset: (v, cap) => numIdx.queryGteBitset(v, cap),
2319
- ltBitset: (v, cap) => numIdx.queryLtBitset(v, cap),
2320
- lteBitset: (v, cap) => numIdx.queryLteBitset(v, cap),
2321
- betweenBitset: (min, max, cap) => numIdx.queryBetweenBitset(min, max, cap),
2322
- allDocIdsBitset: (cap) => numIdx.getAllDocIdsBitset(cap)
2323
- }
2324
- };
2325
- }
2326
- } else if (fieldType === "boolean" || fieldType === "boolean[]") {
2327
- const boolIdx = state.booleanIndexes.get(fieldPath);
2328
- if (boolIdx) {
2329
- fieldIndexes[fieldPath] = {
2330
- type: "boolean",
2331
- index: {
2332
- getTrue: () => boolIdx.queryEq(true),
2333
- getFalse: () => boolIdx.queryEq(false),
2334
- allDocIds: () => boolIdx.getAllDocIds(),
2335
- getTrueBitset: (cap) => boolIdx.queryEqBitset(true, cap),
2336
- getFalseBitset: (cap) => boolIdx.queryEqBitset(false, cap),
2337
- allDocIdsBitset: (cap) => boolIdx.getAllDocIdsBitset(cap)
2338
- }
2339
- };
2340
- }
2341
- } else if (fieldType === "enum" || fieldType === "enum[]") {
2342
- const enumIdx = state.enumIndexes.get(fieldPath);
2343
- if (enumIdx) {
2344
- fieldIndexes[fieldPath] = {
2345
- type: "enum",
2346
- index: {
2347
- getDocIds: (v) => enumIdx.queryEq(v),
2348
- allDocIds: () => enumIdx.getAllDocIds(),
2349
- getDocIdsBitset: (v, cap) => enumIdx.queryEqBitset(v, cap),
2350
- getDocIdsInBitset: (values, cap) => enumIdx.queryInBitset(values, cap),
2351
- allDocIdsBitset: (cap) => enumIdx.getAllDocIdsBitset(cap)
2352
- }
2353
- };
2354
- }
2355
- } else if (fieldType === "geopoint") {
2356
- const geoIdx = state.geoIndexes.get(fieldPath);
2357
- if (geoIdx) {
2358
- fieldIndexes[fieldPath] = {
2359
- type: "geopoint",
2360
- index: geoIdx
2361
- };
2773
+ function indexStringField(state, internalId, fieldPath, text, language, options, fieldLengths, tokensByField) {
2774
+ const fieldTokenList = [];
2775
+ const fieldNameIndex = getOrCreateFieldNameIndex(state.fieldNameTable, fieldPath);
2776
+ const opts = tokenizeOptions(options);
2777
+ const surfaceCounts = /* @__PURE__ */ new Map();
2778
+ if (state.trackPositions) {
2779
+ const tokenFreqs = /* @__PURE__ */ new Map();
2780
+ for (const t of tokenizeIterator(text, language, opts)) {
2781
+ const existing = tokenFreqs.get(t.token);
2782
+ if (existing) {
2783
+ existing.count++;
2784
+ existing.positions.push(t.position);
2785
+ } else {
2786
+ tokenFreqs.set(t.token, { count: 1, positions: [t.position] });
2362
2787
  }
2788
+ fieldTokenList.push(t.token);
2789
+ countSurface(surfaceCounts, t.surface, t.token);
2790
+ }
2791
+ for (const [token, freq] of tokenFreqs) {
2792
+ state.invertedIdx.insert(token, internalId, freq.count, fieldNameIndex, freq.positions);
2793
+ }
2794
+ } else {
2795
+ const tokenCounts = /* @__PURE__ */ new Map();
2796
+ for (const t of tokenizeIterator(text, language, opts)) {
2797
+ tokenCounts.set(t.token, (tokenCounts.get(t.token) ?? 0) + 1);
2798
+ fieldTokenList.push(t.token);
2799
+ countSurface(surfaceCounts, t.surface, t.token);
2800
+ }
2801
+ for (const [token, count] of tokenCounts) {
2802
+ state.invertedIdx.insert(token, internalId, count, fieldNameIndex, null);
2363
2803
  }
2364
2804
  }
2365
- let cachedAllDocIds = null;
2366
- let cachedAllDocIdsBitset = null;
2367
- return {
2368
- fieldIndexes,
2369
- getFieldValue: (internalId, fieldPath) => getFieldValueByInternalId(state.docStore, internalId, fieldPath),
2370
- get allDocIds() {
2371
- if (!cachedAllDocIds) {
2372
- cachedAllDocIds = getAllInternalDocIds(state.docStore);
2373
- }
2374
- return cachedAllDocIds;
2375
- },
2376
- capacity,
2377
- get allDocIdsBitset() {
2378
- if (!cachedAllDocIdsBitset) {
2379
- cachedAllDocIdsBitset = bitsetFromSet(this.allDocIds, capacity);
2805
+ applySurfaceCounts(state, surfaceCounts, 1);
2806
+ fieldLengths[fieldPath] = fieldTokenList.length;
2807
+ tokensByField[fieldPath] = fieldTokenList;
2808
+ }
2809
+ function indexStringArrayField(state, internalId, fieldPath, arr, language, options, fieldLengths, tokensByField) {
2810
+ const fieldTokenList = [];
2811
+ const fieldNameIndex = getOrCreateFieldNameIndex(state.fieldNameTable, fieldPath);
2812
+ const opts = tokenizeOptions(options);
2813
+ const surfaceCounts = /* @__PURE__ */ new Map();
2814
+ if (state.trackPositions) {
2815
+ const tokenFreqs = /* @__PURE__ */ new Map();
2816
+ let positionOffset = 0;
2817
+ for (const item of arr) {
2818
+ let itemTokenCount = 0;
2819
+ for (const t of tokenizeIterator(item, language, opts)) {
2820
+ const existing = tokenFreqs.get(t.token);
2821
+ if (existing) {
2822
+ existing.count++;
2823
+ existing.positions.push(positionOffset + t.position);
2824
+ } else {
2825
+ tokenFreqs.set(t.token, { count: 1, positions: [positionOffset + t.position] });
2826
+ }
2827
+ fieldTokenList.push(t.token);
2828
+ countSurface(surfaceCounts, t.surface, t.token);
2829
+ itemTokenCount++;
2380
2830
  }
2381
- return cachedAllDocIdsBitset;
2831
+ positionOffset += itemTokenCount;
2382
2832
  }
2383
- };
2384
- }
2385
- function applyPartitionFilters(state, filters, schema) {
2386
- const context = buildFilterContext(state, schema);
2387
- const resultBitset = evaluateFilters(filters, context);
2388
- const resolver = state.docStore.resolver();
2389
- const externalResult = /* @__PURE__ */ new Set();
2390
- for (let wi = 0; wi < resultBitset.length; wi++) {
2391
- let word = resultBitset[wi];
2392
- if (word === 0) continue;
2393
- const base = wi << 5;
2394
- while (word !== 0) {
2395
- const tz = Math.clz32(word & -word) ^ 31;
2396
- const internalId = base + tz;
2397
- const externalId = resolver.toExternal(internalId);
2398
- if (externalId !== void 0) {
2399
- externalResult.add(externalId);
2833
+ fieldLengths[fieldPath] = fieldTokenList.length;
2834
+ for (const [token, freq] of tokenFreqs) {
2835
+ state.invertedIdx.insert(token, internalId, freq.count, fieldNameIndex, freq.positions);
2836
+ }
2837
+ } else {
2838
+ const tokenCounts = /* @__PURE__ */ new Map();
2839
+ for (const item of arr) {
2840
+ for (const t of tokenizeIterator(item, language, opts)) {
2841
+ tokenCounts.set(t.token, (tokenCounts.get(t.token) ?? 0) + 1);
2842
+ fieldTokenList.push(t.token);
2843
+ countSurface(surfaceCounts, t.surface, t.token);
2400
2844
  }
2401
- word &= word - 1;
2845
+ }
2846
+ fieldLengths[fieldPath] = fieldTokenList.length;
2847
+ for (const [token, count] of tokenCounts) {
2848
+ state.invertedIdx.insert(token, internalId, count, fieldNameIndex, null);
2402
2849
  }
2403
2850
  }
2404
- return externalResult;
2405
- }
2406
- function applyPartitionFiltersBitset(state, filters, schema) {
2407
- const context = buildFilterContext(state, schema);
2408
- return evaluateFilters(filters, context);
2409
- }
2410
-
2411
- // src/geo/haversine.ts
2412
- var EARTH_RADIUS_METERS = 63710088e-1;
2413
- var DEG_TO_RAD = Math.PI / 180;
2414
- function haversineDistance(lat1, lon1, lat2, lon2) {
2415
- const phi1 = lat1 * DEG_TO_RAD;
2416
- const phi2 = lat2 * DEG_TO_RAD;
2417
- const deltaPhi = (lat2 - lat1) * DEG_TO_RAD;
2418
- const deltaLambda = (lon2 - lon1) * DEG_TO_RAD;
2419
- const halfDeltaPhi = Math.sin(deltaPhi / 2);
2420
- const halfDeltaLambda = Math.sin(deltaLambda / 2);
2421
- const a = halfDeltaPhi * halfDeltaPhi + Math.cos(phi1) * Math.cos(phi2) * halfDeltaLambda * halfDeltaLambda;
2422
- const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
2423
- return EARTH_RADIUS_METERS * c;
2851
+ applySurfaceCounts(state, surfaceCounts, 1);
2852
+ tokensByField[fieldPath] = fieldTokenList;
2424
2853
  }
2425
-
2426
- // src/geo/polygon.ts
2427
- function isPointInPolygon(lat, lon, polygon) {
2428
- if (polygon.length < 3) return false;
2429
- let inside = false;
2430
- const n = polygon.length;
2431
- for (let i = 0, j = n - 1; i < n; j = i++) {
2432
- const lonI = polygon[i].lon;
2433
- const latI = polygon[i].lat;
2434
- const lonJ = polygon[j].lon;
2435
- const latJ = polygon[j].lat;
2436
- if (lonI > lon !== lonJ > lon) {
2437
- const slope = (latJ - latI) / (lonJ - lonI);
2438
- const intersectLat = latI + slope * (lon - lonI);
2439
- if (lat < intersectLat) {
2440
- inside = !inside;
2854
+ function indexDocument(state, docId, document, flatSchema, language, options) {
2855
+ const fieldLengths = {};
2856
+ const tokensByField = {};
2857
+ const internalId = resolveInternalId(state, docId);
2858
+ for (const [fieldPath, fieldType] of Object.entries(flatSchema)) {
2859
+ const value = getNestedValue(document, fieldPath);
2860
+ if (value === void 0 || value === null) continue;
2861
+ ensureFieldIndex(state, fieldPath, fieldType);
2862
+ if (fieldType === "string") {
2863
+ indexStringField(state, internalId, fieldPath, value, language, options, fieldLengths, tokensByField);
2864
+ } else if (fieldType === "number") {
2865
+ getOrCreateNumericIndex(state, fieldPath).insert(internalId, value);
2866
+ } else if (fieldType === "boolean") {
2867
+ getOrCreateBooleanIndex(state, fieldPath).insert(internalId, value);
2868
+ } else if (fieldType === "enum") {
2869
+ state.enumIndexes.get(fieldPath)?.insert(internalId, value);
2870
+ } else if (fieldType === "geopoint") {
2871
+ const geo = value;
2872
+ state.geoIndexes.get(fieldPath)?.insert(internalId, geo.lat, geo.lon);
2873
+ } else if (fieldType === "string[]") {
2874
+ indexStringArrayField(
2875
+ state,
2876
+ internalId,
2877
+ fieldPath,
2878
+ value,
2879
+ language,
2880
+ options,
2881
+ fieldLengths,
2882
+ tokensByField
2883
+ );
2884
+ } else if (fieldType === "number[]") {
2885
+ const numIdx = getOrCreateNumericIndex(state, fieldPath);
2886
+ for (const num of value) numIdx.insert(internalId, num);
2887
+ } else if (fieldType === "boolean[]") {
2888
+ const boolIdx = getOrCreateBooleanIndex(state, fieldPath);
2889
+ for (const b of value) boolIdx.insert(internalId, b);
2890
+ } else if (fieldType === "enum[]") {
2891
+ const enumIdx = state.enumIndexes.get(fieldPath);
2892
+ if (enumIdx) {
2893
+ for (const e of value) enumIdx.insert(internalId, e);
2441
2894
  }
2442
2895
  }
2443
2896
  }
2444
- return inside;
2445
- }
2446
-
2447
- // src/geo/vincenty.ts
2448
- var WGS84_A = 6378137;
2449
- var WGS84_F = 1 / 298.257223563;
2450
- var WGS84_B = WGS84_A * (1 - WGS84_F);
2451
- var DEG_TO_RAD2 = Math.PI / 180;
2452
- var MAX_ITERATIONS = 200;
2453
- var CONVERGENCE_THRESHOLD = 1e-12;
2454
- function vincentyDistance(lat1, lon1, lat2, lon2) {
2455
- const phi1 = lat1 * DEG_TO_RAD2;
2456
- const phi2 = lat2 * DEG_TO_RAD2;
2457
- const L = (lon2 - lon1) * DEG_TO_RAD2;
2458
- const U1 = Math.atan((1 - WGS84_F) * Math.tan(phi1));
2459
- const U2 = Math.atan((1 - WGS84_F) * Math.tan(phi2));
2460
- const sinU1 = Math.sin(U1);
2461
- const cosU1 = Math.cos(U1);
2462
- const sinU2 = Math.sin(U2);
2463
- const cosU2 = Math.cos(U2);
2464
- let lambda = L;
2465
- let prevLambda;
2466
- let sinSigma = 0;
2467
- let cosSigma = 0;
2468
- let sigma = 0;
2469
- let sinAlpha = 0;
2470
- let cos2Alpha = 0;
2471
- let cos2SigmaM = 0;
2472
- let iterations = MAX_ITERATIONS;
2473
- do {
2474
- const sinLambda = Math.sin(lambda);
2475
- const cosLambda = Math.cos(lambda);
2476
- const term1 = cosU2 * sinLambda;
2477
- const term2 = cosU1 * sinU2 - sinU1 * cosU2 * cosLambda;
2478
- sinSigma = Math.sqrt(term1 * term1 + term2 * term2);
2479
- if (sinSigma === 0) return 0;
2480
- cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;
2481
- sigma = Math.atan2(sinSigma, cosSigma);
2482
- sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma;
2483
- cos2Alpha = 1 - sinAlpha * sinAlpha;
2484
- cos2SigmaM = cos2Alpha === 0 ? 0 : cosSigma - 2 * sinU1 * sinU2 / cos2Alpha;
2485
- const C = WGS84_F / 16 * cos2Alpha * (4 + WGS84_F * (4 - 3 * cos2Alpha));
2486
- prevLambda = lambda;
2487
- lambda = L + (1 - C) * WGS84_F * sinAlpha * (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM)));
2488
- } while (Math.abs(lambda - prevLambda) > CONVERGENCE_THRESHOLD && --iterations > 0);
2489
- if (iterations === 0) {
2490
- return haversineDistance(lat1, lon1, lat2, lon2);
2491
- }
2492
- const uSq = cos2Alpha * (WGS84_A * WGS84_A - WGS84_B * WGS84_B) / (WGS84_B * WGS84_B);
2493
- const A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
2494
- const B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
2495
- const deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) - B / 6 * cos2SigmaM * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM * cos2SigmaM)));
2496
- return WGS84_B * A * (sigma - deltaSigma);
2897
+ return { fieldLengths, tokensByField };
2497
2898
  }
2498
-
2499
- // src/geo/geo-index.ts
2500
- function createGeoIndex() {
2501
- const entries = [];
2502
- return {
2503
- get entries() {
2504
- return entries;
2505
- },
2506
- insert(internalId, lat, lon) {
2507
- entries.push({ lat, lon, docId: internalId });
2508
- },
2509
- remove(internalId) {
2510
- for (let i = entries.length - 1; i >= 0; i--) {
2511
- if (entries[i].docId === internalId) {
2512
- entries.splice(i, 1);
2513
- return;
2514
- }
2899
+ function removeFromIndexes(state, docId, storedDoc, flatSchema, language, options) {
2900
+ const fieldLengths = {};
2901
+ const tokensByField = {};
2902
+ const fields = storedDoc.fields;
2903
+ const internalId = resolveInternalId(state, docId);
2904
+ const opts = tokenizeOptions(options);
2905
+ for (const [fieldPath, fieldType] of Object.entries(flatSchema)) {
2906
+ const value = getNestedValue(fields, fieldPath);
2907
+ if (value === void 0 || value === null) continue;
2908
+ if (fieldType === "string") {
2909
+ const result = tokenize(value, language, opts);
2910
+ fieldLengths[fieldPath] = result.tokens.length;
2911
+ const uniqueTokens = /* @__PURE__ */ new Set();
2912
+ const fieldTokenList = [];
2913
+ const surfaceCounts = /* @__PURE__ */ new Map();
2914
+ for (let i = 0; i < result.tokens.length; i++) {
2915
+ const t = result.tokens[i];
2916
+ uniqueTokens.add(t.token);
2917
+ fieldTokenList.push(t.token);
2918
+ countSurface(surfaceCounts, result.surfaces?.[i], t.token);
2515
2919
  }
2516
- },
2517
- radiusQuery(lat, lon, distanceMeters, inside, highPrecision) {
2518
- const result = /* @__PURE__ */ new Set();
2519
- const distanceFn = highPrecision ? vincentyDistance : haversineDistance;
2520
- for (let i = 0; i < entries.length; i++) {
2521
- const entry = entries[i];
2522
- const dist = distanceFn(lat, lon, entry.lat, entry.lon);
2523
- const withinRadius = dist <= distanceMeters;
2524
- if (inside ? withinRadius : !withinRadius) {
2525
- result.add(entry.docId);
2920
+ for (const token of uniqueTokens) state.invertedIdx.remove(token, internalId);
2921
+ applySurfaceCounts(state, surfaceCounts, -1);
2922
+ tokensByField[fieldPath] = fieldTokenList;
2923
+ } else if (fieldType === "number") {
2924
+ state.numericIndexes.get(fieldPath)?.remove(internalId, value);
2925
+ } else if (fieldType === "boolean") {
2926
+ state.booleanIndexes.get(fieldPath)?.remove(internalId, value);
2927
+ } else if (fieldType === "enum") {
2928
+ state.enumIndexes.get(fieldPath)?.remove(internalId, value);
2929
+ } else if (fieldType === "geopoint") {
2930
+ state.geoIndexes.get(fieldPath)?.remove(internalId);
2931
+ } else if (fieldType === "string[]") {
2932
+ const arr = value;
2933
+ const uniqueTokens = /* @__PURE__ */ new Set();
2934
+ const fieldTokenList = [];
2935
+ const surfaceCounts = /* @__PURE__ */ new Map();
2936
+ for (const item of arr) {
2937
+ const result = tokenize(item, language, opts);
2938
+ for (let i = 0; i < result.tokens.length; i++) {
2939
+ const t = result.tokens[i];
2940
+ uniqueTokens.add(t.token);
2941
+ fieldTokenList.push(t.token);
2942
+ countSurface(surfaceCounts, result.surfaces?.[i], t.token);
2526
2943
  }
2527
2944
  }
2528
- return result;
2529
- },
2530
- polygonQuery(points, inside) {
2531
- const result = /* @__PURE__ */ new Set();
2532
- for (let i = 0; i < entries.length; i++) {
2533
- const entry = entries[i];
2534
- const withinPolygon = isPointInPolygon(entry.lat, entry.lon, points);
2535
- if (inside ? withinPolygon : !withinPolygon) {
2536
- result.add(entry.docId);
2537
- }
2945
+ fieldLengths[fieldPath] = fieldTokenList.length;
2946
+ for (const token of uniqueTokens) state.invertedIdx.remove(token, internalId);
2947
+ applySurfaceCounts(state, surfaceCounts, -1);
2948
+ tokensByField[fieldPath] = fieldTokenList;
2949
+ } else if (fieldType === "number[]") {
2950
+ const numIdx = state.numericIndexes.get(fieldPath);
2951
+ if (numIdx) {
2952
+ for (const num of value) numIdx.remove(internalId, num);
2538
2953
  }
2539
- return result;
2540
- },
2541
- clear() {
2542
- entries.length = 0;
2543
- },
2544
- serialize() {
2545
- return entries.map((e) => ({ ...e }));
2546
- },
2547
- deserialize(data) {
2548
- entries.length = 0;
2549
- for (const entry of data) {
2550
- entries.push({ lat: entry.lat, lon: entry.lon, docId: entry.docId });
2954
+ } else if (fieldType === "boolean[]") {
2955
+ const boolIdx = state.booleanIndexes.get(fieldPath);
2956
+ if (boolIdx) {
2957
+ for (const b of value) boolIdx.remove(internalId, b);
2551
2958
  }
2552
- }
2553
- };
2554
- }
2555
-
2556
- // src/core/field-index/boolean.ts
2557
- function createBooleanIndex() {
2558
- const trueDocs = /* @__PURE__ */ new Set();
2559
- const falseDocs = /* @__PURE__ */ new Set();
2560
- return {
2561
- insert(internalId, value) {
2562
- if (value) trueDocs.add(internalId);
2563
- else falseDocs.add(internalId);
2564
- },
2565
- remove(internalId, value) {
2566
- if (value) trueDocs.delete(internalId);
2567
- else falseDocs.delete(internalId);
2568
- },
2569
- queryEq(value) {
2570
- return new Set(value ? trueDocs : falseDocs);
2571
- },
2572
- queryNe(value) {
2573
- return new Set(value ? falseDocs : trueDocs);
2574
- },
2575
- getAllDocIds() {
2576
- const all = new Set(trueDocs);
2577
- for (const id of falseDocs) all.add(id);
2578
- return all;
2579
- },
2580
- queryEqBitset(value, capacity) {
2581
- const source = value ? trueDocs : falseDocs;
2582
- const bs = createBitSet(capacity);
2583
- for (const id of source) {
2584
- bitsetSet(bs, id);
2959
+ } else if (fieldType === "enum[]") {
2960
+ const enumIdx = state.enumIndexes.get(fieldPath);
2961
+ if (enumIdx) {
2962
+ for (const e of value) enumIdx.remove(internalId, e);
2585
2963
  }
2586
- return bs;
2587
- },
2588
- getAllDocIdsBitset(capacity) {
2589
- const bs = createBitSet(capacity);
2590
- for (const id of trueDocs) bitsetSet(bs, id);
2591
- for (const id of falseDocs) bitsetSet(bs, id);
2592
- return bs;
2593
- },
2594
- count() {
2595
- return trueDocs.size + falseDocs.size;
2596
- },
2597
- clear() {
2598
- trueDocs.clear();
2599
- falseDocs.clear();
2600
- },
2601
- serialize() {
2602
- return {
2603
- trueDocs: Array.from(trueDocs),
2604
- falseDocs: Array.from(falseDocs)
2605
- };
2606
- },
2607
- deserialize(data) {
2608
- trueDocs.clear();
2609
- falseDocs.clear();
2610
- for (const id of data.trueDocs) trueDocs.add(id);
2611
- for (const id of data.falseDocs) falseDocs.add(id);
2612
2964
  }
2613
- };
2965
+ }
2966
+ return { fieldLengths, tokensByField };
2614
2967
  }
2615
2968
 
2616
- // src/core/field-index/enum.ts
2617
- function createEnumIndex() {
2618
- const valueMap = /* @__PURE__ */ new Map();
2619
- function allDocs() {
2620
- const result = /* @__PURE__ */ new Set();
2621
- for (const docSet of valueMap.values()) {
2622
- for (const id of docSet) result.add(id);
2623
- }
2624
- return result;
2969
+ // src/core/partition/field-updates.ts
2970
+ function resolveInternalId2(state, docId) {
2971
+ const internalId = state.docStore.getInternalId(docId);
2972
+ if (internalId === void 0) {
2973
+ throw new Error(`Internal ID not found for document "${docId}"`);
2625
2974
  }
2626
- return {
2627
- insert(internalId, value) {
2628
- let docSet = valueMap.get(value);
2629
- if (!docSet) {
2630
- docSet = /* @__PURE__ */ new Set();
2631
- valueMap.set(value, docSet);
2632
- }
2633
- docSet.add(internalId);
2634
- },
2635
- remove(internalId, value) {
2636
- const docSet = valueMap.get(value);
2637
- if (!docSet) return;
2638
- docSet.delete(internalId);
2639
- if (docSet.size === 0) valueMap.delete(value);
2640
- },
2641
- queryEq(value) {
2642
- const docSet = valueMap.get(value);
2643
- return docSet ? new Set(docSet) : /* @__PURE__ */ new Set();
2644
- },
2645
- queryNe(value) {
2646
- const excluded = valueMap.get(value);
2647
- const result = allDocs();
2648
- if (excluded) {
2649
- for (const id of excluded) result.delete(id);
2975
+ return internalId;
2976
+ }
2977
+ function updateFieldIndexOnly(state, docId, oldFields, newDoc, flatSchema) {
2978
+ const internalId = resolveInternalId2(state, docId);
2979
+ for (const [fieldPath, fieldType] of Object.entries(flatSchema)) {
2980
+ if (fieldType === "string" || fieldType === "string[]") continue;
2981
+ const oldVal = getNestedValue(oldFields, fieldPath);
2982
+ const newVal = getNestedValue(newDoc, fieldPath);
2983
+ if (oldVal === newVal) continue;
2984
+ if (oldVal !== void 0 && oldVal !== null && newVal !== void 0 && newVal !== null && typeof oldVal === typeof newVal && JSON.stringify(oldVal) === JSON.stringify(newVal))
2985
+ continue;
2986
+ if (newVal !== void 0 && newVal !== null) {
2987
+ ensureFieldIndex(state, fieldPath, fieldType);
2988
+ }
2989
+ if (fieldType === "number") {
2990
+ const numIdx = state.numericIndexes.get(fieldPath);
2991
+ if (numIdx) {
2992
+ if (oldVal !== void 0 && oldVal !== null) numIdx.remove(internalId, oldVal);
2993
+ if (newVal !== void 0 && newVal !== null) numIdx.insert(internalId, newVal);
2650
2994
  }
2651
- return result;
2652
- },
2653
- queryIn(values) {
2654
- const result = /* @__PURE__ */ new Set();
2655
- for (const val of values) {
2656
- const docSet = valueMap.get(val);
2657
- if (docSet) {
2658
- for (const id of docSet) result.add(id);
2659
- }
2995
+ } else if (fieldType === "boolean") {
2996
+ const boolIdx = state.booleanIndexes.get(fieldPath);
2997
+ if (boolIdx) {
2998
+ if (oldVal !== void 0 && oldVal !== null) boolIdx.remove(internalId, oldVal);
2999
+ if (newVal !== void 0 && newVal !== null) boolIdx.insert(internalId, newVal);
2660
3000
  }
2661
- return result;
2662
- },
2663
- queryNin(values) {
2664
- const excluded = this.queryIn(values);
2665
- const result = allDocs();
2666
- for (const id of excluded) result.delete(id);
2667
- return result;
2668
- },
2669
- getAllDocIds() {
2670
- return allDocs();
2671
- },
2672
- queryEqBitset(value, capacity) {
2673
- const bs = createBitSet(capacity);
2674
- const docSet = valueMap.get(value);
2675
- if (docSet) {
2676
- for (const id of docSet) bitsetSet(bs, id);
3001
+ } else if (fieldType === "enum") {
3002
+ const enumIdx = state.enumIndexes.get(fieldPath);
3003
+ if (enumIdx) {
3004
+ if (oldVal !== void 0 && oldVal !== null) enumIdx.remove(internalId, oldVal);
3005
+ if (newVal !== void 0 && newVal !== null) enumIdx.insert(internalId, newVal);
2677
3006
  }
2678
- return bs;
2679
- },
2680
- queryInBitset(values, capacity) {
2681
- const bs = createBitSet(capacity);
2682
- for (const val of values) {
2683
- const docSet = valueMap.get(val);
2684
- if (docSet) {
2685
- for (const id of docSet) bitsetSet(bs, id);
3007
+ } else if (fieldType === "geopoint") {
3008
+ const geoIdx = state.geoIndexes.get(fieldPath);
3009
+ if (geoIdx) {
3010
+ if (oldVal !== void 0 && oldVal !== null) geoIdx.remove(internalId);
3011
+ if (newVal !== void 0 && newVal !== null) {
3012
+ const geo = newVal;
3013
+ geoIdx.insert(internalId, geo.lat, geo.lon);
2686
3014
  }
2687
3015
  }
2688
- return bs;
2689
- },
2690
- getAllDocIdsBitset(capacity) {
2691
- const bs = createBitSet(capacity);
2692
- for (const docSet of valueMap.values()) {
2693
- for (const id of docSet) bitsetSet(bs, id);
3016
+ } else if (fieldType === "number[]") {
3017
+ const numIdx = state.numericIndexes.get(fieldPath);
3018
+ if (numIdx) {
3019
+ if (oldVal !== void 0 && oldVal !== null) {
3020
+ for (const n of oldVal) numIdx.remove(internalId, n);
3021
+ }
3022
+ if (newVal !== void 0 && newVal !== null) {
3023
+ for (const n of newVal) numIdx.insert(internalId, n);
3024
+ }
2694
3025
  }
2695
- return bs;
2696
- },
2697
- count() {
2698
- let total = 0;
2699
- for (const docSet of valueMap.values()) total += docSet.size;
2700
- return total;
2701
- },
2702
- clear() {
2703
- valueMap.clear();
2704
- },
2705
- serialize() {
2706
- const result = /* @__PURE__ */ Object.create(null);
2707
- for (const [value, docSet] of valueMap) {
2708
- result[value] = Array.from(docSet);
3026
+ } else if (fieldType === "boolean[]") {
3027
+ const boolIdx = state.booleanIndexes.get(fieldPath);
3028
+ if (boolIdx) {
3029
+ if (oldVal !== void 0 && oldVal !== null) {
3030
+ for (const b of oldVal) boolIdx.remove(internalId, b);
3031
+ }
3032
+ if (newVal !== void 0 && newVal !== null) {
3033
+ for (const b of newVal) boolIdx.insert(internalId, b);
3034
+ }
2709
3035
  }
2710
- return result;
2711
- },
2712
- deserialize(data) {
2713
- valueMap.clear();
2714
- for (const value of Object.keys(data)) {
2715
- valueMap.set(value, new Set(data[value]));
3036
+ } else if (fieldType === "enum[]") {
3037
+ const enumIdx = state.enumIndexes.get(fieldPath);
3038
+ if (enumIdx) {
3039
+ if (oldVal !== void 0 && oldVal !== null) {
3040
+ for (const e of oldVal) enumIdx.remove(internalId, e);
3041
+ }
3042
+ if (newVal !== void 0 && newVal !== null) {
3043
+ for (const e of newVal) enumIdx.insert(internalId, e);
3044
+ }
2716
3045
  }
2717
3046
  }
2718
- };
3047
+ }
2719
3048
  }
2720
3049
 
2721
- // src/core/field-index/numeric.ts
2722
- function lowerBound(entries, value) {
2723
- let lo = 0;
2724
- let hi = entries.length;
2725
- while (lo < hi) {
2726
- const mid = lo + hi >>> 1;
2727
- if (entries[mid].value < value) lo = mid + 1;
2728
- else hi = mid;
3050
+ // src/filters/combinators.ts
3051
+ function applyAndBitset(bitsets) {
3052
+ if (bitsets.length === 0) return new Uint32Array(0);
3053
+ if (bitsets.length === 1) return bitsets[0];
3054
+ let result = bitsets[0];
3055
+ for (let i = 1; i < bitsets.length; i++) {
3056
+ result = bitsetAnd(result, bitsets[i]);
2729
3057
  }
2730
- return lo;
3058
+ return result;
2731
3059
  }
2732
- function upperBound(entries, value) {
2733
- let lo = 0;
2734
- let hi = entries.length;
2735
- while (lo < hi) {
2736
- const mid = lo + hi >>> 1;
2737
- if (entries[mid].value <= value) lo = mid + 1;
2738
- else hi = mid;
3060
+ function applyOrBitset(bitsets) {
3061
+ if (bitsets.length === 0) return new Uint32Array(0);
3062
+ if (bitsets.length === 1) return bitsets[0];
3063
+ let result = bitsets[0];
3064
+ for (let i = 1; i < bitsets.length; i++) {
3065
+ result = bitsetOr(result, bitsets[i]);
2739
3066
  }
2740
- return lo;
3067
+ return result;
2741
3068
  }
2742
- function collectDocIds(entries, from, to) {
2743
- const result = /* @__PURE__ */ new Set();
2744
- for (let i = from; i < to; i++) {
2745
- result.add(entries[i].docId);
3069
+ function applyNotBitset(excluded, capacity) {
3070
+ return bitsetNot(excluded, capacity);
3071
+ }
3072
+
3073
+ // src/filters/operators/shared.ts
3074
+ var METERS_PER_KM = 1e3;
3075
+ var METERS_PER_MI = 1609.344;
3076
+ function convertToMeters(distance, unit) {
3077
+ if (unit === "km") return distance * METERS_PER_KM;
3078
+ if (unit === "mi") return distance * METERS_PER_MI;
3079
+ return distance;
3080
+ }
3081
+ function matchesNumericComparison(value, filter) {
3082
+ if (filter.eq !== void 0 && value !== filter.eq) return false;
3083
+ if (filter.ne !== void 0 && value === filter.ne) return false;
3084
+ if (filter.gt !== void 0 && !(value > filter.gt)) return false;
3085
+ if (filter.lt !== void 0 && !(value < filter.lt)) return false;
3086
+ if (filter.gte !== void 0 && !(value >= filter.gte)) return false;
3087
+ if (filter.lte !== void 0 && !(value <= filter.lte)) return false;
3088
+ if (filter.between !== void 0 && !(value >= filter.between[0] && value <= filter.between[1])) return false;
3089
+ return true;
3090
+ }
3091
+
3092
+ // src/filters/operators/bitset.ts
3093
+ function resolveBitset(bs) {
3094
+ return typeof bs === "function" ? bs() : bs;
3095
+ }
3096
+ function scanToBitset(docIds, capacity, getValue, predicate) {
3097
+ const resolved = resolveBitset(docIds);
3098
+ const result = createBitSet(capacity);
3099
+ for (let wi = 0; wi < resolved.length; wi++) {
3100
+ let word = resolved[wi];
3101
+ if (word === 0) continue;
3102
+ const base = wi << 5;
3103
+ while (word !== 0) {
3104
+ const tz = Math.clz32(word & -word) ^ 31;
3105
+ const id = base + tz;
3106
+ if (predicate(getValue(id))) {
3107
+ bitsetSet(result, id);
3108
+ }
3109
+ word &= word - 1;
3110
+ }
2746
3111
  }
2747
3112
  return result;
2748
3113
  }
2749
- function collectDocIdsBitset(entries, from, to, capacity) {
2750
- const bs = createBitSet(capacity);
2751
- for (let i = from; i < to; i++) {
2752
- bitsetSet(bs, entries[i].docId);
3114
+ function applyEqBitset(value, allDocsBitset, capacity, getValue, fieldIndex) {
3115
+ if (fieldIndex?.type === "numeric" && typeof value === "number") {
3116
+ return fieldIndex.index.eqBitset(value, capacity);
2753
3117
  }
2754
- return bs;
3118
+ if (fieldIndex?.type === "boolean" && typeof value === "boolean") {
3119
+ return value ? fieldIndex.index.getTrueBitset(capacity) : fieldIndex.index.getFalseBitset(capacity);
3120
+ }
3121
+ if (fieldIndex?.type === "enum" && typeof value === "string") {
3122
+ return fieldIndex.index.getDocIdsBitset(value, capacity);
3123
+ }
3124
+ return scanToBitset(allDocsBitset, capacity, getValue, (v) => v === value);
2755
3125
  }
2756
- function createNumericIndex() {
2757
- let entries = [];
2758
- let sorted = true;
2759
- function ensureSorted() {
2760
- if (!sorted) {
2761
- entries.sort((a, b) => a.value - b.value);
2762
- sorted = true;
2763
- }
3126
+ function applyNeBitset(value, allDocsBitset, capacity, getValue, fieldIndex) {
3127
+ if (fieldIndex?.type === "numeric" && typeof value === "number") {
3128
+ const all = fieldIndex.index.allDocIdsBitset(capacity);
3129
+ const eq = fieldIndex.index.eqBitset(value, capacity);
3130
+ return bitsetAnd(all, bitsetNot(eq, capacity));
2764
3131
  }
2765
- return {
2766
- insert(internalId, value) {
2767
- entries.push({ value, docId: internalId });
2768
- sorted = false;
2769
- },
2770
- remove(internalId, value) {
2771
- ensureSorted();
2772
- const start = lowerBound(entries, value);
2773
- const end = upperBound(entries, value);
2774
- for (let i = start; i < end; i++) {
2775
- if (entries[i].docId === internalId) {
2776
- entries.splice(i, 1);
2777
- return;
2778
- }
2779
- }
2780
- },
2781
- queryEq(value) {
2782
- ensureSorted();
2783
- return collectDocIds(entries, lowerBound(entries, value), upperBound(entries, value));
2784
- },
2785
- queryNe(value) {
2786
- ensureSorted();
2787
- const excluded = this.queryEq(value);
2788
- const result = /* @__PURE__ */ new Set();
2789
- for (const entry of entries) {
2790
- if (!excluded.has(entry.docId)) result.add(entry.docId);
2791
- }
2792
- return result;
2793
- },
2794
- queryGt(value) {
2795
- ensureSorted();
2796
- return collectDocIds(entries, upperBound(entries, value), entries.length);
2797
- },
2798
- queryGte(value) {
2799
- ensureSorted();
2800
- return collectDocIds(entries, lowerBound(entries, value), entries.length);
2801
- },
2802
- queryLt(value) {
2803
- ensureSorted();
2804
- return collectDocIds(entries, 0, lowerBound(entries, value));
2805
- },
2806
- queryLte(value) {
2807
- ensureSorted();
2808
- return collectDocIds(entries, 0, upperBound(entries, value));
2809
- },
2810
- queryBetween(min, max) {
2811
- ensureSorted();
2812
- return collectDocIds(entries, lowerBound(entries, min), upperBound(entries, max));
2813
- },
2814
- getAllDocIds() {
2815
- return collectDocIds(entries, 0, entries.length);
2816
- },
2817
- queryEqBitset(value, capacity) {
2818
- ensureSorted();
2819
- return collectDocIdsBitset(entries, lowerBound(entries, value), upperBound(entries, value), capacity);
2820
- },
2821
- queryGtBitset(value, capacity) {
2822
- ensureSorted();
2823
- return collectDocIdsBitset(entries, upperBound(entries, value), entries.length, capacity);
2824
- },
2825
- queryGteBitset(value, capacity) {
2826
- ensureSorted();
2827
- return collectDocIdsBitset(entries, lowerBound(entries, value), entries.length, capacity);
2828
- },
2829
- queryLtBitset(value, capacity) {
2830
- ensureSorted();
2831
- return collectDocIdsBitset(entries, 0, lowerBound(entries, value), capacity);
2832
- },
2833
- queryLteBitset(value, capacity) {
2834
- ensureSorted();
2835
- return collectDocIdsBitset(entries, 0, upperBound(entries, value), capacity);
2836
- },
2837
- queryBetweenBitset(min, max, capacity) {
2838
- ensureSorted();
2839
- return collectDocIdsBitset(entries, lowerBound(entries, min), upperBound(entries, max), capacity);
2840
- },
2841
- getAllDocIdsBitset(capacity) {
2842
- return collectDocIdsBitset(entries, 0, entries.length, capacity);
2843
- },
2844
- count() {
2845
- return entries.length;
2846
- },
2847
- clear() {
2848
- entries = [];
2849
- sorted = true;
2850
- },
2851
- serialize() {
2852
- ensureSorted();
2853
- return entries.slice();
2854
- },
2855
- deserialize(data) {
2856
- entries = data.slice();
2857
- sorted = true;
2858
- }
2859
- };
3132
+ if (fieldIndex?.type === "boolean" && typeof value === "boolean") {
3133
+ return value ? fieldIndex.index.getFalseBitset(capacity) : fieldIndex.index.getTrueBitset(capacity);
3134
+ }
3135
+ if (fieldIndex?.type === "enum" && typeof value === "string") {
3136
+ const all = fieldIndex.index.allDocIdsBitset(capacity);
3137
+ const eq = fieldIndex.index.getDocIdsBitset(value, capacity);
3138
+ return bitsetAnd(all, bitsetNot(eq, capacity));
3139
+ }
3140
+ return scanToBitset(allDocsBitset, capacity, getValue, (v) => v !== void 0 && v !== null && v !== value);
2860
3141
  }
2861
-
2862
- // src/core/partition/indexing.ts
2863
- function ensureFieldIndex(state, fieldPath, fieldType) {
2864
- if ((fieldType === "number" || fieldType === "number[]") && !state.numericIndexes.has(fieldPath)) {
2865
- state.numericIndexes.set(fieldPath, createNumericIndex());
2866
- } else if ((fieldType === "boolean" || fieldType === "boolean[]") && !state.booleanIndexes.has(fieldPath)) {
2867
- state.booleanIndexes.set(fieldPath, createBooleanIndex());
2868
- } else if ((fieldType === "enum" || fieldType === "enum[]") && !state.enumIndexes.has(fieldPath)) {
2869
- state.enumIndexes.set(fieldPath, createEnumIndex());
2870
- } else if (fieldType === "geopoint" && !state.geoIndexes.has(fieldPath)) {
2871
- state.geoIndexes.set(fieldPath, createGeoIndex());
3142
+ function applyGtBitset(value, allDocsBitset, capacity, getValue, fieldIndex) {
3143
+ if (fieldIndex?.type === "numeric") {
3144
+ return fieldIndex.index.gtBitset(value, capacity);
3145
+ }
3146
+ return scanToBitset(allDocsBitset, capacity, getValue, (v) => typeof v === "number" && v > value);
3147
+ }
3148
+ function applyLtBitset(value, allDocsBitset, capacity, getValue, fieldIndex) {
3149
+ if (fieldIndex?.type === "numeric") {
3150
+ return fieldIndex.index.ltBitset(value, capacity);
3151
+ }
3152
+ return scanToBitset(allDocsBitset, capacity, getValue, (v) => typeof v === "number" && v < value);
3153
+ }
3154
+ function applyGteBitset(value, allDocsBitset, capacity, getValue, fieldIndex) {
3155
+ if (fieldIndex?.type === "numeric") {
3156
+ return fieldIndex.index.gteBitset(value, capacity);
3157
+ }
3158
+ return scanToBitset(allDocsBitset, capacity, getValue, (v) => typeof v === "number" && v >= value);
3159
+ }
3160
+ function applyLteBitset(value, allDocsBitset, capacity, getValue, fieldIndex) {
3161
+ if (fieldIndex?.type === "numeric") {
3162
+ return fieldIndex.index.lteBitset(value, capacity);
3163
+ }
3164
+ return scanToBitset(allDocsBitset, capacity, getValue, (v) => typeof v === "number" && v <= value);
3165
+ }
3166
+ function applyBetweenBitset(range, allDocsBitset, capacity, getValue, fieldIndex) {
3167
+ if (fieldIndex?.type === "numeric") {
3168
+ return fieldIndex.index.betweenBitset(range[0], range[1], capacity);
2872
3169
  }
3170
+ return scanToBitset(allDocsBitset, capacity, getValue, (v) => typeof v === "number" && v >= range[0] && v <= range[1]);
2873
3171
  }
2874
- function getOrCreateNumericIndex(state, fieldPath) {
2875
- let idx = state.numericIndexes.get(fieldPath);
2876
- if (!idx) {
2877
- idx = createNumericIndex();
2878
- state.numericIndexes.set(fieldPath, idx);
3172
+ function applyInBitset(values, allDocsBitset, capacity, getValue, fieldIndex) {
3173
+ if (fieldIndex?.type === "enum") {
3174
+ return fieldIndex.index.getDocIdsInBitset(values, capacity);
2879
3175
  }
2880
- return idx;
3176
+ const valSet = new Set(values);
3177
+ return scanToBitset(allDocsBitset, capacity, getValue, (v) => typeof v === "string" && valSet.has(v));
2881
3178
  }
2882
- function getOrCreateBooleanIndex(state, fieldPath) {
2883
- let idx = state.booleanIndexes.get(fieldPath);
2884
- if (!idx) {
2885
- idx = createBooleanIndex();
2886
- state.booleanIndexes.set(fieldPath, idx);
3179
+ function applyNinBitset(values, allDocsBitset, capacity, getValue, fieldIndex) {
3180
+ if (fieldIndex?.type === "enum") {
3181
+ const all = fieldIndex.index.allDocIdsBitset(capacity);
3182
+ const matched = fieldIndex.index.getDocIdsInBitset(values, capacity);
3183
+ return bitsetAnd(all, bitsetNot(matched, capacity));
2887
3184
  }
2888
- return idx;
3185
+ const valSet = new Set(values);
3186
+ return scanToBitset(
3187
+ allDocsBitset,
3188
+ capacity,
3189
+ getValue,
3190
+ (v) => v !== void 0 && v !== null && typeof v === "string" && !valSet.has(v)
3191
+ );
2889
3192
  }
2890
- function resolveInternalId(state, docId) {
2891
- const internalId = state.docStore.getInternalId(docId);
2892
- if (internalId === void 0) {
2893
- throw new Error(`Internal ID not found for document "${docId}"`);
2894
- }
2895
- return internalId;
3193
+ function applyStartsWithBitset(prefix, allDocsBitset, capacity, getValue) {
3194
+ return scanToBitset(allDocsBitset, capacity, getValue, (v) => typeof v === "string" && v.startsWith(prefix));
2896
3195
  }
2897
- function indexStringField(state, internalId, fieldPath, text, language, options, fieldLengths, tokensByField) {
2898
- const fieldTokenList = [];
2899
- const fieldNameIndex = getOrCreateFieldNameIndex(state.fieldNameTable, fieldPath);
2900
- const opts = tokenizeOptions(options);
2901
- if (state.trackPositions) {
2902
- const tokenFreqs = /* @__PURE__ */ new Map();
2903
- for (const t of tokenizeIterator(text, language, opts)) {
2904
- const existing = tokenFreqs.get(t.token);
2905
- if (existing) {
2906
- existing.count++;
2907
- existing.positions.push(t.position);
2908
- } else {
2909
- tokenFreqs.set(t.token, { count: 1, positions: [t.position] });
2910
- }
2911
- fieldTokenList.push(t.token);
2912
- }
2913
- for (const [token, freq] of tokenFreqs) {
2914
- state.invertedIdx.insert(token, internalId, freq.count, fieldNameIndex, freq.positions);
3196
+ function applyEndsWithBitset(suffix, allDocsBitset, capacity, getValue) {
3197
+ return scanToBitset(allDocsBitset, capacity, getValue, (v) => typeof v === "string" && v.endsWith(suffix));
3198
+ }
3199
+ function applyContainsAllBitset(values, allDocsBitset, capacity, getValue) {
3200
+ return scanToBitset(allDocsBitset, capacity, getValue, (v) => {
3201
+ if (!Array.isArray(v)) return false;
3202
+ const arr = v;
3203
+ for (const val of values) {
3204
+ if (!arr.includes(val)) return false;
2915
3205
  }
2916
- } else {
2917
- const tokenCounts = /* @__PURE__ */ new Map();
2918
- for (const t of tokenizeIterator(text, language, opts)) {
2919
- tokenCounts.set(t.token, (tokenCounts.get(t.token) ?? 0) + 1);
2920
- fieldTokenList.push(t.token);
3206
+ return true;
3207
+ });
3208
+ }
3209
+ function applyMatchesAnyBitset(values, allDocsBitset, capacity, getValue) {
3210
+ const valSet = new Set(values);
3211
+ return scanToBitset(allDocsBitset, capacity, getValue, (v) => {
3212
+ if (!Array.isArray(v)) return false;
3213
+ for (const item of v) {
3214
+ if (valSet.has(item)) return true;
2921
3215
  }
2922
- for (const [token, count] of tokenCounts) {
2923
- state.invertedIdx.insert(token, internalId, count, fieldNameIndex, null);
3216
+ return false;
3217
+ });
3218
+ }
3219
+ function applySizeBitset(sizeFilter, allDocsBitset, capacity, getValue) {
3220
+ return scanToBitset(
3221
+ allDocsBitset,
3222
+ capacity,
3223
+ getValue,
3224
+ (v) => Array.isArray(v) && matchesNumericComparison(v.length, sizeFilter)
3225
+ );
3226
+ }
3227
+ function applyExistsBitset(allDocsBitset, capacity, getValue) {
3228
+ return scanToBitset(allDocsBitset, capacity, getValue, (v) => v !== void 0 && v !== null);
3229
+ }
3230
+ function applyNotExistsBitset(allDocsBitset, capacity, getValue) {
3231
+ return scanToBitset(allDocsBitset, capacity, getValue, (v) => v === void 0 || v === null);
3232
+ }
3233
+ function applyIsEmptyBitset(allDocsBitset, capacity, getValue) {
3234
+ return scanToBitset(allDocsBitset, capacity, getValue, (v) => {
3235
+ if (v === void 0 || v === null) return true;
3236
+ if (typeof v === "string" && v === "") return true;
3237
+ if (Array.isArray(v) && v.length === 0) return true;
3238
+ return false;
3239
+ });
3240
+ }
3241
+ function applyIsNotEmptyBitset(allDocsBitset, capacity, getValue) {
3242
+ return scanToBitset(allDocsBitset, capacity, getValue, (v) => {
3243
+ if (v === void 0 || v === null) return false;
3244
+ if (typeof v === "string" && v === "") return false;
3245
+ if (Array.isArray(v) && v.length === 0) return false;
3246
+ return true;
3247
+ });
3248
+ }
3249
+ function applyGeoRadiusBitset(filter, geoIndex, capacity) {
3250
+ const distanceMeters = convertToMeters(filter.distance, filter.unit);
3251
+ const setResult = geoIndex.radiusQuery(
3252
+ filter.lat,
3253
+ filter.lon,
3254
+ distanceMeters,
3255
+ filter.inside ?? true,
3256
+ filter.highPrecision ?? false
3257
+ );
3258
+ return bitsetFromSet(setResult, capacity);
3259
+ }
3260
+ function applyGeoPolygonBitset(filter, geoIndex, capacity) {
3261
+ const setResult = geoIndex.polygonQuery(filter.points, filter.inside ?? true);
3262
+ return bitsetFromSet(setResult, capacity);
3263
+ }
3264
+
3265
+ // src/filters/evaluator.ts
3266
+ function evaluateFilters(expression, context) {
3267
+ const bitsets = [];
3268
+ if (expression.fields) {
3269
+ for (const [fieldPath, filter] of Object.entries(expression.fields)) {
3270
+ bitsets.push(evaluateFieldFilter(fieldPath, filter, context));
2924
3271
  }
2925
3272
  }
2926
- fieldLengths[fieldPath] = fieldTokenList.length;
2927
- tokensByField[fieldPath] = fieldTokenList;
3273
+ if (expression.and?.length) {
3274
+ const andBitsets = expression.and.map((expr) => evaluateFilters(expr, context));
3275
+ bitsets.push(applyAndBitset(andBitsets));
3276
+ }
3277
+ if (expression.or?.length) {
3278
+ const orBitsets = expression.or.map((expr) => evaluateFilters(expr, context));
3279
+ bitsets.push(applyOrBitset(orBitsets));
3280
+ }
3281
+ if (expression.not) {
3282
+ const excluded = evaluateFilters(expression.not, context);
3283
+ const universe = context.allDocIdsBitset;
3284
+ bitsets.push(applyAndBitset([universe, applyNotBitset(excluded, context.capacity)]));
3285
+ }
3286
+ if (bitsets.length === 0) return context.allDocIdsBitset;
3287
+ if (bitsets.length === 1) return bitsets[0];
3288
+ return applyAndBitset(bitsets);
2928
3289
  }
2929
- function indexStringArrayField(state, internalId, fieldPath, arr, language, options, fieldLengths, tokensByField) {
2930
- const fieldTokenList = [];
2931
- const fieldNameIndex = getOrCreateFieldNameIndex(state.fieldNameTable, fieldPath);
2932
- const opts = tokenizeOptions(options);
2933
- if (state.trackPositions) {
2934
- const tokenFreqs = /* @__PURE__ */ new Map();
2935
- let positionOffset = 0;
2936
- for (const item of arr) {
2937
- let itemTokenCount = 0;
2938
- for (const t of tokenizeIterator(item, language, opts)) {
2939
- const existing = tokenFreqs.get(t.token);
2940
- if (existing) {
2941
- existing.count++;
2942
- existing.positions.push(positionOffset + t.position);
2943
- } else {
2944
- tokenFreqs.set(t.token, { count: 1, positions: [positionOffset + t.position] });
2945
- }
2946
- fieldTokenList.push(t.token);
2947
- itemTokenCount++;
2948
- }
2949
- positionOffset += itemTokenCount;
2950
- }
2951
- fieldLengths[fieldPath] = fieldTokenList.length;
2952
- for (const [token, freq] of tokenFreqs) {
2953
- state.invertedIdx.insert(token, internalId, freq.count, fieldNameIndex, freq.positions);
2954
- }
2955
- } else {
2956
- const tokenCounts = /* @__PURE__ */ new Map();
2957
- for (const item of arr) {
2958
- for (const t of tokenizeIterator(item, language, opts)) {
2959
- tokenCounts.set(t.token, (tokenCounts.get(t.token) ?? 0) + 1);
2960
- fieldTokenList.push(t.token);
2961
- }
2962
- }
2963
- fieldLengths[fieldPath] = fieldTokenList.length;
2964
- for (const [token, count] of tokenCounts) {
2965
- state.invertedIdx.insert(token, internalId, count, fieldNameIndex, null);
3290
+ function evaluateFieldFilter(fieldPath, filter, context) {
3291
+ const fieldIndex = context.fieldIndexes[fieldPath];
3292
+ const getValue = (internalId) => context.getFieldValue(internalId, fieldPath);
3293
+ const f = filter;
3294
+ const bitsets = [];
3295
+ const { capacity } = context;
3296
+ let allDocsBitset = null;
3297
+ const getAllDocsBitset = () => {
3298
+ if (allDocsBitset === null) {
3299
+ allDocsBitset = context.allDocIdsBitset;
2966
3300
  }
3301
+ return allDocsBitset;
3302
+ };
3303
+ if ("radius" in f && f.radius) {
3304
+ const geoIndex = resolveGeoIndex(fieldPath, fieldIndex);
3305
+ bitsets.push(applyGeoRadiusBitset(filter.radius, geoIndex, capacity));
3306
+ }
3307
+ if ("polygon" in f && f.polygon) {
3308
+ const geoIndex = resolveGeoIndex(fieldPath, fieldIndex);
3309
+ bitsets.push(applyGeoPolygonBitset(filter.polygon, geoIndex, capacity));
3310
+ }
3311
+ if ("eq" in f && f.eq !== void 0) {
3312
+ bitsets.push(applyEqBitset(f.eq, getAllDocsBitset, capacity, getValue, fieldIndex));
3313
+ }
3314
+ if ("ne" in f && f.ne !== void 0) {
3315
+ bitsets.push(applyNeBitset(f.ne, getAllDocsBitset, capacity, getValue, fieldIndex));
3316
+ }
3317
+ if ("gt" in f && f.gt !== void 0) {
3318
+ bitsets.push(applyGtBitset(f.gt, getAllDocsBitset, capacity, getValue, fieldIndex));
3319
+ }
3320
+ if ("lt" in f && f.lt !== void 0) {
3321
+ bitsets.push(applyLtBitset(f.lt, getAllDocsBitset, capacity, getValue, fieldIndex));
3322
+ }
3323
+ if ("gte" in f && f.gte !== void 0) {
3324
+ bitsets.push(applyGteBitset(f.gte, getAllDocsBitset, capacity, getValue, fieldIndex));
3325
+ }
3326
+ if ("lte" in f && f.lte !== void 0) {
3327
+ bitsets.push(applyLteBitset(f.lte, getAllDocsBitset, capacity, getValue, fieldIndex));
3328
+ }
3329
+ if ("between" in f && f.between !== void 0) {
3330
+ bitsets.push(applyBetweenBitset(f.between, getAllDocsBitset, capacity, getValue, fieldIndex));
3331
+ }
3332
+ if ("in" in f && f.in !== void 0) {
3333
+ bitsets.push(applyInBitset(f.in, getAllDocsBitset, capacity, getValue, fieldIndex));
3334
+ }
3335
+ if ("nin" in f && f.nin !== void 0) {
3336
+ bitsets.push(applyNinBitset(f.nin, getAllDocsBitset, capacity, getValue, fieldIndex));
3337
+ }
3338
+ if ("startsWith" in f && f.startsWith !== void 0) {
3339
+ bitsets.push(applyStartsWithBitset(f.startsWith, getAllDocsBitset, capacity, getValue));
3340
+ }
3341
+ if ("endsWith" in f && f.endsWith !== void 0) {
3342
+ bitsets.push(applyEndsWithBitset(f.endsWith, getAllDocsBitset, capacity, getValue));
3343
+ }
3344
+ if ("containsAll" in f && f.containsAll !== void 0) {
3345
+ bitsets.push(
3346
+ applyContainsAllBitset(f.containsAll, getAllDocsBitset, capacity, getValue)
3347
+ );
3348
+ }
3349
+ if ("matchesAny" in f && f.matchesAny !== void 0) {
3350
+ bitsets.push(
3351
+ applyMatchesAnyBitset(f.matchesAny, getAllDocsBitset, capacity, getValue)
3352
+ );
3353
+ }
3354
+ if ("size" in f && f.size !== void 0) {
3355
+ bitsets.push(applySizeBitset(f.size, getAllDocsBitset, capacity, getValue));
2967
3356
  }
2968
- tokensByField[fieldPath] = fieldTokenList;
2969
- }
2970
- function indexDocument(state, docId, document, flatSchema, language, options) {
2971
- const fieldLengths = {};
2972
- const tokensByField = {};
2973
- const internalId = resolveInternalId(state, docId);
2974
- for (const [fieldPath, fieldType] of Object.entries(flatSchema)) {
2975
- const value = getNestedValue(document, fieldPath);
2976
- if (value === void 0 || value === null) continue;
2977
- ensureFieldIndex(state, fieldPath, fieldType);
2978
- if (fieldType === "string") {
2979
- indexStringField(state, internalId, fieldPath, value, language, options, fieldLengths, tokensByField);
2980
- } else if (fieldType === "number") {
2981
- getOrCreateNumericIndex(state, fieldPath).insert(internalId, value);
2982
- } else if (fieldType === "boolean") {
2983
- getOrCreateBooleanIndex(state, fieldPath).insert(internalId, value);
2984
- } else if (fieldType === "enum") {
2985
- state.enumIndexes.get(fieldPath)?.insert(internalId, value);
2986
- } else if (fieldType === "geopoint") {
2987
- const geo = value;
2988
- state.geoIndexes.get(fieldPath)?.insert(internalId, geo.lat, geo.lon);
2989
- } else if (fieldType === "string[]") {
2990
- indexStringArrayField(
2991
- state,
2992
- internalId,
2993
- fieldPath,
2994
- value,
2995
- language,
2996
- options,
2997
- fieldLengths,
2998
- tokensByField
2999
- );
3000
- } else if (fieldType === "number[]") {
3001
- const numIdx = getOrCreateNumericIndex(state, fieldPath);
3002
- for (const num of value) numIdx.insert(internalId, num);
3003
- } else if (fieldType === "boolean[]") {
3004
- const boolIdx = getOrCreateBooleanIndex(state, fieldPath);
3005
- for (const b of value) boolIdx.insert(internalId, b);
3006
- } else if (fieldType === "enum[]") {
3007
- const enumIdx = state.enumIndexes.get(fieldPath);
3008
- if (enumIdx) {
3009
- for (const e of value) enumIdx.insert(internalId, e);
3010
- }
3011
- }
3357
+ if ("exists" in f && f.exists !== void 0) {
3358
+ bitsets.push(
3359
+ f.exists ? applyExistsBitset(getAllDocsBitset, capacity, getValue) : applyNotExistsBitset(getAllDocsBitset, capacity, getValue)
3360
+ );
3012
3361
  }
3013
- return { fieldLengths, tokensByField };
3362
+ if ("notExists" in f && f.notExists !== void 0) {
3363
+ bitsets.push(
3364
+ f.notExists ? applyNotExistsBitset(getAllDocsBitset, capacity, getValue) : applyExistsBitset(getAllDocsBitset, capacity, getValue)
3365
+ );
3366
+ }
3367
+ if ("isEmpty" in f && f.isEmpty !== void 0) {
3368
+ bitsets.push(
3369
+ f.isEmpty ? applyIsEmptyBitset(getAllDocsBitset, capacity, getValue) : applyIsNotEmptyBitset(getAllDocsBitset, capacity, getValue)
3370
+ );
3371
+ }
3372
+ if ("isNotEmpty" in f && f.isNotEmpty !== void 0) {
3373
+ bitsets.push(
3374
+ f.isNotEmpty ? applyIsNotEmptyBitset(getAllDocsBitset, capacity, getValue) : applyIsEmptyBitset(getAllDocsBitset, capacity, getValue)
3375
+ );
3376
+ }
3377
+ if (bitsets.length === 0) return context.allDocIdsBitset;
3378
+ if (bitsets.length === 1) return bitsets[0];
3379
+ return applyAndBitset(bitsets);
3014
3380
  }
3015
- function removeFromIndexes(state, docId, storedDoc, flatSchema, language, options) {
3016
- const fieldLengths = {};
3017
- const tokensByField = {};
3018
- const fields = storedDoc.fields;
3019
- const internalId = resolveInternalId(state, docId);
3020
- for (const [fieldPath, fieldType] of Object.entries(flatSchema)) {
3021
- const value = getNestedValue(fields, fieldPath);
3022
- if (value === void 0 || value === null) continue;
3023
- if (fieldType === "string") {
3024
- const result = tokenize(value, language, tokenizeOptions(options));
3025
- fieldLengths[fieldPath] = result.tokens.length;
3026
- const uniqueTokens = /* @__PURE__ */ new Set();
3027
- const fieldTokenList = [];
3028
- for (const t of result.tokens) {
3029
- uniqueTokens.add(t.token);
3030
- fieldTokenList.push(t.token);
3031
- }
3032
- for (const token of uniqueTokens) state.invertedIdx.remove(token, internalId);
3033
- tokensByField[fieldPath] = fieldTokenList;
3034
- } else if (fieldType === "number") {
3035
- state.numericIndexes.get(fieldPath)?.remove(internalId, value);
3036
- } else if (fieldType === "boolean") {
3037
- state.booleanIndexes.get(fieldPath)?.remove(internalId, value);
3038
- } else if (fieldType === "enum") {
3039
- state.enumIndexes.get(fieldPath)?.remove(internalId, value);
3040
- } else if (fieldType === "geopoint") {
3041
- state.geoIndexes.get(fieldPath)?.remove(internalId);
3042
- } else if (fieldType === "string[]") {
3043
- const arr = value;
3044
- const uniqueTokens = /* @__PURE__ */ new Set();
3045
- const fieldTokenList = [];
3046
- for (const item of arr) {
3047
- const result = tokenize(item, language, tokenizeOptions(options));
3048
- for (const t of result.tokens) {
3049
- uniqueTokens.add(t.token);
3050
- fieldTokenList.push(t.token);
3051
- }
3052
- }
3053
- fieldLengths[fieldPath] = fieldTokenList.length;
3054
- for (const token of uniqueTokens) state.invertedIdx.remove(token, internalId);
3055
- tokensByField[fieldPath] = fieldTokenList;
3056
- } else if (fieldType === "number[]") {
3057
- const numIdx = state.numericIndexes.get(fieldPath);
3058
- if (numIdx) {
3059
- for (const num of value) numIdx.remove(internalId, num);
3060
- }
3061
- } else if (fieldType === "boolean[]") {
3062
- const boolIdx = state.booleanIndexes.get(fieldPath);
3063
- if (boolIdx) {
3064
- for (const b of value) boolIdx.remove(internalId, b);
3065
- }
3066
- } else if (fieldType === "enum[]") {
3067
- const enumIdx = state.enumIndexes.get(fieldPath);
3068
- if (enumIdx) {
3069
- for (const e of value) enumIdx.remove(internalId, e);
3070
- }
3071
- }
3381
+ function resolveGeoIndex(fieldPath, fieldIndex) {
3382
+ if (fieldIndex?.type !== "geopoint") {
3383
+ throw new NarsilError(
3384
+ ErrorCodes.SEARCH_INVALID_FILTER,
3385
+ `Field "${fieldPath}" requires a geopoint index for geo filters`,
3386
+ { fieldPath }
3387
+ );
3072
3388
  }
3073
- return { fieldLengths, tokensByField };
3389
+ return fieldIndex.index;
3074
3390
  }
3075
- function updateFieldIndexOnly(state, docId, oldFields, newDoc, flatSchema) {
3076
- const internalId = resolveInternalId(state, docId);
3077
- for (const [fieldPath, fieldType] of Object.entries(flatSchema)) {
3078
- if (fieldType === "string" || fieldType === "string[]") continue;
3079
- const oldVal = getNestedValue(oldFields, fieldPath);
3080
- const newVal = getNestedValue(newDoc, fieldPath);
3081
- if (oldVal === newVal) continue;
3082
- if (oldVal !== void 0 && oldVal !== null && newVal !== void 0 && newVal !== null && typeof oldVal === typeof newVal && JSON.stringify(oldVal) === JSON.stringify(newVal))
3083
- continue;
3084
- if (newVal !== void 0 && newVal !== null) {
3085
- ensureFieldIndex(state, fieldPath, fieldType);
3086
- }
3087
- if (fieldType === "number") {
3391
+
3392
+ // src/core/partition/filters.ts
3393
+ function buildFilterContext(state, schema) {
3394
+ const flat = getFlatSchema(state, schema);
3395
+ const fieldIndexes = {};
3396
+ const capacity = state.docStore.internalIdCapacity();
3397
+ for (const [fieldPath, fieldType] of Object.entries(flat)) {
3398
+ if (fieldType === "number" || fieldType === "number[]") {
3088
3399
  const numIdx = state.numericIndexes.get(fieldPath);
3089
3400
  if (numIdx) {
3090
- if (oldVal !== void 0 && oldVal !== null) numIdx.remove(internalId, oldVal);
3091
- if (newVal !== void 0 && newVal !== null) numIdx.insert(internalId, newVal);
3401
+ fieldIndexes[fieldPath] = {
3402
+ type: "numeric",
3403
+ index: {
3404
+ eq: (v) => numIdx.queryEq(v),
3405
+ gt: (v) => numIdx.queryGt(v),
3406
+ gte: (v) => numIdx.queryGte(v),
3407
+ lt: (v) => numIdx.queryLt(v),
3408
+ lte: (v) => numIdx.queryLte(v),
3409
+ between: (min, max) => numIdx.queryBetween(min, max),
3410
+ allDocIds: () => numIdx.getAllDocIds(),
3411
+ eqBitset: (v, cap) => numIdx.queryEqBitset(v, cap),
3412
+ gtBitset: (v, cap) => numIdx.queryGtBitset(v, cap),
3413
+ gteBitset: (v, cap) => numIdx.queryGteBitset(v, cap),
3414
+ ltBitset: (v, cap) => numIdx.queryLtBitset(v, cap),
3415
+ lteBitset: (v, cap) => numIdx.queryLteBitset(v, cap),
3416
+ betweenBitset: (min, max, cap) => numIdx.queryBetweenBitset(min, max, cap),
3417
+ allDocIdsBitset: (cap) => numIdx.getAllDocIdsBitset(cap)
3418
+ }
3419
+ };
3092
3420
  }
3093
- } else if (fieldType === "boolean") {
3421
+ } else if (fieldType === "boolean" || fieldType === "boolean[]") {
3094
3422
  const boolIdx = state.booleanIndexes.get(fieldPath);
3095
3423
  if (boolIdx) {
3096
- if (oldVal !== void 0 && oldVal !== null) boolIdx.remove(internalId, oldVal);
3097
- if (newVal !== void 0 && newVal !== null) boolIdx.insert(internalId, newVal);
3424
+ fieldIndexes[fieldPath] = {
3425
+ type: "boolean",
3426
+ index: {
3427
+ getTrue: () => boolIdx.queryEq(true),
3428
+ getFalse: () => boolIdx.queryEq(false),
3429
+ allDocIds: () => boolIdx.getAllDocIds(),
3430
+ getTrueBitset: (cap) => boolIdx.queryEqBitset(true, cap),
3431
+ getFalseBitset: (cap) => boolIdx.queryEqBitset(false, cap),
3432
+ allDocIdsBitset: (cap) => boolIdx.getAllDocIdsBitset(cap)
3433
+ }
3434
+ };
3098
3435
  }
3099
- } else if (fieldType === "enum") {
3436
+ } else if (fieldType === "enum" || fieldType === "enum[]") {
3100
3437
  const enumIdx = state.enumIndexes.get(fieldPath);
3101
3438
  if (enumIdx) {
3102
- if (oldVal !== void 0 && oldVal !== null) enumIdx.remove(internalId, oldVal);
3103
- if (newVal !== void 0 && newVal !== null) enumIdx.insert(internalId, newVal);
3439
+ fieldIndexes[fieldPath] = {
3440
+ type: "enum",
3441
+ index: {
3442
+ getDocIds: (v) => enumIdx.queryEq(v),
3443
+ allDocIds: () => enumIdx.getAllDocIds(),
3444
+ getDocIdsBitset: (v, cap) => enumIdx.queryEqBitset(v, cap),
3445
+ getDocIdsInBitset: (values, cap) => enumIdx.queryInBitset(values, cap),
3446
+ allDocIdsBitset: (cap) => enumIdx.getAllDocIdsBitset(cap)
3447
+ }
3448
+ };
3104
3449
  }
3105
3450
  } else if (fieldType === "geopoint") {
3106
3451
  const geoIdx = state.geoIndexes.get(fieldPath);
3107
3452
  if (geoIdx) {
3108
- if (oldVal !== void 0 && oldVal !== null) geoIdx.remove(internalId);
3109
- if (newVal !== void 0 && newVal !== null) {
3110
- const geo = newVal;
3111
- geoIdx.insert(internalId, geo.lat, geo.lon);
3112
- }
3453
+ fieldIndexes[fieldPath] = {
3454
+ type: "geopoint",
3455
+ index: geoIdx
3456
+ };
3113
3457
  }
3114
- } else if (fieldType === "number[]") {
3115
- const numIdx = state.numericIndexes.get(fieldPath);
3116
- if (numIdx) {
3117
- if (oldVal !== void 0 && oldVal !== null) {
3118
- for (const n of oldVal) numIdx.remove(internalId, n);
3119
- }
3120
- if (newVal !== void 0 && newVal !== null) {
3121
- for (const n of newVal) numIdx.insert(internalId, n);
3122
- }
3458
+ }
3459
+ }
3460
+ let cachedAllDocIds = null;
3461
+ let cachedAllDocIdsBitset = null;
3462
+ return {
3463
+ fieldIndexes,
3464
+ getFieldValue: (internalId, fieldPath) => getFieldValueByInternalId(state.docStore, internalId, fieldPath),
3465
+ get allDocIds() {
3466
+ if (!cachedAllDocIds) {
3467
+ cachedAllDocIds = getAllInternalDocIds(state.docStore);
3123
3468
  }
3124
- } else if (fieldType === "boolean[]") {
3125
- const boolIdx = state.booleanIndexes.get(fieldPath);
3126
- if (boolIdx) {
3127
- if (oldVal !== void 0 && oldVal !== null) {
3128
- for (const b of oldVal) boolIdx.remove(internalId, b);
3129
- }
3130
- if (newVal !== void 0 && newVal !== null) {
3131
- for (const b of newVal) boolIdx.insert(internalId, b);
3132
- }
3469
+ return cachedAllDocIds;
3470
+ },
3471
+ capacity,
3472
+ get allDocIdsBitset() {
3473
+ if (!cachedAllDocIdsBitset) {
3474
+ cachedAllDocIdsBitset = bitsetFromSet(this.allDocIds, capacity);
3133
3475
  }
3134
- } else if (fieldType === "enum[]") {
3135
- const enumIdx = state.enumIndexes.get(fieldPath);
3136
- if (enumIdx) {
3137
- if (oldVal !== void 0 && oldVal !== null) {
3138
- for (const e of oldVal) enumIdx.remove(internalId, e);
3139
- }
3140
- if (newVal !== void 0 && newVal !== null) {
3141
- for (const e of newVal) enumIdx.insert(internalId, e);
3142
- }
3476
+ return cachedAllDocIdsBitset;
3477
+ }
3478
+ };
3479
+ }
3480
+ function applyPartitionFilters(state, filters, schema) {
3481
+ const context = buildFilterContext(state, schema);
3482
+ const resultBitset = evaluateFilters(filters, context);
3483
+ const resolver = state.docStore.resolver();
3484
+ const externalResult = /* @__PURE__ */ new Set();
3485
+ for (let wi = 0; wi < resultBitset.length; wi++) {
3486
+ let word = resultBitset[wi];
3487
+ if (word === 0) continue;
3488
+ const base = wi << 5;
3489
+ while (word !== 0) {
3490
+ const tz = Math.clz32(word & -word) ^ 31;
3491
+ const internalId = base + tz;
3492
+ const externalId = resolver.toExternal(internalId);
3493
+ if (externalId !== void 0) {
3494
+ externalResult.add(externalId);
3143
3495
  }
3496
+ word &= word - 1;
3144
3497
  }
3145
3498
  }
3499
+ return externalResult;
3500
+ }
3501
+ function applyPartitionFiltersBitset(state, filters, schema) {
3502
+ const context = buildFilterContext(state, schema);
3503
+ return evaluateFilters(filters, context);
3146
3504
  }
3147
3505
 
3148
3506
  // src/core/scorer.ts
@@ -3200,6 +3558,7 @@ function accumulateTermScore(docScores, internalId, termScore, collect, fieldNam
3200
3558
  function searchFulltext(state, params) {
3201
3559
  const {
3202
3560
  queryTokens,
3561
+ prefixExpansion,
3203
3562
  fields,
3204
3563
  boost,
3205
3564
  tolerance = 0,
@@ -3224,9 +3583,132 @@ function searchFulltext(state, params) {
3224
3583
  const useIntersection = termMatch === "all" && queryTokens.length > 1;
3225
3584
  const fieldNames = state.fieldNameTable.names;
3226
3585
  const resolver = state.docStore.resolver();
3586
+ function resolveFieldLength(internalId, fieldName, avgLen) {
3587
+ let cachedLengths = fieldLengthCache.get(internalId);
3588
+ if (cachedLengths === void 0) {
3589
+ const externalId = resolver.toExternal(internalId);
3590
+ const storedDoc = externalId !== void 0 ? state.docStore.get(externalId) : void 0;
3591
+ cachedLengths = storedDoc?.fieldLengths ?? null;
3592
+ fieldLengthCache.set(internalId, cachedLengths);
3593
+ }
3594
+ return cachedLengths?.[fieldName] ?? avgLen;
3595
+ }
3596
+ function resolvePrefixMatches(token, expansionTerms) {
3597
+ const found = [];
3598
+ const seen = /* @__PURE__ */ new Set();
3599
+ for (const term of [token, ...expansionTerms]) {
3600
+ if (seen.has(term)) continue;
3601
+ seen.add(term);
3602
+ const postingList = state.invertedIdx.lookup(term);
3603
+ if (!postingList) continue;
3604
+ const docFreq = globalStats ? globalDocFreqs[term] ?? postingList.docIdSet.size : postingList.docIdSet.size;
3605
+ found.push({ token: term, postingList, docFreq });
3606
+ }
3607
+ if (found.length === 0) return [];
3608
+ let blendedDf = 0;
3609
+ for (const f of found) {
3610
+ if (f.docFreq > blendedDf) blendedDf = f.docFreq;
3611
+ }
3612
+ const blendedIdf = computeIDF(blendedDf, totalDocs);
3613
+ return found.map((f) => ({
3614
+ token: f.token,
3615
+ factor: Math.min(1, token.length / f.token.length),
3616
+ postingList: f.postingList,
3617
+ docFreq: blendedDf,
3618
+ idf: blendedIdf
3619
+ }));
3620
+ }
3621
+ function computePrefixContributions(matches, collect) {
3622
+ const best = /* @__PURE__ */ new Map();
3623
+ for (const match of matches) {
3624
+ const perTerm = /* @__PURE__ */ new Map();
3625
+ const list = match.postingList;
3626
+ const hasDeleted = list.deletedDocs.size > 0;
3627
+ for (let pi = 0; pi < list.length; pi++) {
3628
+ const internalId = list.docIds[pi];
3629
+ if (hasDeleted && list.deletedDocs.has(internalId)) continue;
3630
+ if (filterBitset && !bitsetHas(filterBitset, internalId)) continue;
3631
+ const fieldName = fieldNames[list.fieldNameIndices[pi]];
3632
+ if (fields && !fields.includes(fieldName)) continue;
3633
+ const termFrequency = list.termFrequencies[pi];
3634
+ const fieldBoost = boost?.[fieldName] ?? 1;
3635
+ const avgLen = avgFieldLengths[fieldName] ?? 1;
3636
+ const actualFieldLength = resolveFieldLength(internalId, fieldName, avgLen);
3637
+ const termScore = scoreFn(termFrequency, match.docFreq, totalDocs, actualFieldLength, avgLen, bm25Params) * fieldBoost * match.factor;
3638
+ const existing = perTerm.get(internalId);
3639
+ if (existing) {
3640
+ existing.score += termScore;
3641
+ if (collect) {
3642
+ existing.termFrequencies[`${fieldName}:${match.token}`] = termFrequency;
3643
+ existing.fieldLengths[fieldName] = actualFieldLength;
3644
+ }
3645
+ } else if (collect) {
3646
+ perTerm.set(internalId, {
3647
+ score: termScore,
3648
+ token: match.token,
3649
+ idf: match.idf,
3650
+ termFrequencies: { [`${fieldName}:${match.token}`]: termFrequency },
3651
+ fieldLengths: { [fieldName]: actualFieldLength }
3652
+ });
3653
+ } else {
3654
+ perTerm.set(internalId, {
3655
+ score: termScore,
3656
+ token: match.token,
3657
+ idf: match.idf,
3658
+ termFrequencies: EMPTY_COMPONENTS,
3659
+ fieldLengths: EMPTY_COMPONENTS
3660
+ });
3661
+ }
3662
+ }
3663
+ for (const [internalId, contribution] of perTerm) {
3664
+ const current = best.get(internalId);
3665
+ if (!current || contribution.score > current.score) {
3666
+ best.set(internalId, contribution);
3667
+ }
3668
+ }
3669
+ }
3670
+ return best;
3671
+ }
3672
+ function mergePrefixContribution(internalId, contribution, collect) {
3673
+ const existing = docScores.get(internalId);
3674
+ if (existing) {
3675
+ existing.score += contribution.score;
3676
+ if (collect) {
3677
+ Object.assign(existing.termFrequencies, contribution.termFrequencies);
3678
+ Object.assign(existing.fieldLengths, contribution.fieldLengths);
3679
+ existing.idf[contribution.token] = contribution.idf;
3680
+ }
3681
+ return;
3682
+ }
3683
+ if (collect) {
3684
+ docScores.set(internalId, {
3685
+ score: contribution.score,
3686
+ termFrequencies: contribution.termFrequencies,
3687
+ fieldLengths: contribution.fieldLengths,
3688
+ idf: { [contribution.token]: contribution.idf }
3689
+ });
3690
+ } else {
3691
+ docScores.set(internalId, {
3692
+ score: contribution.score,
3693
+ termFrequencies: EMPTY_COMPONENTS,
3694
+ fieldLengths: EMPTY_COMPONENTS,
3695
+ idf: EMPTY_COMPONENTS
3696
+ });
3697
+ }
3698
+ }
3227
3699
  if (useIntersection) {
3228
3700
  const resolved = [];
3701
+ let prefixMatches = [];
3229
3702
  for (const qt of queryTokens) {
3703
+ if (prefixExpansion && qt.token === prefixExpansion.token) {
3704
+ prefixMatches = resolvePrefixMatches(qt.token, prefixExpansion.terms);
3705
+ let totalPostings2 = 0;
3706
+ for (const m of prefixMatches) {
3707
+ totalPostings2 += m.postingList.length;
3708
+ }
3709
+ resolved.push({ token: qt.token, matches: [], totalPostings: totalPostings2, isPrefix: true });
3710
+ continue;
3711
+ }
3230
3712
  const rawMatches = exact ? (() => {
3231
3713
  const postingList = state.invertedIdx.lookup(qt.token);
3232
3714
  return postingList ? [{ token: qt.token, postingList }] : [];
@@ -3243,6 +3725,14 @@ function searchFulltext(state, params) {
3243
3725
  }
3244
3726
  resolved.sort((a, b) => a.totalPostings - b.totalPostings);
3245
3727
  for (let tokenIndex = 0; tokenIndex < resolved.length; tokenIndex++) {
3728
+ if (resolved[tokenIndex].isPrefix) {
3729
+ const contributions = computePrefixContributions(prefixMatches, collectComponents);
3730
+ for (const [internalId, contribution] of contributions) {
3731
+ if (tokenIndex > 0 && !docScores.has(internalId)) continue;
3732
+ mergePrefixContribution(internalId, contribution, collectComponents);
3733
+ }
3734
+ continue;
3735
+ }
3246
3736
  for (const match of resolved[tokenIndex].matches) {
3247
3737
  const list = match.postingList;
3248
3738
  const hasDeleted = list.deletedDocs.size > 0;
@@ -3282,6 +3772,16 @@ function searchFulltext(state, params) {
3282
3772
  }
3283
3773
  } else {
3284
3774
  for (const qt of queryTokens) {
3775
+ if (prefixExpansion && qt.token === prefixExpansion.token) {
3776
+ const contributions = computePrefixContributions(
3777
+ resolvePrefixMatches(qt.token, prefixExpansion.terms),
3778
+ collectComponents
3779
+ );
3780
+ for (const [internalId, contribution] of contributions) {
3781
+ mergePrefixContribution(internalId, contribution, collectComponents);
3782
+ }
3783
+ continue;
3784
+ }
3285
3785
  const matchingPostings = exact ? (() => {
3286
3786
  const postingList = state.invertedIdx.lookup(qt.token);
3287
3787
  return postingList ? [{ token: qt.token, postingList }] : [];
@@ -3468,6 +3968,7 @@ function serializePartition(state, partitionId, indexName, totalPartitions, lang
3468
3968
  enum: serializedEnum,
3469
3969
  geopoint: serializedGeo
3470
3970
  },
3971
+ surfaceForms: state.surfaceRegistry.serialize(),
3471
3972
  statistics: serializedStats
3472
3973
  };
3473
3974
  }
@@ -3527,6 +4028,7 @@ function deserializePartition(state, data, clearFn, schema) {
3527
4028
  geoIdx.deserialize(converted);
3528
4029
  state.geoIndexes.set(path, geoIdx);
3529
4030
  }
4031
+ state.surfaceRegistry.deserialize(data.surfaceForms ?? {});
3530
4032
  state.stats.deserialize(data.statistics);
3531
4033
  const flatSchema = getFlatSchema(state, schema);
3532
4034
  const serializedFields = data.schema;
@@ -3544,6 +4046,72 @@ function deserializePartition(state, data, clearFn, schema) {
3544
4046
  }
3545
4047
  }
3546
4048
 
4049
+ // src/core/partition/suggestions.ts
4050
+ function verbatimOccurrences(state, token) {
4051
+ const postingList = state.invertedIdx.lookup(token);
4052
+ if (!postingList) return 0;
4053
+ const derived = postingList.totalTermFrequency - state.surfaceRegistry.stemChangedTotalFor(token);
4054
+ return derived > 0 ? derived : 0;
4055
+ }
4056
+ function suggestDisplayTerms(state, surfacePrefix, stemmedPrefix, limit) {
4057
+ if (limit <= 0 || surfacePrefix.length === 0) return [];
4058
+ const groups = /* @__PURE__ */ new Map();
4059
+ for (const candidate of state.surfaceRegistry.candidatesForPrefix(surfacePrefix)) {
4060
+ const postingList = state.invertedIdx.lookup(candidate.token);
4061
+ if (!postingList || postingList.docIdSet.size === 0) continue;
4062
+ let group = groups.get(candidate.token);
4063
+ if (!group) {
4064
+ group = { documentFrequency: postingList.docIdSet.size, surfaces: [] };
4065
+ groups.set(candidate.token, group);
4066
+ }
4067
+ group.surfaces.push({ surface: candidate.surface, occurrences: candidate.occurrences });
4068
+ }
4069
+ const termPrefixes = state.surfaceRegistry.size() === 0 && stemmedPrefix !== surfacePrefix && stemmedPrefix.length > 0 ? [surfacePrefix, stemmedPrefix] : [surfacePrefix];
4070
+ for (const prefix of termPrefixes) {
4071
+ for (const suggestion of state.invertedIdx.prefixSearch(prefix, Number.MAX_SAFE_INTEGER)) {
4072
+ const occurrences = verbatimOccurrences(state, suggestion.term);
4073
+ if (occurrences === 0) continue;
4074
+ let group = groups.get(suggestion.term);
4075
+ if (!group) {
4076
+ group = { documentFrequency: suggestion.documentFrequency, surfaces: [] };
4077
+ groups.set(suggestion.term, group);
4078
+ } else if (group.surfaces.some((s) => s.surface === suggestion.term)) {
4079
+ continue;
4080
+ }
4081
+ group.surfaces.push({ surface: suggestion.term, occurrences });
4082
+ }
4083
+ }
4084
+ const results = [];
4085
+ for (const [token, group] of groups) {
4086
+ results.push({ token, documentFrequency: group.documentFrequency, surfaces: group.surfaces });
4087
+ }
4088
+ results.sort((a, b) => b.documentFrequency - a.documentFrequency || (a.token < b.token ? -1 : 1));
4089
+ if (results.length > limit) results.length = limit;
4090
+ return results;
4091
+ }
4092
+ function expandTermPrefix(state, surfacePrefix, stemmedToken, maxExpansions) {
4093
+ if (maxExpansions <= 0 || surfacePrefix.length === 0) return [];
4094
+ const dfByToken = /* @__PURE__ */ new Map();
4095
+ for (const candidate of state.surfaceRegistry.candidatesForPrefix(surfacePrefix)) {
4096
+ if (dfByToken.has(candidate.token)) continue;
4097
+ const postingList = state.invertedIdx.lookup(candidate.token);
4098
+ if (!postingList || postingList.docIdSet.size === 0) continue;
4099
+ dfByToken.set(candidate.token, postingList.docIdSet.size);
4100
+ }
4101
+ const termPrefixes = state.surfaceRegistry.size() === 0 && stemmedToken !== surfacePrefix && stemmedToken.length > 0 ? [surfacePrefix, stemmedToken] : [surfacePrefix];
4102
+ for (const prefix of termPrefixes) {
4103
+ for (const suggestion of state.invertedIdx.prefixSearch(prefix, Number.MAX_SAFE_INTEGER)) {
4104
+ if (dfByToken.has(suggestion.term)) continue;
4105
+ if (verbatimOccurrences(state, suggestion.term) === 0) continue;
4106
+ dfByToken.set(suggestion.term, suggestion.documentFrequency);
4107
+ }
4108
+ }
4109
+ const entries = Array.from(dfByToken.entries());
4110
+ entries.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
4111
+ if (entries.length > maxExpansions) entries.length = maxExpansions;
4112
+ return entries.map((e) => e[0]);
4113
+ }
4114
+
3547
4115
  // src/core/partition/wire-payload.ts
3548
4116
  function serializePartitionToWirePayloadV2(state, partitionId, indexName, totalPartitions, language, schema) {
3549
4117
  const flatSchema = getFlatSchema(state, schema);
@@ -3640,6 +4208,7 @@ function serializePartitionToWirePayloadV2(state, partitionId, indexName, totalP
3640
4208
  enum: wireEnum,
3641
4209
  geopoint: wireGeo
3642
4210
  },
4211
+ surface_forms: state.surfaceRegistry.serialize(),
3643
4212
  statistics: {
3644
4213
  total_documents: serializedStats.totalDocuments,
3645
4214
  total_field_lengths: serializedStats.totalFieldLengths,
@@ -3656,6 +4225,7 @@ function createPartitionIndex(partitionId, trackPositions = true) {
3656
4225
  invertedIdx: createInvertedIndex(fieldNameTable),
3657
4226
  docStore: createDocumentStore(),
3658
4227
  stats: createPartitionStats(),
4228
+ surfaceRegistry: createSurfaceRegistry(),
3659
4229
  numericIndexes: /* @__PURE__ */ new Map(),
3660
4230
  booleanIndexes: /* @__PURE__ */ new Map(),
3661
4231
  enumIndexes: /* @__PURE__ */ new Map(),
@@ -3668,6 +4238,7 @@ function createPartitionIndex(partitionId, trackPositions = true) {
3668
4238
  function clearAll() {
3669
4239
  state.invertedIdx.clear();
3670
4240
  state.docStore.clear();
4241
+ state.surfaceRegistry.clear();
3671
4242
  for (const idx of state.numericIndexes.values()) idx.clear();
3672
4243
  for (const idx of state.booleanIndexes.values()) idx.clear();
3673
4244
  for (const idx of state.enumIndexes.values()) idx.clear();
@@ -3818,6 +4389,8 @@ function createPartitionIndex(partitionId, trackPositions = true) {
3818
4389
  }
3819
4390
  bytes += totalPostings * POSTING_ENTRY_SIZE;
3820
4391
  bytes += termCount * PER_TERM_OVERHEAD;
4392
+ const SURFACE_ENTRY_OVERHEAD = 140;
4393
+ bytes += state.surfaceRegistry.size() * SURFACE_ENTRY_OVERHEAD;
3821
4394
  const FIELD_ENTRY_OVERHEAD = 42;
3822
4395
  bytes += docCount * state.numericIndexes.size * FIELD_ENTRY_OVERHEAD;
3823
4396
  bytes += docCount * state.booleanIndexes.size * FIELD_ENTRY_OVERHEAD;
@@ -3837,8 +4410,11 @@ function createPartitionIndex(partitionId, trackPositions = true) {
3837
4410
  computeFacets(docIds, config, schema) {
3838
4411
  return computeFacets(state, docIds, config, schema);
3839
4412
  },
3840
- suggestTerms(prefix, limit) {
3841
- return state.invertedIdx.prefixSearch(prefix, limit);
4413
+ suggestTerms(surfacePrefix, stemmedPrefix, limit) {
4414
+ return suggestDisplayTerms(state, surfacePrefix, stemmedPrefix, limit);
4415
+ },
4416
+ expandTermPrefix(surfacePrefix, stemmedToken, maxExpansions) {
4417
+ return expandTermPrefix(state, surfacePrefix, stemmedToken, maxExpansions);
3842
4418
  },
3843
4419
  serialize(indexName, totalPartitions, language, schema) {
3844
4420
  return serializePartition(state, partitionId, indexName, totalPartitions, language, schema);
@@ -3899,9 +4475,15 @@ function createRebalancer() {
3899
4475
  });
3900
4476
  const newPartitions = [];
3901
4477
  for (let i = 0; i < newPartitionCount; i++) {
3902
- newPartitions.push(createPartitionIndex(i));
4478
+ newPartitions.push(createPartitionIndex(i, manager.config.trackPositions ?? true));
3903
4479
  }
3904
4480
  let documentsProcessed = 0;
4481
+ const insertOptions = {
4482
+ validate: false,
4483
+ stopWordOverride: manager.config.stopWords,
4484
+ customTokenizer: manager.config.tokenizer,
4485
+ collectSurfaces: manager.config.surfaceForms === true
4486
+ };
3905
4487
  for (let chunkStart = 0; chunkStart < collectedDocs.length; chunkStart += CHUNK_SIZE) {
3906
4488
  const chunkEnd = Math.min(chunkStart + CHUNK_SIZE, collectedDocs.length);
3907
4489
  for (let i = chunkStart; i < chunkEnd; i++) {
@@ -3911,7 +4493,7 @@ function createRebalancer() {
3911
4493
  if (targetPartition.has(docId)) {
3912
4494
  continue;
3913
4495
  }
3914
- targetPartition.insert(docId, document, manager.schema, manager.language, { validate: false });
4496
+ targetPartition.insert(docId, document, manager.schema, manager.language, insertOptions);
3915
4497
  documentsProcessed++;
3916
4498
  }
3917
4499
  onProgress?.({
@@ -4168,6 +4750,36 @@ function mergeFacets(partitionFacets) {
4168
4750
  }
4169
4751
 
4170
4752
  // src/search/fulltext.ts
4753
+ var PREFIX_MAX_EXPANSIONS = 50;
4754
+ function resolvePrefixExpansion(partition, params, language, options, queryTokens, lastToken) {
4755
+ const term = params.term;
4756
+ if (term === void 0) return void 0;
4757
+ const unstemmed = tokenize(term, language, {
4758
+ stem: false,
4759
+ removeStopWords: true,
4760
+ customTokenizer: options?.customTokenizer,
4761
+ stopWordOverride: options?.stopWords
4762
+ });
4763
+ let surfacePrefix;
4764
+ for (const t of unstemmed.tokens) {
4765
+ if (t.position === lastToken.position) {
4766
+ surfacePrefix = t.token;
4767
+ break;
4768
+ }
4769
+ }
4770
+ if (surfacePrefix === void 0 || surfacePrefix.length === 0) return void 0;
4771
+ const expansions = partition.expandTermPrefix(surfacePrefix, lastToken.token, PREFIX_MAX_EXPANSIONS);
4772
+ const terms = [];
4773
+ for (const expanded of expansions) {
4774
+ if (expanded !== lastToken.token) terms.push(expanded);
4775
+ }
4776
+ for (const qt of queryTokens) {
4777
+ if (qt.token === lastToken.token) {
4778
+ return { token: qt.token, terms };
4779
+ }
4780
+ }
4781
+ return void 0;
4782
+ }
4171
4783
  function fulltextSearch(partition, params, language, schema, options) {
4172
4784
  if (!params.term || params.term.trim().length === 0) {
4173
4785
  return { scored: [], totalMatched: 0 };
@@ -4189,6 +4801,11 @@ function fulltextSearch(partition, params, language, schema, options) {
4189
4801
  return { scored: [], totalMatched: 0 };
4190
4802
  }
4191
4803
  const queryTokens = deduplicateTokens(queryTokenResult.tokens);
4804
+ let prefixExpansion;
4805
+ if (params.prefix === true && params.exact !== true) {
4806
+ const lastToken = queryTokenResult.tokens[queryTokenResult.tokens.length - 1];
4807
+ prefixExpansion = resolvePrefixExpansion(partition, params, language, options, queryTokens, lastToken);
4808
+ }
4192
4809
  let filterBitset;
4193
4810
  if (params.filters) {
4194
4811
  filterBitset = partition.applyFiltersBitset(params.filters, schema);
@@ -4202,6 +4819,7 @@ function fulltextSearch(partition, params, language, schema, options) {
4202
4819
  const collectComponents = params.includeScoreComponents === true || params.termMatch !== void 0 && params.termMatch !== "any";
4203
4820
  const rawResult = partition.searchFulltext({
4204
4821
  queryTokens,
4822
+ prefixExpansion,
4205
4823
  fields: params.fields,
4206
4824
  boost: params.boost,
4207
4825
  tolerance: params.tolerance ?? 0,
@@ -4217,7 +4835,14 @@ function fulltextSearch(partition, params, language, schema, options) {
4217
4835
  let scored = rawResult.scored;
4218
4836
  const termMatch = params.termMatch ?? "any";
4219
4837
  if (termMatch !== "any") {
4220
- scored = filterByTermCoverage(scored, queryTokens, termMatch, params.tolerance ?? 0, params.exact ?? false);
4838
+ scored = filterByTermCoverage(
4839
+ scored,
4840
+ queryTokens,
4841
+ termMatch,
4842
+ params.tolerance ?? 0,
4843
+ params.exact ?? false,
4844
+ prefixExpansion
4845
+ );
4221
4846
  }
4222
4847
  if (params.minScore !== void 0 && params.minScore > 0) {
4223
4848
  const threshold = params.minScore;
@@ -4252,26 +4877,40 @@ function deduplicateTokens(tokens) {
4252
4877
  }
4253
4878
  return result;
4254
4879
  }
4255
- function filterByTermCoverage(scored, queryTokens, policy, tolerance, exact) {
4880
+ function filterByTermCoverage(scored, queryTokens, policy, tolerance, exact, prefixExpansion) {
4256
4881
  const requiredCount = policy === "all" ? queryTokens.length : policy;
4257
4882
  if (requiredCount <= 0) return scored;
4258
4883
  if (requiredCount > queryTokens.length) return [];
4884
+ const expansionTerms = prefixExpansion ? new Set(prefixExpansion.terms) : void 0;
4259
4885
  return scored.filter((doc) => {
4260
- const matched = countDocTermMatches(doc, queryTokens, tolerance, exact);
4886
+ const matched = countDocTermMatches(doc, queryTokens, tolerance, exact, prefixExpansion?.token, expansionTerms);
4261
4887
  return matched >= requiredCount;
4262
4888
  });
4263
4889
  }
4264
- function countDocTermMatches(doc, queryTokens, tolerance, exact) {
4890
+ function countDocTermMatches(doc, queryTokens, tolerance, exact, prefixToken, expansionTerms) {
4265
4891
  const indexTokens = Object.keys(doc.idf);
4266
4892
  if (indexTokens.length === 0) return 0;
4267
4893
  let count = 0;
4268
4894
  for (const qt of queryTokens) {
4895
+ if (prefixToken !== void 0 && qt.token === prefixToken) {
4896
+ if (prefixTokenSatisfied(qt.token, indexTokens, expansionTerms)) {
4897
+ count++;
4898
+ }
4899
+ continue;
4900
+ }
4269
4901
  if (queryTermSatisfied(qt.token, indexTokens, tolerance, exact)) {
4270
4902
  count++;
4271
4903
  }
4272
4904
  }
4273
4905
  return count;
4274
4906
  }
4907
+ function prefixTokenSatisfied(prefixToken, indexTokens, expansionTerms) {
4908
+ for (const indexToken of indexTokens) {
4909
+ if (indexToken === prefixToken) return true;
4910
+ if (expansionTerms?.has(indexToken)) return true;
4911
+ }
4912
+ return false;
4913
+ }
4275
4914
  function queryTermSatisfied(queryToken, indexTokens, tolerance, exact) {
4276
4915
  if (exact || tolerance === 0) {
4277
4916
  return indexTokens.includes(queryToken);
@@ -4531,13 +5170,15 @@ function createPartitionManager(indexName, config, language, router, initialPart
4531
5170
  function resolveInsertOptions(options) {
4532
5171
  const applyStrict = config.strict === true;
4533
5172
  const applyAnalyzer = config.stopWords !== void 0 || config.tokenizer !== void 0;
4534
- if (!applyStrict && !applyAnalyzer) return options;
5173
+ const applySurfaces = config.surfaceForms === true;
5174
+ if (!applyStrict && !applyAnalyzer && !applySurfaces) return options;
4535
5175
  const resolved = { ...options };
4536
5176
  if (applyStrict) resolved.strict = true;
4537
5177
  if (applyAnalyzer) {
4538
5178
  resolved.stopWordOverride = options?.stopWordOverride ?? config.stopWords;
4539
5179
  resolved.customTokenizer = options?.customTokenizer ?? config.tokenizer;
4540
5180
  }
5181
+ if (applySurfaces) resolved.collectSurfaces = true;
4541
5182
  return resolved;
4542
5183
  }
4543
5184
  function rebuildDocPartitionMap() {
@@ -4634,7 +5275,7 @@ function createPartitionManager(indexName, config, language, router, initialPart
4634
5275
  if (pid === void 0) {
4635
5276
  throw new NarsilError(ErrorCodes.DOC_NOT_FOUND, `Document "${docId}" not found in any partition`, { docId });
4636
5277
  }
4637
- partitions[pid].remove(docId, config.schema, language);
5278
+ partitions[pid].remove(docId, config.schema, language, resolveInsertOptions());
4638
5279
  docPartitionMap.delete(docId);
4639
5280
  },
4640
5281
  beginBatchRemove() {
@@ -7625,141 +8266,6 @@ function readHeader(data) {
7625
8266
  reserved
7626
8267
  };
7627
8268
  }
7628
- var VALID_HNSW_METRICS2 = /* @__PURE__ */ new Set(["cosine", "dotProduct", "euclidean"]);
7629
- function validateHnswMetric2(value) {
7630
- if (value === void 0 || value === null) return void 0;
7631
- if (typeof value === "string" && VALID_HNSW_METRICS2.has(value)) {
7632
- return value;
7633
- }
7634
- return void 0;
7635
- }
7636
- function wireToPartition(raw) {
7637
- const documents = {};
7638
- for (const [docId, doc] of Object.entries(raw.documents ?? {})) {
7639
- documents[docId] = {
7640
- fields: doc.fields,
7641
- fieldLengths: doc.field_lengths ?? {}
7642
- };
7643
- }
7644
- const invertedIndex = {};
7645
- for (const [token, list] of Object.entries(raw.inverted_index ?? {})) {
7646
- invertedIndex[token] = {
7647
- docFrequency: list.doc_freq,
7648
- postings: (list.postings ?? []).map((p) => ({
7649
- docId: p.doc_id,
7650
- termFrequency: p.term_freq,
7651
- field: p.field,
7652
- positions: p.positions ?? []
7653
- }))
7654
- };
7655
- }
7656
- const numeric = {};
7657
- for (const [field, entries] of Object.entries(raw.field_indexes?.numeric ?? {})) {
7658
- numeric[field] = entries.map((e) => ({ value: e.value, docId: e.doc_id }));
7659
- }
7660
- const boolean = {};
7661
- for (const [field, idx] of Object.entries(raw.field_indexes?.boolean ?? {})) {
7662
- boolean[field] = { trueDocs: idx.true_docs ?? [], falseDocs: idx.false_docs ?? [] };
7663
- }
7664
- const geopoint = {};
7665
- for (const [field, entries] of Object.entries(raw.field_indexes?.geopoint ?? {})) {
7666
- geopoint[field] = entries.map((e) => ({ lat: e.lat, lon: e.lon, docId: e.doc_id }));
7667
- }
7668
- const vectorData = {};
7669
- for (const [field, data] of Object.entries(raw.vector_data ?? {})) {
7670
- vectorData[field] = {
7671
- dimension: data.dimension,
7672
- vectors: (data.vectors ?? []).map((v) => ({ docId: v.doc_id, vector: v.vector })),
7673
- hnswGraph: data.hnsw_graph ? {
7674
- entryPoint: data.hnsw_graph.entry_point,
7675
- maxLayer: data.hnsw_graph.max_layer,
7676
- m: data.hnsw_graph.m,
7677
- efConstruction: data.hnsw_graph.ef_construction,
7678
- metric: validateHnswMetric2(data.hnsw_graph.metric),
7679
- nodes: data.hnsw_graph.nodes
7680
- } : null,
7681
- sq8: data.sq8 ? {
7682
- alpha: data.sq8.alpha,
7683
- offset: data.sq8.offset,
7684
- quantizedVectors: data.sq8.quantized_vectors,
7685
- vectorSums: data.sq8.vector_sums,
7686
- vectorSumSqs: data.sq8.vector_sum_sqs
7687
- } : null
7688
- };
7689
- }
7690
- return {
7691
- indexName: raw.index_name,
7692
- partitionId: raw.partition_id,
7693
- totalPartitions: raw.total_partitions,
7694
- language: raw.language ?? "english",
7695
- schema: raw.schema ?? {},
7696
- docCount: raw.doc_count ?? 0,
7697
- avgDocLength: raw.avg_doc_length ?? 0,
7698
- documents,
7699
- invertedIndex,
7700
- fieldIndexes: {
7701
- numeric,
7702
- boolean,
7703
- enum: raw.field_indexes?.enum ?? {},
7704
- geopoint
7705
- },
7706
- vectorData,
7707
- statistics: {
7708
- totalDocuments: raw.statistics?.total_documents ?? 0,
7709
- totalFieldLengths: raw.statistics?.total_field_lengths ?? {},
7710
- averageFieldLengths: raw.statistics?.average_field_lengths ?? {},
7711
- docFrequencies: raw.statistics?.doc_frequencies ?? {}
7712
- }
7713
- };
7714
- }
7715
- function deserializePayloadV1(data) {
7716
- const raw = decode(data);
7717
- return wireToPartition(raw);
7718
- }
7719
- function metadataToWire(meta) {
7720
- const wire = {
7721
- index_name: meta.indexName,
7722
- schema: meta.schema,
7723
- language: meta.language,
7724
- partition_count: meta.partitionCount,
7725
- bm25_params: meta.bm25Params,
7726
- created_at: meta.createdAt,
7727
- engine_version: meta.engineVersion
7728
- };
7729
- if (meta.vectorFields) {
7730
- wire.vector_fields = meta.vectorFields;
7731
- }
7732
- if (meta.embedding) {
7733
- wire.embedding = meta.embedding.adapter !== void 0 ? { adapter: meta.embedding.adapter, fields: meta.embedding.fields } : { fields: meta.embedding.fields };
7734
- }
7735
- return wire;
7736
- }
7737
- function wireToMetadata(raw) {
7738
- const meta = {
7739
- indexName: raw.index_name,
7740
- schema: raw.schema ?? {},
7741
- language: raw.language ?? "english",
7742
- partitionCount: raw.partition_count ?? 1,
7743
- bm25Params: raw.bm25_params ?? { k1: 1.2, b: 0.75 },
7744
- createdAt: raw.created_at ?? 0,
7745
- engineVersion: raw.engine_version ?? "0.0.0"
7746
- };
7747
- if (raw.vector_fields) {
7748
- meta.vectorFields = raw.vector_fields;
7749
- }
7750
- if (raw.embedding && typeof raw.embedding === "object" && typeof raw.embedding.fields === "object") {
7751
- meta.embedding = typeof raw.embedding.adapter === "string" ? { adapter: raw.embedding.adapter, fields: raw.embedding.fields } : { fields: raw.embedding.fields };
7752
- }
7753
- return meta;
7754
- }
7755
- function serializeMetadata(meta) {
7756
- const wire = metadataToWire(meta);
7757
- return encode(wire);
7758
- }
7759
- function deserializeMetadata(data) {
7760
- const raw = decode(data);
7761
- return wireToMetadata(raw);
7762
- }
7763
8269
 
7764
8270
  // src/serialization/envelope.ts
7765
8271
  var CURRENT_ENVELOPE_VERSION = 1;
@@ -8775,6 +9281,7 @@ function mergeTimeOrderedSegments(ordered, fallback) {
8775
9281
  collectDocuments(merged, part, winner, segIndex);
8776
9282
  collectInvertedIndex(merged, part, winner, segIndex);
8777
9283
  collectFieldIndexes(merged, part, winner, segIndex);
9284
+ collectSurfaceForms(merged, part);
8778
9285
  }
8779
9286
  recomputeStatistics(merged);
8780
9287
  validateMergedPartition(merged);
@@ -8902,6 +9409,26 @@ function collectFieldIndexes(merged, part, winner, segIndex) {
8902
9409
  }
8903
9410
  }
8904
9411
  }
9412
+ function collectSurfaceForms(merged, part) {
9413
+ if (!part.surfaceForms) return;
9414
+ let target = merged.surfaceForms;
9415
+ if (!target) {
9416
+ target = /* @__PURE__ */ Object.create(null);
9417
+ merged.surfaceForms = target;
9418
+ }
9419
+ for (const [surface, value] of Object.entries(part.surfaceForms)) {
9420
+ const count = typeof value === "number" ? value : value[0];
9421
+ const token = typeof value === "number" ? surface : value[1];
9422
+ const existing = target[surface];
9423
+ if (existing === void 0) {
9424
+ target[surface] = value;
9425
+ continue;
9426
+ }
9427
+ const existingCount = typeof existing === "number" ? existing : existing[0];
9428
+ const total = existingCount + count;
9429
+ target[surface] = token === surface ? total : [total, token];
9430
+ }
9431
+ }
8905
9432
  function recomputeStatistics(merged) {
8906
9433
  const totalFieldLengths = /* @__PURE__ */ Object.create(null);
8907
9434
  let totalDocuments = 0;
@@ -9112,6 +9639,9 @@ function reconstructSchemaFromMetadata(metadata) {
9112
9639
  if (metadata.embedding) {
9113
9640
  config.embedding = metadata.embedding.adapter !== void 0 ? { adapter: metadata.embedding.adapter, fields: metadata.embedding.fields } : { fields: metadata.embedding.fields };
9114
9641
  }
9642
+ if (metadata.surfaceForms === true) {
9643
+ config.surfaceForms = true;
9644
+ }
9115
9645
  return config;
9116
9646
  }
9117
9647
 
@@ -10572,6 +11102,9 @@ function buildMetadata(indexName, hooks) {
10572
11102
  if (config.embedding) {
10573
11103
  metadata.embedding = config.embedding;
10574
11104
  }
11105
+ if (config.surfaceForms) {
11106
+ metadata.surfaceForms = true;
11107
+ }
10575
11108
  return metadata;
10576
11109
  }
10577
11110
  function createDurabilityIntegration(tier, hooks) {
@@ -11102,7 +11635,8 @@ function createDurabilityFromConfig(config, wiring) {
11102
11635
  language: entry.language.name,
11103
11636
  k1: entry.config.bm25?.k1 ?? 1.2,
11104
11637
  b: entry.config.bm25?.b ?? 0.75,
11105
- ...embedding !== void 0 ? { embedding } : {}
11638
+ ...embedding !== void 0 ? { embedding } : {},
11639
+ ...entry.config.surfaceForms === true ? { surfaceForms: true } : {}
11106
11640
  };
11107
11641
  },
11108
11642
  createIndexFromMetadata: wiring.createIndexFromMetadata,
@@ -12586,10 +13120,15 @@ function highlightField(text, queryTokens, language, options) {
12586
13120
  return qt.token.toLowerCase();
12587
13121
  });
12588
13122
  const matchedRanges = [];
13123
+ const prefixToken = options?.prefixToken;
12589
13124
  for (let i = 0; i < fieldResult.tokens.length; i++) {
12590
13125
  if (i >= charOffsets.length) break;
12591
13126
  const fieldToken = fieldResult.tokens[i].token;
12592
13127
  const stemmedField = language.stemmer ? language.stemmer(fieldToken) : fieldToken;
13128
+ if (prefixToken !== void 0 && fieldToken.startsWith(prefixToken)) {
13129
+ matchedRanges.push({ start: charOffsets[i].start, end: charOffsets[i].end });
13130
+ continue;
13131
+ }
12593
13132
  for (const stemmedQuery of stemmedQueryTokens) {
12594
13133
  if (stemmedField === stemmedQuery) {
12595
13134
  matchedRanges.push({ start: charOffsets[i].start, end: charOffsets[i].end });
@@ -12633,6 +13172,22 @@ function applyHighlights(hits, params, language, config) {
12633
13172
  customTokenizer: config.tokenizer,
12634
13173
  stopWordOverride: config.stopWords
12635
13174
  });
13175
+ let prefixToken;
13176
+ if (params.prefix === true && params.exact !== true && queryTokenResult.tokens.length > 0) {
13177
+ const lastPosition = queryTokenResult.tokens[queryTokenResult.tokens.length - 1].position;
13178
+ const unstemmed = tokenize(params.term ?? "", language, {
13179
+ stem: false,
13180
+ removeStopWords: true,
13181
+ customTokenizer: config.tokenizer,
13182
+ stopWordOverride: config.stopWords
13183
+ });
13184
+ for (const t of unstemmed.tokens) {
13185
+ if (t.position === lastPosition) {
13186
+ prefixToken = t.token;
13187
+ break;
13188
+ }
13189
+ }
13190
+ }
12636
13191
  for (const hit of hits) {
12637
13192
  const highlights = {};
12638
13193
  for (const field of params.highlight.fields) {
@@ -12642,7 +13197,8 @@ function applyHighlights(hits, params, language, config) {
12642
13197
  highlights[field] = highlightField(fieldValue, queryTokenResult.tokens, language, {
12643
13198
  preTag: params.highlight.preTag,
12644
13199
  postTag: params.highlight.postTag,
12645
- maxSnippetLength: params.highlight.maxSnippetLength
13200
+ maxSnippetLength: params.highlight.maxSnippetLength,
13201
+ prefixToken
12646
13202
  });
12647
13203
  }
12648
13204
  }
@@ -13291,25 +13847,36 @@ function executeSuggest(manager, language, params) {
13291
13847
  return { terms: [], elapsed: performance.now() - t0 };
13292
13848
  }
13293
13849
  const stemmed = language.stemmer ? language.stemmer(lastToken) : lastToken;
13294
- const prefixes = stemmed !== lastToken ? [lastToken, stemmed] : [lastToken];
13295
- const partitions = manager.getAllPartitions();
13296
13850
  const merged = /* @__PURE__ */ new Map();
13297
13851
  const perPartitionLimit = limit * 2;
13298
- for (const partition of partitions) {
13299
- const seen = /* @__PURE__ */ new Set();
13300
- for (const prefix of prefixes) {
13301
- const suggestions = partition.suggestTerms(prefix, perPartitionLimit);
13302
- for (const s of suggestions) {
13303
- if (seen.has(s.term)) continue;
13304
- seen.add(s.term);
13305
- merged.set(s.term, (merged.get(s.term) ?? 0) + s.documentFrequency);
13852
+ for (const partition of manager.getAllPartitions()) {
13853
+ for (const suggestion of partition.suggestTerms(lastToken, stemmed, perPartitionLimit)) {
13854
+ let entry = merged.get(suggestion.token);
13855
+ if (!entry) {
13856
+ entry = { documentFrequency: 0, surfaceOccurrences: /* @__PURE__ */ new Map() };
13857
+ merged.set(suggestion.token, entry);
13858
+ }
13859
+ entry.documentFrequency += suggestion.documentFrequency;
13860
+ for (const s of suggestion.surfaces) {
13861
+ entry.surfaceOccurrences.set(s.surface, (entry.surfaceOccurrences.get(s.surface) ?? 0) + s.occurrences);
13306
13862
  }
13307
13863
  }
13308
13864
  }
13309
- const terms = Array.from(merged.entries()).map(([term, documentFrequency]) => ({ term, documentFrequency })).sort((a, b) => b.documentFrequency - a.documentFrequency);
13865
+ const terms = Array.from(merged.values()).map((entry) => ({ term: pickDisplaySurface(entry.surfaceOccurrences), documentFrequency: entry.documentFrequency })).sort((a, b) => b.documentFrequency - a.documentFrequency || (a.term < b.term ? -1 : 1));
13310
13866
  if (terms.length > limit) terms.length = limit;
13311
13867
  return { terms, elapsed: performance.now() - t0 };
13312
13868
  }
13869
+ function pickDisplaySurface(surfaceOccurrences) {
13870
+ let best = "";
13871
+ let bestCount = -1;
13872
+ for (const [surface, occurrences] of surfaceOccurrences) {
13873
+ if (occurrences > bestCount || occurrences === bestCount && surface < best) {
13874
+ best = surface;
13875
+ bestCount = occurrences;
13876
+ }
13877
+ }
13878
+ return best;
13879
+ }
13313
13880
 
13314
13881
  // src/engine/vector-maintenance.ts
13315
13882
  function resolveVectorIndexes(manager, indexName, fieldName) {