@delali/narsil 0.2.4-next.49 → 0.2.4-next.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/dist/{options-Dtk99qUs.d.ts → capabilities-JrVQOwi4.d.ts} +55 -47
  3. package/dist/chunk-AV4IJKD2.mjs +35 -0
  4. package/dist/{chunk-M6JGMKA5.mjs → chunk-BEIK3EOZ.mjs} +503 -2273
  5. package/dist/chunk-CMVG2UEF.mjs +2038 -0
  6. package/dist/chunk-CV5H7XL7.mjs +876 -0
  7. package/dist/{chunk-KGRYQVZ5.mjs → chunk-MHE7YWAU.mjs} +2 -1
  8. package/dist/{chunk-56PJBIJ6.mjs → chunk-ND34I3SV.mjs} +427 -1455
  9. package/dist/chunk-PIWKZPP7.mjs +64 -0
  10. package/dist/{chunk-HGJC5L65.mjs → chunk-PMBHH44R.mjs} +121 -2
  11. package/dist/chunk-RP23PWMV.mjs +11 -0
  12. package/dist/{chunk-RDGXYRF4.mjs → chunk-WYJXSLER.mjs} +8 -19
  13. package/dist/{chunk-JXHUHFJ5.mjs → chunk-X6YZO4MQ.mjs} +2 -11
  14. package/dist/{chunk-OWCFK42Q.mjs → chunk-XE6XR2OE.mjs} +3 -10
  15. package/dist/chunk-ZHUQKZBE.mjs +1292 -0
  16. package/dist/{client-DX7xPcNA.d.ts → client-CBOVHdjp.d.ts} +31 -7
  17. package/dist/client.d.ts +7 -7
  18. package/dist/client.mjs +26 -10
  19. package/dist/{config-B41W23Us.d.ts → config-Czh2YNDi.d.ts} +2 -2
  20. package/dist/{config-Co-zAXqO.d.ts → config-OON7IJ_B.d.ts} +26 -5
  21. package/dist/direct-executor-OREXU72C.mjs +12 -0
  22. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  23. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  24. package/dist/distribution.d.ts +8 -8
  25. package/dist/distribution.mjs +28 -11
  26. package/dist/{engine-CABZg25p.d.ts → engine-9PtFY50q.d.ts} +114 -62
  27. package/dist/index.browser.mjs +3255 -2516
  28. package/dist/index.d.ts +7 -7
  29. package/dist/index.mjs +11 -6
  30. package/dist/{search-FjWhZN3f.d.ts → memory-Bzd5xcEW.d.ts} +88 -79
  31. package/dist/persistence/durability/checkpoint-worker.d.ts +1 -1
  32. package/dist/persistence/durability/checkpoint-worker.mjs +11 -6
  33. package/dist/react.d.ts +4 -4
  34. package/dist/{schema-B7juWg8Q.d.ts → schema-BqRx9t4E.d.ts} +22 -3
  35. package/dist/server.d.ts +47 -42
  36. package/dist/server.mjs +321 -817
  37. package/dist/thread-PZLEO6JL.mjs +310 -0
  38. package/dist/{types-DsSSjY4d.d.ts → types-DXPxaXPO.d.ts} +1 -1
  39. package/dist/vector/hnsw-build-worker.mjs +2 -1
  40. package/dist/vector/search-worker.mjs +3 -121
  41. package/dist/workers/entry.mjs +47 -3
  42. package/package.json +5 -1
  43. package/dist/direct-executor-HRTDWMVB.mjs +0 -9
@@ -0,0 +1,1292 @@
1
+ import { MAX_INDEX_NAME_LENGTH, INDEX_NAME_PATTERN, MAX_DOC_ID_LENGTH, DEFAULT_LIMIT, DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, MAX_SUGGEST_SCATTER_LIMIT, DEFAULT_OFFSET } from './chunk-XE6XR2OE.mjs';
2
+ import { clampRowCount, sortSignatureOf, requireSortableFields, decodePageCursor, requireMatchingCursor, resolveProjection, encodePageCursor, requireWithinResultWindow, normalizeSort, fanOutQuery, flattenSchema, applySorting, applyPagination, countsWithoutScores, fanOutMatchCount, tokenize, fnv1aBytes, fulltextMatches, mergeFacets, pruneStatsToQueryTerms, partitionsIn } from './chunk-CMVG2UEF.mjs';
3
+ import { compareCodePoints, compareComparableKeys } from './chunk-2GWVQWGA.mjs';
4
+ import { NarsilError, ErrorCodes } from './chunk-P4ELXVJQ.mjs';
5
+
6
+ // src/search/cursor-binding.ts
7
+ var ABSENT_TAG = 0;
8
+ var NULL_TAG = 1;
9
+ var FALSE_TAG = 2;
10
+ var TRUE_TAG = 3;
11
+ var NUMBER_TAG = 4;
12
+ var STRING_TAG = 5;
13
+ var LIST_TAG = 6;
14
+ var MAP_TAG = 7;
15
+ var encoder = new TextEncoder();
16
+ function ensureCapacity(stream, extra) {
17
+ if (stream.length + extra <= stream.bytes.length) return;
18
+ const grown = new Uint8Array(Math.max(stream.bytes.length * 2, stream.length + extra));
19
+ grown.set(stream.bytes.subarray(0, stream.length));
20
+ stream.bytes = grown;
21
+ }
22
+ function writeByte(stream, value) {
23
+ ensureCapacity(stream, 1);
24
+ stream.bytes[stream.length] = value;
25
+ stream.length += 1;
26
+ }
27
+ function writeUint32(stream, value) {
28
+ ensureCapacity(stream, 4);
29
+ stream.bytes[stream.length] = value >>> 24 & 255;
30
+ stream.bytes[stream.length + 1] = value >>> 16 & 255;
31
+ stream.bytes[stream.length + 2] = value >>> 8 & 255;
32
+ stream.bytes[stream.length + 3] = value & 255;
33
+ stream.length += 4;
34
+ }
35
+ var numberBytes = new DataView(new ArrayBuffer(8));
36
+ function writeNumber(stream, value) {
37
+ numberBytes.setFloat64(0, value, false);
38
+ ensureCapacity(stream, 8);
39
+ for (let i = 0; i < 8; i++) {
40
+ stream.bytes[stream.length + i] = numberBytes.getUint8(i);
41
+ }
42
+ stream.length += 8;
43
+ }
44
+ function writeString(stream, value) {
45
+ const encoded = encoder.encode(value);
46
+ writeUint32(stream, encoded.length);
47
+ ensureCapacity(stream, encoded.length);
48
+ stream.bytes.set(encoded, stream.length);
49
+ stream.length += encoded.length;
50
+ }
51
+ var NumberList = class {
52
+ constructor(members) {
53
+ this.members = members;
54
+ }
55
+ };
56
+ var NUMBER_MEMBER_BYTES = 9;
57
+ var LIST_HEADER_BYTES = 5;
58
+ function writeNumberList(stream, list) {
59
+ const count = list.members.length;
60
+ ensureCapacity(stream, LIST_HEADER_BYTES + count * NUMBER_MEMBER_BYTES);
61
+ const bytes = stream.bytes;
62
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
63
+ let at = stream.length;
64
+ bytes[at] = LIST_TAG;
65
+ view.setUint32(at + 1, count, false);
66
+ at += LIST_HEADER_BYTES;
67
+ for (let i = 0; i < count; i++) {
68
+ bytes[at] = NUMBER_TAG;
69
+ view.setFloat64(at + 1, Math.fround(list.members[i]), false);
70
+ at += NUMBER_MEMBER_BYTES;
71
+ }
72
+ stream.length = at;
73
+ }
74
+ function writeValue(stream, value) {
75
+ if (value instanceof NumberList) {
76
+ writeNumberList(stream, value);
77
+ return;
78
+ }
79
+ if (value === void 0) {
80
+ writeByte(stream, ABSENT_TAG);
81
+ return;
82
+ }
83
+ if (value === null) {
84
+ writeByte(stream, NULL_TAG);
85
+ return;
86
+ }
87
+ if (typeof value === "boolean") {
88
+ writeByte(stream, value ? TRUE_TAG : FALSE_TAG);
89
+ return;
90
+ }
91
+ if (typeof value === "number") {
92
+ writeByte(stream, NUMBER_TAG);
93
+ writeNumber(stream, value);
94
+ return;
95
+ }
96
+ if (typeof value === "string") {
97
+ writeByte(stream, STRING_TAG);
98
+ writeString(stream, value);
99
+ return;
100
+ }
101
+ if (Array.isArray(value)) {
102
+ writeByte(stream, LIST_TAG);
103
+ writeUint32(stream, value.length);
104
+ for (const member of value) {
105
+ writeValue(stream, member ?? null);
106
+ }
107
+ return;
108
+ }
109
+ const record = value;
110
+ const keys = Object.keys(record).sort(compareCodePoints);
111
+ writeByte(stream, MAP_TAG);
112
+ writeUint32(stream, keys.length);
113
+ for (const key of keys) {
114
+ writeByte(stream, STRING_TAG);
115
+ writeString(stream, key);
116
+ writeValue(stream, record[key] ?? null);
117
+ }
118
+ }
119
+ function boundVectorOf(vector) {
120
+ if (vector === void 0) return void 0;
121
+ const bound = { ...vector };
122
+ if (vector.value !== void 0) {
123
+ bound.value = new NumberList(vector.value);
124
+ }
125
+ if (vector.text !== void 0) {
126
+ delete bound.value;
127
+ }
128
+ return bound;
129
+ }
130
+ var FILTERS_SLOT = 2;
131
+ var BOUND_SLOT_COUNT = 15;
132
+ function bindingOf(values) {
133
+ const stream = { bytes: new Uint8Array(256), length: 0 };
134
+ for (const value of values) {
135
+ writeValue(stream, value);
136
+ }
137
+ return fnv1aBytes(stream.bytes.subarray(0, stream.length)).toString(16).padStart(8, "0");
138
+ }
139
+ function queryBindingOf(params) {
140
+ return bindingOf([
141
+ params.term,
142
+ params.fields,
143
+ params.filters,
144
+ params.boost,
145
+ params.minScore,
146
+ params.termMatch,
147
+ params.tolerance,
148
+ params.prefixLength,
149
+ params.prefix,
150
+ params.exact,
151
+ params.pinned,
152
+ params.mode,
153
+ params.hybrid,
154
+ boundVectorOf(params.vector),
155
+ params.scoring ?? "local"
156
+ ]);
157
+ }
158
+ function listBindingOf(filters) {
159
+ const values = new Array(BOUND_SLOT_COUNT).fill(void 0);
160
+ values[FILTERS_SLOT] = filters;
161
+ return bindingOf(values);
162
+ }
163
+
164
+ // src/engine/validation.ts
165
+ function now() {
166
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
167
+ return performance.now();
168
+ }
169
+ return Date.now();
170
+ }
171
+ function validateIndexName(name) {
172
+ if (!name || name.length === 0) {
173
+ throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, "Index name must not be empty", { indexName: name });
174
+ }
175
+ if (name.length > MAX_INDEX_NAME_LENGTH) {
176
+ throw new NarsilError(
177
+ ErrorCodes.INDEX_NOT_FOUND,
178
+ `Index name must not exceed ${MAX_INDEX_NAME_LENGTH} characters`,
179
+ {
180
+ indexName: name,
181
+ length: name.length
182
+ }
183
+ );
184
+ }
185
+ if (!INDEX_NAME_PATTERN.test(name)) {
186
+ throw new NarsilError(
187
+ ErrorCodes.INDEX_NOT_FOUND,
188
+ `Index name "${name}" contains invalid characters; use alphanumeric, dots, hyphens, and underscores only`,
189
+ { indexName: name }
190
+ );
191
+ }
192
+ if (name.includes("..")) {
193
+ throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index name "${name}" must not contain ".."`, { indexName: name });
194
+ }
195
+ }
196
+ function validateDocId(docId) {
197
+ if (!docId || docId.length === 0) {
198
+ throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not be empty", { docId });
199
+ }
200
+ if (docId.length > MAX_DOC_ID_LENGTH) {
201
+ throw new NarsilError(
202
+ ErrorCodes.DOC_VALIDATION_FAILED,
203
+ `Document ID must not exceed ${MAX_DOC_ID_LENGTH} characters`,
204
+ {
205
+ docId,
206
+ length: docId.length
207
+ }
208
+ );
209
+ }
210
+ if (docId.includes("\0")) {
211
+ throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not contain null bytes", { docId });
212
+ }
213
+ }
214
+ function validatePartitionConfig(partitions) {
215
+ const { maxDocsPerPartition, maxPartitions, watermark } = partitions;
216
+ if (maxDocsPerPartition !== void 0 && (!Number.isInteger(maxDocsPerPartition) || maxDocsPerPartition < 1)) {
217
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.maxDocsPerPartition must be a positive integer", {
218
+ maxDocsPerPartition
219
+ });
220
+ }
221
+ if (maxPartitions !== void 0 && (!Number.isInteger(maxPartitions) || maxPartitions < 1)) {
222
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.maxPartitions must be a positive integer", {
223
+ maxPartitions
224
+ });
225
+ }
226
+ if (watermark !== void 0 && (typeof watermark !== "number" || !(watermark > 0) || watermark > 1)) {
227
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.watermark must be above 0 and at most 1", {
228
+ watermark
229
+ });
230
+ }
231
+ }
232
+ function requirePositiveSafeInteger(field, value) {
233
+ if (value === void 0) return;
234
+ if (!Number.isSafeInteger(value) || value < 1) {
235
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, `${field} must be a positive safe integer`, { field, value });
236
+ }
237
+ }
238
+ function validateWorkerConfig(workers, lifecycle) {
239
+ if (workers === void 0) return;
240
+ requirePositiveSafeInteger("workers.count", workers.count);
241
+ requirePositiveSafeInteger("workers.promotionThreshold", workers.promotionThreshold);
242
+ requirePositiveSafeInteger("workers.idleTimeoutMs", workers.idleTimeoutMs);
243
+ if (workers.mainCopyQueries !== void 0 && workers.mainCopyQueries !== "lone" && workers.mainCopyQueries !== "none") {
244
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "workers.mainCopyQueries must be 'lone' or 'none'", {
245
+ mainCopyQueries: workers.mainCopyQueries
246
+ });
247
+ }
248
+ const closeAfter = lifecycle?.idleTimeoutMs;
249
+ if (workers.idleTimeoutMs !== void 0 && closeAfter !== void 0 && workers.idleTimeoutMs > closeAfter) {
250
+ throw new NarsilError(
251
+ ErrorCodes.CONFIG_INVALID,
252
+ "workers.idleTimeoutMs must be at most lifecycle.idleTimeoutMs, so that an index drops its copies before it closes",
253
+ { workersIdleTimeoutMs: workers.idleTimeoutMs, lifecycleIdleTimeoutMs: closeAfter }
254
+ );
255
+ }
256
+ }
257
+ function validateBM25Params(bm25) {
258
+ if (bm25 === void 0) return;
259
+ const { k1, b } = bm25;
260
+ if (k1 !== void 0 && (!Number.isFinite(k1) || k1 < 0)) {
261
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "bm25.k1 must be a finite number at or above 0", { k1 });
262
+ }
263
+ if (b !== void 0 && (!Number.isFinite(b) || b < 0 || b > 1)) {
264
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "bm25.b must be between 0 and 1", { b });
265
+ }
266
+ }
267
+ function clampLimit(limit) {
268
+ return clampRowCount(limit, DEFAULT_LIMIT);
269
+ }
270
+ function clampOffset(offset) {
271
+ return clampRowCount(offset, DEFAULT_OFFSET);
272
+ }
273
+
274
+ // src/engine/list-documents/index.ts
275
+ function clampListLimit(limit) {
276
+ const clamped = Math.max(1, clampLimit(limit));
277
+ requireWithinResultWindow(clamped, 0);
278
+ return clamped;
279
+ }
280
+ function firstIdAfter(sorted, anchor) {
281
+ let low = 0;
282
+ let high = sorted.length;
283
+ while (low < high) {
284
+ const mid = low + high >>> 1;
285
+ if (compareCodePoints(sorted[mid], anchor) <= 0) low = mid + 1;
286
+ else high = mid;
287
+ }
288
+ return low;
289
+ }
290
+ function collectFilterMatches(partitions, filters, schema) {
291
+ const perPartition = partitions.map((partition) => partition.filterMatches(filters, schema));
292
+ let total = 0;
293
+ for (const matches of perPartition) total += matches.count;
294
+ return { matchesFor: (partitionIndex) => perPartition[partitionIndex] ?? null, total };
295
+ }
296
+ function collectCandidates(partition, anchor, matches, wanted, out) {
297
+ const sorted = partition.sortedDocIds();
298
+ let index = anchor === null ? 0 : firstIdAfter(sorted, anchor);
299
+ let taken = 0;
300
+ while (index < sorted.length && taken < wanted) {
301
+ const docId = sorted[index];
302
+ index++;
303
+ if (matches !== null && !matches.hasExternal(docId)) continue;
304
+ out.push(docId);
305
+ taken++;
306
+ }
307
+ }
308
+ function pageInIdOrder(partitions, cursor, filtered, limit, binding) {
309
+ const anchor = cursor === null ? null : cursor.anchor;
310
+ const candidates = [];
311
+ for (let index = 0; index < partitions.length; index++) {
312
+ collectCandidates(
313
+ partitions[index],
314
+ anchor,
315
+ filtered === null ? null : filtered.matchesFor(index),
316
+ limit + 1,
317
+ candidates
318
+ );
319
+ }
320
+ candidates.sort(compareCodePoints);
321
+ const hasMore = candidates.length > limit;
322
+ const ids = hasMore ? candidates.slice(0, limit) : candidates;
323
+ const nextCursor = hasMore && ids.length > 0 ? { anchor: ids[ids.length - 1], score: null, sortKey: null, sortSignature: null, binding } : null;
324
+ return { ids, nextCursor };
325
+ }
326
+ function pageInSortOrder(partitions, cursor, filtered, limit, sort, signature, schema, binding) {
327
+ const normalized = normalizeSort(sort);
328
+ const fields = normalized.map((entry) => entry.field);
329
+ const directions = normalized.map((entry) => entry.direction);
330
+ const flatSchema = flattenSchema(schema);
331
+ const fieldTypes = fields.map((field) => flatSchema[field]);
332
+ const request = {
333
+ fields,
334
+ directions,
335
+ fieldTypes,
336
+ limit: limit + 1,
337
+ anchorKey: cursor === null ? null : cursor.sortKey,
338
+ anchorId: cursor === null ? null : cursor.anchor
339
+ };
340
+ const merged = [];
341
+ for (let index = 0; index < partitions.length; index++) {
342
+ const matches = filtered === null ? null : filtered.matchesFor(index);
343
+ for (const entry of partitions[index].sortedPage({ ...request, matches })) merged.push(entry);
344
+ }
345
+ merged.sort((a, b) => compareComparableKeys(a.key, b.key, directions) || compareCodePoints(a.id, b.id));
346
+ const hasMore = merged.length > limit;
347
+ const page = hasMore ? merged.slice(0, limit) : merged;
348
+ const last = page[page.length - 1];
349
+ const nextCursor = hasMore && last !== void 0 ? { anchor: last.id, score: null, sortKey: last.key, sortSignature: signature, binding } : null;
350
+ return { ids: page.map((entry) => entry.id), nextCursor };
351
+ }
352
+ function countAcrossPartitions(manager, partitionIds, partitions) {
353
+ if (partitionIds === void 0) {
354
+ return manager.countDocuments();
355
+ }
356
+ let total = 0;
357
+ for (const partition of partitions) {
358
+ total += partition.count();
359
+ }
360
+ return total;
361
+ }
362
+ function executeListDocuments(params, context) {
363
+ const { manager, schema } = context;
364
+ const startTime = now();
365
+ const limit = clampListLimit(params.limit);
366
+ const signature = sortSignatureOf(params.sort);
367
+ requireSortableFields(params.sort, schema);
368
+ const binding = listBindingOf(params.filters);
369
+ let cursor = null;
370
+ if (params.cursor !== void 0) {
371
+ cursor = decodePageCursor(params.cursor);
372
+ requireMatchingCursor(cursor, params.cursor, signature, false, binding);
373
+ }
374
+ const partitions = context.partitionIds === void 0 ? manager.getAllPartitions() : context.partitionIds.map((partitionId) => manager.partitionAt(partitionId)).filter((partition) => partition !== void 0);
375
+ const filtered = params.filters === void 0 ? null : collectFilterMatches(partitions, params.filters, schema);
376
+ const total = filtered === null ? countAcrossPartitions(manager, context.partitionIds, partitions) : filtered.total;
377
+ const page = params.sort === void 0 || signature === null ? pageInIdOrder(partitions, cursor, filtered, limit, binding) : pageInSortOrder(partitions, cursor, filtered, limit, params.sort, signature, schema, binding);
378
+ const projection = resolveProjection(params.document);
379
+ const documents = [];
380
+ for (const docId of page.ids) {
381
+ if (projection.kind === "none") {
382
+ documents.push({ id: docId, document: {} });
383
+ continue;
384
+ }
385
+ const stored = manager.get(docId, projection);
386
+ if (stored === void 0) continue;
387
+ documents.push({ id: docId, document: stored });
388
+ }
389
+ if (page.nextCursor === null) {
390
+ for (const partition of partitions) partition.releaseSortedDocIds();
391
+ }
392
+ return {
393
+ documents,
394
+ cursor: page.nextCursor === null ? null : encodePageCursor(page.nextCursor),
395
+ total,
396
+ elapsed: now() - startTime
397
+ };
398
+ }
399
+
400
+ // src/search/grouping.ts
401
+ function getNestedValue(obj, path) {
402
+ const segments = path.split(".");
403
+ let current = obj;
404
+ for (const segment of segments) {
405
+ if (current === null || current === void 0 || typeof current !== "object") {
406
+ return void 0;
407
+ }
408
+ if (!Object.hasOwn(current, segment)) {
409
+ return void 0;
410
+ }
411
+ current = current[segment];
412
+ }
413
+ return current;
414
+ }
415
+ function applyGrouping(hits, group, getDocument) {
416
+ if (!group.fields || group.fields.length === 0) {
417
+ return [{ values: {}, hits }];
418
+ }
419
+ const groupMap = /* @__PURE__ */ new Map();
420
+ for (const hit of hits) {
421
+ const doc = getDocument(hit.id);
422
+ const fieldValues = [];
423
+ const valuesRecord = {};
424
+ for (const field of group.fields) {
425
+ const value = doc ? getNestedValue(doc, field) : void 0;
426
+ fieldValues.push(value);
427
+ valuesRecord[field] = value;
428
+ }
429
+ const compositeKey = JSON.stringify(fieldValues);
430
+ const existing = groupMap.get(compositeKey);
431
+ if (existing) {
432
+ existing.hits.push(hit);
433
+ } else {
434
+ groupMap.set(compositeKey, { values: valuesRecord, hits: [hit] });
435
+ }
436
+ }
437
+ const groups = [];
438
+ for (const entry of groupMap.values()) {
439
+ let groupHits = entry.hits;
440
+ if (group.maxPerGroup !== void 0 && group.maxPerGroup > 0) {
441
+ groupHits = groupHits.slice(0, group.maxPerGroup);
442
+ }
443
+ const result = { values: entry.values, hits: groupHits };
444
+ if (group.reduce) {
445
+ const folded = foldGroupReducer(
446
+ group.reduce,
447
+ groupHits.map((hit) => ({ document: getDocument(hit.id), score: hit.score ?? 0 }))
448
+ );
449
+ if (folded.reducerError !== void 0) {
450
+ result.reducerError = folded.reducerError;
451
+ } else {
452
+ result.reduced = folded.reduced;
453
+ }
454
+ }
455
+ groups.push(result);
456
+ }
457
+ groups.sort((a, b) => {
458
+ const aScore = a.hits.length > 0 ? a.hits[0].score ?? 0 : 0;
459
+ const bScore = b.hits.length > 0 ? b.hits[0].score ?? 0 : 0;
460
+ if (aScore !== bScore) return bScore - aScore;
461
+ const aId = a.hits.length > 0 ? a.hits[0].id : "";
462
+ const bId = b.hits.length > 0 ? b.hits[0].id : "";
463
+ return compareCodePoints(aId, bId);
464
+ });
465
+ if (group.limit !== void 0 && Number.isFinite(group.limit) && group.limit >= 0) {
466
+ const cap = Math.floor(group.limit);
467
+ if (groups.length > cap) {
468
+ groups.length = cap;
469
+ }
470
+ }
471
+ return groups;
472
+ }
473
+ function foldGroupReducer(reduce, rows) {
474
+ try {
475
+ let accumulator = reduce.initialValue();
476
+ for (const row of rows) {
477
+ if (row.document !== void 0) {
478
+ accumulator = reduce.reducer(accumulator, row.document, row.score);
479
+ }
480
+ }
481
+ return { reduced: accumulator };
482
+ } catch (err) {
483
+ return { reducerError: err instanceof Error ? err.message : String(err) };
484
+ }
485
+ }
486
+
487
+ // src/search/pinning.ts
488
+ function dedupePinnedEntries(pinned) {
489
+ const deduped = [];
490
+ const seen = /* @__PURE__ */ new Set();
491
+ for (const entry of pinned) {
492
+ if (seen.has(entry.docId)) continue;
493
+ seen.add(entry.docId);
494
+ deduped.push(entry);
495
+ }
496
+ return deduped;
497
+ }
498
+ function applyPinning(hits, pinned, resolveDoc) {
499
+ const result = hits.slice();
500
+ const deduped = dedupePinnedEntries(pinned);
501
+ const pinnedDocIds = new Set(deduped.map((entry) => entry.docId));
502
+ for (let i = result.length - 1; i >= 0; i--) {
503
+ if (pinnedDocIds.has(result[i].id)) {
504
+ result.splice(i, 1);
505
+ }
506
+ }
507
+ const sorted = deduped.sort((a, b) => a.position - b.position);
508
+ for (const entry of sorted) {
509
+ const doc = resolveDoc(entry.docId);
510
+ if (!doc) continue;
511
+ const pos = Math.max(0, Math.min(entry.position, result.length));
512
+ result.splice(pos, 0, doc);
513
+ }
514
+ return result;
515
+ }
516
+
517
+ // src/highlighting/highlighter.ts
518
+ function buildTokenCharOffsets(text, originalTokens) {
519
+ const offsets = [];
520
+ let searchFrom = 0;
521
+ for (const token of originalTokens) {
522
+ const idx = text.indexOf(token, searchFrom);
523
+ if (idx === -1) continue;
524
+ offsets.push({ start: idx, end: idx + token.length });
525
+ searchFrom = idx + token.length;
526
+ }
527
+ return offsets;
528
+ }
529
+ function mergeOverlapping(ranges) {
530
+ if (ranges.length === 0) return [];
531
+ const sorted = [...ranges].sort((a, b) => a.start - b.start);
532
+ const merged = [sorted[0]];
533
+ for (let i = 1; i < sorted.length; i++) {
534
+ const prev = merged[merged.length - 1];
535
+ const curr = sorted[i];
536
+ if (curr.start <= prev.end) {
537
+ prev.end = Math.max(prev.end, curr.end);
538
+ } else {
539
+ merged.push(curr);
540
+ }
541
+ }
542
+ return merged;
543
+ }
544
+ function findDensestWindow(positions, text, windowSize) {
545
+ if (positions.length === 0) {
546
+ return { start: 0, end: Math.min(windowSize, text.length) };
547
+ }
548
+ let bestStart = 0;
549
+ let bestCount = 0;
550
+ for (const pos of positions) {
551
+ const windowStart = Math.max(0, pos.start - Math.floor(windowSize / 2));
552
+ const windowEnd = Math.min(text.length, windowStart + windowSize);
553
+ const adjustedStart = Math.max(0, windowEnd - windowSize);
554
+ let count = 0;
555
+ for (const p of positions) {
556
+ if (p.start >= adjustedStart && p.end <= windowEnd) {
557
+ count++;
558
+ }
559
+ }
560
+ if (count > bestCount) {
561
+ bestCount = count;
562
+ bestStart = adjustedStart;
563
+ }
564
+ }
565
+ return { start: bestStart, end: Math.min(text.length, bestStart + windowSize) };
566
+ }
567
+ function applySnippetTruncation(taggedText, originalText, positions, maxLen, preTag, postTag) {
568
+ if (maxLen <= 0 || originalText.length <= maxLen) {
569
+ return { snippet: taggedText, adjustedPositions: positions };
570
+ }
571
+ const window = findDensestWindow(positions, originalText, maxLen);
572
+ const snippetRaw = originalText.substring(window.start, window.end);
573
+ const adjustedPositions = [];
574
+ for (const pos of positions) {
575
+ if (pos.start >= window.start && pos.end <= window.end) {
576
+ adjustedPositions.push({
577
+ start: pos.start - window.start,
578
+ end: pos.end - window.start
579
+ });
580
+ }
581
+ }
582
+ let snippet = snippetRaw;
583
+ const sortedDesc = [...adjustedPositions].sort((a, b) => b.start - a.start);
584
+ for (const pos of sortedDesc) {
585
+ snippet = snippet.substring(0, pos.start) + preTag + snippet.substring(pos.start, pos.end) + postTag + snippet.substring(pos.end);
586
+ }
587
+ const needsLeadingEllipsis = window.start > 0;
588
+ const needsTrailingEllipsis = window.end < originalText.length;
589
+ if (needsLeadingEllipsis) snippet = `...${snippet}`;
590
+ if (needsTrailingEllipsis) snippet = `${snippet}...`;
591
+ return { snippet, adjustedPositions };
592
+ }
593
+ function highlightField(text, queryTokens, language, options) {
594
+ const preTag = options?.preTag ?? "<mark>";
595
+ const postTag = options?.postTag ?? "</mark>";
596
+ const maxSnippetLength = options?.maxSnippetLength ?? 200;
597
+ if (text.length === 0) {
598
+ return { snippet: text, positions: [] };
599
+ }
600
+ const fieldResult = tokenize(text, language, { stem: false, removeStopWords: false });
601
+ if (fieldResult.tokens.length === 0) {
602
+ return { snippet: text, positions: [] };
603
+ }
604
+ const lowercaseText = text.normalize("NFC").toLowerCase();
605
+ const charOffsets = buildTokenCharOffsets(lowercaseText, fieldResult.originalTokens);
606
+ const stemmedQueryTokens = queryTokens.map((qt) => {
607
+ if (language.stemmer) {
608
+ return language.stemmer(qt.token.toLowerCase());
609
+ }
610
+ return qt.token.toLowerCase();
611
+ });
612
+ const matchedRanges = [];
613
+ const prefixToken = options?.prefixToken;
614
+ for (let i = 0; i < fieldResult.tokens.length; i++) {
615
+ if (i >= charOffsets.length) break;
616
+ const fieldToken = fieldResult.tokens[i].token;
617
+ const stemmedField = language.stemmer ? language.stemmer(fieldToken) : fieldToken;
618
+ if (prefixToken !== void 0 && fieldToken.startsWith(prefixToken)) {
619
+ matchedRanges.push({ start: charOffsets[i].start, end: charOffsets[i].end });
620
+ continue;
621
+ }
622
+ for (const stemmedQuery of stemmedQueryTokens) {
623
+ if (stemmedField === stemmedQuery) {
624
+ matchedRanges.push({ start: charOffsets[i].start, end: charOffsets[i].end });
625
+ break;
626
+ }
627
+ }
628
+ }
629
+ const mergedPositions = mergeOverlapping(matchedRanges);
630
+ if (mergedPositions.length === 0) {
631
+ if (maxSnippetLength > 0 && text.length > maxSnippetLength) {
632
+ const truncated = text.substring(0, maxSnippetLength);
633
+ return { snippet: `${truncated}...`, positions: [] };
634
+ }
635
+ return { snippet: text, positions: [] };
636
+ }
637
+ if (maxSnippetLength > 0 && text.length > maxSnippetLength) {
638
+ const { snippet, adjustedPositions } = applySnippetTruncation(
639
+ text,
640
+ text,
641
+ mergedPositions,
642
+ maxSnippetLength,
643
+ preTag,
644
+ postTag
645
+ );
646
+ return { snippet, positions: adjustedPositions };
647
+ }
648
+ let result = text;
649
+ const sortedDesc = [...mergedPositions].sort((a, b) => b.start - a.start);
650
+ for (const pos of sortedDesc) {
651
+ result = result.substring(0, pos.start) + preTag + result.substring(pos.start, pos.end) + postTag + result.substring(pos.end);
652
+ }
653
+ return { snippet: result, positions: mergedPositions };
654
+ }
655
+
656
+ // src/engine/query/highlight.ts
657
+ function applyHighlights(hits, params, language, analysis) {
658
+ if (!params.highlight) return;
659
+ const queryTokenResult = tokenize(params.term ?? "", language, {
660
+ stem: true,
661
+ removeStopWords: true,
662
+ customTokenizer: analysis.customTokenizer,
663
+ stopWordOverride: analysis.stopWords
664
+ });
665
+ let prefixToken;
666
+ if (params.prefix === true && params.exact !== true && queryTokenResult.tokens.length > 0) {
667
+ const lastPosition = queryTokenResult.tokens[queryTokenResult.tokens.length - 1].position;
668
+ const unstemmed = tokenize(params.term ?? "", language, {
669
+ stem: false,
670
+ removeStopWords: true,
671
+ customTokenizer: analysis.customTokenizer,
672
+ stopWordOverride: analysis.stopWords
673
+ });
674
+ for (const t of unstemmed.tokens) {
675
+ if (t.position === lastPosition) {
676
+ prefixToken = t.token;
677
+ break;
678
+ }
679
+ }
680
+ }
681
+ for (const hit of hits) {
682
+ const highlights = {};
683
+ for (const field of params.highlight.fields) {
684
+ const doc = hit.document;
685
+ const fieldValue = doc[field];
686
+ if (typeof fieldValue === "string") {
687
+ highlights[field] = highlightField(fieldValue, queryTokenResult.tokens, language, {
688
+ preTag: params.highlight.preTag,
689
+ postTag: params.highlight.postTag,
690
+ maxSnippetLength: params.highlight.maxSnippetLength,
691
+ prefixToken
692
+ });
693
+ }
694
+ }
695
+ hit.highlights = highlights;
696
+ }
697
+ }
698
+
699
+ // src/engine/query/shared.ts
700
+ function partitionsFor(manager, partitionIds) {
701
+ return partitionsIn(manager, partitionIds);
702
+ }
703
+ function coverageFor(manager, partitionIds) {
704
+ if (partitionIds === void 0) {
705
+ return {
706
+ totalPartitions: manager.partitionCount,
707
+ queriedPartitions: manager.partitionCount,
708
+ timedOutPartitions: 0,
709
+ failedPartitions: 0
710
+ };
711
+ }
712
+ const queriedPartitions = partitionsFor(manager, partitionIds).length;
713
+ return {
714
+ totalPartitions: partitionIds.length,
715
+ queriedPartitions,
716
+ timedOutPartitions: 0,
717
+ failedPartitions: partitionIds.length - queriedPartitions
718
+ };
719
+ }
720
+ function partitionsForVectorSearch(manager, vecIndex, partitionIds) {
721
+ if (partitionIds === void 0) {
722
+ return void 0;
723
+ }
724
+ if (!vecIndex.partitionsKnown()) {
725
+ vecIndex.assignPartitions((docId) => manager.partitionIdOf(docId));
726
+ }
727
+ return new Set(partitionIds);
728
+ }
729
+ function scoringConfigFor(params, context) {
730
+ const scoringMode = params.scoring ?? context.config.defaultScoring ?? "local";
731
+ if (scoringMode !== "broadcast") {
732
+ return { scoringMode, partitionIds: context.partitionIds };
733
+ }
734
+ return { scoringMode, globalStats: context.broadcastStats?.(context.indexName), partitionIds: context.partitionIds };
735
+ }
736
+ function broadcastStatsForWorker(params, context, scoring) {
737
+ if (scoring.scoringMode !== "broadcast" || scoring.globalStats === void 0) {
738
+ return void 0;
739
+ }
740
+ const term = params.term;
741
+ if (term === void 0 || term.trim().length === 0) {
742
+ return void 0;
743
+ }
744
+ return pruneStatsToQueryTerms(scoring.globalStats, term, context.language, context.manager.analysis);
745
+ }
746
+ function searchOptionsFor(manager) {
747
+ return {
748
+ bm25Params: manager.config.bm25,
749
+ stopWords: manager.analysis.stopWords,
750
+ customTokenizer: manager.analysis.customTokenizer
751
+ };
752
+ }
753
+ function collectFilterDocIds(manager, params, schema, partitionIds) {
754
+ const filterDocIds = /* @__PURE__ */ new Set();
755
+ if (!params.filters) return filterDocIds;
756
+ for (const partition of partitionsFor(manager, partitionIds)) {
757
+ const partitionFiltered = partition.applyFilters(params.filters, schema);
758
+ for (const docId of partitionFiltered) {
759
+ filterDocIds.add(docId);
760
+ }
761
+ }
762
+ return filterDocIds;
763
+ }
764
+ function vectorResultsToScored(results) {
765
+ return results.map((r) => ({
766
+ docId: r.docId,
767
+ score: r.score,
768
+ termFrequencies: {},
769
+ fieldLengths: {},
770
+ idf: {}
771
+ }));
772
+ }
773
+ function vectorSearchersOf(context) {
774
+ return context.vectorSearchers ?? context.manager.getVectorIndexes();
775
+ }
776
+ function resolveVectorIndex(context, fieldName) {
777
+ return vectorSearchersOf(context).get(fieldName);
778
+ }
779
+ function clampAlpha(alpha) {
780
+ if (alpha === void 0) return 0.5;
781
+ if (!Number.isFinite(alpha)) return 0.5;
782
+ if (alpha < 0) return 0;
783
+ if (alpha > 1) return 1;
784
+ return alpha;
785
+ }
786
+
787
+ // src/engine/query/sorted.ts
788
+ function sortsWithoutScores(params) {
789
+ return params.includeScores !== true && params.includeScoreComponents !== true && params.minScore === void 0 && params.group === void 0 && params.pinned === void 0 && (params.termMatch === void 0 || params.termMatch === "any");
790
+ }
791
+ function executeSortedQueryPage(params, context, limit, offset, sortSignature) {
792
+ const { manager, language, config } = context;
793
+ const schema = config.schema;
794
+ let anchorKey = null;
795
+ let anchorId = null;
796
+ if (params.searchAfter !== void 0) {
797
+ const decoded = decodePageCursor(params.searchAfter);
798
+ requireMatchingCursor(decoded, params.searchAfter, sortSignature, true, context.cursorBinding);
799
+ anchorKey = decoded.sortKey;
800
+ anchorId = decoded.anchor;
801
+ }
802
+ const normalized = normalizeSort(params.sort);
803
+ const fields = normalized.map((entry) => entry.field);
804
+ const directions = normalized.map((entry) => entry.direction);
805
+ const flatSchema = flattenSchema(schema);
806
+ const fieldTypes = fields.map((field) => flatSchema[field]);
807
+ const options = searchOptionsFor(manager);
808
+ const partitions = partitionsFor(manager, context.partitionIds);
809
+ const partitionLimit = offset + limit + 1;
810
+ const merged = [];
811
+ const partitionFacets = [];
812
+ let count = 0;
813
+ for (const partition of partitions) {
814
+ const matches = fulltextMatches(partition, params, language, schema, options);
815
+ if (params.facets !== void 0) {
816
+ partitionFacets.push(
817
+ partition.computeFacets(
818
+ matches === null ? /* @__PURE__ */ new Set() : { ordinalBitset: matches.ordinalBitset() },
819
+ params.facets,
820
+ schema
821
+ )
822
+ );
823
+ }
824
+ if (matches === null || matches.count === 0) continue;
825
+ count += matches.count;
826
+ const page2 = partition.sortedPage({
827
+ fields,
828
+ directions,
829
+ fieldTypes,
830
+ limit: partitionLimit,
831
+ anchorKey,
832
+ anchorId,
833
+ matches
834
+ });
835
+ for (const entry of page2) merged.push(entry);
836
+ }
837
+ merged.sort((a, b) => compareComparableKeys(a.key, b.key, directions) || compareCodePoints(a.id, b.id));
838
+ const hasMore = merged.length > offset + limit;
839
+ const page = merged.slice(offset, offset + limit);
840
+ const last = page[page.length - 1];
841
+ let cursor;
842
+ if (hasMore && last !== void 0) {
843
+ cursor = encodePageCursor({
844
+ anchor: last.id,
845
+ score: null,
846
+ sortKey: last.key,
847
+ sortSignature,
848
+ binding: context.cursorBinding
849
+ });
850
+ }
851
+ return {
852
+ hits: page.map((entry) => ({ id: entry.id, document: void 0 })),
853
+ count,
854
+ cursor,
855
+ facets: partitionFacets.length > 0 ? mergeFacets(partitionFacets) : void 0
856
+ };
857
+ }
858
+
859
+ // src/search/fusion.ts
860
+ function reciprocalRankFusion(lists, options) {
861
+ const k = options.k > 0 ? options.k : 60;
862
+ const scores = /* @__PURE__ */ new Map();
863
+ const docData = /* @__PURE__ */ new Map();
864
+ for (const list of lists) {
865
+ for (let rank = 0; rank < list.length; rank++) {
866
+ const doc = list[rank];
867
+ const rrfContribution = 1 / (k + rank + 1);
868
+ scores.set(doc.docId, (scores.get(doc.docId) ?? 0) + rrfContribution);
869
+ if (!docData.has(doc.docId)) {
870
+ docData.set(doc.docId, doc);
871
+ }
872
+ }
873
+ }
874
+ const result = [];
875
+ for (const [docId, score] of scores) {
876
+ const original = docData.get(docId);
877
+ if (!original) continue;
878
+ result.push({
879
+ docId,
880
+ score,
881
+ termFrequencies: original.termFrequencies,
882
+ fieldLengths: original.fieldLengths,
883
+ idf: original.idf
884
+ });
885
+ }
886
+ result.sort((a, b) => b.score - a.score || compareCodePoints(a.docId, b.docId));
887
+ return result;
888
+ }
889
+ function linearCombination(textResults, vectorResults, options) {
890
+ const { alpha } = options;
891
+ const normalizedText = minMaxNormalize(textResults);
892
+ const normalizedVector = minMaxNormalize(vectorResults);
893
+ const textScoreMap = /* @__PURE__ */ new Map();
894
+ for (const doc of normalizedText) {
895
+ textScoreMap.set(doc.docId, doc.score);
896
+ }
897
+ const textDocMap = /* @__PURE__ */ new Map();
898
+ for (const doc of textResults) {
899
+ textDocMap.set(doc.docId, doc);
900
+ }
901
+ const vectorScoreMap = /* @__PURE__ */ new Map();
902
+ for (const doc of normalizedVector) {
903
+ vectorScoreMap.set(doc.docId, doc.score);
904
+ }
905
+ const allDocIds = /* @__PURE__ */ new Set();
906
+ for (const doc of textResults) allDocIds.add(doc.docId);
907
+ for (const doc of vectorResults) allDocIds.add(doc.docId);
908
+ const result = [];
909
+ for (const docId of allDocIds) {
910
+ const tScore = textScoreMap.get(docId) ?? 0;
911
+ const vScore = vectorScoreMap.get(docId) ?? 0;
912
+ const combined = alpha * vScore + (1 - alpha) * tScore;
913
+ const original = textDocMap.get(docId);
914
+ result.push({
915
+ docId,
916
+ score: combined,
917
+ termFrequencies: original?.termFrequencies ?? {},
918
+ fieldLengths: original?.fieldLengths ?? {},
919
+ idf: original?.idf ?? {}
920
+ });
921
+ }
922
+ result.sort((a, b) => b.score - a.score || compareCodePoints(a.docId, b.docId));
923
+ return result;
924
+ }
925
+ function minMaxNormalize(docs) {
926
+ if (docs.length === 0) return [];
927
+ let min = docs[0].score;
928
+ let max = docs[0].score;
929
+ for (let i = 1; i < docs.length; i++) {
930
+ if (docs[i].score < min) min = docs[i].score;
931
+ if (docs[i].score > max) max = docs[i].score;
932
+ }
933
+ const range = max - min;
934
+ if (range === 0) {
935
+ return docs.map((d) => ({ docId: d.docId, score: 1 }));
936
+ }
937
+ return docs.map((d) => ({ docId: d.docId, score: (d.score - min) / range }));
938
+ }
939
+
940
+ // src/engine/query/vector.ts
941
+ async function executeVectorSearch(params, context, limit, offset) {
942
+ const { manager, config, partitionIds } = context;
943
+ const vectorConfig = params.vector;
944
+ if (!vectorConfig || !vectorConfig.value) {
945
+ return { scored: [], totalMatched: 0 };
946
+ }
947
+ const vecIndex = resolveVectorIndex(context, vectorConfig.field);
948
+ if (!vecIndex) {
949
+ return { scored: [], totalMatched: 0 };
950
+ }
951
+ let filterDocIds;
952
+ let filterPartitions;
953
+ if (params.filters) {
954
+ filterDocIds = collectFilterDocIds(manager, params, config.schema, partitionIds);
955
+ if (filterDocIds.size === 0) {
956
+ return { scored: [], totalMatched: 0 };
957
+ }
958
+ } else {
959
+ filterPartitions = partitionsForVectorSearch(manager, vecIndex, partitionIds);
960
+ }
961
+ const queryVec = new Float32Array(vectorConfig.value);
962
+ const k = limit + offset + 1;
963
+ const results = await vecIndex.searchParallel(queryVec, k, {
964
+ metric: vectorConfig.metric ?? "cosine",
965
+ minSimilarity: vectorConfig.similarity ?? -Infinity,
966
+ ...filterDocIds !== void 0 ? { filterDocIds } : {},
967
+ ...filterPartitions !== void 0 ? { filterPartitions } : {},
968
+ efSearch: vectorConfig.efSearch
969
+ });
970
+ const scored = vectorResultsToScored(results);
971
+ return { scored, totalMatched: scored.length };
972
+ }
973
+ async function textLeg(textOnlyParams, context) {
974
+ const { manager, language, config, workerSearch, indexName } = context;
975
+ const scoring = scoringConfigFor(textOnlyParams, context);
976
+ if (workerSearch) {
977
+ const viaWorker = await workerSearch(
978
+ indexName,
979
+ textOnlyParams,
980
+ broadcastStatsForWorker(textOnlyParams, context, scoring),
981
+ context.partitionIds
982
+ );
983
+ if (viaWorker) return viaWorker;
984
+ }
985
+ return fanOutQuery(manager, textOnlyParams, language, config.schema, scoring, searchOptionsFor(manager));
986
+ }
987
+ async function vectorLeg(vectorConfig, queryVector, context, filterDocIds, k) {
988
+ const { manager } = context;
989
+ const vecIndex = resolveVectorIndex(context, vectorConfig.field);
990
+ if (!vecIndex) return [];
991
+ const filterPartitions = filterDocIds === void 0 ? partitionsForVectorSearch(manager, vecIndex, context.partitionIds) : void 0;
992
+ const results = await vecIndex.searchParallel(queryVector, k, {
993
+ metric: vectorConfig.metric ?? "cosine",
994
+ minSimilarity: vectorConfig.similarity ?? -Infinity,
995
+ ...filterDocIds !== void 0 ? { filterDocIds } : {},
996
+ ...filterPartitions !== void 0 ? { filterPartitions } : {},
997
+ efSearch: vectorConfig.efSearch
998
+ });
999
+ return vectorResultsToScored(results);
1000
+ }
1001
+ async function executeHybridSearch(params, context, limit, offset) {
1002
+ const { manager, config } = context;
1003
+ const { vector: vectorConfig, mode: _mode, hybrid: _hybrid, ...textOnlyParams } = params;
1004
+ let filterDocIds;
1005
+ if (params.filters) {
1006
+ filterDocIds = collectFilterDocIds(manager, params, config.schema, context.partitionIds);
1007
+ if (filterDocIds.size === 0) {
1008
+ return { scored: [], totalMatched: 0 };
1009
+ }
1010
+ }
1011
+ if (!vectorConfig || !vectorConfig.value) {
1012
+ const textOnly = await textLeg(textOnlyParams, context);
1013
+ return { scored: textOnly.scored, totalMatched: textOnly.totalMatched, facets: textOnly.facets };
1014
+ }
1015
+ const vectorPending = vectorLeg(
1016
+ vectorConfig,
1017
+ new Float32Array(vectorConfig.value),
1018
+ context,
1019
+ filterDocIds,
1020
+ limit + offset + 1
1021
+ );
1022
+ const [textFanOutResult, vectorScored] = await Promise.all([textLeg(textOnlyParams, context), vectorPending]);
1023
+ const hybridConfig = params.hybrid ?? {};
1024
+ const strategy = hybridConfig.strategy ?? "rrf";
1025
+ let fusedScored;
1026
+ if (strategy === "rrf") {
1027
+ const rrfK = hybridConfig.k !== void 0 && hybridConfig.k > 0 ? hybridConfig.k : 60;
1028
+ fusedScored = reciprocalRankFusion([textFanOutResult.scored, vectorScored], { k: rrfK });
1029
+ } else {
1030
+ const alpha = clampAlpha(hybridConfig.alpha);
1031
+ fusedScored = linearCombination(textFanOutResult.scored, vectorScored, { alpha });
1032
+ }
1033
+ if (params.minScore !== void 0 && params.minScore > 0) {
1034
+ const threshold = params.minScore;
1035
+ fusedScored = fusedScored.filter((doc) => doc.score >= threshold);
1036
+ }
1037
+ return {
1038
+ scored: fusedScored,
1039
+ totalMatched: fusedScored.length,
1040
+ facets: textFanOutResult.facets
1041
+ };
1042
+ }
1043
+
1044
+ // src/engine/query/index.ts
1045
+ async function executeQuery(params, context) {
1046
+ const { manager, language, config, workerSearch, indexName } = context;
1047
+ const startTime = now();
1048
+ const limit = clampLimit(params.limit);
1049
+ const offset = clampOffset(params.offset);
1050
+ requireWithinResultWindow(limit, offset);
1051
+ const sortFields = normalizeSort(params.sort);
1052
+ const sortSignature = sortSignatureOf(params.sort);
1053
+ const hasTerm = params.term !== void 0 && params.term.trim().length > 0;
1054
+ const hasVector = params.vector !== void 0 && params.vector.value !== void 0;
1055
+ const isHybridMode = params.mode === "hybrid" || hasTerm && hasVector;
1056
+ const isVectorOnly = (params.mode === "vector" || hasVector && !hasTerm) && !isHybridMode;
1057
+ if (sortSignature !== null && (isHybridMode || params.hybrid !== void 0)) {
1058
+ throw new NarsilError(
1059
+ ErrorCodes.SEARCH_INVALID_MODE,
1060
+ "A hybrid query cannot carry a sort, because fusion defines the order of hybrid results",
1061
+ { sort: sortFields.map((entry) => entry.field) }
1062
+ );
1063
+ }
1064
+ requireSortableFields(params.sort, config.schema);
1065
+ let paginated;
1066
+ let nextCursor;
1067
+ let count;
1068
+ let facets;
1069
+ let groups;
1070
+ if (sortSignature !== null && hasTerm && !isVectorOnly && !isHybridMode && sortsWithoutScores(params)) {
1071
+ const page = executeSortedQueryPage(params, context, limit, offset, sortSignature);
1072
+ paginated = page.hits;
1073
+ nextCursor = page.cursor;
1074
+ count = page.count;
1075
+ facets = page.facets;
1076
+ } else {
1077
+ const requestedVectorField = params.vector?.field;
1078
+ const hasGlobalVectorIndex = requestedVectorField !== void 0 && vectorSearchersOf(context).has(requestedVectorField);
1079
+ let fanOutResult;
1080
+ if (isVectorOnly && hasGlobalVectorIndex) {
1081
+ fanOutResult = await executeVectorSearch(params, context, limit, offset);
1082
+ } else if (isHybridMode && hasGlobalVectorIndex) {
1083
+ fanOutResult = await executeHybridSearch(params, context, limit, offset);
1084
+ } else {
1085
+ const scoring = scoringConfigFor(params, context);
1086
+ const workerResult = workerSearch ? await workerSearch(indexName, params, broadcastStatsForWorker(params, context, scoring), context.partitionIds) : null;
1087
+ if (workerResult) {
1088
+ fanOutResult = workerResult;
1089
+ } else {
1090
+ fanOutResult = await fanOutQuery(manager, params, language, config.schema, scoring, searchOptionsFor(manager));
1091
+ }
1092
+ }
1093
+ const needsFullHits = params.sort !== void 0 || params.group !== void 0 || params.pinned !== void 0 || params.searchAfter !== void 0;
1094
+ let hits;
1095
+ if (needsFullHits) {
1096
+ hits = fanOutResult.scored.map((scored) => ({
1097
+ id: scored.docId,
1098
+ score: scored.score,
1099
+ document: void 0,
1100
+ scoreComponents: params.includeScoreComponents ? { termFrequencies: scored.termFrequencies, fieldLengths: scored.fieldLengths, idf: scored.idf } : void 0
1101
+ }));
1102
+ } else {
1103
+ const end = Math.min(offset + limit + 1, fanOutResult.scored.length);
1104
+ hits = new Array(end);
1105
+ for (let i = 0; i < end; i++) {
1106
+ const scored = fanOutResult.scored[i];
1107
+ hits[i] = {
1108
+ id: scored.docId,
1109
+ score: scored.score,
1110
+ document: void 0,
1111
+ scoreComponents: params.includeScoreComponents ? { termFrequencies: scored.termFrequencies, fieldLengths: scored.fieldLengths, idf: scored.idf } : void 0
1112
+ };
1113
+ }
1114
+ }
1115
+ const sortFieldNames = sortFields.map((entry) => entry.field);
1116
+ const sortFlatSchema = sortFieldNames.length === 0 ? {} : flattenSchema(config.schema);
1117
+ const sortFieldTypes = sortFieldNames.map((field) => sortFlatSchema[field]);
1118
+ const sortKeyCache = /* @__PURE__ */ new Map();
1119
+ const sortKeyOf = (docId) => {
1120
+ let key = sortKeyCache.get(docId);
1121
+ if (key === void 0) {
1122
+ key = manager.sortValues(docId, sortFieldNames, sortFieldTypes);
1123
+ sortKeyCache.set(docId, key);
1124
+ }
1125
+ return key;
1126
+ };
1127
+ if (params.sort) {
1128
+ hits = applySorting(hits, params.sort, sortKeyOf);
1129
+ }
1130
+ if (params.group) {
1131
+ groups = applyGrouping(hits, params.group, (docId) => manager.getRef(docId));
1132
+ }
1133
+ if (params.pinned && params.searchAfter === void 0 && context.partitionIds === void 0) {
1134
+ hits = applyPinning(hits, params.pinned, (docId) => {
1135
+ const doc = manager.getRef(docId);
1136
+ if (!doc) return void 0;
1137
+ return { id: docId, score: 0, document: doc };
1138
+ });
1139
+ }
1140
+ let sortContext;
1141
+ if (sortSignature !== null) {
1142
+ sortContext = {
1143
+ signature: sortSignature,
1144
+ directions: sortFields.map((entry) => entry.direction),
1145
+ sortKeyOf
1146
+ };
1147
+ }
1148
+ const pinnedIds = params.pinned !== void 0 && params.searchAfter === void 0 ? new Set(params.pinned.map((entry) => entry.docId)) : void 0;
1149
+ const paged = applyPagination(
1150
+ hits,
1151
+ limit,
1152
+ offset,
1153
+ context.cursorBinding,
1154
+ params.searchAfter,
1155
+ sortContext,
1156
+ pinnedIds
1157
+ );
1158
+ paginated = paged.paginated;
1159
+ nextCursor = paged.nextCursor;
1160
+ count = fanOutResult.totalMatched;
1161
+ facets = fanOutResult.facets;
1162
+ if (sortSignature !== null && params.includeScores !== true) {
1163
+ for (const hit of hits) hit.score = void 0;
1164
+ }
1165
+ }
1166
+ const projection = resolveProjection(params.document);
1167
+ if (projection.kind === "none") {
1168
+ for (const hit of paginated) {
1169
+ hit.document = {};
1170
+ }
1171
+ } else {
1172
+ for (const hit of paginated) {
1173
+ hit.document = manager.get(hit.id, projection) ?? {};
1174
+ }
1175
+ }
1176
+ if (groups) {
1177
+ for (const group of groups) {
1178
+ for (const hit of group.hits) {
1179
+ if (projection.kind === "none") {
1180
+ hit.document = {};
1181
+ continue;
1182
+ }
1183
+ hit.document = manager.get(hit.id, projection) ?? {};
1184
+ }
1185
+ }
1186
+ }
1187
+ if (params.highlight) {
1188
+ applyHighlights(paginated, params, language, manager.analysis);
1189
+ }
1190
+ const elapsed = now() - startTime;
1191
+ return {
1192
+ hits: paginated,
1193
+ count,
1194
+ elapsed,
1195
+ cursor: nextCursor,
1196
+ facets,
1197
+ groups,
1198
+ coverage: coverageFor(manager, context.partitionIds)
1199
+ };
1200
+ }
1201
+ async function executePreflight(params, context) {
1202
+ const { manager, language, config, workerSearch, indexName } = context;
1203
+ const startTime = now();
1204
+ const hasTerm = params.term !== void 0 && params.term.trim().length > 0;
1205
+ const hasVector = params.vector !== void 0 && params.vector.value !== void 0;
1206
+ const isHybridMode = params.mode === "hybrid" || hasTerm && hasVector;
1207
+ const isVectorOnly = (params.mode === "vector" || hasVector && !hasTerm) && !isHybridMode;
1208
+ const requestedVectorField = params.vector?.field;
1209
+ const hasGlobalVectorIndex = requestedVectorField !== void 0 && vectorSearchersOf(context).has(requestedVectorField);
1210
+ let totalMatched;
1211
+ const preflightLimit = 1e3;
1212
+ const preflightOffset = 0;
1213
+ if (isVectorOnly && hasGlobalVectorIndex) {
1214
+ const result = await executeVectorSearch(params, context, preflightLimit, preflightOffset);
1215
+ totalMatched = result.totalMatched;
1216
+ } else if (isHybridMode && hasGlobalVectorIndex) {
1217
+ const result = await executeHybridSearch(params, context, preflightLimit, preflightOffset);
1218
+ totalMatched = result.totalMatched;
1219
+ } else if (countsWithoutScores(params)) {
1220
+ totalMatched = fanOutMatchCount(manager, params, language, config.schema, {
1221
+ searchOptions: searchOptionsFor(manager),
1222
+ partitionIds: context.partitionIds
1223
+ });
1224
+ } else {
1225
+ const scoring = scoringConfigFor(params, context);
1226
+ const workerResult = workerSearch ? await workerSearch(indexName, params, broadcastStatsForWorker(params, context, scoring), context.partitionIds) : null;
1227
+ if (workerResult) {
1228
+ totalMatched = workerResult.totalMatched;
1229
+ } else {
1230
+ const fanOutResult = await fanOutQuery(
1231
+ manager,
1232
+ params,
1233
+ language,
1234
+ config.schema,
1235
+ scoring,
1236
+ searchOptionsFor(manager)
1237
+ );
1238
+ totalMatched = fanOutResult.totalMatched;
1239
+ }
1240
+ }
1241
+ const elapsed = now() - startTime;
1242
+ return { count: totalMatched, elapsed };
1243
+ }
1244
+
1245
+ // src/engine/suggest.ts
1246
+ function executeSuggest(manager, language, params, partitionIds) {
1247
+ const t0 = performance.now();
1248
+ const ceiling = partitionIds === void 0 ? MAX_SUGGEST_LIMIT : MAX_SUGGEST_SCATTER_LIMIT;
1249
+ const limit = Math.max(1, Math.min(clampRowCount(params.limit, DEFAULT_SUGGEST_LIMIT), ceiling));
1250
+ const rawPrefix = params.prefix.trim();
1251
+ if (rawPrefix.length === 0) {
1252
+ return { terms: [], elapsed: performance.now() - t0 };
1253
+ }
1254
+ const unstemmed = tokenize(rawPrefix, language, { stem: false, removeStopWords: false });
1255
+ const lastToken = unstemmed.tokens.length > 0 ? unstemmed.tokens[unstemmed.tokens.length - 1].token : rawPrefix.toLowerCase();
1256
+ if (lastToken.length === 0) {
1257
+ return { terms: [], elapsed: performance.now() - t0 };
1258
+ }
1259
+ const stemmed = language.stemmer ? language.stemmer(lastToken) : lastToken;
1260
+ const merged = /* @__PURE__ */ new Map();
1261
+ const perPartitionLimit = limit * 2;
1262
+ const partitions = partitionIds === void 0 ? manager.getAllPartitions() : partitionIds.map((partitionId) => manager.partitionAt(partitionId)).filter((partition) => partition !== void 0);
1263
+ for (const partition of partitions) {
1264
+ for (const suggestion of partition.suggestTerms(lastToken, stemmed, perPartitionLimit)) {
1265
+ let entry = merged.get(suggestion.token);
1266
+ if (!entry) {
1267
+ entry = { documentFrequency: 0, surfaceOccurrences: /* @__PURE__ */ new Map() };
1268
+ merged.set(suggestion.token, entry);
1269
+ }
1270
+ entry.documentFrequency += suggestion.documentFrequency;
1271
+ for (const s of suggestion.surfaces) {
1272
+ entry.surfaceOccurrences.set(s.surface, (entry.surfaceOccurrences.get(s.surface) ?? 0) + s.occurrences);
1273
+ }
1274
+ }
1275
+ }
1276
+ const terms = Array.from(merged.values()).map((entry) => ({ term: pickDisplaySurface(entry.surfaceOccurrences), documentFrequency: entry.documentFrequency })).sort((a, b) => b.documentFrequency - a.documentFrequency || compareCodePoints(a.term, b.term));
1277
+ if (terms.length > limit) terms.length = limit;
1278
+ return { terms, elapsed: performance.now() - t0 };
1279
+ }
1280
+ function pickDisplaySurface(surfaceOccurrences) {
1281
+ let best = "";
1282
+ let bestCount = -1;
1283
+ for (const [surface, occurrences] of surfaceOccurrences) {
1284
+ if (occurrences > bestCount || occurrences === bestCount && compareCodePoints(surface, best) < 0) {
1285
+ best = surface;
1286
+ bestCount = occurrences;
1287
+ }
1288
+ }
1289
+ return best;
1290
+ }
1291
+
1292
+ export { clampLimit, dedupePinnedEntries, executeListDocuments, executePreflight, executeQuery, executeSuggest, foldGroupReducer, listBindingOf, now, queryBindingOf, validateBM25Params, validateDocId, validateIndexName, validatePartitionConfig, validateWorkerConfig };