@delali/narsil 0.2.4-next.55 → 0.3.0

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 (52) hide show
  1. package/README.md +39 -28
  2. package/dist/adapters/filesystem.mjs +1 -1
  3. package/dist/adapters/indexeddb.mjs +1 -1
  4. package/dist/{capabilities-DlYw0Ov0.d.ts → capabilities-DSsOGz6F.d.ts} +2 -2
  5. package/dist/{chunk-BH5SYB5W.mjs → chunk-24RXHMEL.mjs} +604 -150
  6. package/dist/{chunk-D2S642IV.mjs → chunk-3LHGNRYE.mjs} +1 -1
  7. package/dist/{chunk-TVT5V4E7.mjs → chunk-B2TFROFX.mjs} +299 -168
  8. package/dist/{chunk-L225SHNB.mjs → chunk-L6TNPLAR.mjs} +19 -5
  9. package/dist/{chunk-U2AL3SON.mjs → chunk-THYRHHO2.mjs} +3 -3
  10. package/dist/{chunk-NTZYA3SJ.mjs → chunk-U7KYW4BA.mjs} +3 -3
  11. package/dist/{chunk-FRQVMFY7.mjs → chunk-USKFU7J5.mjs} +1 -0
  12. package/dist/{chunk-4NVPQ6S5.mjs → chunk-UXNWQZST.mjs} +12 -3
  13. package/dist/{chunk-RGS7FMCB.mjs → chunk-VMRR3DQU.mjs} +74 -16
  14. package/dist/{chunk-2GWVQWGA.mjs → chunk-YMZCMH5M.mjs} +45 -1
  15. package/dist/{chunk-UTOFOJOR.mjs → chunk-YNVIUGF5.mjs} +6 -2
  16. package/dist/{chunk-5T6AOR37.mjs → chunk-ZNZBXMP5.mjs} +601 -60
  17. package/dist/{chunk-XNQAIO74.mjs → chunk-ZZT3EAUC.mjs} +177 -309
  18. package/dist/{client-CQUqt9Ir.d.ts → client-BQK6Tf6A.d.ts} +3 -3
  19. package/dist/client.d.ts +8 -8
  20. package/dist/client.mjs +1 -0
  21. package/dist/{config-CNas6wJH.d.ts → config-BhldZgaK.d.ts} +2 -2
  22. package/dist/{config-bXxnJrQc.d.ts → config-DmXULPa-.d.ts} +15 -7
  23. package/dist/direct-executor-ALAVE7FC.mjs +12 -0
  24. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  25. package/dist/distribution/coordinator/etcd.mjs +2 -2
  26. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  27. package/dist/distribution/coordinator/in-memory.mjs +1 -1
  28. package/dist/distribution/transport/grpc.mjs +1 -1
  29. package/dist/distribution.d.ts +8 -8
  30. package/dist/distribution.mjs +382 -162
  31. package/dist/embeddings/openai.mjs +2 -2
  32. package/dist/{engine-Dj4I91Jv.d.ts → engine-CIOyFozK.d.ts} +15 -12
  33. package/dist/index.browser.mjs +1565 -528
  34. package/dist/index.d.ts +7 -7
  35. package/dist/index.mjs +11 -11
  36. package/dist/invalidation/filesystem.mjs +1 -1
  37. package/dist/{memory-xrXOZMbw.d.ts → memory-yFaVn2xZ.d.ts} +199 -107
  38. package/dist/persistence/durability/checkpoint-worker.d.ts +1 -1
  39. package/dist/persistence/durability/checkpoint-worker.mjs +11 -11
  40. package/dist/react.d.ts +6 -6
  41. package/dist/react.mjs +1 -0
  42. package/dist/{schema-mLpXZBEh.d.ts → schema-d5Afu03A.d.ts} +7 -2
  43. package/dist/server.d.ts +10 -10
  44. package/dist/server.mjs +10 -10
  45. package/dist/{tasks-Df73Tjgq.d.ts → tasks-CH2Dod8w.d.ts} +1 -1
  46. package/dist/{thread-5UP6MGED.mjs → thread-DYHEQRVY.mjs} +14 -10
  47. package/dist/{types-BTIVO8DI.d.ts → types-DAK7mnUZ.d.ts} +1 -1
  48. package/dist/vector/search-worker.d.ts +1 -1
  49. package/dist/vector/search-worker.mjs +4 -4
  50. package/dist/workers/entry.mjs +4 -4
  51. package/package.json +9 -9
  52. package/dist/direct-executor-RBA5O7EN.mjs +0 -12
@@ -1,24 +1,24 @@
1
1
  import { MAX_MESSAGE_SIZE_BYTES } from './chunk-UNEEFTEU.mjs';
2
- import { validateIndexName, indexConfigKey, putIndexMetadata, getIndexMetadata } from './chunk-NTZYA3SJ.mjs';
3
- import { createEngineCore, checkHeapAfterRecovery, createNarsilFromCore, runEngineQueryStats, runEngineListDocuments, runEngineSuggest, runEnginePreflight, runEngineQuery, createEngineIndex, applyIndexEntry, applyDeleteEntry, validateEntryBatchPayload, createAckMessage, validateEntryPayload, validateForwardBatchPayload, createForwardBatchResultMessage, createForwardBatchMessage, validateForwardBatchResultPayload, createForwardMessage, validateReplicationEntry, computeEntryChecksum, createEntryMessage, createEntryBatchMessage, createInsyncConfirmMessage, validateAckPayload, createInsyncRemoveMessage, validateInsyncConfirmPayload, createInsyncAddMessage } from './chunk-BH5SYB5W.mjs';
2
+ import { validateIndexName, indexConfigKey, putIndexMetadata, getIndexMetadata } from './chunk-U7KYW4BA.mjs';
3
+ import { requireKnownConfig, resolveRunnableConfig, createEngineCore, startEngineCore, createNarsilFromCore, runEngineQueryStats, runEngineListDocuments, runEngineSuggest, runEnginePreflight, runEngineQuery, createEngineIndex, applyIndexEntry, applyDeleteEntry, validateEntryBatchPayload, createAckMessage, validateEntryPayload, validateForwardBatchPayload, createForwardBatchResultMessage, createForwardBatchMessage, validateForwardBatchResultPayload, createForwardMessage, validateReplicationEntry, computeEntryChecksum, createEntryMessage, createEntryBatchMessage, createInsyncConfirmMessage, validateAckPayload, createInsyncRemoveMessage, validateInsyncConfirmPayload, createInsyncAddMessage } from './chunk-24RXHMEL.mjs';
4
4
  import { ReplicationMessageTypes, TransportErrorCodes, ClusterMessageTypes, QueryMessageTypes, TransportError } from './chunk-GCM3JFVA.mjs';
5
- import { providedDocId, MAX_PARTITION_COUNT, MAX_REPLICATION_FACTOR, WIRE_BATCH_BUDGET } from './chunk-4NVPQ6S5.mjs';
6
- import { generateId, deserializePayloadV2, createPartitionIndex, getLanguage, FILTER_EXPRESSION_KEYS, FIELD_FILTER_OPERATORS } from './chunk-XNQAIO74.mjs';
5
+ import { providedDocId, MAX_PARTITION_COUNT, MAX_REPLICATION_FACTOR, WIRE_BATCH_BUDGET } from './chunk-UXNWQZST.mjs';
6
+ import { generateId, deserializePayloadV2, createPartitionIndex, getLanguage, FILTER_EXPRESSION_KEYS, FIELD_FILTER_OPERATORS } from './chunk-ZZT3EAUC.mjs';
7
7
  import './chunk-AV4IJKD2.mjs';
8
- import { now, listBindingOf, queryBindingOf, foldGroupReducer, clampLimit, dedupePinnedEntries } from './chunk-TVT5V4E7.mjs';
9
- import { DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, MAX_SUGGEST_SCATTER_LIMIT, MAX_INDEX_NAME_LENGTH, MAX_DOC_ID_LENGTH, SUGGEST_OVERSAMPLE_FACTOR, SUGGEST_OVERSAMPLE_PADDING, INDEX_NAME_PATTERN } from './chunk-D2S642IV.mjs';
10
- import { resolveProjection, clampRowCount, sortSignatureOf, requireMatchingCursor, decodePageCursor, normalizeSort, encodePageCursor, applyProjection, readSortValues, requireWithinResultWindow, fnv1a, mergePartitionStats } from './chunk-5T6AOR37.mjs';
11
- import { DEFAULT_PAGE_SIZE, MAX_SORT_FIELDS, MAX_CURSOR_LENGTH, RESULT_WINDOW, MAX_NESTING_DEPTH } from './chunk-L225SHNB.mjs';
8
+ import { now, listBindingOf, queryBindingOf, hitsKeptPerGroup, clampLimit, resolveHybridFusion, dedupePinnedEntries, foldGroupReducer } from './chunk-B2TFROFX.mjs';
9
+ import { DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, MAX_SUGGEST_SCATTER_LIMIT, MAX_INDEX_NAME_LENGTH, MAX_DOC_ID_LENGTH, SUGGEST_OVERSAMPLE_FACTOR, SUGGEST_OVERSAMPLE_PADDING, INDEX_NAME_PATTERN } from './chunk-3LHGNRYE.mjs';
10
+ import { oversampledShardSize, resolveProjection, clampRowCount, sortSignatureOf, requireMatchingCursor, decodePageCursor, normalizeSort, sortModesOf, encodePageCursor, applyProjection, readSortValues, requireWithinResultWindow, fnv1a, sortSignatureEntry, MAX_FILTER_DEPTH, fnv1aUint32s, mergePartitionStats, MIN_POLYGON_POINTS } from './chunk-ZNZBXMP5.mjs';
11
+ import { DEFAULT_PAGE_SIZE, MAX_SORT_FIELDS, MAX_CURSOR_LENGTH, RESULT_WINDOW, MAX_NESTING_DEPTH } from './chunk-L6TNPLAR.mjs';
12
12
  import './chunk-TGP5M5R7.mjs';
13
- import './chunk-U2AL3SON.mjs';
13
+ import './chunk-THYRHHO2.mjs';
14
14
  import { MIN_OVERSAMPLE } from './chunk-ZJ4CSAB2.mjs';
15
- import './chunk-UTOFOJOR.mjs';
15
+ import './chunk-YNVIUGF5.mjs';
16
16
  import { crc32 } from './chunk-3L4U6EJQ.mjs';
17
17
  import './chunk-IFBKLBGB.mjs';
18
18
  import './chunk-U2BKYEC7.mjs';
19
- import { compareCodePoints, toComparableSortValue, compareComparableKeys, compareSortValues } from './chunk-2GWVQWGA.mjs';
19
+ import { compareCodePoints, toComparableSortValue, compareComparableKeys, isSortMode, SORT_MODES, compareSortValues } from './chunk-YMZCMH5M.mjs';
20
20
  import './chunk-POWOTUKJ.mjs';
21
- import { ErrorCodes, NarsilError, describeError } from './chunk-FRQVMFY7.mjs';
21
+ import { ErrorCodes, NarsilError, describeError } from './chunk-USKFU7J5.mjs';
22
22
  import { encode, decode } from '@msgpack/msgpack';
23
23
 
24
24
  // src/distribution/cluster/constants.ts
@@ -2515,6 +2515,11 @@ function createDataNodeLifecycle(config) {
2515
2515
  return handle;
2516
2516
  }
2517
2517
 
2518
+ // src/distribution/cluster-node/local-index-gone.ts
2519
+ function localIndexIsGone(err) {
2520
+ return err instanceof NarsilError && (err.code === ErrorCodes.INDEX_NOT_FOUND || err.code === ErrorCodes.INSTANCE_SHUT_DOWN);
2521
+ }
2522
+
2518
2523
  // src/distribution/cluster-node/bootstrap-cleanup.ts
2519
2524
  async function cleanupRemovedPartition(indexName, partitionId, deps) {
2520
2525
  try {
@@ -2530,7 +2535,7 @@ async function cleanupRemovedPartition(indexName, partitionId, deps) {
2530
2535
  if (deps.onError === void 0) {
2531
2536
  return;
2532
2537
  }
2533
- if (err instanceof NarsilError && err.code === ErrorCodes.INDEX_NOT_FOUND) {
2538
+ if (localIndexIsGone(err)) {
2534
2539
  return;
2535
2540
  }
2536
2541
  const cause = err instanceof Error ? err.message : String(err);
@@ -3311,7 +3316,7 @@ async function dropRestoredIndexQuietly(engine, indexName, deps = {}) {
3311
3316
  }
3312
3317
  await engine.dropIndex(indexName);
3313
3318
  } catch (err) {
3314
- if (err instanceof NarsilError && err.code === ErrorCodes.INDEX_NOT_FOUND) {
3319
+ if (localIndexIsGone(err)) {
3315
3320
  return;
3316
3321
  }
3317
3322
  if (deps.onError === void 0) {
@@ -3365,7 +3370,7 @@ async function dropExistingIndex(engine, indexName, primaryNodeId) {
3365
3370
  await engine.dropIndex(indexName);
3366
3371
  return null;
3367
3372
  } catch (err) {
3368
- if (err instanceof NarsilError && err.code === ErrorCodes.INDEX_NOT_FOUND) {
3373
+ if (localIndexIsGone(err)) {
3369
3374
  return null;
3370
3375
  }
3371
3376
  const cause = err instanceof Error ? err.message : String(err);
@@ -5186,20 +5191,15 @@ async function applyLocalReplicationEntry(core, entry) {
5186
5191
 
5187
5192
  // src/distribution/cluster-node/local-engine.ts
5188
5193
  async function createClusterLocalEngine(config, hooks) {
5189
- const core = createEngineCore(config, hooks);
5190
- if (core.durability !== null) {
5191
- await core.durability.manager.recover(config?.lifecycle !== void 0);
5192
- checkHeapAfterRecovery(core);
5193
- }
5194
- if (core.invalidation !== null) {
5195
- await core.invalidation.start();
5196
- }
5197
- if (config?.lifecycle === void 0) await core.analysisRebuild.reviewStaleIndexes();
5194
+ requireKnownConfig(config);
5195
+ const runnableConfig = await resolveRunnableConfig(config);
5196
+ const core = createEngineCore(runnableConfig, hooks);
5197
+ await startEngineCore(core, runnableConfig);
5198
5198
  core.orchestrator.shareMainThread();
5199
- const engine = createNarsilFromCore(core, config);
5199
+ const engine = createNarsilFromCore(core, runnableConfig);
5200
5200
  const heldPartitions = createHeldPartitionRecord(core);
5201
5201
  return Object.assign(engine, {
5202
- createIndexWithUuid: (name, indexConfig, indexUuid) => createEngineIndex(core, config, name, indexConfig, indexUuid),
5202
+ createIndexWithUuid: (name, indexConfig, indexUuid) => createEngineIndex(core, runnableConfig, name, indexConfig, indexUuid),
5203
5203
  acquireIndexForReplication: (indexName) => core.indexState.acquire(indexName, false),
5204
5204
  indexUuidOf: (indexName) => core.indexRegistry.get(indexName)?.indexUuid,
5205
5205
  stampIndexUuid: (indexName, indexUuid) => stampIndexUuid(core, indexName, indexUuid),
@@ -6075,8 +6075,8 @@ var MAX_FIELD_NAME_LENGTH = 255;
6075
6075
  var MAX_FIELDS_LIST = 256;
6076
6076
  var MAX_BOOST_FIELDS = 256;
6077
6077
  var MAX_FACETS = 64;
6078
+ var MAX_FACET_RANGES = 1e3;
6078
6079
  var MAX_FETCH_DOCUMENT_IDS = 1e4;
6079
- var MAX_FILTER_DEPTH = 30;
6080
6080
  var MAX_FILTER_FIELDS = 256;
6081
6081
  var MAX_FILTER_ARRAY_SIZE = 65536;
6082
6082
  var MAX_FILTER_STRING_LENGTH = 1024;
@@ -6428,6 +6428,13 @@ function validateGeoPolygon(value, fieldLabel, errorCode) {
6428
6428
  throwInvalid(errorCode, `Invalid payload: "${fieldLabel}.points" must be an array`);
6429
6429
  }
6430
6430
  const points = value.points;
6431
+ if (points.length < MIN_POLYGON_POINTS) {
6432
+ throwInvalid(
6433
+ errorCode,
6434
+ `Invalid payload: "${fieldLabel}.points" must hold at least ${MIN_POLYGON_POINTS} points to enclose an area`,
6435
+ { length: points.length, minimum: MIN_POLYGON_POINTS }
6436
+ );
6437
+ }
6431
6438
  if (points.length > MAX_FILTER_ARRAY_SIZE) {
6432
6439
  throwInvalid(
6433
6440
  errorCode,
@@ -6584,13 +6591,9 @@ function validateFilterExpression(value, fieldLabel, errorCode = SEARCH_INVALID_
6584
6591
  }
6585
6592
  }
6586
6593
 
6587
- // src/distribution/query/oversample.ts
6588
- function oversampledShardSize(size) {
6589
- return Math.ceil(size * 1.5) + 10;
6590
- }
6591
-
6592
6594
  // src/distribution/query/validators/search-options.ts
6593
6595
  var ALLOWED_MODES = ["fulltext", "vector", "hybrid"];
6596
+ var ALLOWED_FACET_ORDERS = ["asc", "desc"];
6594
6597
  var ALLOWED_METRICS = ["cosine", "dotProduct", "euclidean"];
6595
6598
  var ALLOWED_HYBRID_STRATEGIES = ["rrf", "linear"];
6596
6599
  function validateHybridParams(value) {
@@ -6643,6 +6646,55 @@ function validateGroupParams(value) {
6643
6646
  );
6644
6647
  }
6645
6648
  }
6649
+ function validateFacetRanges(value, fieldLabel) {
6650
+ if (!Array.isArray(value)) {
6651
+ throwInvalid(CONFIG_INVALID, `Invalid SearchPayload: "${fieldLabel}" must be an array or null`);
6652
+ }
6653
+ if (value.length > MAX_FACET_RANGES) {
6654
+ throwInvalid(
6655
+ CONFIG_INVALID,
6656
+ `Invalid SearchPayload: "${fieldLabel}" exceeds maximum length of ${MAX_FACET_RANGES}`,
6657
+ { length: value.length, limit: MAX_FACET_RANGES }
6658
+ );
6659
+ }
6660
+ for (let i = 0; i < value.length; i++) {
6661
+ const range = value[i];
6662
+ if (!isRecord2(range) || typeof range.from !== "number" || typeof range.to !== "number") {
6663
+ throwInvalid(CONFIG_INVALID, `Invalid SearchPayload: "${fieldLabel}[${i}]" must hold a numeric from and to`);
6664
+ }
6665
+ }
6666
+ }
6667
+ function validateFacetParams(value) {
6668
+ if (!Array.isArray(value)) {
6669
+ throwInvalid(SEARCH_INVALID_FIELD, 'Invalid SearchPayload: "params.facets" must be an array or null');
6670
+ }
6671
+ if (value.length > MAX_FACETS) {
6672
+ throwInvalid(
6673
+ SEARCH_INVALID_FIELD,
6674
+ `Invalid SearchPayload: "params.facets" exceeds maximum length of ${MAX_FACETS}`,
6675
+ {
6676
+ length: value.length,
6677
+ limit: MAX_FACETS
6678
+ }
6679
+ );
6680
+ }
6681
+ for (let i = 0; i < value.length; i++) {
6682
+ const entry = value[i];
6683
+ if (!isRecord2(entry)) {
6684
+ throwInvalid(CONFIG_INVALID, `Invalid SearchPayload: "params.facets[${i}]" must be an object`);
6685
+ }
6686
+ validateFieldName(entry.field, `params.facets[${i}].field`, SEARCH_INVALID_FIELD);
6687
+ if (entry.sort !== null && !ALLOWED_FACET_ORDERS.includes(entry.sort)) {
6688
+ throwInvalid(
6689
+ SEARCH_INVALID_MODE,
6690
+ `Invalid SearchPayload: "params.facets[${i}].sort" must be one of: ${ALLOWED_FACET_ORDERS.join(", ")}, or null`
6691
+ );
6692
+ }
6693
+ if (entry.ranges !== null) {
6694
+ validateFacetRanges(entry.ranges, `params.facets[${i}].ranges`);
6695
+ }
6696
+ }
6697
+ }
6646
6698
  function validateTermMatchParam(value) {
6647
6699
  if (value === "all" || value === "any") {
6648
6700
  return;
@@ -6824,6 +6876,12 @@ function validateSortParams(value) {
6824
6876
  `Invalid SearchPayload: "params.sort[${i}].direction" must be one of: ${ALLOWED_SORT_DIRECTIONS.join(", ")}`
6825
6877
  );
6826
6878
  }
6879
+ if (entry.mode !== void 0 && !isSortMode(entry.mode)) {
6880
+ throwInvalid(
6881
+ SEARCH_INVALID_MODE,
6882
+ `Invalid SearchPayload: "params.sort[${i}].mode" must be one of: ${SORT_MODES.join(", ")}`
6883
+ );
6884
+ }
6827
6885
  }
6828
6886
  }
6829
6887
  function validateBoostParams(value) {
@@ -6880,7 +6938,7 @@ function validateParams(params) {
6880
6938
  validateGroupParams(params.group);
6881
6939
  }
6882
6940
  if (params.facets !== null) {
6883
- validateStringArray(params.facets, "params.facets", MAX_FACETS, 255, SEARCH_INVALID_FIELD);
6941
+ validateFacetParams(params.facets);
6884
6942
  }
6885
6943
  if (params.facetSize !== null) {
6886
6944
  validateNonNegativeInteger(params.facetSize, "params.facetSize", MAX_LIMIT, CONFIG_INVALID);
@@ -7055,6 +7113,12 @@ function validateListSort(value) {
7055
7113
  if (entry.direction !== "asc" && entry.direction !== "desc") {
7056
7114
  throwInvalid(SEARCH_INVALID_MODE, `Invalid ListPayload: "sort[${i}].direction" must be "asc" or "desc"`);
7057
7115
  }
7116
+ if (entry.mode !== void 0 && !isSortMode(entry.mode)) {
7117
+ throwInvalid(
7118
+ SEARCH_INVALID_MODE,
7119
+ `Invalid ListPayload: "sort[${i}].mode" must be one of: ${SORT_MODES.join(", ")}`
7120
+ );
7121
+ }
7058
7122
  }
7059
7123
  }
7060
7124
  function validateListPayload(decoded) {
@@ -7528,6 +7592,13 @@ function createPreflightResultMessage(payload, sourceId, requestId) {
7528
7592
  };
7529
7593
  }
7530
7594
 
7595
+ // src/distribution/query/types.ts
7596
+ var DEFAULT_QUERY_CONFIG = {
7597
+ allowPartialResults: true,
7598
+ partitionTimeout: 5e3,
7599
+ defaultFacetSize: 10
7600
+ };
7601
+
7531
7602
  // src/distribution/cluster-node/query-conversion.ts
7532
7603
  function wireParamsToLocal(wire, facetShardSize) {
7533
7604
  return {
@@ -7590,18 +7661,33 @@ function convertWireSortToLocal(wireSort) {
7590
7661
  if (wireSort === null || wireSort.length === 0) {
7591
7662
  return void 0;
7592
7663
  }
7593
- return wireSort.map((entry) => ({ field: entry.field, direction: entry.direction }));
7664
+ return normalizeSort(wireSort);
7665
+ }
7666
+ function countsEveryValue(facet) {
7667
+ return facet.ranges !== null || facet.sort === "asc";
7594
7668
  }
7595
7669
  function convertWireFacetConfigToLocal(facets, limit) {
7596
7670
  if (facets === null || facets.length === 0) {
7597
7671
  return void 0;
7598
7672
  }
7599
7673
  const result = {};
7600
- for (const field of facets) {
7601
- result[field] = limit !== null ? { limit } : {};
7674
+ for (const facet of facets) {
7675
+ const options = {};
7676
+ if (countsEveryValue(facet)) options.limit = MAX_FACET_BUCKETS;
7677
+ else if (limit !== null) options.limit = limit;
7678
+ if (facet.sort !== null) options.sort = facet.sort;
7679
+ if (facet.ranges !== null) options.ranges = facet.ranges.map((range) => ({ from: range.from, to: range.to }));
7680
+ result[facet.field] = options;
7602
7681
  }
7603
7682
  return result;
7604
7683
  }
7684
+ function ascendingFacetFields(facets) {
7685
+ const ascending = /* @__PURE__ */ new Set();
7686
+ for (const facet of facets ?? []) {
7687
+ if (facet.sort === "asc") ascending.add(facet.field);
7688
+ }
7689
+ return ascending;
7690
+ }
7605
7691
  function localParamsToWire(params) {
7606
7692
  return {
7607
7693
  term: params.term ?? null,
@@ -7609,7 +7695,7 @@ function localParamsToWire(params) {
7609
7695
  sort: convertLocalSortToWire(params.sort),
7610
7696
  group: convertLocalGroupToWire(params.group),
7611
7697
  facets: convertLocalFacetsToWire(params.facets),
7612
- facetSize: null,
7698
+ facetSize: wireFacetSize(params.facets),
7613
7699
  limit: clampRowCount(params.limit, DEFAULT_PAGE_SIZE),
7614
7700
  offset: clampRowCount(params.offset, 0),
7615
7701
  searchAfter: params.searchAfter ?? null,
@@ -7629,7 +7715,19 @@ function localParamsToWire(params) {
7629
7715
  hybrid: convertLocalHybridToWire(params.hybrid)
7630
7716
  };
7631
7717
  }
7632
- function distributedResultToLocal(result, documents = /* @__PURE__ */ new Map()) {
7718
+ function countIsExactFor(params) {
7719
+ const vector = params.vector;
7720
+ if (vector === void 0 || vector.value === void 0 && vector.text === void 0) return true;
7721
+ const hasTerm = params.term !== void 0 && params.term.trim().length > 0;
7722
+ if (params.mode === "hybrid" || hasTerm) return false;
7723
+ return vector.similarity === void 0;
7724
+ }
7725
+ function distributedCountIsExact(params, coverage) {
7726
+ if (coverage.queriedPartitions < coverage.totalPartitions) return false;
7727
+ if (coverage.timedOutPartitions > 0 || coverage.failedPartitions > 0) return false;
7728
+ return countIsExactFor(params);
7729
+ }
7730
+ function distributedResultToLocal(result, countExact, documents = /* @__PURE__ */ new Map(), requestedFacets) {
7633
7731
  return {
7634
7732
  hits: result.scored.map((entry) => ({
7635
7733
  id: entry.docId,
@@ -7637,9 +7735,10 @@ function distributedResultToLocal(result, documents = /* @__PURE__ */ new Map())
7637
7735
  document: documents.get(entry.docId) ?? {}
7638
7736
  })),
7639
7737
  count: result.totalHits,
7738
+ countExact,
7640
7739
  elapsed: 0,
7641
7740
  cursor: result.cursor ?? void 0,
7642
- facets: result.facets !== null ? convertWireFacetsToLocal(result.facets, result.facetErrorBounds) : void 0,
7741
+ facets: result.facets !== null ? convertWireFacetsToLocal(result, result.facets, requestedFacets) : void 0,
7643
7742
  coverage: result.coverage
7644
7743
  };
7645
7744
  }
@@ -7653,15 +7752,22 @@ function convertLocalGroupToWire(group) {
7653
7752
  }
7654
7753
  return {
7655
7754
  fields: [...group.fields],
7656
- maxPerGroup: group.maxPerGroup ?? 1,
7755
+ maxPerGroup: group.reduce !== void 0 ? MAX_LIMIT : hitsKeptPerGroup(group.maxPerGroup),
7657
7756
  limit: group.limit ?? null
7658
7757
  };
7659
7758
  }
7759
+ function wireFacetOrder(sort) {
7760
+ return sort === "asc" || sort === "desc" ? sort : null;
7761
+ }
7660
7762
  function convertLocalFacetsToWire(facets) {
7661
7763
  if (facets === void 0) {
7662
7764
  return null;
7663
7765
  }
7664
- return Object.keys(facets);
7766
+ return Object.entries(facets).map(([field, options]) => ({
7767
+ field,
7768
+ sort: wireFacetOrder(options?.sort),
7769
+ ranges: options?.ranges?.map((range) => ({ from: range.from, to: range.to })) ?? null
7770
+ }));
7665
7771
  }
7666
7772
  function convertLocalVectorToWire(vector) {
7667
7773
  if (vector === void 0) {
@@ -7687,18 +7793,34 @@ function convertLocalHybridToWire(hybrid) {
7687
7793
  alpha: hybrid.alpha ?? null
7688
7794
  };
7689
7795
  }
7690
- function convertWireFacetsToLocal(wireFacets, errorBounds) {
7691
- const result = {};
7796
+ function facetLimitOf(options) {
7797
+ const limit = options?.limit;
7798
+ const ranges = options?.ranges;
7799
+ if ((limit === void 0 || !Number.isFinite(limit)) && Array.isArray(ranges)) {
7800
+ return Math.min(Math.max(ranges.length, 1), MAX_FACET_SIZE);
7801
+ }
7802
+ if (limit === void 0 || !Number.isFinite(limit)) return DEFAULT_QUERY_CONFIG.defaultFacetSize;
7803
+ return Math.min(Math.max(Math.floor(limit), 1), MAX_FACET_SIZE);
7804
+ }
7805
+ function wireFacetSize(facets) {
7806
+ if (facets === void 0) return null;
7807
+ let size = 0;
7808
+ for (const options of Object.values(facets)) size = Math.max(size, facetLimitOf(options));
7809
+ return size === 0 ? null : size;
7810
+ }
7811
+ function convertWireFacetsToLocal(result, wireFacets, requested) {
7812
+ const converted = {};
7692
7813
  for (const [field, buckets] of Object.entries(wireFacets)) {
7814
+ const mergedBound = result.facetErrorBounds?.[field] ?? 0;
7815
+ const kept = requested === void 0 ? buckets.length : Math.min(buckets.length, facetLimitOf(requested[field]));
7693
7816
  const values = {};
7694
- let totalCount = 0;
7695
- for (const bucket of buckets) {
7696
- values[bucket.value] = bucket.count;
7697
- totalCount += bucket.count;
7698
- }
7699
- result[field] = { values, count: totalCount, errorBound: errorBounds?.[field] ?? 0 };
7817
+ for (let index = 0; index < kept; index++) values[buckets[index].value] = buckets[index].count;
7818
+ let largestCut = 0;
7819
+ for (let index = kept; index < buckets.length; index++) largestCut = Math.max(largestCut, buckets[index].count);
7820
+ const errorBound = kept < buckets.length ? (result.facetUndercounts?.[field] ?? mergedBound) + largestCut : mergedBound;
7821
+ converted[field] = { values, count: kept, errorBound: Math.max(errorBound, mergedBound) };
7700
7822
  }
7701
- return result;
7823
+ return converted;
7702
7824
  }
7703
7825
 
7704
7826
  // src/distribution/cluster-node/message-handler/queries.ts
@@ -7715,11 +7837,13 @@ async function handleSearch(message, respond, deps) {
7715
7837
  payload.partitionIds,
7716
7838
  payload.globalStats ?? void 0
7717
7839
  );
7718
- const sortFields = queryParams.sort !== void 0 ? normalizeSort(queryParams.sort).map((entry) => entry.field) : null;
7840
+ const normalizedSort = queryParams.sort !== void 0 ? normalizeSort(queryParams.sort) : null;
7841
+ const sortFields = normalizedSort === null ? null : normalizedSort.map((entry) => entry.field);
7842
+ const sortModes = normalizedSort === null ? [] : sortModesOf(normalizedSort);
7719
7843
  const scored = queryResult.hits.map((hit) => ({
7720
7844
  docId: hit.id,
7721
7845
  score: hit.score ?? null,
7722
- sortValues: sortFields !== null ? readSortValues(hit.document, sortFields).map(toWireSortValue) : null
7846
+ sortValues: sortFields !== null ? readSortValues(hit.document, sortFields, sortModes).map(toWireSortValue) : null
7723
7847
  }));
7724
7848
  const results = payload.partitionIds.map((partitionId) => ({
7725
7849
  partitionId,
@@ -7730,7 +7854,7 @@ async function handleSearch(message, respond, deps) {
7730
7854
  results,
7731
7855
  facets: convertLocalFacetsToWire2(queryResult.facets),
7732
7856
  facetErrorBounds: convertLocalFacetBoundsToWire(queryResult.facets),
7733
- groups: await convertLocalGroupsToWire(queryResult.groups, sortFields, deps, payload.indexName)
7857
+ groups: await convertLocalGroupsToWire(queryResult.groups, sortFields, sortModes, deps, payload.indexName)
7734
7858
  };
7735
7859
  await respond({
7736
7860
  type: QueryMessageTypes.SEARCH_RESULT,
@@ -7739,21 +7863,20 @@ async function handleSearch(message, respond, deps) {
7739
7863
  payload: encode(resultPayload)
7740
7864
  });
7741
7865
  }
7742
- async function convertLocalGroupsToWire(groups, sortFields, deps, indexName) {
7866
+ async function convertLocalGroupsToWire(groups, sortFields, sortModes, deps, indexName) {
7743
7867
  if (groups === void 0) {
7744
7868
  return null;
7745
7869
  }
7870
+ const unreadIds = sortFields === null ? [] : groups.flatMap((group) => group.hits.filter((hit) => hit.document === void 0).map((hit) => hit.id));
7871
+ const readDocuments = unreadIds.length > 0 ? await deps.engine.getMultiple(indexName, unreadIds) : /* @__PURE__ */ new Map();
7746
7872
  const wireGroups = [];
7747
7873
  for (const group of groups) {
7748
- const scored = [];
7749
- for (const hit of group.hits) {
7750
- let sortValues = null;
7751
- if (sortFields !== null) {
7752
- const document = hit.document ?? await deps.engine.get(indexName, hit.id);
7753
- sortValues = readSortValues(document, sortFields).map(toWireSortValue);
7754
- }
7755
- scored.push({ docId: hit.id, score: hit.score ?? null, sortValues });
7756
- }
7874
+ const scored = group.hits.map((hit) => {
7875
+ if (sortFields === null) return { docId: hit.id, score: hit.score ?? null, sortValues: null };
7876
+ const document = hit.document ?? readDocuments.get(hit.id);
7877
+ const sortValues = readSortValues(document, sortFields, sortModes).map(toWireSortValue);
7878
+ return { docId: hit.id, score: hit.score ?? null, sortValues };
7879
+ });
7757
7880
  wireGroups.push({ values: group.values, scored });
7758
7881
  }
7759
7882
  return wireGroups;
@@ -7843,11 +7966,13 @@ async function handleList(message, respond, deps) {
7843
7966
  document: payload.fields === null ? void 0 : { include: payload.fields }
7844
7967
  };
7845
7968
  const result = await deps.engine.listPartitions(payload.indexName, listParams, payload.partitionIds);
7846
- const sortFields = payload.sort === null ? null : normalizeSort(payload.sort).map((entry) => entry.field);
7969
+ const normalizedSort = payload.sort === null ? null : normalizeSort(payload.sort);
7970
+ const sortFields = normalizedSort === null ? null : normalizedSort.map((entry) => entry.field);
7971
+ const sortModes = normalizedSort === null ? [] : sortModesOf(normalizedSort);
7847
7972
  const entries = result.documents.map((listed) => ({
7848
7973
  docId: listed.id,
7849
7974
  document: listed.document,
7850
- sortValues: sortFields === null ? null : readSortValues(listed.document, sortFields).map(toWireSortValue2)
7975
+ sortValues: sortFields === null ? null : readSortValues(listed.document, sortFields, sortModes).map(toWireSortValue2)
7851
7976
  }));
7852
7977
  await respond(
7853
7978
  createListResultMessage(
@@ -9176,7 +9301,7 @@ async function dropLocalCopy(name, engine) {
9176
9301
  try {
9177
9302
  await engine.dropIndex(name);
9178
9303
  } catch (dropError) {
9179
- if (dropError instanceof NarsilError && dropError.code === ErrorCodes.INDEX_NOT_FOUND) {
9304
+ if (localIndexIsGone(dropError)) {
9180
9305
  return;
9181
9306
  }
9182
9307
  throw dropError;
@@ -9461,6 +9586,30 @@ function createDataNodeHandler(deps) {
9461
9586
  function randomSelector(candidates, _partitionId) {
9462
9587
  return candidates[Math.floor(Math.random() * candidates.length)];
9463
9588
  }
9589
+ function queryKeyedSelector(binding) {
9590
+ const queryHash = Number.parseInt(binding, 16) >>> 0;
9591
+ const nodeHashes = /* @__PURE__ */ new Map();
9592
+ function nodeHashOf(nodeId) {
9593
+ let hash = nodeHashes.get(nodeId);
9594
+ if (hash === void 0) {
9595
+ hash = fnv1a(nodeId);
9596
+ nodeHashes.set(nodeId, hash);
9597
+ }
9598
+ return hash;
9599
+ }
9600
+ return (candidates, partitionId) => {
9601
+ let chosen = candidates[0];
9602
+ let highestWeight = -1;
9603
+ for (const candidate of candidates) {
9604
+ const weight = fnv1aUint32s([queryHash, partitionId, nodeHashOf(candidate)]);
9605
+ if (weight > highestWeight) {
9606
+ highestWeight = weight;
9607
+ chosen = candidate;
9608
+ }
9609
+ }
9610
+ return chosen;
9611
+ };
9612
+ }
9464
9613
  function collectActiveCandidates(assignment) {
9465
9614
  if (assignment.state !== "ACTIVE") {
9466
9615
  return [];
@@ -9552,11 +9701,12 @@ async function fetchDistributedDocuments(config, nodeId, engine, indexName, resu
9552
9701
  const fields = fetchFieldsFor(projection);
9553
9702
  for (const [targetNodeId, documentIds] of nodeToDocumentIds) {
9554
9703
  if (targetNodeId === nodeId) {
9555
- for (const { docId } of documentIds) {
9556
- const document = await engine.get(indexName, docId);
9557
- if (document !== void 0) {
9558
- documents.set(docId, applyProjection(document, projection));
9559
- }
9704
+ const local = await engine.getMultiple(
9705
+ indexName,
9706
+ documentIds.map((ref) => ref.docId)
9707
+ );
9708
+ for (const [docId, document] of local) {
9709
+ documents.set(docId, applyProjection(document, projection));
9560
9710
  }
9561
9711
  continue;
9562
9712
  }
@@ -9619,18 +9769,39 @@ async function readDistributedDocuments(config, nodeId, engine, indexName, docId
9619
9769
  }
9620
9770
  continue;
9621
9771
  }
9622
- const fetchMessage = createFetchMessage({ indexName, documentIds, fields: null, highlight: null }, nodeId);
9623
- const response = await sendToNode2(config, targetNodeId, fetchMessage, {
9624
- message: `Node '${targetNodeId}' did not answer a document fetch for index '${indexName}'`,
9625
- details: { indexName }
9626
- });
9627
- const payload = validateFetchResultPayload(decode(response.payload));
9628
- for (const fetched of payload.documents) {
9629
- documents.set(fetched.docId, fetched.document);
9772
+ for (let start = 0; start < documentIds.length; start += MAX_FETCH_DOCUMENT_IDS) {
9773
+ const chunk = documentIds.slice(start, start + MAX_FETCH_DOCUMENT_IDS);
9774
+ const fetchMessage = createFetchMessage({ indexName, documentIds: chunk, fields: null, highlight: null }, nodeId);
9775
+ const response = await sendToNode2(config, targetNodeId, fetchMessage, {
9776
+ message: `Node '${targetNodeId}' did not answer a document fetch for index '${indexName}'`,
9777
+ details: { indexName }
9778
+ });
9779
+ const payload = validateFetchResultPayload(decode(response.payload));
9780
+ for (const fetched of payload.documents) {
9781
+ documents.set(fetched.docId, fetched.document);
9782
+ }
9630
9783
  }
9631
9784
  }
9632
9785
  return documents;
9633
9786
  }
9787
+ var QUERY_REJECTION_CODES = /* @__PURE__ */ new Set([
9788
+ ErrorCodes.SEARCH_INVALID_FIELD,
9789
+ ErrorCodes.SEARCH_INVALID_FILTER,
9790
+ ErrorCodes.SEARCH_INVALID_MODE,
9791
+ ErrorCodes.SEARCH_INVALID_CURSOR,
9792
+ ErrorCodes.SEARCH_RESULT_WINDOW_EXCEEDED,
9793
+ ErrorCodes.VECTOR_DIMENSION_MISMATCH,
9794
+ ErrorCodes.CONFIG_INVALID
9795
+ ]);
9796
+ var nodeRejections = /* @__PURE__ */ new WeakSet();
9797
+ function queryRejectionOf(response, decoded) {
9798
+ const body = decoded;
9799
+ if (!response.type.endsWith(".error") && body?.error !== true) return null;
9800
+ if (typeof body?.code !== "string" || !QUERY_REJECTION_CODES.has(body.code)) return null;
9801
+ const rejection = new NarsilError(body.code, typeof body.message === "string" ? body.message : body.code);
9802
+ nodeRejections.add(rejection);
9803
+ return rejection;
9804
+ }
9634
9805
  async function collectDistributedStats(indexName, params, nodeToPartitions, deps, config) {
9635
9806
  const terms = params.term !== null ? params.term.split(/\s+/).filter((t) => t.length > 0) : [];
9636
9807
  const statsPromises = [];
@@ -9664,6 +9835,7 @@ async function collectDistributedStats(indexName, params, nodeToPartitions, deps
9664
9835
  async function fanOutSearch(indexName, params, globalStats, facetShardSize, nodeToPartitions, deps, config) {
9665
9836
  const nodeEntries = Array.from(nodeToPartitions.entries());
9666
9837
  const promises = [];
9838
+ let rejection = null;
9667
9839
  for (const [nodeId, partitionIds] of nodeEntries) {
9668
9840
  const message = createSearchMessage(
9669
9841
  { indexName, partitionIds, params, globalStats, facetShardSize },
@@ -9683,16 +9855,16 @@ async function fanOutSearch(indexName, params, globalStats, facetShardSize, node
9683
9855
  status: result !== null ? "success" : "timeout",
9684
9856
  results: result
9685
9857
  }),
9686
- (error) => ({
9687
- nodeId,
9688
- partitionIds,
9689
- status: isTimeoutError(error) ? "timeout" : "failed",
9690
- results: null
9691
- })
9858
+ (error) => {
9859
+ if (error instanceof NarsilError && nodeRejections.has(error)) rejection ??= error;
9860
+ return { nodeId, partitionIds, status: isTimeoutError(error) ? "timeout" : "failed", results: null };
9861
+ }
9692
9862
  );
9693
9863
  promises.push(promise);
9694
9864
  }
9695
- return Promise.all(promises);
9865
+ const outcomes = await Promise.all(promises);
9866
+ if (rejection !== null) throw rejection;
9867
+ return outcomes;
9696
9868
  }
9697
9869
  var OUTCOME_SEVERITY = { success: 0, timeout: 1, failed: 2 };
9698
9870
  function buildCoverage(totalPartitions, unavailablePartitions, ...outcomeSets) {
@@ -9739,6 +9911,8 @@ async function sendWithTimeout(transport, targets, message, timeout, validate) {
9739
9911
  });
9740
9912
  const sendPromise = sendToFirstReachableTarget2(transport, targets, message).then((response) => {
9741
9913
  const decoded = decode(response.payload);
9914
+ const rejection = queryRejectionOf(response, decoded);
9915
+ if (rejection !== null) throw rejection;
9742
9916
  return validate(decoded);
9743
9917
  });
9744
9918
  try {
@@ -9889,9 +10063,10 @@ function popScoredHeap(heap, nodeFirst) {
9889
10063
  }
9890
10064
  return top;
9891
10065
  }
9892
- function mergeDistributedFacets(allFacets, allBounds, maxBuckets = DEFAULT_MAX_FACET_BUCKETS) {
10066
+ function mergeDistributedFacets(allFacets, allBounds, maxBuckets = DEFAULT_MAX_FACET_BUCKETS, ascending = /* @__PURE__ */ new Set()) {
9893
10067
  const merged = /* @__PURE__ */ new Map();
9894
10068
  const bounds = /* @__PURE__ */ new Map();
10069
+ const undercounts = {};
9895
10070
  for (const facetMap of allFacets) {
9896
10071
  for (const [field, buckets] of Object.entries(facetMap)) {
9897
10072
  let fieldMap = merged.get(field);
@@ -9917,18 +10092,21 @@ function mergeDistributedFacets(allFacets, allBounds, maxBuckets = DEFAULT_MAX_F
9917
10092
  for (const [value, count] of valueMap) {
9918
10093
  buckets.push({ value, count });
9919
10094
  }
10095
+ const lowestFirst = ascending.has(field);
9920
10096
  buckets.sort((a, b) => {
9921
- if (a.count !== b.count) return b.count - a.count;
10097
+ if (a.count !== b.count) return lowestFirst ? a.count - b.count : b.count - a.count;
9922
10098
  return compareCodePoints(a.value, b.value);
9923
10099
  });
9924
10100
  facets[field] = buckets.slice(0, maxBuckets);
9925
- let bound = bounds.get(field) ?? 0;
10101
+ const undercount = bounds.get(field) ?? 0;
10102
+ let largestLeftOut = 0;
9926
10103
  for (let index = maxBuckets; index < buckets.length; index++) {
9927
- if (buckets[index].count > bound) bound = buckets[index].count;
10104
+ largestLeftOut = Math.max(largestLeftOut, buckets[index].count);
9928
10105
  }
9929
- errorBounds[field] = bound;
10106
+ undercounts[field] = undercount;
10107
+ errorBounds[field] = undercount + largestLeftOut;
9930
10108
  }
9931
- return { facets, errorBounds };
10109
+ return { facets, errorBounds, undercounts };
9932
10110
  }
9933
10111
 
9934
10112
  // src/distribution/query/group-merge.ts
@@ -9977,12 +10155,6 @@ function mergeGroupsFor(params, allGroups, sortFields) {
9977
10155
  }
9978
10156
 
9979
10157
  // src/distribution/query/fusion.ts
9980
- function clampAlpha(alpha) {
9981
- if (!Number.isFinite(alpha)) return 0.5;
9982
- if (alpha < 0) return 0;
9983
- if (alpha > 1) return 1;
9984
- return alpha;
9985
- }
9986
10158
  function compareFusedEntries(a, b) {
9987
10159
  const aScore = a.score ?? 0;
9988
10160
  const bScore = b.score ?? 0;
@@ -9992,7 +10164,7 @@ function compareFusedEntries(a, b) {
9992
10164
  return compareCodePoints(a.docId, b.docId);
9993
10165
  }
9994
10166
  function distributedRRF(lists, options) {
9995
- const k = options.k > 0 ? options.k : 60;
10167
+ const k = options.k;
9996
10168
  const scores = /* @__PURE__ */ new Map();
9997
10169
  for (const list of lists) {
9998
10170
  for (let rank = 0; rank < list.length; rank++) {
@@ -10073,17 +10245,20 @@ function placePinnedEntries(merged, pinned, depth, allMatchesPresent) {
10073
10245
  }
10074
10246
  }
10075
10247
  const ordered = deduped.sort((a, b) => a.position - b.position);
10248
+ const placedFromOutside = [];
10076
10249
  for (const entry of ordered) {
10077
10250
  if (!allMatchesPresent && entry.position >= depth) continue;
10078
10251
  const organic = removed.get(entry.docId);
10252
+ if (organic === void 0) placedFromOutside.push(entry.docId);
10079
10253
  const placed = { docId: entry.docId, score: 0, sortValues: organic?.sortValues ?? null };
10080
10254
  result.splice(Math.max(0, Math.min(entry.position, result.length)), 0, placed);
10081
10255
  }
10082
- return result;
10256
+ return { entries: result, placedFromOutside };
10083
10257
  }
10084
10258
 
10085
10259
  // src/distribution/query/hybrid.ts
10086
10260
  async function executeHybridQuery(indexName, params, limit, offset, facetShardSize, facetSize, totalPartitions, routing, deps, config) {
10261
+ const fusion = resolveHybridFusion(params.hybrid);
10087
10262
  const depth = limit + offset;
10088
10263
  const textParams = { ...params, vector: null, hybrid: null, mode: null, limit: depth, offset: 0 };
10089
10264
  const vectorParams = {
@@ -10092,6 +10267,8 @@ async function executeHybridQuery(indexName, params, limit, offset, facetShardSi
10092
10267
  hybrid: null,
10093
10268
  mode: null,
10094
10269
  group: null,
10270
+ facets: null,
10271
+ facetSize: null,
10095
10272
  limit: depth,
10096
10273
  offset: 0
10097
10274
  };
@@ -10144,39 +10321,34 @@ async function executeHybridQuery(indexName, params, limit, offset, facetShardSi
10144
10321
  }
10145
10322
  const mergedText = mergeAndTruncateScoredEntries(textScored, depth);
10146
10323
  const mergedVector = mergeAndTruncateScoredEntries(vectorScored, depth);
10147
- const hybrid = params.hybrid;
10148
10324
  let fused;
10149
- if ((hybrid?.strategy ?? "rrf") === "rrf") {
10150
- fused = distributedRRF([mergedText, mergedVector], { k: hybrid?.k ?? 60 });
10325
+ if (fusion.strategy === "rrf") {
10326
+ fused = distributedRRF([mergedText, mergedVector], { k: fusion.k });
10151
10327
  } else {
10152
- fused = distributedLinearCombination(mergedText, mergedVector, { alpha: clampAlpha(hybrid?.alpha ?? 0.5) });
10328
+ fused = distributedLinearCombination(mergedText, mergedVector, { alpha: fusion.alpha });
10153
10329
  }
10154
10330
  const allMatchesPresent = coverage.queriedPartitions === coverage.totalPartitions && totalHits <= mergedText.length && mergedVector.length < depth;
10155
- const fusedWithPinned = params.pinned !== null ? placePinnedEntries(fused, params.pinned, depth, allMatchesPresent) : fused;
10156
- const truncated = fusedWithPinned.slice(offset, depth);
10157
- const mergedFacets = allFacets.length > 0 ? mergeDistributedFacets(allFacets, allFacetBounds, facetSize) : null;
10331
+ const placement = params.pinned !== null ? placePinnedEntries(fused, params.pinned, depth, allMatchesPresent) : { entries: fused, placedFromOutside: [] };
10332
+ const truncated = placement.entries.slice(offset, depth);
10333
+ const mergedFacets = allFacets.length > 0 ? mergeDistributedFacets(allFacets, allFacetBounds, facetSize, ascendingFacetFields(params.facets)) : null;
10158
10334
  return {
10159
10335
  scored: truncated,
10160
10336
  totalHits,
10161
10337
  facets: mergedFacets?.facets ?? null,
10162
10338
  facetErrorBounds: mergedFacets?.errorBounds ?? null,
10339
+ facetUndercounts: mergedFacets?.undercounts ?? null,
10163
10340
  groups: mergeGroupsFor(params, allGroups, null),
10164
10341
  cursor: null,
10165
- coverage
10342
+ coverage,
10343
+ pinnedFromOutside: placement.placedFromOutside,
10344
+ mergeHeldEveryMatch: allMatchesPresent
10166
10345
  };
10167
10346
  }
10168
10347
 
10169
- // src/distribution/query/types.ts
10170
- var DEFAULT_QUERY_CONFIG = {
10171
- allowPartialResults: true,
10172
- partitionTimeout: 5e3,
10173
- defaultFacetSize: 10
10174
- };
10175
-
10176
10348
  // src/distribution/query/routing.ts
10177
10349
  function wireSortSignature(sort) {
10178
10350
  if (sort === null || sort.length === 0) return null;
10179
- return JSON.stringify(sort.map((field) => [field.field, field.direction]));
10351
+ return JSON.stringify(sort.map(sortSignatureEntry));
10180
10352
  }
10181
10353
  function resolveAndClampFacetSize(paramsFacetSize, configDefault) {
10182
10354
  const raw = paramsFacetSize !== null && Number.isFinite(paramsFacetSize) && Number.isInteger(paramsFacetSize) ? paramsFacetSize : configDefault;
@@ -10215,7 +10387,7 @@ async function distributedQuery(indexName, params, deps, config, selector) {
10215
10387
  coverage: { totalPartitions: 0, queriedPartitions: 0, timedOutPartitions: 0, failedPartitions: 0 }
10216
10388
  };
10217
10389
  }
10218
- const routing = selectReplicasForQuery(allocationTable, randomSelector);
10390
+ const routing = selectReplicasForQuery(allocationTable, queryKeyedSelector(binding));
10219
10391
  const totalPartitions = allocationTable.assignments.size;
10220
10392
  if (routing.unavailablePartitions.length > 0 && !resolvedConfig.allowPartialResults) {
10221
10393
  throw new NarsilError(
@@ -10323,24 +10495,28 @@ async function executeSingleFanOut(indexName, params, binding, limit, offset, fa
10323
10495
  sortFields.map((field) => field.direction)
10324
10496
  ) : mergeAndTruncateScoredEntries(allScored, depth);
10325
10497
  const allMatchesPresent = coverage.queriedPartitions === coverage.totalPartitions && totalHits <= merged.length;
10326
- const placed = params.pinned !== null && params.searchAfter === null ? placePinnedEntries(merged, params.pinned, depth, allMatchesPresent) : merged;
10327
- const mergedScored = placed.slice(offset, depth);
10328
- const mergedFacets = allFacets.length > 0 ? mergeDistributedFacets(allFacets, allFacetBounds, facetSize) : null;
10498
+ const placement = params.pinned !== null && params.searchAfter === null ? placePinnedEntries(merged, params.pinned, depth, allMatchesPresent) : { entries: merged, placedFromOutside: [] };
10499
+ const mergedScored = placement.entries.slice(offset, depth);
10500
+ const mergedFacets = allFacets.length > 0 ? mergeDistributedFacets(allFacets, allFacetBounds, facetSize, ascendingFacetFields(params.facets)) : null;
10329
10501
  const mergedGroups = mergeGroupsFor(params, allGroups, sortFields);
10330
10502
  let cursor = null;
10331
10503
  const lastEntry = lastOrganicEntry(mergedScored, params.searchAfter === null ? params.pinned : null);
10504
+ const reachedDepth = params.searchAfter === null ? 0 : decodePageCursor(params.searchAfter).depth ?? 0;
10505
+ const nextDepth = reachedDepth + offset + mergedScored.length;
10332
10506
  if (lastEntry !== void 0) {
10333
10507
  cursor = sortFields !== null ? encodePageCursor({
10334
10508
  anchor: lastEntry.docId,
10335
10509
  score: null,
10336
10510
  sortKey: (lastEntry.sortValues ?? []).map(toComparableSortValue),
10337
10511
  sortSignature: wireSortSignature(sortFields),
10512
+ depth: nextDepth,
10338
10513
  binding
10339
10514
  }) : encodePageCursor({
10340
10515
  anchor: lastEntry.docId,
10341
10516
  score: lastEntry.score,
10342
10517
  sortKey: null,
10343
10518
  sortSignature: null,
10519
+ depth: nextDepth,
10344
10520
  binding
10345
10521
  });
10346
10522
  }
@@ -10349,9 +10525,12 @@ async function executeSingleFanOut(indexName, params, binding, limit, offset, fa
10349
10525
  totalHits,
10350
10526
  facets: mergedFacets?.facets ?? null,
10351
10527
  facetErrorBounds: mergedFacets?.errorBounds ?? null,
10528
+ facetUndercounts: mergedFacets?.undercounts ?? null,
10352
10529
  groups: mergedGroups,
10353
10530
  cursor,
10354
- coverage
10531
+ coverage,
10532
+ pinnedFromOutside: placement.placedFromOutside,
10533
+ mergeHeldEveryMatch: allMatchesPresent
10355
10534
  };
10356
10535
  }
10357
10536
  function failOutcomesMissingSortValues(outcomes) {
@@ -10374,28 +10553,44 @@ async function assembleDistributedGroups(deps, indexName, params, distributed, a
10374
10553
  }
10375
10554
  const reduce = group.reduce;
10376
10555
  const needsDocuments = reduce !== void 0 || projection.kind !== "none";
10377
- const groupDocIds = [...new Set(wireGroups.flatMap((entry) => entry.scored.map((scored) => scored.docId)))];
10378
- const documents = needsDocuments && groupDocIds.length > 0 ? await readDistributedDocuments(deps.config, deps.nodeId, deps.engine, indexName, groupDocIds, allocation) : /* @__PURE__ */ new Map();
10379
- return wireGroups.map((entry) => {
10380
- const hits = entry.scored.map((scored) => ({
10381
- id: scored.docId,
10382
- score: scored.score ?? void 0,
10383
- document: projectedGroupDocument(documents.get(scored.docId), projection)
10384
- }));
10385
- const result = { values: entry.values, hits };
10386
- if (reduce !== void 0) {
10387
- const folded = foldGroupReducer(
10388
- reduce,
10389
- entry.scored.map((scored) => ({ document: documents.get(scored.docId), score: scored.score ?? 0 }))
10390
- );
10391
- if (folded.reducerError !== void 0) {
10392
- result.reducerError = folded.reducerError;
10393
- } else {
10394
- result.reduced = folded.reduced;
10395
- }
10396
- }
10397
- return result;
10398
- });
10556
+ const keptPerGroup = hitsKeptPerGroup(group.maxPerGroup);
10557
+ const docIdsRead = (entry) => (reduce === void 0 ? entry.scored.slice(0, keptPerGroup) : entry.scored).map((scored) => scored.docId);
10558
+ const results = [];
10559
+ let batch = [];
10560
+ let batchDocIds = /* @__PURE__ */ new Set();
10561
+ async function assembleBatch() {
10562
+ const documents = needsDocuments && batchDocIds.size > 0 ? await readDistributedDocuments(deps.config, deps.nodeId, deps.engine, indexName, [...batchDocIds], allocation) : /* @__PURE__ */ new Map();
10563
+ for (const entry of batch) results.push(assembleGroup(entry, documents, keptPerGroup, projection, reduce));
10564
+ batch = [];
10565
+ batchDocIds = /* @__PURE__ */ new Set();
10566
+ }
10567
+ for (const entry of wireGroups) {
10568
+ const docIds = docIdsRead(entry);
10569
+ if (batch.length > 0 && batchDocIds.size + docIds.length > MAX_FETCH_DOCUMENT_IDS) await assembleBatch();
10570
+ batch.push(entry);
10571
+ for (const docId of docIds) batchDocIds.add(docId);
10572
+ }
10573
+ if (batch.length > 0) await assembleBatch();
10574
+ return results;
10575
+ }
10576
+ function assembleGroup(entry, documents, keptPerGroup, projection, reduce) {
10577
+ const hits = entry.scored.slice(0, keptPerGroup).map((scored) => ({
10578
+ id: scored.docId,
10579
+ score: scored.score ?? void 0,
10580
+ document: projectedGroupDocument(documents.get(scored.docId), projection)
10581
+ }));
10582
+ const result = { values: entry.values, hits };
10583
+ if (reduce === void 0) return result;
10584
+ const folded = foldGroupReducer(
10585
+ reduce,
10586
+ entry.scored.map((scored) => ({ document: documents.get(scored.docId), score: scored.score ?? 0 }))
10587
+ );
10588
+ if (folded.reducerError !== void 0) {
10589
+ result.reducerError = folded.reducerError;
10590
+ } else {
10591
+ result.reduced = folded.reduced;
10592
+ }
10593
+ return result;
10399
10594
  }
10400
10595
  function projectedGroupDocument(document, projection) {
10401
10596
  if (document === void 0 || projection.kind === "none") {
@@ -10421,17 +10616,31 @@ function splitPinnedByReachability(pinnedIds, allocation) {
10421
10616
  }
10422
10617
  return { verifiable, unverifiable };
10423
10618
  }
10424
- async function dropUnstoredPinnedEntries(deps, indexName, params, distributed, allocation, projection, documents) {
10425
- if (params.pinned === void 0 || params.searchAfter !== void 0) {
10426
- return distributed.scored;
10427
- }
10619
+ async function readPinPresence(deps, indexName, params, allocation) {
10620
+ if (params.pinned === void 0 || params.searchAfter !== void 0) return null;
10428
10621
  const pinnedIds = new Set(params.pinned.map((entry) => entry.docId));
10429
10622
  const { verifiable, unverifiable } = splitPinnedByReachability(pinnedIds, allocation);
10430
- const stored = projection.kind === "none" ? await readDistributedDocuments(deps.config, deps.nodeId, deps.engine, indexName, verifiable, allocation) : documents;
10623
+ const documents = verifiable.length === 0 ? /* @__PURE__ */ new Map() : await readDistributedDocuments(deps.config, deps.nodeId, deps.engine, indexName, verifiable, allocation);
10624
+ return { pinnedIds, stored: new Set(documents.keys()), unverifiable };
10625
+ }
10626
+ function dropUnstoredPinnedEntries(distributed, presence) {
10627
+ if (presence === null) return distributed.scored;
10431
10628
  return distributed.scored.filter(
10432
- (entry) => !pinnedIds.has(entry.docId) || unverifiable.has(entry.docId) || stored.has(entry.docId)
10629
+ (entry) => !presence.pinnedIds.has(entry.docId) || presence.unverifiable.has(entry.docId) || presence.stored.has(entry.docId)
10433
10630
  );
10434
10631
  }
10632
+ function countStoredPinsFromOutside(distributed, presence) {
10633
+ const outside = distributed.pinnedFromOutside ?? [];
10634
+ if (outside.length === 0) return { stored: 0, everyPinVerified: true };
10635
+ if (presence === null) return { stored: 0, everyPinVerified: false };
10636
+ let count = 0;
10637
+ let everyPinVerified = true;
10638
+ for (const docId of outside) {
10639
+ if (presence.unverifiable.has(docId)) everyPinVerified = false;
10640
+ else if (presence.stored.has(docId)) count++;
10641
+ }
10642
+ return { stored: count, everyPinVerified };
10643
+ }
10435
10644
  function strictScatterGroups(allocation, indexName) {
10436
10645
  const routing = selectReplicasForQuery(allocation, randomSelector);
10437
10646
  if (routing.unavailablePartitions.length > 0) {
@@ -10581,7 +10790,7 @@ function toWireSortValue3(value) {
10581
10790
  if (typeof value === "boolean") return value;
10582
10791
  return null;
10583
10792
  }
10584
- async function gatherLocalPage(deps, indexName, payload, partitionIds, sortFieldNames) {
10793
+ async function gatherLocalPage(deps, indexName, payload, partitionIds, sortFieldNames, sortModes) {
10585
10794
  const listParams = {
10586
10795
  cursor: payload.cursor ?? void 0,
10587
10796
  limit: payload.limit,
@@ -10593,7 +10802,7 @@ async function gatherLocalPage(deps, indexName, payload, partitionIds, sortField
10593
10802
  const entries = result.documents.map((listed) => ({
10594
10803
  docId: listed.id,
10595
10804
  document: listed.document,
10596
- sortValues: sortFieldNames === null ? null : readSortValues(listed.document, sortFieldNames).map(toWireSortValue3)
10805
+ sortValues: sortFieldNames === null ? null : readSortValues(listed.document, sortFieldNames, sortModes).map(toWireSortValue3)
10597
10806
  }));
10598
10807
  return { entries, total: result.total, hasMore: result.cursor !== null };
10599
10808
  }
@@ -10632,7 +10841,8 @@ async function listCluster(deps, indexName, params) {
10632
10841
  indexName,
10633
10842
  { ...payload, partitionIds: group.partitionIds },
10634
10843
  group.partitionIds,
10635
- sortFieldNames
10844
+ sortFieldNames,
10845
+ sortModesOf(normalizedSort)
10636
10846
  );
10637
10847
  }
10638
10848
  const message = createListMessage({ ...payload, partitionIds: group.partitionIds }, deps.nodeId);
@@ -10757,7 +10967,11 @@ async function preflightCluster(deps, indexName, params) {
10757
10967
  count += result2.count;
10758
10968
  analysisStale = analysisStale || result2.analysisStale;
10759
10969
  }
10760
- const result = { count, elapsed: performance.now() - startTime };
10970
+ const result = {
10971
+ count,
10972
+ countExact: countIsExactFor(params),
10973
+ elapsed: performance.now() - startTime
10974
+ };
10761
10975
  if (analysisStale) {
10762
10976
  result.analysisStale = true;
10763
10977
  }
@@ -10808,16 +11022,22 @@ async function queryCluster(deps, indexName, params) {
10808
11022
  allocation,
10809
11023
  projection
10810
11024
  );
10811
- const scored = await dropUnstoredPinnedEntries(
10812
- deps,
10813
- indexName,
10814
- params,
10815
- distributed,
10816
- allocation,
10817
- projection,
10818
- documents
11025
+ const pinPresence = await readPinPresence(deps, indexName, params, allocation);
11026
+ const scored = dropUnstoredPinnedEntries(distributed, pinPresence);
11027
+ const pins = countStoredPinsFromOutside(distributed, pinPresence);
11028
+ let countExact = distributedCountIsExact(params, distributed.coverage);
11029
+ let totalHits = distributed.totalHits;
11030
+ if (distributed.mergeHeldEveryMatch === true && pins.everyPinVerified) {
11031
+ totalHits += pins.stored;
11032
+ } else if (pins.stored > 0 || !pins.everyPinVerified) {
11033
+ countExact = false;
11034
+ }
11035
+ const result = distributedResultToLocal(
11036
+ { ...distributed, scored, totalHits },
11037
+ countExact,
11038
+ documents,
11039
+ params.facets
10819
11040
  );
10820
- const result = distributedResultToLocal({ ...distributed, scored }, documents);
10821
11041
  const groups = await assembleDistributedGroups(deps, indexName, params, distributed, allocation, projection);
10822
11042
  if (groups !== void 0) {
10823
11043
  result.groups = groups;