@delali/narsil 0.2.3 → 0.2.4-next.43

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 (36) hide show
  1. package/dist/adapters/filesystem.mjs +1 -1
  2. package/dist/adapters/indexeddb.mjs +1 -1
  3. package/dist/{chunk-MMSWEEZT.mjs → chunk-KEDL6FFZ.mjs} +315 -51
  4. package/dist/{chunk-WU2UZEVD.mjs → chunk-O4Q5JYNK.mjs} +1 -1
  5. package/dist/{chunk-GB2IIJJG.mjs → chunk-TLRUUMVE.mjs} +1 -4
  6. package/dist/{chunk-GRWFY2HI.mjs → chunk-YGQMKTKG.mjs} +120 -36
  7. package/dist/{client-BobVnoIK.d.ts → client-B5uO5jL1.d.ts} +3 -3
  8. package/dist/client.d.ts +8 -8
  9. package/dist/client.mjs +1 -4
  10. package/dist/{config-Byyl1QqH.d.ts → config-B09-cud9.d.ts} +2 -2
  11. package/dist/{config-fLC4lbmg.d.ts → config-NgUCkc6a.d.ts} +2 -2
  12. package/dist/{direct-executor-MSJEVPZU.mjs → direct-executor-7O23GEHE.mjs} +2 -2
  13. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  14. package/dist/distribution/coordinator/etcd.mjs +1 -1
  15. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  16. package/dist/distribution/transport/grpc.mjs +1 -1
  17. package/dist/distribution.d.ts +8 -8
  18. package/dist/distribution.mjs +705 -272
  19. package/dist/embeddings/openai.mjs +1 -1
  20. package/dist/{engine-BE45D5ZG.d.ts → engine-CjLhdDIm.d.ts} +21 -20
  21. package/dist/index.browser.mjs +436 -91
  22. package/dist/index.d.ts +7 -7
  23. package/dist/index.mjs +4 -4
  24. package/dist/{options-EXYFcem-.d.ts → options-BakECW-r.d.ts} +2 -2
  25. package/dist/persistence/durability/checkpoint-worker.d.ts +1 -1
  26. package/dist/persistence/durability/checkpoint-worker.mjs +4 -4
  27. package/dist/react.d.ts +6 -6
  28. package/dist/react.mjs +1 -4
  29. package/dist/{schema-ZRbk8BgH.d.ts → schema-DXOd96kx.d.ts} +16 -5
  30. package/dist/{search-DHuXwJYk.d.ts → search-BGouJxH4.d.ts} +22 -9
  31. package/dist/server.d.ts +10 -10
  32. package/dist/server.mjs +6 -9
  33. package/dist/{tasks-CnMLpfk-.d.ts → tasks-Bn4R1NPN.d.ts} +1 -1
  34. package/dist/{types-B5Tsaalz.d.ts → types-BTE33VZS.d.ts} +1 -1
  35. package/dist/workers/entry.mjs +2 -2
  36. package/package.json +2 -2
@@ -3,7 +3,6 @@ import { encode, decode } from '@msgpack/msgpack';
3
3
  // src/errors.ts
4
4
  var ErrorCodes = {
5
5
  SCHEMA_INVALID_TYPE: "SCHEMA_INVALID_TYPE",
6
- SCHEMA_MISSING_FIELD: "SCHEMA_MISSING_FIELD",
7
6
  SCHEMA_DEPTH_EXCEEDED: "SCHEMA_DEPTH_EXCEEDED",
8
7
  SCHEMA_INVALID_VECTOR_DIMENSION: "SCHEMA_INVALID_VECTOR_DIMENSION",
9
8
  SCHEMA_INVALID_GEOPOINT: "SCHEMA_INVALID_GEOPOINT",
@@ -25,7 +24,6 @@ var ErrorCodes = {
25
24
  PERSISTENCE_WAL_CORRUPT: "PERSISTENCE_WAL_CORRUPT",
26
25
  PERSISTENCE_FSYNC_FAILED: "PERSISTENCE_FSYNC_FAILED",
27
26
  SEARCH_INVALID_FIELD: "SEARCH_INVALID_FIELD",
28
- SEARCH_INVALID_VECTOR_SIZE: "SEARCH_INVALID_VECTOR_SIZE",
29
27
  VECTOR_DIMENSION_MISMATCH: "VECTOR_DIMENSION_MISMATCH",
30
28
  SEARCH_INVALID_FILTER: "SEARCH_INVALID_FILTER",
31
29
  SEARCH_INVALID_MODE: "SEARCH_INVALID_MODE",
@@ -49,8 +47,6 @@ var ErrorCodes = {
49
47
  ALLOCATION_NO_DATA_NODES: "ALLOCATION_NO_DATA_NODES",
50
48
  ALLOCATION_INVALID_CONFIG: "ALLOCATION_INVALID_CONFIG",
51
49
  ALLOCATION_FAILED: "ALLOCATION_FAILED",
52
- CONTROLLER_LEASE_LOST: "CONTROLLER_LEASE_LOST",
53
- CONTROLLER_NOT_ACTIVE: "CONTROLLER_NOT_ACTIVE",
54
50
  CONTROLLER_METADATA_INVALID: "CONTROLLER_METADATA_INVALID",
55
51
  NODE_BOOTSTRAP_FAILED: "NODE_BOOTSTRAP_FAILED",
56
52
  NODE_ALREADY_JOINED: "NODE_ALREADY_JOINED",
@@ -106,6 +102,7 @@ var ServerErrorCodes = {
106
102
  TASK_NOT_FOUND: "TASK_NOT_FOUND",
107
103
  TASK_NOT_CANCELLABLE: "TASK_NOT_CANCELLABLE",
108
104
  TASK_OWNED_BY_ANOTHER_INSTANCE: "TASK_OWNED_BY_ANOTHER_INSTANCE",
105
+ TASK_INTERRUPTED: "TASK_INTERRUPTED",
109
106
  TOO_MANY_REQUESTS: "TOO_MANY_REQUESTS",
110
107
  HOOK_ERROR: "HOOK_ERROR",
111
108
  INTERNAL_ERROR: "INTERNAL_ERROR"
@@ -2622,7 +2619,7 @@ function wireToMetadata(raw) {
2622
2619
  if (isPositiveInteger2(promotion.threshold)) {
2623
2620
  restored.threshold = promotion.threshold;
2624
2621
  }
2625
- if (isPositiveInteger2(promotion.filter_threshold)) {
2622
+ if (isSelectivityRatio(promotion.filter_threshold)) {
2626
2623
  restored.filterThreshold = promotion.filter_threshold;
2627
2624
  }
2628
2625
  if (Object.keys(hnswConfig).length > 0) {
@@ -2649,6 +2646,9 @@ function isNonNegativeInteger(value) {
2649
2646
  function isPositiveInteger2(value) {
2650
2647
  return typeof value === "number" && Number.isInteger(value) && value >= 1;
2651
2648
  }
2649
+ function isSelectivityRatio(value) {
2650
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
2651
+ }
2652
2652
  function isWatermarkFraction(value) {
2653
2653
  return typeof value === "number" && value > 0 && value <= 1;
2654
2654
  }
@@ -8079,7 +8079,7 @@ function createRebalancer() {
8079
8079
  skipClone: true,
8080
8080
  stopWordOverride: manager.analysis.stopWords,
8081
8081
  customTokenizer: manager.analysis.customTokenizer,
8082
- collectSurfaces: manager.config.surfaceForms === true
8082
+ collectSurfaces: manager.config.surfaceForms !== false
8083
8083
  };
8084
8084
  for (const partition of currentPartitions) {
8085
8085
  const docIds = [...partition.docIds()];
@@ -8140,8 +8140,7 @@ function createRebalancer() {
8140
8140
  var FNV_OFFSET_BASIS = 2166136261;
8141
8141
  var FNV_PRIME = 16777619;
8142
8142
  var encoder = new TextEncoder();
8143
- function fnv1a(input) {
8144
- const bytes = encoder.encode(input);
8143
+ function fnv1aBytes(bytes) {
8145
8144
  let hash = FNV_OFFSET_BASIS;
8146
8145
  for (let i = 0; i < bytes.length; i++) {
8147
8146
  hash ^= bytes[i];
@@ -8149,6 +8148,9 @@ function fnv1a(input) {
8149
8148
  }
8150
8149
  return hash >>> 0;
8151
8150
  }
8151
+ function fnv1a(input) {
8152
+ return fnv1aBytes(encoder.encode(input));
8153
+ }
8152
8154
 
8153
8155
  // src/partitioning/router.ts
8154
8156
  function createPartitionRouter() {
@@ -10189,7 +10191,7 @@ function applySorting(hits, sort, sortKeyOf) {
10189
10191
  }
10190
10192
 
10191
10193
  // src/search/cursor.ts
10192
- var CURSOR_VERSION = 2;
10194
+ var CURSOR_VERSION = 3;
10193
10195
  var MAX_CURSOR_LENGTH = 40960;
10194
10196
  var MAX_SORT_FIELDS = 8;
10195
10197
  var MAX_SORT_FIELD_NAME_LENGTH = 255;
@@ -10244,6 +10246,7 @@ function encodePageCursor(cursor) {
10244
10246
  } else if (cursor.score !== null) {
10245
10247
  payload.s = cursor.score;
10246
10248
  }
10249
+ payload.q = cursor.binding;
10247
10250
  return encodeCursorText(JSON.stringify(payload));
10248
10251
  }
10249
10252
  function decodeSortKey(value, cursor) {
@@ -10288,8 +10291,11 @@ function decodePageCursor(cursor) {
10288
10291
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
10289
10292
  throw invalidCursor(cursor, "expected an object");
10290
10293
  }
10291
- const { v, a, s, k, o } = parsed;
10294
+ const { v, a, s, k, o, q } = parsed;
10292
10295
  if (v !== CURSOR_VERSION) throw invalidCursor(cursor, `unsupported cursor version ${String(v)}`);
10296
+ if (typeof q !== "string" || !/^[0-9a-f]{8}$/.test(q)) {
10297
+ throw invalidCursor(cursor, '"q" must be 8 lowercase hex digits');
10298
+ }
10293
10299
  if (typeof a !== "string" || a.length === 0) throw invalidCursor(cursor, '"a" must be a non-empty string');
10294
10300
  if (exceedsCodePoints(a, MAX_ANCHOR_CODE_POINTS)) {
10295
10301
  throw invalidCursor(cursor, '"a" is too long to be a document id');
@@ -10307,10 +10313,14 @@ function decodePageCursor(cursor) {
10307
10313
  anchor: a,
10308
10314
  score: typeof s === "number" ? s : null,
10309
10315
  sortKey,
10310
- sortSignature: typeof o === "string" ? o : null
10316
+ sortSignature: typeof o === "string" ? o : null,
10317
+ binding: q
10311
10318
  };
10312
10319
  }
10313
- function requireMatchingCursor(cursor, encoded, signature, scoreAnchored) {
10320
+ function requireMatchingCursor(cursor, encoded, signature, scoreAnchored, binding) {
10321
+ if (cursor.binding !== binding) {
10322
+ throw invalidCursor(encoded, "the cursor belongs to a different query");
10323
+ }
10314
10324
  if (signature !== null) {
10315
10325
  if (cursor.sortSignature !== signature) {
10316
10326
  throw invalidCursor(encoded, "the cursor belongs to a different sort order");
@@ -10344,6 +10354,12 @@ function requireWithinResultWindow(limit, offset) {
10344
10354
  { limit, offset, window: RESULT_WINDOW }
10345
10355
  );
10346
10356
  }
10357
+ function lastOrganicResult(sliced, pinnedIds) {
10358
+ for (let i = sliced.length - 1; i >= 0; i--) {
10359
+ if (pinnedIds === void 0 || !pinnedIds.has(sliced[i].id)) return sliced[i];
10360
+ }
10361
+ return void 0;
10362
+ }
10347
10363
  function ordersAfterAnchor(result, anchor, sort) {
10348
10364
  if (sort !== void 0 && anchor.sortKey !== null) {
10349
10365
  const comparison = compareSortValues(sort.sortKeyOf(result.id), anchor.sortKey, sort.directions) || compareCodePoints(result.id, anchor.anchor);
@@ -10354,10 +10370,10 @@ function ordersAfterAnchor(result, anchor, sort) {
10354
10370
  if (result.score < anchor.score) return true;
10355
10371
  return result.score === anchor.score && compareCodePoints(result.id, anchor.anchor) > 0;
10356
10372
  }
10357
- function applyPagination(results, limit, offset, cursor, sort) {
10373
+ function applyPagination(results, limit, offset, binding, cursor, sort, pinnedIds) {
10358
10374
  const decoded = cursor ? decodePageCursor(cursor) : null;
10359
10375
  if (decoded !== null && cursor !== void 0) {
10360
- requireMatchingCursor(decoded, cursor, sort?.signature ?? null, true);
10376
+ requireMatchingCursor(decoded, cursor, sort?.signature ?? null, true, binding);
10361
10377
  }
10362
10378
  if (limit <= 0) {
10363
10379
  return { paginated: [] };
@@ -10376,18 +10392,20 @@ function applyPagination(results, limit, offset, cursor, sort) {
10376
10392
  const sliced = results.slice(afterOffset, afterOffset + limit);
10377
10393
  let nextCursor;
10378
10394
  const hasMore = afterOffset + limit < results.length;
10379
- if (hasMore && sliced.length > 0) {
10380
- const lastResult = sliced[sliced.length - 1];
10395
+ const lastResult = lastOrganicResult(sliced, pinnedIds);
10396
+ if (hasMore && lastResult !== void 0) {
10381
10397
  nextCursor = sort !== void 0 ? encodePageCursor({
10382
10398
  anchor: lastResult.id,
10383
10399
  score: null,
10384
10400
  sortKey: sort.sortKeyOf(lastResult.id).map(toComparableSortValue),
10385
- sortSignature: sort.signature
10401
+ sortSignature: sort.signature,
10402
+ binding
10386
10403
  }) : encodePageCursor({
10387
10404
  anchor: lastResult.id,
10388
10405
  score: lastResult.score ?? null,
10389
10406
  sortKey: null,
10390
- sortSignature: null
10407
+ sortSignature: null,
10408
+ binding
10391
10409
  });
10392
10410
  }
10393
10411
  return { paginated: sliced, nextCursor };
@@ -10587,9 +10605,24 @@ function queryTermSatisfied(queryToken, indexTokens, tolerance, exact) {
10587
10605
  return false;
10588
10606
  }
10589
10607
 
10608
+ // src/partitioning/partition-selection.ts
10609
+ function partitionsIn(manager, partitionIds) {
10610
+ if (partitionIds === void 0) {
10611
+ return manager.getAllPartitions();
10612
+ }
10613
+ const partitions = [];
10614
+ for (const partitionId of partitionIds) {
10615
+ const partition = manager.partitionAt(partitionId);
10616
+ if (partition !== void 0) {
10617
+ partitions.push(partition);
10618
+ }
10619
+ }
10620
+ return partitions;
10621
+ }
10622
+
10590
10623
  // src/partitioning/fan-out.ts
10591
10624
  async function fanOutQuery(manager, params, language, schema, config, searchOptions) {
10592
- const partitions = config.partitionIds ? config.partitionIds.map((id) => manager.partitionAt(id)).filter((p) => p !== void 0) : manager.getAllPartitions();
10625
+ const partitions = partitionsIn(manager, config.partitionIds);
10593
10626
  if (partitions.length === 0) {
10594
10627
  return { scored: [], totalMatched: 0 };
10595
10628
  }
@@ -11018,6 +11051,19 @@ function createPartitionManager(indexName, config, language, router, initialPart
11018
11051
  return manager;
11019
11052
  }
11020
11053
 
11054
+ // src/partitioning/match-count.ts
11055
+ function countsWithoutScores(params) {
11056
+ return params.minScore === void 0 && (params.termMatch === void 0 || params.termMatch === "any");
11057
+ }
11058
+ function fanOutMatchCount(manager, params, language, schema, options) {
11059
+ let count = 0;
11060
+ for (const partition of partitionsIn(manager, options?.partitionIds)) {
11061
+ const matches = fulltextMatches(partition, params, language, schema, options?.searchOptions);
11062
+ if (matches !== null) count += matches.count;
11063
+ }
11064
+ return count;
11065
+ }
11066
+
11021
11067
  // src/vector/scalar-quantization-calibration.ts
11022
11068
  var PADDING_FACTOR = 0.01;
11023
11069
  function computeCalibrationBounds(vectors, dimensions) {
@@ -11247,7 +11293,7 @@ function createScalarQuantizer(dimensions, ordinalSource) {
11247
11293
  let liveCount = 0;
11248
11294
  let constants = deriveSq8Constants(0, 0, dimensions);
11249
11295
  let calibrated = false;
11250
- function ensureCapacity2(needed) {
11296
+ function ensureCapacity3(needed) {
11251
11297
  if (needed <= capacity) return;
11252
11298
  let newCap = capacity === 0 ? INITIAL_CAPACITY2 : capacity;
11253
11299
  while (newCap < needed) newCap *= 2;
@@ -11377,7 +11423,7 @@ function createScalarQuantizer(dimensions, ordinalSource) {
11377
11423
  liveCount = 0;
11378
11424
  for (const [docId, vec] of collected) {
11379
11425
  const ord = resolveOrdinal(docId);
11380
- ensureCapacity2(ord + 1);
11426
+ ensureCapacity3(ord + 1);
11381
11427
  quantizeInto(ord, vec);
11382
11428
  storeEntry(docId, ord);
11383
11429
  }
@@ -11387,7 +11433,7 @@ function createScalarQuantizer(dimensions, ordinalSource) {
11387
11433
  calibrateFromVectors([vector]);
11388
11434
  }
11389
11435
  const ord = resolveOrdinal(docId);
11390
- ensureCapacity2(ord + 1);
11436
+ ensureCapacity3(ord + 1);
11391
11437
  quantizeInto(ord, vector);
11392
11438
  storeEntry(docId, ord);
11393
11439
  },
@@ -11454,7 +11500,7 @@ function createScalarQuantizer(dimensions, ordinalSource) {
11454
11500
  },
11455
11501
  restoreEntry(docId, quantized, sum, sumSq) {
11456
11502
  const ord = resolveOrdinal(docId);
11457
- ensureCapacity2(ord + 1);
11503
+ ensureCapacity3(ord + 1);
11458
11504
  const base = ord * dimensions;
11459
11505
  quantizedArena.set(quantized.subarray(0, dimensions), base);
11460
11506
  sums[ord] = sum;
@@ -11605,7 +11651,7 @@ function createVectorStore() {
11605
11651
  arena = new Float32Array(simd.memory.buffer);
11606
11652
  }
11607
11653
  }
11608
- function ensureCapacity2(needed) {
11654
+ function ensureCapacity3(needed) {
11609
11655
  if (needed <= capacity) return;
11610
11656
  let newCap = capacity === 0 ? INITIAL_CAPACITY3 : capacity;
11611
11657
  while (newCap < needed) newCap *= 2;
@@ -11687,7 +11733,7 @@ function createVectorStore() {
11687
11733
  recycledOrds.delete(docId);
11688
11734
  docToOrd.set(docId, recycled);
11689
11735
  ordToDoc[recycled] = docId;
11690
- ensureCapacity2(recycled + 1);
11736
+ ensureCapacity3(recycled + 1);
11691
11737
  writeVector(recycled, vector);
11692
11738
  recordPartition(recycled, partitionId, false);
11693
11739
  liveCount++;
@@ -11696,7 +11742,7 @@ function createVectorStore() {
11696
11742
  const ord = ordToDoc.length;
11697
11743
  docToOrd.set(docId, ord);
11698
11744
  ordToDoc.push(docId);
11699
- ensureCapacity2(ord + 1);
11745
+ ensureCapacity3(ord + 1);
11700
11746
  writeVector(ord, vector);
11701
11747
  recordPartition(ord, partitionId, false);
11702
11748
  liveCount++;
@@ -11862,7 +11908,7 @@ function createVectorStore() {
11862
11908
  }
11863
11909
  if (snapshot.dimension === 0 || snapshot.slots === 0) return;
11864
11910
  initStorage(snapshot.dimension);
11865
- ensureCapacity2(snapshot.slots);
11911
+ ensureCapacity3(snapshot.slots);
11866
11912
  arena.set(snapshot.vectors.subarray(0, snapshot.slots * snapshot.dimension), scratchFloatLength);
11867
11913
  mags.set(snapshot.magnitudes.subarray(0, Math.min(snapshot.slots, mags.length)));
11868
11914
  ensurePartitionSlots(snapshot.slots);
@@ -13015,20 +13061,43 @@ function resolveWorkerCount(requested) {
13015
13061
  }
13016
13062
  function createWorkerPool(config) {
13017
13063
  const workerCount = resolveWorkerCount(config.count);
13018
- const workers = [];
13064
+ const workers = /* @__PURE__ */ new Map();
13065
+ const deadSlots = /* @__PURE__ */ new Set();
13019
13066
  const indexAssignment = /* @__PURE__ */ new Map();
13020
13067
  let isShutdown = false;
13068
+ function handleWorkerDeath(slotIndex, error) {
13069
+ if (isShutdown || deadSlots.has(slotIndex)) {
13070
+ return;
13071
+ }
13072
+ deadSlots.add(slotIndex);
13073
+ const slot = workers.get(slotIndex);
13074
+ const indexNames = slot ? [...slot.indexes].sort() : [];
13075
+ workers.delete(slotIndex);
13076
+ config.onWorkerCrash?.(slotIndex, indexNames, error);
13077
+ }
13021
13078
  function ensureWorker(slotIndex) {
13022
- if (!workers[slotIndex]) {
13023
- workers[slotIndex] = {
13024
- executor: config.workerFactory(slotIndex),
13079
+ if (deadSlots.has(slotIndex)) {
13080
+ return void 0;
13081
+ }
13082
+ let slot = workers.get(slotIndex);
13083
+ if (!slot) {
13084
+ slot = {
13085
+ executor: config.workerFactory(slotIndex, (error) => handleWorkerDeath(slotIndex, error)),
13025
13086
  indexes: /* @__PURE__ */ new Set()
13026
13087
  };
13088
+ workers.set(slotIndex, slot);
13027
13089
  }
13028
- return workers[slotIndex];
13090
+ return slot;
13029
13091
  }
13030
13092
  function assignSlot(indexName) {
13031
- return fnv1a(indexName) % workerCount;
13093
+ const preferred = fnv1a(indexName) % workerCount;
13094
+ for (let probe = 0; probe < workerCount; probe++) {
13095
+ const candidate = (preferred + probe) % workerCount;
13096
+ if (!deadSlots.has(candidate)) {
13097
+ return candidate;
13098
+ }
13099
+ }
13100
+ throw new NarsilError(ErrorCodes.WORKER_CRASHED, `Every worker in the pool has crashed`);
13032
13101
  }
13033
13102
  function addIndex(indexName) {
13034
13103
  if (isShutdown) {
@@ -13039,6 +13108,9 @@ function createWorkerPool(config) {
13039
13108
  }
13040
13109
  const slotIndex = assignSlot(indexName);
13041
13110
  const slot = ensureWorker(slotIndex);
13111
+ if (!slot) {
13112
+ throw new NarsilError(ErrorCodes.WORKER_CRASHED, `Worker ${slotIndex} for index "${indexName}" has crashed`);
13113
+ }
13042
13114
  slot.indexes.add(indexName);
13043
13115
  indexAssignment.set(indexName, slotIndex);
13044
13116
  }
@@ -13047,10 +13119,7 @@ function createWorkerPool(config) {
13047
13119
  if (slotIndex === void 0) {
13048
13120
  return;
13049
13121
  }
13050
- const slot = workers[slotIndex];
13051
- if (slot) {
13052
- slot.indexes.delete(indexName);
13053
- }
13122
+ workers.get(slotIndex)?.indexes.delete(indexName);
13054
13123
  indexAssignment.delete(indexName);
13055
13124
  }
13056
13125
  function getExecutor(indexName) {
@@ -13061,15 +13130,22 @@ function createWorkerPool(config) {
13061
13130
  if (slotIndex === void 0) {
13062
13131
  throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index "${indexName}" is not registered in the worker pool`);
13063
13132
  }
13064
- return workers[slotIndex].executor;
13133
+ const slot = workers.get(slotIndex);
13134
+ if (slot) {
13135
+ return slot.executor;
13136
+ }
13137
+ for (const survivor of workers.values()) {
13138
+ if (survivor.indexes.has(indexName)) {
13139
+ return survivor.executor;
13140
+ }
13141
+ }
13142
+ throw new NarsilError(ErrorCodes.WORKER_CRASHED, `Every worker holding index "${indexName}" has crashed`);
13065
13143
  }
13066
13144
  async function getMemoryStats() {
13067
13145
  const indices = [];
13068
13146
  const pending = [];
13069
- for (let i = 0; i < workers.length; i++) {
13070
- const slot = workers[i];
13071
- if (!slot) continue;
13072
- indices.push(i);
13147
+ for (const [slotIndex, slot] of workers) {
13148
+ indices.push(slotIndex);
13073
13149
  pending.push(
13074
13150
  slot.executor.execute({ type: "memoryReport", requestId: createRequestId() }).catch(() => null)
13075
13151
  );
@@ -13092,7 +13168,7 @@ function createWorkerPool(config) {
13092
13168
  return;
13093
13169
  }
13094
13170
  isShutdown = true;
13095
- const shutdownPromises = workers.filter(Boolean).map((slot) => {
13171
+ const shutdownPromises = [...workers.values()].map((slot) => {
13096
13172
  const timeoutPromise = new Promise((resolve) => {
13097
13173
  setTimeout(resolve, 5e3);
13098
13174
  });
@@ -13102,17 +13178,16 @@ function createWorkerPool(config) {
13102
13178
  indexAssignment.clear();
13103
13179
  }
13104
13180
  function getAllExecutors() {
13105
- return workers.filter(Boolean).map((slot) => slot.executor);
13181
+ return [...workers.values()].map((slot) => slot.executor);
13106
13182
  }
13107
13183
  function addIndexToAll(indexName) {
13108
13184
  if (isShutdown) {
13109
13185
  throw new NarsilError(ErrorCodes.WORKER_CRASHED, "Worker pool has been shut down");
13110
13186
  }
13111
13187
  for (let i = 0; i < workerCount; i++) {
13112
- const slot = ensureWorker(i);
13113
- slot.indexes.add(indexName);
13188
+ ensureWorker(i)?.indexes.add(indexName);
13114
13189
  }
13115
- indexAssignment.set(indexName, 0);
13190
+ indexAssignment.set(indexName, assignSlot(indexName));
13116
13191
  }
13117
13192
  return {
13118
13193
  getExecutor,
@@ -14365,6 +14440,12 @@ function createDirectExecutor() {
14365
14440
  }
14366
14441
  case "preflight": {
14367
14442
  const entry = requireIndex(action.indexName);
14443
+ if (countsWithoutScores(action.params)) {
14444
+ const count = fanOutMatchCount(entry.manager, action.params, entry.language, entry.config.schema, {
14445
+ searchOptions: entry.searchOptions
14446
+ });
14447
+ return { count };
14448
+ }
14368
14449
  const result = await fanOutQuery(
14369
14450
  entry.manager,
14370
14451
  action.params,
@@ -15178,6 +15259,9 @@ function writeHeader(header) {
15178
15259
  }
15179
15260
  return buffer;
15180
15261
  }
15262
+ function hasNrslMagic(data) {
15263
+ return data.length >= MAGIC_BYTES.length && data[0] === MAGIC_BYTES[0] && data[1] === MAGIC_BYTES[1] && data[2] === MAGIC_BYTES[2] && data[3] === MAGIC_BYTES[3];
15264
+ }
15181
15265
  function readHeader(data) {
15182
15266
  if (data.length < HEADER_SIZE) {
15183
15267
  throw new NarsilError(
@@ -15213,6 +15297,7 @@ function readHeader(data) {
15213
15297
  // src/serialization/envelope.ts
15214
15298
  var CURRENT_ENVELOPE_VERSION = 1;
15215
15299
  var SNAPSHOT_ENVELOPE_VERSION = 2;
15300
+ var INDEX_SNAPSHOT_ENVELOPE_VERSION = 3;
15216
15301
  function parseEngineVersion(version) {
15217
15302
  const parts = version.split(".");
15218
15303
  return [parseInt(parts[0] ?? "0", 10), parseInt(parts[1] ?? "0", 10), parseInt(parts[2] ?? "0", 10)];
@@ -15357,6 +15442,19 @@ function concatEnvelopeParts(parts) {
15357
15442
  async function unpackEnvelopeBytes(data) {
15358
15443
  return unpackEnvelope(data, SNAPSHOT_ENVELOPE_VERSION);
15359
15444
  }
15445
+ async function packIndexSnapshotEnvelope(payloadBytes) {
15446
+ return packEnvelope(payloadBytes, { checksum: true, envelopeFormatVersion: INDEX_SNAPSHOT_ENVELOPE_VERSION });
15447
+ }
15448
+ async function unpackIndexSnapshotEnvelope(data) {
15449
+ const { header, payloadBytes } = await unpackEnvelope(data, INDEX_SNAPSHOT_ENVELOPE_VERSION);
15450
+ if (header.envelopeFormatVersion !== INDEX_SNAPSHOT_ENVELOPE_VERSION) {
15451
+ throw new NarsilError(
15452
+ ErrorCodes.ENVELOPE_VERSION_MISMATCH,
15453
+ `Expected an index snapshot envelope of format v${INDEX_SNAPSHOT_ENVELOPE_VERSION}, received format v${header.envelopeFormatVersion}`
15454
+ );
15455
+ }
15456
+ return payloadBytes;
15457
+ }
15360
15458
  async function writeMetadataEnvelope(meta, options = {}) {
15361
15459
  const payloadBytes = serializeMetadata(meta);
15362
15460
  return packEnvelope(payloadBytes, options);
@@ -18933,14 +19031,34 @@ async function awaitCompactions(state) {
18933
19031
  var DEFAULT_BACKPRESSURE_LIMIT = 100;
18934
19032
  var DEFAULT_REQUEST_TIMEOUT = 3e4;
18935
19033
  var SHUTDOWN_TIMEOUT = 5e3;
19034
+ var CONSECUTIVE_TIMEOUTS_BEFORE_DEATH = 3;
19035
+ function errorFromEventLike(event) {
19036
+ if (event instanceof Error) {
19037
+ return event;
19038
+ }
19039
+ if (typeof event === "object" && event !== null) {
19040
+ const detail = event;
19041
+ if (detail.error instanceof Error) {
19042
+ return detail.error;
19043
+ }
19044
+ if (typeof detail.message === "string" && detail.message.length > 0) {
19045
+ return new Error(detail.message);
19046
+ }
19047
+ }
19048
+ return new Error("The worker reported an error event carrying no detail");
19049
+ }
18936
19050
  function createWorkerExecutor(worker, config) {
18937
- const backpressureLimit = DEFAULT_BACKPRESSURE_LIMIT;
18938
- const requestTimeout = DEFAULT_REQUEST_TIMEOUT;
19051
+ const backpressureLimit = config?.backpressureLimit ?? DEFAULT_BACKPRESSURE_LIMIT;
19052
+ const requestTimeout = config?.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
18939
19053
  const pending = /* @__PURE__ */ new Map();
19054
+ let deathError = null;
19055
+ let shutdownRequested = false;
19056
+ let consecutiveTimeouts = 0;
18940
19057
  function processResponse(msg) {
18941
19058
  if (!isValidWorkerResponse(msg)) {
18942
19059
  return;
18943
19060
  }
19061
+ consecutiveTimeouts = 0;
18944
19062
  const response = msg;
18945
19063
  const entry = pending.get(response.requestId);
18946
19064
  if (!entry) {
@@ -18954,15 +19072,36 @@ function createWorkerExecutor(worker, config) {
18954
19072
  entry.resolve(response.data);
18955
19073
  }
18956
19074
  }
19075
+ function handleDeath(cause) {
19076
+ if (deathError !== null) {
19077
+ return;
19078
+ }
19079
+ const message = cause instanceof Error ? cause.message : String(cause);
19080
+ deathError = new NarsilError(ErrorCodes.WORKER_CRASHED, `Worker died: ${message}`);
19081
+ for (const [id, entry] of pending) {
19082
+ clearTimeout(entry.timeoutId);
19083
+ entry.reject(deathError);
19084
+ pending.delete(id);
19085
+ }
19086
+ if (!shutdownRequested) {
19087
+ config?.onDeath?.(cause instanceof Error ? cause : new Error(message));
19088
+ }
19089
+ }
18957
19090
  if (typeof worker.on === "function") {
18958
19091
  worker.on("message", (msg) => processResponse(msg));
19092
+ worker.on("error", (cause) => handleDeath(cause));
19093
+ worker.on("exit", (code) => handleDeath(new Error(`Worker exited with code ${String(code)}`)));
18959
19094
  } else if (typeof worker.addEventListener === "function") {
18960
19095
  worker.addEventListener("message", (event) => {
18961
19096
  const msg = event.data;
18962
19097
  processResponse(msg);
18963
19098
  });
19099
+ worker.addEventListener("error", (event) => handleDeath(errorFromEventLike(event)));
18964
19100
  }
18965
19101
  function execute(action) {
19102
+ if (deathError !== null) {
19103
+ return Promise.reject(deathError);
19104
+ }
18966
19105
  if (pending.size >= backpressureLimit) {
18967
19106
  return Promise.reject(
18968
19107
  new NarsilError(ErrorCodes.WORKER_BUSY, `Backpressure limit of ${backpressureLimit} pending requests reached`)
@@ -18974,6 +19113,10 @@ function createWorkerExecutor(worker, config) {
18974
19113
  const timeoutId = setTimeout(() => {
18975
19114
  pending.delete(requestId);
18976
19115
  reject2(new NarsilError(ErrorCodes.WORKER_TIMEOUT, `Request ${requestId} timed out after ${requestTimeout}ms`));
19116
+ consecutiveTimeouts += 1;
19117
+ if (consecutiveTimeouts >= CONSECUTIVE_TIMEOUTS_BEFORE_DEATH) {
19118
+ handleDeath(new Error(`no answer to ${CONSECUTIVE_TIMEOUTS_BEFORE_DEATH} consecutive requests`));
19119
+ }
18977
19120
  }, requestTimeout);
18978
19121
  pending.set(requestId, {
18979
19122
  resolve,
@@ -18984,6 +19127,10 @@ function createWorkerExecutor(worker, config) {
18984
19127
  });
18985
19128
  }
18986
19129
  async function shutdown() {
19130
+ shutdownRequested = true;
19131
+ if (deathError !== null) {
19132
+ return;
19133
+ }
18987
19134
  const requestId = createRequestId();
18988
19135
  const shutdownAction = { type: "shutdown", requestId };
18989
19136
  const shutdownPromise = new Promise((resolve, reject2) => {
@@ -19024,9 +19171,9 @@ async function createWorkerFactory(entryPoint) {
19024
19171
  const runtime = detectRuntime();
19025
19172
  const resolvedEntry = resolveEntryPoint();
19026
19173
  if (runtime.supportsWebWorkers) {
19027
- return function webFactory(_workerId) {
19174
+ return function webFactory(_workerId, onDeath) {
19028
19175
  const instance = new Worker(resolvedEntry, { type: "module" });
19029
- return createWorkerExecutor(instance);
19176
+ return createWorkerExecutor(instance, { onDeath });
19030
19177
  };
19031
19178
  }
19032
19179
  throw new NarsilError(ErrorCodes.WORKER_CRASHED, `No worker support available on runtime "${runtime.runtime}"`);
@@ -19082,7 +19229,13 @@ async function runPromotion(state, reason) {
19082
19229
  return;
19083
19230
  }
19084
19231
  const factory = await createWorkerFactory();
19085
- const pool = createWorkerPool({ count: state.config?.workers?.count, workerFactory: factory });
19232
+ const pool = createWorkerPool({
19233
+ count: state.config?.workers?.count,
19234
+ workerFactory: factory,
19235
+ onWorkerCrash(workerId, indexNames, error) {
19236
+ state.callbacks?.onWorkerCrash?.(workerId, indexNames, error);
19237
+ }
19238
+ });
19086
19239
  for (const name of promotable) {
19087
19240
  pool.addIndexToAll(name);
19088
19241
  }
@@ -19403,6 +19556,9 @@ function createEngineCore(config) {
19403
19556
  if (handlers) {
19404
19557
  for (const handler of handlers) handler({ workerCount, reason });
19405
19558
  }
19559
+ void Promise.resolve(pluginRegistry.runHook("onWorkerPromote", { workerCount, reason })).catch((err) => {
19560
+ console.warn("onWorkerPromote plugin hook failed:", err instanceof Error ? err.message : String(err));
19561
+ });
19406
19562
  },
19407
19563
  onPromotionFailure(reason, error, retryable) {
19408
19564
  const handlers = eventHandlers.get("workerPromoteFailure");
@@ -19411,6 +19567,14 @@ function createEngineCore(config) {
19411
19567
  return;
19412
19568
  }
19413
19569
  for (const handler of handlers) handler({ reason, error, retryable });
19570
+ },
19571
+ onWorkerCrash(workerId, indexNames, error) {
19572
+ const handlers = eventHandlers.get("workerCrash");
19573
+ if (!handlers || handlers.size === 0) {
19574
+ console.warn(`Worker ${workerId} crashed:`, error);
19575
+ return;
19576
+ }
19577
+ for (const handler of handlers) handler({ workerId, indexNames, error });
19414
19578
  }
19415
19579
  });
19416
19580
  const rebalancer = createRebalancer();
@@ -21370,6 +21534,15 @@ async function executeRebalance(manager, indexName, targetPartitionCount, ctx) {
21370
21534
  }
21371
21535
  }
21372
21536
  });
21537
+ try {
21538
+ await ctx.pluginRegistry.runHook("onPartitionSplit", {
21539
+ indexName,
21540
+ oldPartitionCount: oldCount,
21541
+ newPartitionCount: targetPartitionCount
21542
+ });
21543
+ } catch (hookErr) {
21544
+ console.warn("onPartitionSplit plugin hook failed:", hookErr instanceof Error ? hookErr.message : String(hookErr));
21545
+ }
21373
21546
  await replayQueued(manager, indexName, waq, ctx);
21374
21547
  if (ctx.durabilityManager) {
21375
21548
  await ctx.durabilityManager.persistMetadata(indexName);
@@ -21584,7 +21757,7 @@ async function createSnapshot(manager, entry) {
21584
21757
  const config = entry.config;
21585
21758
  const bm25 = config.bm25;
21586
21759
  const { encode: encode14 } = await import('@msgpack/msgpack');
21587
- return encode14({
21760
+ const payload = encode14({
21588
21761
  version: 2,
21589
21762
  schema: config.schema,
21590
21763
  language: entry.language.name,
@@ -21614,15 +21787,17 @@ async function createSnapshot(manager, entry) {
21614
21787
  partitions: partitionBuffers,
21615
21788
  vectorIndexes: vectorPayloads
21616
21789
  });
21790
+ return packIndexSnapshotEnvelope(payload);
21617
21791
  }
21618
21792
  async function restoreFromSnapshot(indexName, data, deps) {
21619
21793
  if (!(data instanceof Uint8Array)) {
21620
21794
  throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Snapshot data must be a Uint8Array");
21621
21795
  }
21796
+ const payloadBytes = hasNrslMagic(data) ? await unpackIndexSnapshotEnvelope(data) : data;
21622
21797
  const { decode: decode14 } = await import('@msgpack/msgpack');
21623
21798
  let decoded;
21624
21799
  try {
21625
- decoded = decode14(data);
21800
+ decoded = decode14(payloadBytes);
21626
21801
  } catch (err) {
21627
21802
  throw new NarsilError(
21628
21803
  ErrorCodes.DOC_VALIDATION_FAILED,
@@ -21817,6 +21992,137 @@ function readProcessMemory() {
21817
21992
  }
21818
21993
  }
21819
21994
 
21995
+ // src/search/cursor-binding.ts
21996
+ var ABSENT_TAG = 0;
21997
+ var NULL_TAG = 1;
21998
+ var FALSE_TAG = 2;
21999
+ var TRUE_TAG = 3;
22000
+ var NUMBER_TAG = 4;
22001
+ var STRING_TAG = 5;
22002
+ var LIST_TAG = 6;
22003
+ var MAP_TAG = 7;
22004
+ var encoder4 = new TextEncoder();
22005
+ function ensureCapacity2(stream, extra) {
22006
+ if (stream.length + extra <= stream.bytes.length) return;
22007
+ const grown = new Uint8Array(Math.max(stream.bytes.length * 2, stream.length + extra));
22008
+ grown.set(stream.bytes.subarray(0, stream.length));
22009
+ stream.bytes = grown;
22010
+ }
22011
+ function writeByte(stream, value) {
22012
+ ensureCapacity2(stream, 1);
22013
+ stream.bytes[stream.length] = value;
22014
+ stream.length += 1;
22015
+ }
22016
+ function writeUint32(stream, value) {
22017
+ ensureCapacity2(stream, 4);
22018
+ stream.bytes[stream.length] = value >>> 24 & 255;
22019
+ stream.bytes[stream.length + 1] = value >>> 16 & 255;
22020
+ stream.bytes[stream.length + 2] = value >>> 8 & 255;
22021
+ stream.bytes[stream.length + 3] = value & 255;
22022
+ stream.length += 4;
22023
+ }
22024
+ var numberBytes = new DataView(new ArrayBuffer(8));
22025
+ function writeNumber(stream, value) {
22026
+ numberBytes.setFloat64(0, value, false);
22027
+ ensureCapacity2(stream, 8);
22028
+ for (let i = 0; i < 8; i++) {
22029
+ stream.bytes[stream.length + i] = numberBytes.getUint8(i);
22030
+ }
22031
+ stream.length += 8;
22032
+ }
22033
+ function writeString(stream, value) {
22034
+ const encoded = encoder4.encode(value);
22035
+ writeUint32(stream, encoded.length);
22036
+ ensureCapacity2(stream, encoded.length);
22037
+ stream.bytes.set(encoded, stream.length);
22038
+ stream.length += encoded.length;
22039
+ }
22040
+ function writeValue(stream, value) {
22041
+ if (value === void 0) {
22042
+ writeByte(stream, ABSENT_TAG);
22043
+ return;
22044
+ }
22045
+ if (value === null) {
22046
+ writeByte(stream, NULL_TAG);
22047
+ return;
22048
+ }
22049
+ if (typeof value === "boolean") {
22050
+ writeByte(stream, value ? TRUE_TAG : FALSE_TAG);
22051
+ return;
22052
+ }
22053
+ if (typeof value === "number") {
22054
+ writeByte(stream, NUMBER_TAG);
22055
+ writeNumber(stream, value);
22056
+ return;
22057
+ }
22058
+ if (typeof value === "string") {
22059
+ writeByte(stream, STRING_TAG);
22060
+ writeString(stream, value);
22061
+ return;
22062
+ }
22063
+ if (Array.isArray(value)) {
22064
+ writeByte(stream, LIST_TAG);
22065
+ writeUint32(stream, value.length);
22066
+ for (const member of value) {
22067
+ writeValue(stream, member ?? null);
22068
+ }
22069
+ return;
22070
+ }
22071
+ const record = value;
22072
+ const keys = Object.keys(record).sort(compareCodePoints);
22073
+ writeByte(stream, MAP_TAG);
22074
+ writeUint32(stream, keys.length);
22075
+ for (const key of keys) {
22076
+ writeByte(stream, STRING_TAG);
22077
+ writeString(stream, key);
22078
+ writeValue(stream, record[key] ?? null);
22079
+ }
22080
+ }
22081
+ function boundVectorOf(vector) {
22082
+ if (vector === void 0) return void 0;
22083
+ const bound = { ...vector };
22084
+ if (vector.value !== void 0) {
22085
+ bound.value = Array.from(vector.value, Math.fround);
22086
+ }
22087
+ if (vector.text !== void 0) {
22088
+ delete bound.value;
22089
+ }
22090
+ return bound;
22091
+ }
22092
+ var FILTERS_SLOT = 2;
22093
+ var BOUND_SLOT_COUNT = 15;
22094
+ function bindingOf(values) {
22095
+ const stream = { bytes: new Uint8Array(256), length: 0 };
22096
+ for (const value of values) {
22097
+ writeValue(stream, value);
22098
+ }
22099
+ return fnv1aBytes(stream.bytes.subarray(0, stream.length)).toString(16).padStart(8, "0");
22100
+ }
22101
+ function queryBindingOf(params) {
22102
+ return bindingOf([
22103
+ params.term,
22104
+ params.fields,
22105
+ params.filters,
22106
+ params.boost,
22107
+ params.minScore,
22108
+ params.termMatch,
22109
+ params.tolerance,
22110
+ params.prefixLength,
22111
+ params.prefix,
22112
+ params.exact,
22113
+ params.pinned,
22114
+ params.mode,
22115
+ params.hybrid,
22116
+ boundVectorOf(params.vector),
22117
+ params.scoring ?? "local"
22118
+ ]);
22119
+ }
22120
+ function listBindingOf(filters) {
22121
+ const values = new Array(BOUND_SLOT_COUNT).fill(void 0);
22122
+ values[FILTERS_SLOT] = filters;
22123
+ return bindingOf(values);
22124
+ }
22125
+
21820
22126
  // src/engine/list-documents/index.ts
21821
22127
  function clampListLimit(limit) {
21822
22128
  const clamped = Math.max(1, clampLimit(limit));
@@ -21851,7 +22157,7 @@ function collectCandidates(partition, anchor, matches, wanted, out) {
21851
22157
  taken++;
21852
22158
  }
21853
22159
  }
21854
- function pageInIdOrder(partitions, cursor, filtered, limit) {
22160
+ function pageInIdOrder(partitions, cursor, filtered, limit, binding) {
21855
22161
  const anchor = cursor === null ? null : cursor.anchor;
21856
22162
  const candidates = [];
21857
22163
  for (let index = 0; index < partitions.length; index++) {
@@ -21866,10 +22172,10 @@ function pageInIdOrder(partitions, cursor, filtered, limit) {
21866
22172
  candidates.sort(compareCodePoints);
21867
22173
  const hasMore = candidates.length > limit;
21868
22174
  const ids = hasMore ? candidates.slice(0, limit) : candidates;
21869
- const nextCursor = hasMore && ids.length > 0 ? { anchor: ids[ids.length - 1], score: null, sortKey: null, sortSignature: null } : null;
22175
+ const nextCursor = hasMore && ids.length > 0 ? { anchor: ids[ids.length - 1], score: null, sortKey: null, sortSignature: null, binding } : null;
21870
22176
  return { ids, nextCursor };
21871
22177
  }
21872
- function pageInSortOrder(partitions, cursor, filtered, limit, sort, signature, schema) {
22178
+ function pageInSortOrder(partitions, cursor, filtered, limit, sort, signature, schema, binding) {
21873
22179
  const normalized = normalizeSort(sort);
21874
22180
  const fields = normalized.map((entry) => entry.field);
21875
22181
  const directions = normalized.map((entry) => entry.direction);
@@ -21892,7 +22198,7 @@ function pageInSortOrder(partitions, cursor, filtered, limit, sort, signature, s
21892
22198
  const hasMore = merged.length > limit;
21893
22199
  const page = hasMore ? merged.slice(0, limit) : merged;
21894
22200
  const last = page[page.length - 1];
21895
- const nextCursor = hasMore && last !== void 0 ? { anchor: last.id, score: null, sortKey: last.key, sortSignature: signature } : null;
22201
+ const nextCursor = hasMore && last !== void 0 ? { anchor: last.id, score: null, sortKey: last.key, sortSignature: signature, binding } : null;
21896
22202
  return { ids: page.map((entry) => entry.id), nextCursor };
21897
22203
  }
21898
22204
  function countAcrossPartitions(manager, partitionIds, partitions) {
@@ -21911,15 +22217,16 @@ function executeListDocuments(params, context) {
21911
22217
  const limit = clampListLimit(params.limit);
21912
22218
  const signature = sortSignatureOf(params.sort);
21913
22219
  requireSortableFields(params.sort, schema);
22220
+ const binding = listBindingOf(params.filters);
21914
22221
  let cursor = null;
21915
22222
  if (params.cursor !== void 0) {
21916
22223
  cursor = decodePageCursor(params.cursor);
21917
- requireMatchingCursor(cursor, params.cursor, signature, false);
22224
+ requireMatchingCursor(cursor, params.cursor, signature, false, binding);
21918
22225
  }
21919
22226
  const partitions = context.partitionIds === void 0 ? manager.getAllPartitions() : context.partitionIds.map((partitionId) => manager.partitionAt(partitionId)).filter((partition) => partition !== void 0);
21920
22227
  const filtered = params.filters === void 0 ? null : collectFilterMatches(partitions, params.filters, schema);
21921
22228
  const total = filtered === null ? countAcrossPartitions(manager, context.partitionIds, partitions) : filtered.total;
21922
- const page = params.sort === void 0 || signature === null ? pageInIdOrder(partitions, cursor, filtered, limit) : pageInSortOrder(partitions, cursor, filtered, limit, params.sort, signature, schema);
22229
+ const page = params.sort === void 0 || signature === null ? pageInIdOrder(partitions, cursor, filtered, limit, binding) : pageInSortOrder(partitions, cursor, filtered, limit, params.sort, signature, schema, binding);
21923
22230
  const projection = resolveProjection(params.document);
21924
22231
  const documents = [];
21925
22232
  for (const docId of page.ids) {
@@ -21987,18 +22294,14 @@ function applyGrouping(hits, group, getDocument) {
21987
22294
  }
21988
22295
  const result = { values: entry.values, hits: groupHits };
21989
22296
  if (group.reduce) {
21990
- try {
21991
- let accumulator = group.reduce.initialValue();
21992
- for (const hit of groupHits) {
21993
- const doc = getDocument(hit.id);
21994
- if (doc) {
21995
- accumulator = group.reduce.reducer(accumulator, doc, hit.score ?? 0);
21996
- }
21997
- }
21998
- ;
21999
- result.reduced = accumulator;
22000
- } catch (err) {
22001
- result.reducerError = err instanceof Error ? err.message : String(err);
22297
+ const folded = foldGroupReducer(
22298
+ group.reduce,
22299
+ groupHits.map((hit) => ({ document: getDocument(hit.id), score: hit.score ?? 0 }))
22300
+ );
22301
+ if (folded.reducerError !== void 0) {
22302
+ result.reducerError = folded.reducerError;
22303
+ } else {
22304
+ result.reduced = folded.reduced;
22002
22305
  }
22003
22306
  }
22004
22307
  groups.push(result);
@@ -22011,19 +22314,49 @@ function applyGrouping(hits, group, getDocument) {
22011
22314
  const bId = b.hits.length > 0 ? b.hits[0].id : "";
22012
22315
  return compareCodePoints(aId, bId);
22013
22316
  });
22317
+ if (group.limit !== void 0 && Number.isFinite(group.limit) && group.limit >= 0) {
22318
+ const cap = Math.floor(group.limit);
22319
+ if (groups.length > cap) {
22320
+ groups.length = cap;
22321
+ }
22322
+ }
22014
22323
  return groups;
22015
22324
  }
22325
+ function foldGroupReducer(reduce, rows) {
22326
+ try {
22327
+ let accumulator = reduce.initialValue();
22328
+ for (const row of rows) {
22329
+ if (row.document !== void 0) {
22330
+ accumulator = reduce.reducer(accumulator, row.document, row.score);
22331
+ }
22332
+ }
22333
+ return { reduced: accumulator };
22334
+ } catch (err) {
22335
+ return { reducerError: err instanceof Error ? err.message : String(err) };
22336
+ }
22337
+ }
22016
22338
 
22017
22339
  // src/search/pinning.ts
22340
+ function dedupePinnedEntries(pinned) {
22341
+ const deduped = [];
22342
+ const seen = /* @__PURE__ */ new Set();
22343
+ for (const entry of pinned) {
22344
+ if (seen.has(entry.docId)) continue;
22345
+ seen.add(entry.docId);
22346
+ deduped.push(entry);
22347
+ }
22348
+ return deduped;
22349
+ }
22018
22350
  function applyPinning(hits, pinned, resolveDoc) {
22019
22351
  const result = hits.slice();
22020
- const pinnedDocIds = new Set(pinned.map((p) => p.docId));
22352
+ const deduped = dedupePinnedEntries(pinned);
22353
+ const pinnedDocIds = new Set(deduped.map((entry) => entry.docId));
22021
22354
  for (let i = result.length - 1; i >= 0; i--) {
22022
22355
  if (pinnedDocIds.has(result[i].id)) {
22023
22356
  result.splice(i, 1);
22024
22357
  }
22025
22358
  }
22026
- const sorted = pinned.slice().sort((a, b) => a.position - b.position);
22359
+ const sorted = deduped.sort((a, b) => a.position - b.position);
22027
22360
  for (const entry of sorted) {
22028
22361
  const doc = resolveDoc(entry.docId);
22029
22362
  if (!doc) continue;
@@ -22217,17 +22550,7 @@ function applyHighlights(hits, params, language, analysis) {
22217
22550
 
22218
22551
  // src/engine/query/shared.ts
22219
22552
  function partitionsFor(manager, partitionIds) {
22220
- if (partitionIds === void 0) {
22221
- return manager.getAllPartitions();
22222
- }
22223
- const partitions = [];
22224
- for (const partitionId of partitionIds) {
22225
- const partition = manager.partitionAt(partitionId);
22226
- if (partition !== void 0) {
22227
- partitions.push(partition);
22228
- }
22229
- }
22230
- return partitions;
22553
+ return partitionsIn(manager, partitionIds);
22231
22554
  }
22232
22555
  function coverageFor(manager, partitionIds) {
22233
22556
  if (partitionIds === void 0) {
@@ -22321,7 +22644,7 @@ function executeSortedQueryPage(params, context, limit, offset, sortSignature) {
22321
22644
  let anchorId = null;
22322
22645
  if (params.searchAfter !== void 0) {
22323
22646
  const decoded = decodePageCursor(params.searchAfter);
22324
- requireMatchingCursor(decoded, params.searchAfter, sortSignature, true);
22647
+ requireMatchingCursor(decoded, params.searchAfter, sortSignature, true, context.cursorBinding);
22325
22648
  anchorKey = decoded.sortKey;
22326
22649
  anchorId = decoded.anchor;
22327
22650
  }
@@ -22366,7 +22689,13 @@ function executeSortedQueryPage(params, context, limit, offset, sortSignature) {
22366
22689
  const last = page[page.length - 1];
22367
22690
  let cursor;
22368
22691
  if (hasMore && last !== void 0) {
22369
- cursor = encodePageCursor({ anchor: last.id, score: null, sortKey: last.key, sortSignature });
22692
+ cursor = encodePageCursor({
22693
+ anchor: last.id,
22694
+ score: null,
22695
+ sortKey: last.key,
22696
+ sortSignature,
22697
+ binding: context.cursorBinding
22698
+ });
22370
22699
  }
22371
22700
  return {
22372
22701
  hits: page.map((entry) => ({ id: entry.id, document: void 0 })),
@@ -22652,7 +22981,7 @@ async function executeQuery(params, context) {
22652
22981
  if (params.group) {
22653
22982
  groups = applyGrouping(hits, params.group, (docId) => manager.getRef(docId));
22654
22983
  }
22655
- if (params.pinned) {
22984
+ if (params.pinned && params.searchAfter === void 0 && context.partitionIds === void 0) {
22656
22985
  hits = applyPinning(hits, params.pinned, (docId) => {
22657
22986
  const doc = manager.getRef(docId);
22658
22987
  if (!doc) return void 0;
@@ -22667,7 +22996,16 @@ async function executeQuery(params, context) {
22667
22996
  sortKeyOf
22668
22997
  };
22669
22998
  }
22670
- const paged = applyPagination(hits, limit, offset, params.searchAfter, sortContext);
22999
+ const pinnedIds = params.pinned !== void 0 && params.searchAfter === void 0 ? new Set(params.pinned.map((entry) => entry.docId)) : void 0;
23000
+ const paged = applyPagination(
23001
+ hits,
23002
+ limit,
23003
+ offset,
23004
+ context.cursorBinding,
23005
+ params.searchAfter,
23006
+ sortContext,
23007
+ pinnedIds
23008
+ );
22671
23009
  paginated = paged.paginated;
22672
23010
  nextCursor = paged.nextCursor;
22673
23011
  count = fanOutResult.totalMatched;
@@ -22736,6 +23074,11 @@ async function executePreflight(params, context) {
22736
23074
  } else if (isHybridMode && hasGlobalVectorIndex) {
22737
23075
  const result = await executeHybridSearch(params, context, preflightLimit, preflightOffset);
22738
23076
  totalMatched = result.totalMatched;
23077
+ } else if (countsWithoutScores(params)) {
23078
+ totalMatched = fanOutMatchCount(manager, params, language, config.schema, {
23079
+ searchOptions: searchOptionsFor(manager),
23080
+ partitionIds: context.partitionIds
23081
+ });
22739
23082
  } else {
22740
23083
  const scoring = scoringConfigFor(params, context);
22741
23084
  const workerResult = workerSearch ? await workerSearch(indexName, params, broadcastStatsForWorker(params, context, scoring), context.partitionIds) : null;
@@ -22881,7 +23224,8 @@ async function runEngineQuery(core, indexName, params, options) {
22881
23224
  workerSearch,
22882
23225
  indexName,
22883
23226
  broadcastStats: broadcastStatsFor(core),
22884
- partitionIds: options?.partitionIds
23227
+ partitionIds: options?.partitionIds,
23228
+ cursorBinding: queryBindingOf(params)
22885
23229
  });
22886
23230
  try {
22887
23231
  await core.pluginRegistry.runHook("afterSearch", {
@@ -22915,7 +23259,8 @@ async function runEnginePreflight(core, indexName, params, options) {
22915
23259
  workerSearch,
22916
23260
  indexName,
22917
23261
  broadcastStats: broadcastStatsFor(core),
22918
- partitionIds: options?.partitionIds
23262
+ partitionIds: options?.partitionIds,
23263
+ cursorBinding: queryBindingOf(params)
22919
23264
  });
22920
23265
  if (core.analysisRebuild.isStale(indexName)) {
22921
23266
  result.analysisStale = true;