@delali/narsil 0.2.4-next.42 → 0.2.4-next.44

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-GB2IIJJG.mjs → chunk-P4ELXVJQ.mjs} +2 -4
  4. package/dist/{chunk-MMSWEEZT.mjs → chunk-WASNWIEK.mjs} +1452 -373
  5. package/dist/{chunk-GRWFY2HI.mjs → chunk-XNCJFU2E.mjs} +239 -61
  6. package/dist/{chunk-WU2UZEVD.mjs → chunk-XWICBGYT.mjs} +1 -1
  7. package/dist/{client-BobVnoIK.d.ts → client-C5BkUqB8.d.ts} +26 -3
  8. package/dist/client.d.ts +8 -8
  9. package/dist/client.mjs +12 -6
  10. package/dist/{config-fLC4lbmg.d.ts → config-B-ZI-ft5.d.ts} +2 -2
  11. package/dist/{config-Byyl1QqH.d.ts → config-BZRExglL.d.ts} +20 -3
  12. package/dist/{direct-executor-MSJEVPZU.mjs → direct-executor-IMXNRS7O.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 +24 -10
  18. package/dist/distribution.mjs +1721 -1129
  19. package/dist/embeddings/openai.mjs +1 -1
  20. package/dist/{engine-BE45D5ZG.d.ts → engine-Cjr1uBLa.d.ts} +47 -24
  21. package/dist/index.browser.mjs +1668 -416
  22. package/dist/index.d.ts +9 -9
  23. package/dist/index.mjs +4 -4
  24. package/dist/{options-EXYFcem-.d.ts → options-BC68VPoE.d.ts} +10 -3
  25. package/dist/persistence/durability/checkpoint-worker.d.ts +3 -1
  26. package/dist/persistence/durability/checkpoint-worker.mjs +4 -4
  27. package/dist/react.d.ts +6 -6
  28. package/dist/react.mjs +2 -4
  29. package/dist/{schema-ZRbk8BgH.d.ts → schema-B7juWg8Q.d.ts} +17 -6
  30. package/dist/{search-DHuXwJYk.d.ts → search-DDHjfXhJ.d.ts} +35 -11
  31. package/dist/server.d.ts +10 -10
  32. package/dist/server.mjs +30 -12
  33. package/dist/{tasks-CnMLpfk-.d.ts → tasks-Bn4R1NPN.d.ts} +1 -1
  34. package/dist/{types-B5Tsaalz.d.ts → types-DsSSjY4d.d.ts} +1 -1
  35. package/dist/workers/entry.mjs +2 -2
  36. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
- import { createEngineCore, createNarsilFromCore, runEngineQueryStats, runEngineListDocuments, runEngineSuggest, runEnginePreflight, runEngineQuery, createEngineIndex, applyIndexEntry, applyDeleteEntry, DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, now, validateEntryBatchPayload, createAckMessage, validateEntryPayload, validateForwardBatchPayload, createForwardBatchResultMessage, clampLimit, createForwardBatchMessage, validateForwardBatchResultPayload, createForwardMessage, validateReplicationEntry, MAX_SUGGEST_SCATTER_LIMIT, chunkByBudget, WIRE_BATCH_BUDGET, SUGGEST_OVERSAMPLE_FACTOR, SUGGEST_OVERSAMPLE_PADDING, createEntryMessage, computeEntryChecksum, createEntryBatchMessage, createInsyncConfirmMessage, validateAckPayload, createInsyncRemoveMessage, validateInsyncConfirmPayload, createInsyncAddMessage } from './chunk-MMSWEEZT.mjs';
1
+ import { createEngineCore, createNarsilFromCore, runEngineQueryStats, runEngineListDocuments, runEngineSuggest, runEnginePreflight, runEngineQuery, createEngineIndex, applyIndexEntry, applyDeleteEntry, DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, now, listBindingOf, validateEntryBatchPayload, createAckMessage, validateEntryPayload, validateForwardBatchPayload, createForwardBatchResultMessage, queryBindingOf, foldGroupReducer, clampLimit, createForwardBatchMessage, validateForwardBatchResultPayload, createForwardMessage, validateReplicationEntry, MAX_SUGGEST_SCATTER_LIMIT, chunkByBudget, WIRE_BATCH_BUDGET, computeEntryChecksum, dedupePinnedEntries, SUGGEST_OVERSAMPLE_FACTOR, SUGGEST_OVERSAMPLE_PADDING, createEntryMessage, createEntryBatchMessage, createInsyncConfirmMessage, validateAckPayload, createInsyncRemoveMessage, validateInsyncConfirmPayload, createInsyncAddMessage } from './chunk-WASNWIEK.mjs';
2
2
  import { ReplicationMessageTypes, TransportErrorCodes, ClusterMessageTypes, QueryMessageTypes, TransportError, MAX_MESSAGE_SIZE_BYTES } from './chunk-JQUL2T6I.mjs';
3
- import { validateIndexName, indexConfigKey, putIndexMetadata, getIndexMetadata, MAX_PARTITION_COUNT, MAX_INDEX_NAME_LENGTH } from './chunk-WU2UZEVD.mjs';
4
- import { deserializePayloadV2, createPartitionIndex, resolveProjection, clampRowCount, sortSignatureOf, requireMatchingCursor, decodePageCursor, normalizeSort, encodePageCursor, applyProjection, readSortValues, getLanguage, DEFAULT_PAGE_SIZE, requireWithinResultWindow, fnv1a, FILTER_EXPRESSION_KEYS, MAX_CURSOR_LENGTH, mergePartitionStats, FIELD_FILTER_OPERATORS, MAX_NESTING_DEPTH } from './chunk-GRWFY2HI.mjs';
3
+ import { validateIndexName, indexConfigKey, putIndexMetadata, getIndexMetadata, MAX_PARTITION_COUNT, MAX_INDEX_NAME_LENGTH } from './chunk-XWICBGYT.mjs';
4
+ import { deserializePayloadV2, createPartitionIndex, resolveProjection, clampRowCount, sortSignatureOf, requireMatchingCursor, decodePageCursor, normalizeSort, encodePageCursor, applyProjection, readSortValues, getLanguage, DEFAULT_PAGE_SIZE, requireWithinResultWindow, fnv1a, FILTER_EXPRESSION_KEYS, MAX_CURSOR_LENGTH, mergePartitionStats, RESULT_WINDOW, FIELD_FILTER_OPERATORS, MAX_NESTING_DEPTH } from './chunk-XNCJFU2E.mjs';
5
5
  import './chunk-TGP5M5R7.mjs';
6
6
  import './chunk-OBXS7OZM.mjs';
7
7
  import './chunk-WDL632CN.mjs';
@@ -9,7 +9,7 @@ import { crc32 } from './chunk-KMMJMDUA.mjs';
9
9
  import './chunk-U2BKYEC7.mjs';
10
10
  import { compareCodePoints, toComparableSortValue, compareComparableKeys, compareSortValues } from './chunk-BFIH7OV5.mjs';
11
11
  import { generateId } from './chunk-WTONLDU3.mjs';
12
- import { ErrorCodes, NarsilError, describeError } from './chunk-GB2IIJJG.mjs';
12
+ import { ErrorCodes, NarsilError, describeError } from './chunk-P4ELXVJQ.mjs';
13
13
  import { encode, decode } from '@msgpack/msgpack';
14
14
 
15
15
  // src/distribution/cluster/controller/types.ts
@@ -4618,7 +4618,7 @@ function batchBytes(entries) {
4618
4618
  function replicaHasFallenOutOfRetention(log, cursor) {
4619
4619
  const oldest = log.oldestSeqNo;
4620
4620
  if (oldest === void 0) {
4621
- return false;
4621
+ return cursor.appliedSeqNo < log.localLogEnd;
4622
4622
  }
4623
4623
  return cursor.appliedSeqNo + 1 < oldest;
4624
4624
  }
@@ -4844,473 +4844,766 @@ async function writeHeldPartitions(core, indexName, next, indexesBehindOnDisk) {
4844
4844
  indexesBehindOnDisk.delete(indexName);
4845
4845
  }
4846
4846
 
4847
- // src/distribution/cluster-node/local-engine.ts
4848
- async function createClusterLocalEngine(config) {
4849
- const core = createEngineCore(config);
4850
- if (core.durability !== null) {
4851
- await core.durability.manager.recover();
4852
- }
4853
- if (core.invalidation !== null) {
4854
- await core.invalidation.start();
4855
- }
4856
- await core.analysisRebuild.reviewStaleIndexes();
4857
- const engine = createNarsilFromCore(core, config);
4858
- const heldPartitions = createHeldPartitionRecord(core);
4859
- return Object.assign(engine, {
4860
- createIndexWithUuid: (name, indexConfig, indexUuid) => createEngineIndex(core, config, name, indexConfig, indexUuid),
4861
- indexUuidOf: (indexName) => core.indexRegistry.get(indexName)?.indexUuid,
4862
- stampIndexUuid: (indexName, indexUuid) => stampIndexUuid(core, indexName, indexUuid),
4863
- highestPersistedSeqNoOf: (indexName, partitionId) => core.durability?.manager.highestPersistedSeqNo(indexName, partitionId) ?? 0,
4864
- heldPartitionsOf: (indexName) => heldPartitions.held(indexName),
4865
- recordHeldPartition: (indexName, partitionId) => heldPartitions.record(indexName, partitionId),
4866
- forgetHeldPartition: (indexName, partitionId) => heldPartitions.forget(indexName, partitionId),
4867
- applyReplicationEntry: (entry) => applyReplicationEntry(core, entry),
4868
- serializeReplicationPartition: (indexName, partitionId) => serializeReplicationPartition(core, indexName, partitionId),
4869
- restoreReplicationPartition: (indexName, partitionId, bytes, schema, partitionCount) => restoreReplicationPartition(core, engine, indexName, partitionId, bytes, schema, partitionCount),
4870
- queryPartitions: (indexName, params, partitionIds, globalStats) => runEngineQuery(core, indexName, params, { partitionIds, globalStats }),
4871
- preflightPartitions: (indexName, params, partitionIds) => runEnginePreflight(core, indexName, params, { partitionIds }),
4872
- suggestPartitions: (indexName, params, partitionIds) => runEngineSuggest(core, indexName, params, partitionIds),
4873
- listPartitions: (indexName, params, partitionIds) => runEngineListDocuments(core, indexName, params, partitionIds),
4874
- collectQueryStats: (indexName, terms, partitionIds) => runEngineQueryStats(core, indexName, terms, partitionIds)
4875
- });
4876
- }
4877
- async function stampIndexUuid(core, indexName, indexUuid) {
4878
- const entry = core.indexRegistry.get(indexName);
4879
- if (entry === void 0 || entry.indexUuid === indexUuid) {
4880
- return;
4881
- }
4882
- core.indexRegistry.set(indexName, { ...entry, indexUuid });
4883
- if (core.durability !== null) {
4884
- await core.durability.manager.persistMetadata(indexName);
4885
- }
4886
- }
4887
- async function serializeReplicationPartition(core, indexName, partitionId) {
4888
- core.guardShutdown();
4889
- return core.requireManager(indexName).serializePartitionToBytes(partitionId);
4890
- }
4891
- async function restoreReplicationPartition(core, engine, indexName, partitionId, bytes, schema, partitionCount) {
4892
- core.guardShutdown();
4893
- validatePartitionRestoreTarget(indexName, partitionId, partitionCount);
4894
- let partition;
4895
- try {
4896
- partition = deserializePayloadV2(bytes);
4897
- } catch (err) {
4898
- const cause = err instanceof Error ? err.message : String(err);
4899
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, `partition snapshot decode failed: ${cause}`, {
4900
- indexName,
4901
- partitionId,
4902
- cause
4903
- });
4904
- }
4905
- validatePartitionSnapshotPayload(indexName, partitionId, partitionCount, schema, partition);
4906
- const trackPositions = core.indexRegistry.get(indexName)?.config.trackPositions ?? true;
4907
- createPartitionIndex(partitionId, trackPositions).deserialize(partition, schema);
4908
- const restoreIndex = await ensureReplicationIndex(core, engine, indexName, schema, partition.language, partitionCount);
4909
- if (restoreIndex.created) {
4910
- await core.orchestrator.replicateToWorkers({
4911
- type: "createIndex",
4912
- indexName,
4913
- config: restoreIndex.config,
4914
- requestId: `replicate-partition-index-${indexName}`
4915
- });
4916
- }
4917
- const manager = core.requireManager(indexName);
4918
- manager.deserializePartition(partitionId, partition);
4919
- await core.orchestrator.replicateToWorkers({
4920
- type: "deserialize",
4921
- indexName,
4922
- partitionId,
4923
- data: partition,
4924
- requestId: `replicate-partition-restore-${indexName}-${partitionId}`
4925
- });
4926
- }
4927
- function validatePartitionRestoreTarget(indexName, partitionId, partitionCount) {
4928
- if (!Number.isInteger(partitionId) || partitionId < 0) {
4929
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partitionId must be a non-negative integer", {
4930
- indexName,
4931
- partitionId
4932
- });
4933
- }
4934
- if (!Number.isInteger(partitionCount) || partitionCount <= 0 || partitionCount > MAX_PARTITION_COUNT) {
4935
- throw new NarsilError(
4936
- ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED,
4937
- `partitionCount must be an integer between 1 and ${MAX_PARTITION_COUNT}`,
4938
- { indexName, partitionCount }
4939
- );
4940
- }
4941
- if (partitionId >= partitionCount) {
4942
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partitionId is outside partitionCount", {
4943
- indexName,
4944
- partitionId,
4945
- partitionCount
4946
- });
4947
- }
4847
+ // src/distribution/replication/types.ts
4848
+ var DEFAULT_LOG_RETENTION_BYTES = 268435456;
4849
+
4850
+ // src/distribution/replication/log.ts
4851
+ var ENTRY_FIXED_OVERHEAD_BYTES2 = 40;
4852
+ function estimateEntrySize(entry) {
4853
+ return ENTRY_FIXED_OVERHEAD_BYTES2 + entry.indexName.length + entry.documentId.length + (entry.document?.byteLength ?? 0);
4948
4854
  }
4949
- function validatePartitionSnapshotPayload(indexName, partitionId, partitionCount, schema, partition) {
4950
- if (partition.indexName !== indexName) {
4951
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot indexName mismatch", {
4952
- expectedIndexName: indexName,
4953
- receivedIndexName: partition.indexName
4954
- });
4955
- }
4956
- if (partition.partitionId !== partitionId) {
4957
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot partitionId mismatch", {
4958
- indexName,
4959
- expectedPartitionId: partitionId,
4960
- receivedPartitionId: partition.partitionId
4961
- });
4962
- }
4963
- if (partition.totalPartitions !== partitionCount) {
4964
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot partition count mismatch", {
4965
- indexName,
4966
- partitionId,
4967
- expectedPartitionCount: partitionCount,
4968
- receivedPartitionCount: partition.totalPartitions
4969
- });
4970
- }
4971
- const expectedSchema = flattenSchema(schema);
4972
- for (const [field, expectedType] of Object.entries(expectedSchema)) {
4973
- if (partition.schema[field] !== expectedType) {
4974
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot schema mismatch", {
4975
- indexName,
4976
- partitionId,
4977
- field,
4978
- expected: expectedType,
4979
- received: partition.schema[field] ?? "(absent)"
4980
- });
4981
- }
4855
+ var computeChecksum = computeEntryChecksum;
4856
+ function createReplicationLog(partitionId, config) {
4857
+ const retentionBytes = config?.logRetentionBytes ?? DEFAULT_LOG_RETENTION_BYTES;
4858
+ let nextSeqNo = config?.startSeqNo ?? 1;
4859
+ let localLogEnd = nextSeqNo - 1;
4860
+ let localLogEndPrimaryTerm = config?.lastPrimaryTerm ?? 0;
4861
+ let commitPoint = nextSeqNo - 1;
4862
+ let entries = [];
4863
+ let headIndex = 0;
4864
+ let totalSizeBytes = 0;
4865
+ function liveCount() {
4866
+ return entries.length - headIndex;
4982
4867
  }
4983
- for (const field of Object.keys(partition.schema)) {
4984
- if (!(field in expectedSchema)) {
4985
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot schema contains extra field", {
4986
- indexName,
4987
- partitionId,
4988
- field
4989
- });
4868
+ function compact() {
4869
+ if (headIndex > 0) {
4870
+ entries = entries.slice(headIndex);
4871
+ headIndex = 0;
4990
4872
  }
4991
4873
  }
4992
- }
4993
- async function ensureReplicationIndex(core, engine, indexName, schema, languageName, partitionCount) {
4994
- const language = getLanguage(languageName);
4995
- const existing = core.indexRegistry.get(indexName);
4996
- let created = false;
4997
- if (existing === void 0) {
4998
- const indexConfig = {
4999
- schema,
5000
- language: language.name,
5001
- partitions: { maxPartitions: partitionCount }
5002
- };
5003
- try {
5004
- await engine.createIndex(indexName, indexConfig);
5005
- created = true;
5006
- } catch (err) {
5007
- if (!(err instanceof NarsilError) || err.code !== ErrorCodes.INDEX_ALREADY_EXISTS) {
5008
- throw err;
4874
+ function findIndexBySeqNo(seqNo) {
4875
+ let low = headIndex;
4876
+ let high = entries.length - 1;
4877
+ while (low <= high) {
4878
+ const mid = low + high >>> 1;
4879
+ const midSeqNo = entries[mid].seqNo;
4880
+ if (midSeqNo < seqNo) {
4881
+ low = mid + 1;
4882
+ } else if (midSeqNo > seqNo) {
4883
+ high = mid - 1;
4884
+ } else {
4885
+ return mid;
5009
4886
  }
5010
4887
  }
4888
+ return low;
5011
4889
  }
5012
- const entry = core.requireIndex(indexName);
5013
- if (entry.language.name !== language.name) {
5014
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot language mismatch", {
5015
- indexName,
5016
- expectedLanguage: entry.language.name,
5017
- receivedLanguage: language.name
5018
- });
5019
- }
5020
- validateExistingSchema(indexName, schema, entry.config.schema);
5021
- const manager = core.requireManager(indexName);
5022
- if (manager.partitionCount > partitionCount) {
5023
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index has more partitions than allocation", {
5024
- indexName,
5025
- localPartitionCount: manager.partitionCount,
5026
- allocationPartitionCount: partitionCount
5027
- });
5028
- }
5029
- if (entry.config.partitions === void 0) {
5030
- entry.config.partitions = { maxPartitions: partitionCount };
5031
- } else if ((entry.config.partitions.maxPartitions ?? manager.partitionCount) < partitionCount) {
5032
- entry.config.partitions.maxPartitions = partitionCount;
4890
+ function evictOldEntries() {
4891
+ while (liveCount() > 0 && totalSizeBytes > retentionBytes) {
4892
+ totalSizeBytes -= estimateEntrySize(entries[headIndex]);
4893
+ entries[headIndex] = null;
4894
+ headIndex += 1;
4895
+ }
4896
+ if (totalSizeBytes < 0) {
4897
+ totalSizeBytes = 0;
4898
+ }
4899
+ if (headIndex >= 256 || headIndex > liveCount()) {
4900
+ compact();
4901
+ }
5033
4902
  }
5034
- while (manager.partitionCount < partitionCount) {
5035
- manager.addPartition();
4903
+ function assertEntryFitsRetention(entry) {
4904
+ const entrySize = ENTRY_FIXED_OVERHEAD_BYTES2 + entry.indexName.length + entry.documentId.length + (entry.document?.byteLength ?? 0);
4905
+ if (entrySize > retentionBytes) {
4906
+ throw new NarsilError(
4907
+ ErrorCodes.REPLICATION_LOG_FULL,
4908
+ `Replication log entry of ${entrySize} bytes exceeds the log retention limit of ${retentionBytes} bytes and cannot be buffered`,
4909
+ { partitionId, entrySize, retentionBytes, seqNo: entry.seqNo }
4910
+ );
4911
+ }
5036
4912
  }
5037
- return { config: entry.config, created };
5038
- }
5039
- function validateExistingSchema(indexName, expected, actual) {
5040
- const expectedSchema = flattenSchema(expected);
5041
- const actualSchema = flattenSchema(actual);
5042
- for (const [field, expectedType] of Object.entries(expectedSchema)) {
5043
- if (actualSchema[field] !== expectedType) {
5044
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index schema mismatch", {
5045
- indexName,
5046
- field,
5047
- expected: expectedType,
5048
- received: actualSchema[field] ?? "(absent)"
5049
- });
4913
+ function storeEntry(entry) {
4914
+ entries.push(entry);
4915
+ if (entry.seqNo >= localLogEnd) {
4916
+ localLogEnd = entry.seqNo;
4917
+ localLogEndPrimaryTerm = entry.primaryTerm;
5050
4918
  }
4919
+ totalSizeBytes += estimateEntrySize(entry);
4920
+ evictOldEntries();
4921
+ return { ...entry };
5051
4922
  }
5052
- for (const field of Object.keys(actualSchema)) {
5053
- if (!(field in expectedSchema)) {
5054
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index schema contains extra field", {
5055
- indexName,
5056
- field
5057
- });
4923
+ function readEntry(seqNo) {
4924
+ if (liveCount() === 0) return void 0;
4925
+ const idx = findIndexBySeqNo(seqNo);
4926
+ if (idx < entries.length && entries[idx].seqNo === seqNo) {
4927
+ return { ...entries[idx] };
5058
4928
  }
4929
+ return void 0;
5059
4930
  }
5060
- }
5061
- function flattenSchema(schema) {
5062
- const flat = {};
5063
- flattenSchemaInto(schema, "", flat);
5064
- return flat;
5065
- }
5066
- function flattenSchemaInto(schema, prefix, flat) {
5067
- for (const [field, value] of Object.entries(schema)) {
5068
- const path = prefix.length === 0 ? field : `${prefix}.${field}`;
5069
- if (isNestedSchema(value)) {
5070
- flattenSchemaInto(value, path, flat);
5071
- continue;
5072
- }
5073
- flat[path] = value;
4931
+ function verifyEntryChecksum(entry) {
4932
+ const expected = computeChecksum({
4933
+ seqNo: entry.seqNo,
4934
+ primaryTerm: entry.primaryTerm,
4935
+ operation: entry.operation,
4936
+ partitionId: entry.partitionId,
4937
+ indexName: entry.indexName,
4938
+ documentId: entry.documentId,
4939
+ document: entry.document
4940
+ });
4941
+ return expected === entry.checksum;
5074
4942
  }
5075
- }
5076
- function isNestedSchema(value) {
5077
- return typeof value === "object" && value !== null;
5078
- }
5079
- async function applyReplicationEntry(core, entry) {
5080
- core.guardShutdown();
5081
- const indexEntry = core.requireIndex(entry.indexName);
5082
- const manager = core.requireManager(entry.indexName);
5083
- const vecIndexes = manager.getVectorIndexes();
5084
- if (entry.operation === "INDEX") {
5085
- applyIndexEntry(entry, manager, indexEntry.vectorFieldPaths, vecIndexes);
5086
- const appliedDocument = manager.get(entry.documentId);
5087
- if (appliedDocument !== void 0) {
5088
- await core.orchestrator.replicateToWorkers({
5089
- type: "insert",
5090
- indexName: entry.indexName,
5091
- docId: entry.documentId,
5092
- document: appliedDocument,
5093
- requestId: `replicate-entry-insert-${entry.indexName}-${entry.partitionId}-${entry.seqNo}`
5094
- });
4943
+ return {
4944
+ append(partial) {
4945
+ assertEntryFitsRetention(partial);
4946
+ const seqNo = nextSeqNo;
4947
+ nextSeqNo += 1;
4948
+ const entryWithoutChecksum = {
4949
+ seqNo,
4950
+ primaryTerm: partial.primaryTerm,
4951
+ operation: partial.operation,
4952
+ partitionId,
4953
+ indexName: partial.indexName,
4954
+ documentId: partial.documentId,
4955
+ document: partial.document
4956
+ };
4957
+ const checksum = computeChecksum(entryWithoutChecksum);
4958
+ const entry = { ...entryWithoutChecksum, checksum };
4959
+ return storeEntry(entry);
4960
+ },
4961
+ appendCommitted(entry) {
4962
+ if (!Number.isSafeInteger(entry.seqNo) || entry.seqNo < 1) {
4963
+ throw new NarsilError(
4964
+ ErrorCodes.REPLICATION_ENTRY_INVALID,
4965
+ `Invalid replication sequence number ${entry.seqNo}`,
4966
+ {
4967
+ seqNo: entry.seqNo,
4968
+ partitionId
4969
+ }
4970
+ );
4971
+ }
4972
+ if (entry.partitionId !== partitionId) {
4973
+ throw new NarsilError(
4974
+ ErrorCodes.REPLICATION_ENTRY_INVALID,
4975
+ `Replication entry partition ${entry.partitionId} does not match log partition ${partitionId}`,
4976
+ { entryPartitionId: entry.partitionId, logPartitionId: partitionId }
4977
+ );
4978
+ }
4979
+ const existing = readEntry(entry.seqNo);
4980
+ if (existing !== void 0) {
4981
+ if (existing.checksum !== entry.checksum) {
4982
+ throw new NarsilError(
4983
+ ErrorCodes.REPLICATION_ENTRY_INVALID,
4984
+ `Conflicting replication entry at sequence number ${entry.seqNo}`,
4985
+ { seqNo: entry.seqNo, partitionId }
4986
+ );
4987
+ }
4988
+ return existing;
4989
+ }
4990
+ if (entry.seqNo !== nextSeqNo) {
4991
+ throw new NarsilError(
4992
+ ErrorCodes.REPLICATION_ENTRY_INVALID,
4993
+ `Out-of-order replication entry ${entry.seqNo}; expected ${nextSeqNo}`,
4994
+ { seqNo: entry.seqNo, expectedSeqNo: nextSeqNo, partitionId }
4995
+ );
4996
+ }
4997
+ if (!verifyEntryChecksum(entry)) {
4998
+ throw new NarsilError(
4999
+ ErrorCodes.REPLICATION_ENTRY_CORRUPT,
5000
+ `Invalid checksum for replication entry ${entry.seqNo}`,
5001
+ { seqNo: entry.seqNo, partitionId }
5002
+ );
5003
+ }
5004
+ assertEntryFitsRetention(entry);
5005
+ nextSeqNo = entry.seqNo + 1;
5006
+ return storeEntry({ ...entry });
5007
+ },
5008
+ getEntriesFrom(fromSeqNo) {
5009
+ if (liveCount() === 0) return [];
5010
+ const idx = findIndexBySeqNo(fromSeqNo);
5011
+ if (idx >= entries.length) return [];
5012
+ return entries.slice(idx).map((e) => ({ ...e }));
5013
+ },
5014
+ getEntry(seqNo) {
5015
+ return readEntry(seqNo);
5016
+ },
5017
+ verifyChecksum(entry) {
5018
+ return verifyEntryChecksum(entry);
5019
+ },
5020
+ get localLogEnd() {
5021
+ return localLogEnd;
5022
+ },
5023
+ get localLogEndPrimaryTerm() {
5024
+ return localLogEndPrimaryTerm;
5025
+ },
5026
+ get commitPoint() {
5027
+ return commitPoint;
5028
+ },
5029
+ advanceCommitPoint(seqNo) {
5030
+ const bounded = Math.min(seqNo, localLogEnd);
5031
+ if (bounded > commitPoint) {
5032
+ commitPoint = bounded;
5033
+ }
5034
+ },
5035
+ get oldestSeqNo() {
5036
+ if (liveCount() === 0) return void 0;
5037
+ return entries[headIndex].seqNo;
5038
+ },
5039
+ get newestSeqNo() {
5040
+ if (liveCount() === 0) return void 0;
5041
+ return entries[entries.length - 1].seqNo;
5042
+ },
5043
+ get entryCount() {
5044
+ return liveCount();
5045
+ },
5046
+ get sizeBytes() {
5047
+ return totalSizeBytes;
5048
+ },
5049
+ clear() {
5050
+ entries = [];
5051
+ headIndex = 0;
5052
+ totalSizeBytes = 0;
5053
+ localLogEnd = nextSeqNo - 1;
5054
+ commitPoint = nextSeqNo - 1;
5095
5055
  }
5096
- return;
5097
- }
5098
- applyDeleteEntry(entry, manager, vecIndexes);
5099
- await core.orchestrator.replicateToWorkers({
5100
- type: "remove",
5101
- indexName: entry.indexName,
5102
- docId: entry.documentId,
5103
- requestId: `replicate-entry-remove-${entry.indexName}-${entry.partitionId}-${entry.seqNo}`
5104
- });
5056
+ };
5105
5057
  }
5106
5058
 
5107
- // src/distribution/cluster-node/local-index-reconcile.ts
5108
- async function reconcileLocalIndexes(deps) {
5109
- const dispositions = /* @__PURE__ */ new Map();
5110
- for (const index of deps.engine.listIndexes()) {
5111
- const disposition = await reconcileOne(deps, index.name);
5112
- dispositions.set(index.name, disposition);
5113
- }
5114
- return dispositions;
5059
+ // src/distribution/cluster-node/replication-logs.ts
5060
+ function replicationLogKey(indexName, partitionId) {
5061
+ return `${indexName}:${partitionId}`;
5115
5062
  }
5116
- async function adoptClusterIdentity(engine, coordinator, indexName) {
5117
- if (engine.indexUuidOf(indexName) !== null) {
5118
- return;
5119
- }
5120
- const metadata = await getIndexMetadata(coordinator, indexName);
5121
- if (metadata === null) {
5122
- return;
5063
+ function getReplicationLog(replicationLogs, indexName, partitionId, replicationConfig) {
5064
+ const key = replicationLogKey(indexName, partitionId);
5065
+ let log = replicationLogs.get(key);
5066
+ if (log === void 0) {
5067
+ log = createReplicationLog(partitionId, { logRetentionBytes: replicationConfig?.logRetentionBytes });
5068
+ replicationLogs.set(key, log);
5123
5069
  }
5124
- await engine.stampIndexUuid(indexName, metadata.indexUuid);
5070
+ return log;
5125
5071
  }
5126
- function orphanedIndexError(nodeId, indexName) {
5127
- return new NarsilError(
5128
- ErrorCodes.INDEX_ORPHANED,
5129
- `Node '${nodeId}' holds a local copy of index '${indexName}' the cluster does not claim, so it answers nothing for that name. Drop it to reclaim the space`,
5130
- { indexName, nodeId }
5072
+ function seedReplicationLog(replicationLogs, indexName, partitionId, startSeqNo, lastPrimaryTerm = 0, replicationConfig) {
5073
+ replicationLogs.set(
5074
+ replicationLogKey(indexName, partitionId),
5075
+ createReplicationLog(partitionId, {
5076
+ startSeqNo,
5077
+ lastPrimaryTerm,
5078
+ logRetentionBytes: replicationConfig?.logRetentionBytes
5079
+ })
5131
5080
  );
5132
5081
  }
5133
- async function reconcileOne(deps, indexName) {
5134
- const localUuid = deps.engine.indexUuidOf(indexName);
5135
- let clusterUuid;
5136
- try {
5137
- clusterUuid = (await getIndexMetadata(deps.coordinator, indexName))?.indexUuid ?? null;
5138
- } catch (error) {
5139
- report(deps, indexName, `its metadata could not be read: ${describeError(error)}`);
5140
- return "orphaned";
5141
- }
5142
- if (clusterUuid === null) {
5143
- report(deps, indexName, "the coordinator holds no metadata for that name");
5144
- return "orphaned";
5145
- }
5146
- if (localUuid === clusterUuid) {
5147
- return "adopted";
5082
+ function deleteIndexReplicationLogs(replicationLogs, indexName) {
5083
+ const prefix = `${indexName}:`;
5084
+ for (const key of replicationLogs.keys()) {
5085
+ if (key.startsWith(prefix)) replicationLogs.delete(key);
5148
5086
  }
5149
- if (localUuid === null || localUuid === void 0) {
5150
- await deps.engine.stampIndexUuid(indexName, clusterUuid);
5151
- return "adopted";
5087
+ }
5088
+
5089
+ // src/distribution/cluster-node/local-replication.ts
5090
+ async function seedReopenedPrimaryLogs(input) {
5091
+ const allocation = await input.coordinator.getAllocation(input.indexName);
5092
+ if (allocation === null) return;
5093
+ for (const partitionId of input.engine.heldPartitionsOf(input.indexName) ?? []) {
5094
+ const assignment = allocation.assignments.get(partitionId);
5095
+ if (assignment?.primary !== input.nodeId) continue;
5096
+ const floor = Math.max(input.engine.highestPersistedSeqNoOf(input.indexName, partitionId), assignment.commitPoint);
5097
+ seedReplicationLog(
5098
+ input.replicationLogs,
5099
+ input.indexName,
5100
+ partitionId,
5101
+ floor + 1,
5102
+ assignment.primaryTerm,
5103
+ input.replicationConfig
5104
+ );
5152
5105
  }
5153
- await deps.engine.dropIndex(indexName);
5154
- return "superseded";
5155
5106
  }
5156
- function report(deps, indexName, reason) {
5157
- if (deps.onError === void 0) {
5158
- return;
5107
+ async function applyLocalReplicationEntry(core, entry) {
5108
+ core.guardShutdown();
5109
+ const release = await core.indexState.acquire(entry.indexName);
5110
+ try {
5111
+ const indexEntry = core.requireIndex(entry.indexName);
5112
+ const manager = core.requireManager(entry.indexName);
5113
+ const vecIndexes = manager.getVectorIndexes();
5114
+ if (entry.operation === "INDEX") {
5115
+ applyIndexEntry(entry, manager, indexEntry.vectorFieldPaths, vecIndexes);
5116
+ const appliedDocument = manager.get(entry.documentId);
5117
+ if (appliedDocument !== void 0) {
5118
+ await core.orchestrator.replicateToWorkers({
5119
+ type: "insert",
5120
+ indexName: entry.indexName,
5121
+ docId: entry.documentId,
5122
+ document: appliedDocument,
5123
+ requestId: `replicate-entry-insert-${entry.indexName}-${entry.partitionId}-${entry.seqNo}`
5124
+ });
5125
+ }
5126
+ return;
5127
+ }
5128
+ applyDeleteEntry(entry, manager, vecIndexes);
5129
+ await core.orchestrator.replicateToWorkers({
5130
+ type: "remove",
5131
+ indexName: entry.indexName,
5132
+ docId: entry.documentId,
5133
+ requestId: `replicate-entry-remove-${entry.indexName}-${entry.partitionId}-${entry.seqNo}`
5134
+ });
5135
+ } finally {
5136
+ release();
5159
5137
  }
5160
- deps.onError(
5161
- new NarsilError(
5162
- ErrorCodes.INDEX_ORPHANED,
5163
- `Node '${deps.nodeId}' holds a local copy of index '${indexName}' the cluster does not claim, because ${reason}. The node keeps the data and serves none of it until an operator drops it.`,
5164
- { indexName, nodeId: deps.nodeId }
5165
- )
5166
- );
5167
5138
  }
5168
5139
 
5169
- // src/distribution/cluster-node/snapshot-auth.ts
5170
- var BOOTSTRAPPABLE_STATES = /* @__PURE__ */ new Set(["ACTIVE", "INITIALISING", "MIGRATING"]);
5171
- async function authorizeSnapshotRequest(coordinator, indexName, sourceId) {
5172
- let allocation;
5173
- try {
5174
- allocation = await coordinator.getAllocation(indexName);
5175
- } catch (err) {
5176
- const errMessage = err instanceof Error ? err.message : String(err);
5177
- return {
5178
- outcome: "denied",
5179
- code: ErrorCodes.SNAPSHOT_SYNC_ALLOCATION_UNAVAILABLE,
5180
- reason: `allocation lookup failed: ${errMessage}`
5181
- };
5140
+ // src/distribution/cluster-node/local-engine.ts
5141
+ async function createClusterLocalEngine(config, hooks) {
5142
+ const core = createEngineCore(config, hooks);
5143
+ if (core.durability !== null) {
5144
+ await core.durability.manager.recover(config?.lifecycle !== void 0);
5182
5145
  }
5183
- if (allocation === null) {
5184
- return {
5185
- outcome: "denied",
5186
- code: ErrorCodes.SNAPSHOT_SYNC_UNAUTHORIZED,
5187
- reason: `index '${indexName}' has no allocation`
5188
- };
5146
+ if (core.invalidation !== null) {
5147
+ await core.invalidation.start();
5189
5148
  }
5190
- let sawAssignment = false;
5191
- for (const assignment of allocation.assignments.values()) {
5192
- const isAssigned = assignment.primary === sourceId || assignment.replicas.includes(sourceId);
5193
- if (!isAssigned) {
5194
- continue;
5195
- }
5196
- sawAssignment = true;
5197
- if (!BOOTSTRAPPABLE_STATES.has(assignment.state)) {
5198
- continue;
5149
+ if (config?.lifecycle === void 0) await core.analysisRebuild.reviewStaleIndexes();
5150
+ const engine = createNarsilFromCore(core, config);
5151
+ const heldPartitions = createHeldPartitionRecord(core);
5152
+ return Object.assign(engine, {
5153
+ createIndexWithUuid: (name, indexConfig, indexUuid) => createEngineIndex(core, config, name, indexConfig, indexUuid),
5154
+ acquireIndexForReplication: (indexName) => core.indexState.acquire(indexName, false),
5155
+ indexUuidOf: (indexName) => core.indexRegistry.get(indexName)?.indexUuid,
5156
+ stampIndexUuid: (indexName, indexUuid) => stampIndexUuid(core, indexName, indexUuid),
5157
+ highestPersistedSeqNoOf: (indexName, partitionId) => core.durability?.manager.highestPersistedSeqNo(indexName, partitionId) ?? 0,
5158
+ heldPartitionsOf: (indexName) => heldPartitions.held(indexName),
5159
+ recordHeldPartition: (indexName, partitionId) => heldPartitions.record(indexName, partitionId),
5160
+ forgetHeldPartition: (indexName, partitionId) => heldPartitions.forget(indexName, partitionId),
5161
+ applyReplicationEntry: (entry) => applyLocalReplicationEntry(core, entry),
5162
+ serializeReplicationPartition: (indexName, partitionId) => serializeReplicationPartition(core, indexName, partitionId),
5163
+ restoreReplicationPartition: (indexName, partitionId, bytes, schema, partitionCount) => restoreReplicationPartition(core, engine, indexName, partitionId, bytes, schema, partitionCount),
5164
+ queryPartitions: (indexName, params, partitionIds, globalStats) => runEngineQuery(core, indexName, params, { partitionIds, globalStats }),
5165
+ preflightPartitions: (indexName, params, partitionIds) => runEnginePreflight(core, indexName, params, { partitionIds }),
5166
+ suggestPartitions: (indexName, params, partitionIds) => runEngineSuggest(core, indexName, params, partitionIds),
5167
+ listPartitions: (indexName, params, partitionIds) => runEngineListDocuments(core, indexName, params, partitionIds),
5168
+ collectQueryStats: (indexName, terms, partitionIds) => runEngineQueryStats(core, indexName, terms, partitionIds),
5169
+ partitionStatsForRead: async (indexName) => {
5170
+ const release = await core.indexState.acquire(indexName);
5171
+ try {
5172
+ return core.requireManager(indexName).getPartitionStats();
5173
+ } finally {
5174
+ release();
5175
+ }
5199
5176
  }
5200
- return { outcome: "authorized" };
5177
+ });
5178
+ }
5179
+ async function stampIndexUuid(core, indexName, indexUuid) {
5180
+ const entry = core.indexRegistry.get(indexName);
5181
+ if (entry === void 0 || entry.indexUuid === indexUuid) {
5182
+ return;
5201
5183
  }
5202
- if (sawAssignment) {
5203
- return {
5204
- outcome: "denied",
5205
- code: ErrorCodes.SNAPSHOT_SYNC_NOT_ASSIGNED,
5206
- reason: `node '${sourceId}' is listed for index '${indexName}' but not in a bootstrappable state`
5207
- };
5184
+ core.indexRegistry.set(indexName, { ...entry, indexUuid });
5185
+ if (core.durability !== null) {
5186
+ await core.durability.manager.persistMetadata(indexName);
5208
5187
  }
5209
- return {
5210
- outcome: "denied",
5211
- code: ErrorCodes.SNAPSHOT_SYNC_UNAUTHORIZED,
5212
- reason: `node '${sourceId}' is not an assigned replica for index '${indexName}'`
5213
- };
5214
5188
  }
5215
-
5216
- // src/distribution/cluster-node/snapshot-cache.ts
5217
- var DEFAULT_MAX_CONCURRENT_SNAPSHOTS = 2;
5218
- var DEFAULT_MAX_PER_SOURCE_SNAPSHOTS = 1;
5219
- var DEFAULT_MAX_STREAMS_PER_INDEX = 4;
5220
- function createSnapshotCacheState(maxConcurrent = DEFAULT_MAX_CONCURRENT_SNAPSHOTS, maxPerSource = DEFAULT_MAX_PER_SOURCE_SNAPSHOTS, maxStreamsPerIndex = DEFAULT_MAX_STREAMS_PER_INDEX) {
5221
- if (!Number.isInteger(maxConcurrent) || maxConcurrent <= 0) {
5222
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, "maxConcurrent must be a positive integer", {
5223
- maxConcurrent
5224
- });
5189
+ async function serializeReplicationPartition(core, indexName, partitionId) {
5190
+ core.guardShutdown();
5191
+ const release = await core.indexState.acquire(indexName, false);
5192
+ try {
5193
+ return core.requireManager(indexName).serializePartitionToBytes(partitionId);
5194
+ } finally {
5195
+ release();
5225
5196
  }
5226
- if (!Number.isInteger(maxPerSource) || maxPerSource <= 0) {
5227
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, "maxPerSource must be a positive integer", {
5228
- maxPerSource
5197
+ }
5198
+ async function restoreReplicationPartition(core, engine, indexName, partitionId, bytes, schema, partitionCount) {
5199
+ core.guardShutdown();
5200
+ validatePartitionRestoreTarget(indexName, partitionId, partitionCount);
5201
+ let partition;
5202
+ try {
5203
+ partition = deserializePayloadV2(bytes);
5204
+ } catch (err) {
5205
+ const cause = err instanceof Error ? err.message : String(err);
5206
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, `partition snapshot decode failed: ${cause}`, {
5207
+ indexName,
5208
+ partitionId,
5209
+ cause
5229
5210
  });
5230
5211
  }
5231
- if (!Number.isInteger(maxStreamsPerIndex) || maxStreamsPerIndex <= 0) {
5232
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, "maxStreamsPerIndex must be a positive integer", {
5233
- maxStreamsPerIndex
5212
+ validatePartitionSnapshotPayload(indexName, partitionId, partitionCount, schema, partition);
5213
+ const trackPositions = core.indexRegistry.get(indexName)?.config.trackPositions ?? true;
5214
+ createPartitionIndex(partitionId, trackPositions).deserialize(partition, schema);
5215
+ let release = core.indexRegistry.has(indexName) ? await core.indexState.acquire(indexName, false) : null;
5216
+ try {
5217
+ const restoreIndex = await ensureReplicationIndex(
5218
+ core,
5219
+ engine,
5220
+ indexName,
5221
+ schema,
5222
+ partition.language,
5223
+ partitionCount
5224
+ );
5225
+ if (release === null) release = await core.indexState.acquire(indexName, false);
5226
+ if (restoreIndex.created) {
5227
+ await core.orchestrator.replicateToWorkers({
5228
+ type: "createIndex",
5229
+ indexName,
5230
+ config: restoreIndex.config,
5231
+ requestId: `replicate-partition-index-${indexName}`
5232
+ });
5233
+ }
5234
+ const manager = core.requireManager(indexName);
5235
+ manager.deserializePartition(partitionId, partition);
5236
+ await core.orchestrator.replicateToWorkers({
5237
+ type: "deserialize",
5238
+ indexName,
5239
+ partitionId,
5240
+ data: partition,
5241
+ requestId: `replicate-partition-restore-${indexName}-${partitionId}`
5234
5242
  });
5243
+ } finally {
5244
+ release?.();
5235
5245
  }
5236
- return {
5237
- cache: /* @__PURE__ */ new Map(),
5238
- active: 0,
5239
- perSource: /* @__PURE__ */ new Map(),
5240
- streamActive: /* @__PURE__ */ new Map(),
5241
- maxConcurrent,
5242
- maxPerSource,
5243
- maxStreamsPerIndex
5244
- };
5245
- }
5246
- function sourceSlotKey(sourceId, indexName) {
5247
- return `${sourceId}\0${indexName}`;
5248
5246
  }
5249
- function acquireSourceSlot(state, sourceId, indexName) {
5250
- const key = sourceSlotKey(sourceId, indexName);
5251
- const current = state.perSource.get(key) ?? 0;
5252
- if (current >= state.maxPerSource) {
5247
+ function validatePartitionRestoreTarget(indexName, partitionId, partitionCount) {
5248
+ if (!Number.isInteger(partitionId) || partitionId < 0) {
5249
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partitionId must be a non-negative integer", {
5250
+ indexName,
5251
+ partitionId
5252
+ });
5253
+ }
5254
+ if (!Number.isInteger(partitionCount) || partitionCount <= 0 || partitionCount > MAX_PARTITION_COUNT) {
5253
5255
  throw new NarsilError(
5254
- ErrorCodes.SNAPSHOT_SYNC_CAPACITY_EXHAUSTED,
5255
- `snapshot producer per-source cap reached for '${sourceId}' index '${indexName}'`,
5256
- { sourceId, indexName, cap: state.maxPerSource }
5256
+ ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED,
5257
+ `partitionCount must be an integer between 1 and ${MAX_PARTITION_COUNT}`,
5258
+ { indexName, partitionCount }
5257
5259
  );
5258
5260
  }
5259
- state.perSource.set(key, current + 1);
5260
- return { key, released: false };
5261
+ if (partitionId >= partitionCount) {
5262
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partitionId is outside partitionCount", {
5263
+ indexName,
5264
+ partitionId,
5265
+ partitionCount
5266
+ });
5267
+ }
5261
5268
  }
5262
- function releaseSourceSlot(state, handle) {
5263
- if (handle.released) {
5264
- return;
5269
+ function validatePartitionSnapshotPayload(indexName, partitionId, partitionCount, schema, partition) {
5270
+ if (partition.indexName !== indexName) {
5271
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot indexName mismatch", {
5272
+ expectedIndexName: indexName,
5273
+ receivedIndexName: partition.indexName
5274
+ });
5265
5275
  }
5266
- handle.released = true;
5267
- const current = state.perSource.get(handle.key);
5268
- if (current === void 0) {
5269
- return;
5276
+ if (partition.partitionId !== partitionId) {
5277
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot partitionId mismatch", {
5278
+ indexName,
5279
+ expectedPartitionId: partitionId,
5280
+ receivedPartitionId: partition.partitionId
5281
+ });
5270
5282
  }
5271
- if (current <= 1) {
5272
- state.perSource.delete(handle.key);
5273
- return;
5283
+ if (partition.totalPartitions !== partitionCount) {
5284
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot partition count mismatch", {
5285
+ indexName,
5286
+ partitionId,
5287
+ expectedPartitionCount: partitionCount,
5288
+ receivedPartitionCount: partition.totalPartitions
5289
+ });
5274
5290
  }
5275
- state.perSource.set(handle.key, current - 1);
5276
- }
5277
- function acquireStreamSlot(state, indexName) {
5278
- const current = state.streamActive.get(indexName) ?? 0;
5279
- if (current >= state.maxStreamsPerIndex) {
5280
- throw new NarsilError(
5281
- ErrorCodes.SNAPSHOT_SYNC_CAPACITY_EXHAUSTED,
5282
- `snapshot stream fan-out cap reached for index '${indexName}'`,
5283
- { indexName, cap: state.maxStreamsPerIndex }
5284
- );
5291
+ const expectedSchema = flattenSchema(schema);
5292
+ for (const [field, expectedType] of Object.entries(expectedSchema)) {
5293
+ if (partition.schema[field] !== expectedType) {
5294
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot schema mismatch", {
5295
+ indexName,
5296
+ partitionId,
5297
+ field,
5298
+ expected: expectedType,
5299
+ received: partition.schema[field] ?? "(absent)"
5300
+ });
5301
+ }
5302
+ }
5303
+ for (const field of Object.keys(partition.schema)) {
5304
+ if (!(field in expectedSchema)) {
5305
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot schema contains extra field", {
5306
+ indexName,
5307
+ partitionId,
5308
+ field
5309
+ });
5310
+ }
5285
5311
  }
5286
- state.streamActive.set(indexName, current + 1);
5287
- return { indexName, released: false };
5288
5312
  }
5289
- function releaseStreamSlot(state, handle) {
5290
- if (handle.released) {
5291
- return;
5313
+ async function ensureReplicationIndex(core, engine, indexName, schema, languageName, partitionCount) {
5314
+ const language = getLanguage(languageName);
5315
+ const existing = core.indexRegistry.get(indexName);
5316
+ let created = false;
5317
+ if (existing === void 0) {
5318
+ const indexConfig = {
5319
+ schema,
5320
+ language: language.name,
5321
+ partitions: { maxPartitions: partitionCount }
5322
+ };
5323
+ try {
5324
+ await engine.createIndex(indexName, indexConfig);
5325
+ created = true;
5326
+ } catch (err) {
5327
+ if (!(err instanceof NarsilError) || err.code !== ErrorCodes.INDEX_ALREADY_EXISTS) {
5328
+ throw err;
5329
+ }
5330
+ }
5292
5331
  }
5293
- handle.released = true;
5294
- const current = state.streamActive.get(handle.indexName);
5295
- if (current === void 0) {
5296
- return;
5332
+ const entry = core.requireIndex(indexName);
5333
+ if (entry.language.name !== language.name) {
5334
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot language mismatch", {
5335
+ indexName,
5336
+ expectedLanguage: entry.language.name,
5337
+ receivedLanguage: language.name
5338
+ });
5297
5339
  }
5298
- if (current <= 1) {
5299
- state.streamActive.delete(handle.indexName);
5300
- return;
5340
+ validateExistingSchema(indexName, schema, entry.config.schema);
5341
+ const manager = core.requireManager(indexName);
5342
+ if (manager.partitionCount > partitionCount) {
5343
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index has more partitions than allocation", {
5344
+ indexName,
5345
+ localPartitionCount: manager.partitionCount,
5346
+ allocationPartitionCount: partitionCount
5347
+ });
5301
5348
  }
5302
- state.streamActive.set(handle.indexName, current - 1);
5303
- }
5304
- async function acquireSnapshotBuild(state, indexName, builder) {
5305
- const existing = state.cache.get(indexName);
5306
- if (existing !== void 0) {
5307
- return existing.promise;
5349
+ if (entry.config.partitions === void 0) {
5350
+ entry.config.partitions = { maxPartitions: partitionCount };
5351
+ } else if ((entry.config.partitions.maxPartitions ?? manager.partitionCount) < partitionCount) {
5352
+ entry.config.partitions.maxPartitions = partitionCount;
5308
5353
  }
5309
- if (state.active >= state.maxConcurrent) {
5310
- throw new NarsilError(
5311
- ErrorCodes.SNAPSHOT_SYNC_CAPACITY_EXHAUSTED,
5312
- "snapshot producer capacity exhausted; retry later",
5313
- { active: state.active, max: state.maxConcurrent }
5354
+ while (manager.partitionCount < partitionCount) {
5355
+ manager.addPartition();
5356
+ }
5357
+ return { config: entry.config, created };
5358
+ }
5359
+ function validateExistingSchema(indexName, expected, actual) {
5360
+ const expectedSchema = flattenSchema(expected);
5361
+ const actualSchema = flattenSchema(actual);
5362
+ for (const [field, expectedType] of Object.entries(expectedSchema)) {
5363
+ if (actualSchema[field] !== expectedType) {
5364
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index schema mismatch", {
5365
+ indexName,
5366
+ field,
5367
+ expected: expectedType,
5368
+ received: actualSchema[field] ?? "(absent)"
5369
+ });
5370
+ }
5371
+ }
5372
+ for (const field of Object.keys(actualSchema)) {
5373
+ if (!(field in expectedSchema)) {
5374
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index schema contains extra field", {
5375
+ indexName,
5376
+ field
5377
+ });
5378
+ }
5379
+ }
5380
+ }
5381
+ function flattenSchema(schema) {
5382
+ const flat = {};
5383
+ flattenSchemaInto(schema, "", flat);
5384
+ return flat;
5385
+ }
5386
+ function flattenSchemaInto(schema, prefix, flat) {
5387
+ for (const [field, value] of Object.entries(schema)) {
5388
+ const path = prefix.length === 0 ? field : `${prefix}.${field}`;
5389
+ if (isNestedSchema(value)) {
5390
+ flattenSchemaInto(value, path, flat);
5391
+ continue;
5392
+ }
5393
+ flat[path] = value;
5394
+ }
5395
+ }
5396
+ function isNestedSchema(value) {
5397
+ return typeof value === "object" && value !== null;
5398
+ }
5399
+
5400
+ // src/distribution/cluster-node/local-index-reconcile.ts
5401
+ async function reconcileLocalIndexes(deps) {
5402
+ const dispositions = /* @__PURE__ */ new Map();
5403
+ for (const index of deps.engine.listIndexes()) {
5404
+ const disposition = await reconcileOne(deps, index.name);
5405
+ dispositions.set(index.name, disposition);
5406
+ }
5407
+ return dispositions;
5408
+ }
5409
+ async function adoptClusterIdentity(engine, coordinator, indexName) {
5410
+ if (engine.indexUuidOf(indexName) !== null) {
5411
+ return;
5412
+ }
5413
+ const metadata = await getIndexMetadata(coordinator, indexName);
5414
+ if (metadata === null) {
5415
+ return;
5416
+ }
5417
+ await engine.stampIndexUuid(indexName, metadata.indexUuid);
5418
+ }
5419
+ function orphanedIndexError(nodeId, indexName) {
5420
+ return new NarsilError(
5421
+ ErrorCodes.INDEX_ORPHANED,
5422
+ `Node '${nodeId}' holds a local copy of index '${indexName}' the cluster does not claim, so it answers nothing for that name. Drop it to reclaim the space`,
5423
+ { indexName, nodeId }
5424
+ );
5425
+ }
5426
+ async function reconcileOne(deps, indexName) {
5427
+ const localUuid = deps.engine.indexUuidOf(indexName);
5428
+ let clusterUuid;
5429
+ try {
5430
+ clusterUuid = (await getIndexMetadata(deps.coordinator, indexName))?.indexUuid ?? null;
5431
+ } catch (error) {
5432
+ report(deps, indexName, `its metadata could not be read: ${describeError(error)}`);
5433
+ return "orphaned";
5434
+ }
5435
+ if (clusterUuid === null) {
5436
+ report(deps, indexName, "the coordinator holds no metadata for that name");
5437
+ return "orphaned";
5438
+ }
5439
+ if (localUuid === clusterUuid) {
5440
+ return "adopted";
5441
+ }
5442
+ if (localUuid === null || localUuid === void 0) {
5443
+ await deps.engine.stampIndexUuid(indexName, clusterUuid);
5444
+ return "adopted";
5445
+ }
5446
+ await deps.engine.dropIndex(indexName);
5447
+ return "superseded";
5448
+ }
5449
+ function report(deps, indexName, reason) {
5450
+ if (deps.onError === void 0) {
5451
+ return;
5452
+ }
5453
+ deps.onError(
5454
+ new NarsilError(
5455
+ ErrorCodes.INDEX_ORPHANED,
5456
+ `Node '${deps.nodeId}' holds a local copy of index '${indexName}' the cluster does not claim, because ${reason}. The node keeps the data and serves none of it until an operator drops it.`,
5457
+ { indexName, nodeId: deps.nodeId }
5458
+ )
5459
+ );
5460
+ }
5461
+
5462
+ // src/distribution/cluster-node/snapshot-auth.ts
5463
+ var BOOTSTRAPPABLE_STATES = /* @__PURE__ */ new Set(["ACTIVE", "INITIALISING", "MIGRATING"]);
5464
+ async function authorizeSnapshotRequest(coordinator, indexName, sourceId) {
5465
+ let allocation;
5466
+ try {
5467
+ allocation = await coordinator.getAllocation(indexName);
5468
+ } catch (err) {
5469
+ const errMessage = err instanceof Error ? err.message : String(err);
5470
+ return {
5471
+ outcome: "denied",
5472
+ code: ErrorCodes.SNAPSHOT_SYNC_ALLOCATION_UNAVAILABLE,
5473
+ reason: `allocation lookup failed: ${errMessage}`
5474
+ };
5475
+ }
5476
+ if (allocation === null) {
5477
+ return {
5478
+ outcome: "denied",
5479
+ code: ErrorCodes.SNAPSHOT_SYNC_UNAUTHORIZED,
5480
+ reason: `index '${indexName}' has no allocation`
5481
+ };
5482
+ }
5483
+ let sawAssignment = false;
5484
+ for (const assignment of allocation.assignments.values()) {
5485
+ const isAssigned = assignment.primary === sourceId || assignment.replicas.includes(sourceId);
5486
+ if (!isAssigned) {
5487
+ continue;
5488
+ }
5489
+ sawAssignment = true;
5490
+ if (!BOOTSTRAPPABLE_STATES.has(assignment.state)) {
5491
+ continue;
5492
+ }
5493
+ return { outcome: "authorized" };
5494
+ }
5495
+ if (sawAssignment) {
5496
+ return {
5497
+ outcome: "denied",
5498
+ code: ErrorCodes.SNAPSHOT_SYNC_NOT_ASSIGNED,
5499
+ reason: `node '${sourceId}' is listed for index '${indexName}' but not in a bootstrappable state`
5500
+ };
5501
+ }
5502
+ return {
5503
+ outcome: "denied",
5504
+ code: ErrorCodes.SNAPSHOT_SYNC_UNAUTHORIZED,
5505
+ reason: `node '${sourceId}' is not an assigned replica for index '${indexName}'`
5506
+ };
5507
+ }
5508
+
5509
+ // src/distribution/cluster-node/snapshot-cache.ts
5510
+ var DEFAULT_MAX_CONCURRENT_SNAPSHOTS = 2;
5511
+ var DEFAULT_MAX_PER_SOURCE_SNAPSHOTS = 1;
5512
+ var DEFAULT_MAX_STREAMS_PER_INDEX = 4;
5513
+ function createSnapshotCacheState(maxConcurrent = DEFAULT_MAX_CONCURRENT_SNAPSHOTS, maxPerSource = DEFAULT_MAX_PER_SOURCE_SNAPSHOTS, maxStreamsPerIndex = DEFAULT_MAX_STREAMS_PER_INDEX) {
5514
+ if (!Number.isInteger(maxConcurrent) || maxConcurrent <= 0) {
5515
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "maxConcurrent must be a positive integer", {
5516
+ maxConcurrent
5517
+ });
5518
+ }
5519
+ if (!Number.isInteger(maxPerSource) || maxPerSource <= 0) {
5520
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "maxPerSource must be a positive integer", {
5521
+ maxPerSource
5522
+ });
5523
+ }
5524
+ if (!Number.isInteger(maxStreamsPerIndex) || maxStreamsPerIndex <= 0) {
5525
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "maxStreamsPerIndex must be a positive integer", {
5526
+ maxStreamsPerIndex
5527
+ });
5528
+ }
5529
+ return {
5530
+ cache: /* @__PURE__ */ new Map(),
5531
+ active: 0,
5532
+ perSource: /* @__PURE__ */ new Map(),
5533
+ streamActive: /* @__PURE__ */ new Map(),
5534
+ maxConcurrent,
5535
+ maxPerSource,
5536
+ maxStreamsPerIndex
5537
+ };
5538
+ }
5539
+ function sourceSlotKey(sourceId, indexName) {
5540
+ return `${sourceId}\0${indexName}`;
5541
+ }
5542
+ function acquireSourceSlot(state, sourceId, indexName) {
5543
+ const key = sourceSlotKey(sourceId, indexName);
5544
+ const current = state.perSource.get(key) ?? 0;
5545
+ if (current >= state.maxPerSource) {
5546
+ throw new NarsilError(
5547
+ ErrorCodes.SNAPSHOT_SYNC_CAPACITY_EXHAUSTED,
5548
+ `snapshot producer per-source cap reached for '${sourceId}' index '${indexName}'`,
5549
+ { sourceId, indexName, cap: state.maxPerSource }
5550
+ );
5551
+ }
5552
+ state.perSource.set(key, current + 1);
5553
+ return { key, released: false };
5554
+ }
5555
+ function releaseSourceSlot(state, handle) {
5556
+ if (handle.released) {
5557
+ return;
5558
+ }
5559
+ handle.released = true;
5560
+ const current = state.perSource.get(handle.key);
5561
+ if (current === void 0) {
5562
+ return;
5563
+ }
5564
+ if (current <= 1) {
5565
+ state.perSource.delete(handle.key);
5566
+ return;
5567
+ }
5568
+ state.perSource.set(handle.key, current - 1);
5569
+ }
5570
+ function acquireStreamSlot(state, indexName) {
5571
+ const current = state.streamActive.get(indexName) ?? 0;
5572
+ if (current >= state.maxStreamsPerIndex) {
5573
+ throw new NarsilError(
5574
+ ErrorCodes.SNAPSHOT_SYNC_CAPACITY_EXHAUSTED,
5575
+ `snapshot stream fan-out cap reached for index '${indexName}'`,
5576
+ { indexName, cap: state.maxStreamsPerIndex }
5577
+ );
5578
+ }
5579
+ state.streamActive.set(indexName, current + 1);
5580
+ return { indexName, released: false };
5581
+ }
5582
+ function releaseStreamSlot(state, handle) {
5583
+ if (handle.released) {
5584
+ return;
5585
+ }
5586
+ handle.released = true;
5587
+ const current = state.streamActive.get(handle.indexName);
5588
+ if (current === void 0) {
5589
+ return;
5590
+ }
5591
+ if (current <= 1) {
5592
+ state.streamActive.delete(handle.indexName);
5593
+ return;
5594
+ }
5595
+ state.streamActive.set(handle.indexName, current - 1);
5596
+ }
5597
+ async function acquireSnapshotBuild(state, indexName, builder) {
5598
+ const existing = state.cache.get(indexName);
5599
+ if (existing !== void 0) {
5600
+ return existing.promise;
5601
+ }
5602
+ if (state.active >= state.maxConcurrent) {
5603
+ throw new NarsilError(
5604
+ ErrorCodes.SNAPSHOT_SYNC_CAPACITY_EXHAUSTED,
5605
+ "snapshot producer capacity exhausted; retry later",
5606
+ { active: state.active, max: state.maxConcurrent }
5314
5607
  );
5315
5608
  }
5316
5609
  state.active += 1;
@@ -5748,6 +6041,11 @@ var MAX_LIMIT = 1e4;
5748
6041
  var MAX_OFFSET = 1e4;
5749
6042
  var MAX_HYBRID_K = 1e4;
5750
6043
  var MIN_HYBRID_K = 1;
6044
+ var MAX_PREFIX_LENGTH = 1024;
6045
+ var MAX_PINNED_ENTRIES = 1e3;
6046
+ var MAX_PINNED_POSITION = RESULT_WINDOW;
6047
+ var MAX_EF_SEARCH = 1e4;
6048
+ var MAX_GROUP_FIELDS = 64;
5751
6049
  var NULL_BYTE = String.fromCharCode(0);
5752
6050
  function throwInvalid(code, message, details) {
5753
6051
  throw new NarsilError(code, message, details);
@@ -6224,13 +6522,153 @@ function validateFilterExpression(value, fieldLabel, errorCode = SEARCH_INVALID_
6224
6522
  }
6225
6523
  }
6226
6524
 
6227
- // src/distribution/query/validators/search.ts
6228
- var MAX_FACET_SHARD_SIZE = Math.ceil(1e3 * 1.5) + 10;
6229
- var MAX_VECTOR_DIMENSION = 8192;
6230
- var MAX_VECTOR_TEXT_LENGTH = 16384;
6525
+ // src/distribution/query/oversample.ts
6526
+ function oversampledShardSize(size) {
6527
+ return Math.ceil(size * 1.5) + 10;
6528
+ }
6529
+
6530
+ // src/distribution/query/validators/search-options.ts
6531
+ var ALLOWED_MODES = ["fulltext", "vector", "hybrid"];
6532
+ var ALLOWED_METRICS = ["cosine", "dotProduct", "euclidean"];
6533
+ var ALLOWED_HYBRID_STRATEGIES = ["rrf", "linear"];
6534
+ function validateHybridParams(value) {
6535
+ if (!isRecord2(value)) {
6536
+ throwInvalid(CONFIG_INVALID, 'Invalid SearchPayload: "params.hybrid" must be an object');
6537
+ }
6538
+ if (value.strategy !== null && !ALLOWED_HYBRID_STRATEGIES.includes(value.strategy)) {
6539
+ throwInvalid(
6540
+ SEARCH_INVALID_MODE,
6541
+ `Invalid SearchPayload: "params.hybrid.strategy" must be one of: ${ALLOWED_HYBRID_STRATEGIES.join(", ")}, or null`
6542
+ );
6543
+ }
6544
+ if (value.k !== null && (!isInteger(value.k) || value.k < MIN_HYBRID_K || value.k > MAX_HYBRID_K)) {
6545
+ throwInvalid(
6546
+ CONFIG_INVALID,
6547
+ `Invalid SearchPayload: "params.hybrid.k" must be an integer between ${MIN_HYBRID_K} and ${MAX_HYBRID_K}, or null`
6548
+ );
6549
+ }
6550
+ if (value.alpha !== null && (!isFiniteNumber(value.alpha) || value.alpha < 0 || value.alpha > 1)) {
6551
+ throwInvalid(
6552
+ CONFIG_INVALID,
6553
+ 'Invalid SearchPayload: "params.hybrid.alpha" must be a finite number in [0, 1], or null'
6554
+ );
6555
+ }
6556
+ }
6557
+ function validateGroupParams(value) {
6558
+ if (!isRecord2(value)) {
6559
+ throwInvalid(CONFIG_INVALID, 'Invalid SearchPayload: "params.group" must be an object');
6560
+ }
6561
+ const fields = validateStringArray(
6562
+ value.fields,
6563
+ "params.group.fields",
6564
+ MAX_GROUP_FIELDS,
6565
+ MAX_FIELD_NAME_LENGTH,
6566
+ SEARCH_INVALID_FIELD
6567
+ );
6568
+ if (fields.length === 0) {
6569
+ throwInvalid(SEARCH_INVALID_FIELD, 'Invalid SearchPayload: "params.group.fields" must name at least one field');
6570
+ }
6571
+ if (!isInteger(value.maxPerGroup) || value.maxPerGroup <= 0 || value.maxPerGroup > MAX_LIMIT) {
6572
+ throwInvalid(
6573
+ CONFIG_INVALID,
6574
+ `Invalid SearchPayload: "params.group.maxPerGroup" must be a positive integer at most ${MAX_LIMIT}`
6575
+ );
6576
+ }
6577
+ if (value.limit !== null && (!isInteger(value.limit) || value.limit < 0 || value.limit > MAX_LIMIT)) {
6578
+ throwInvalid(
6579
+ CONFIG_INVALID,
6580
+ `Invalid SearchPayload: "params.group.limit" must be a non-negative integer at most ${MAX_LIMIT}, or null`
6581
+ );
6582
+ }
6583
+ }
6584
+ function validateTermMatchParam(value) {
6585
+ if (value === "all" || value === "any") {
6586
+ return;
6587
+ }
6588
+ if (typeof value === "string") {
6589
+ throwInvalid(SEARCH_INVALID_MODE, 'Invalid SearchPayload: "params.termMatch" must be "all", "any", or a count');
6590
+ }
6591
+ if (!isInteger(value) || value < 1 || value > MAX_TERMS_COUNT) {
6592
+ throwInvalid(
6593
+ CONFIG_INVALID,
6594
+ `Invalid SearchPayload: a "params.termMatch" count must be an integer between 1 and ${MAX_TERMS_COUNT}`
6595
+ );
6596
+ }
6597
+ }
6598
+ function validatePrefixLengthParam(value) {
6599
+ if (!isInteger(value) || value < 0 || value > MAX_PREFIX_LENGTH) {
6600
+ throwInvalid(
6601
+ CONFIG_INVALID,
6602
+ `Invalid SearchPayload: "params.prefixLength" must be a non-negative integer at most ${MAX_PREFIX_LENGTH}`
6603
+ );
6604
+ }
6605
+ }
6606
+ function validateBooleanParam(value, fieldLabel) {
6607
+ if (typeof value !== "boolean") {
6608
+ throwInvalid(CONFIG_INVALID, `Invalid SearchPayload: "params.${fieldLabel}" must be a boolean or null`);
6609
+ }
6610
+ }
6611
+ function validatePinnedParam(value) {
6612
+ if (!Array.isArray(value)) {
6613
+ throwInvalid(CONFIG_INVALID, 'Invalid SearchPayload: "params.pinned" must be an array or null');
6614
+ }
6615
+ if (value.length > MAX_PINNED_ENTRIES) {
6616
+ throwInvalid(
6617
+ CONFIG_INVALID,
6618
+ `Invalid SearchPayload: "params.pinned" exceeds maximum length of ${MAX_PINNED_ENTRIES}`,
6619
+ { length: value.length, limit: MAX_PINNED_ENTRIES }
6620
+ );
6621
+ }
6622
+ for (let i = 0; i < value.length; i++) {
6623
+ const entry = value[i];
6624
+ if (!isRecord2(entry)) {
6625
+ throwInvalid(CONFIG_INVALID, `Invalid SearchPayload: "params.pinned[${i}]" must be an object`);
6626
+ }
6627
+ if (typeof entry.docId !== "string" || entry.docId.length === 0 || entry.docId.length > MAX_DOC_ID_LENGTH) {
6628
+ throwInvalid(
6629
+ CONFIG_INVALID,
6630
+ `Invalid SearchPayload: "params.pinned[${i}].docId" must be a string of 1 to ${MAX_DOC_ID_LENGTH} characters`
6631
+ );
6632
+ }
6633
+ if (!isInteger(entry.position) || entry.position < 0 || entry.position > MAX_PINNED_POSITION) {
6634
+ throwInvalid(
6635
+ CONFIG_INVALID,
6636
+ `Invalid SearchPayload: "params.pinned[${i}].position" must be an integer between 0 and ${MAX_PINNED_POSITION}`
6637
+ );
6638
+ }
6639
+ }
6640
+ }
6641
+ function validateModeParam(value) {
6642
+ if (!ALLOWED_MODES.includes(value)) {
6643
+ throwInvalid(
6644
+ SEARCH_INVALID_MODE,
6645
+ `Invalid SearchPayload: "params.mode" must be one of: ${ALLOWED_MODES.join(", ")}`
6646
+ );
6647
+ }
6648
+ }
6649
+ function validateVectorMetricParam(value) {
6650
+ if (!ALLOWED_METRICS.includes(value)) {
6651
+ throwInvalid(
6652
+ SEARCH_INVALID_MODE,
6653
+ `Invalid SearchPayload: "params.vector.metric" must be one of: ${ALLOWED_METRICS.join(", ")}`
6654
+ );
6655
+ }
6656
+ }
6657
+ function validateEfSearchParam(value) {
6658
+ if (!isInteger(value) || value < 1 || value > MAX_EF_SEARCH) {
6659
+ throwInvalid(
6660
+ CONFIG_INVALID,
6661
+ `Invalid SearchPayload: "params.vector.efSearch" must be an integer between 1 and ${MAX_EF_SEARCH}`
6662
+ );
6663
+ }
6664
+ }
6665
+
6666
+ // src/distribution/query/validators/search.ts
6667
+ var MAX_FACET_SHARD_SIZE = oversampledShardSize(1e3);
6668
+ var MAX_VECTOR_DIMENSION = 8192;
6669
+ var MAX_VECTOR_TEXT_LENGTH = 16384;
6231
6670
  var ALLOWED_SCORING = ["local", "dfs", "broadcast"];
6232
6671
  var ALLOWED_SORT_DIRECTIONS = ["asc", "desc"];
6233
- var ALLOWED_HYBRID_STRATEGIES = ["rrf", "linear"];
6234
6672
  function validateIndexNameField2(value, fieldLabel) {
6235
6673
  if (typeof value !== "string") {
6236
6674
  throwInvalid(CONFIG_INVALID, `Invalid SearchPayload: "${fieldLabel}" must be a string`);
@@ -6282,37 +6720,11 @@ function validateVectorParams(value) {
6282
6720
  throwInvalid(CONFIG_INVALID, 'Invalid SearchPayload: "params.vector.similarity" must be a finite number or null');
6283
6721
  }
6284
6722
  }
6285
- }
6286
- function validateHybridParams(value) {
6287
- if (!isRecord2(value)) {
6288
- throwInvalid(CONFIG_INVALID, 'Invalid SearchPayload: "params.hybrid" must be an object');
6289
- }
6290
- if (!ALLOWED_HYBRID_STRATEGIES.includes(value.strategy)) {
6291
- throwInvalid(
6292
- SEARCH_INVALID_MODE,
6293
- `Invalid SearchPayload: "params.hybrid.strategy" must be one of: ${ALLOWED_HYBRID_STRATEGIES.join(", ")}`
6294
- );
6295
- }
6296
- if (!isInteger(value.k) || value.k < MIN_HYBRID_K || value.k > MAX_HYBRID_K) {
6297
- throwInvalid(
6298
- CONFIG_INVALID,
6299
- `Invalid SearchPayload: "params.hybrid.k" must be an integer between ${MIN_HYBRID_K} and ${MAX_HYBRID_K}`
6300
- );
6301
- }
6302
- if (!isFiniteNumber(value.alpha) || value.alpha < 0 || value.alpha > 1) {
6303
- throwInvalid(CONFIG_INVALID, 'Invalid SearchPayload: "params.hybrid.alpha" must be a finite number in [0, 1]');
6304
- }
6305
- }
6306
- function validateGroupParams(value) {
6307
- if (!isRecord2(value)) {
6308
- throwInvalid(CONFIG_INVALID, 'Invalid SearchPayload: "params.group" must be an object');
6723
+ if (value.metric !== null) {
6724
+ validateVectorMetricParam(value.metric);
6309
6725
  }
6310
- validateFieldName(value.field, "params.group.field", SEARCH_INVALID_FIELD);
6311
- if (!isInteger(value.maxPerGroup) || value.maxPerGroup <= 0 || value.maxPerGroup > MAX_LIMIT) {
6312
- throwInvalid(
6313
- CONFIG_INVALID,
6314
- `Invalid SearchPayload: "params.group.maxPerGroup" must be a positive integer at most ${MAX_LIMIT}`
6315
- );
6726
+ if (value.efSearch !== null) {
6727
+ validateEfSearchParam(value.efSearch);
6316
6728
  }
6317
6729
  }
6318
6730
  function validateSortParams(value) {
@@ -6454,6 +6866,24 @@ function validateParams(params) {
6454
6866
  `Invalid SearchPayload: "params.scoring" must be one of: ${ALLOWED_SCORING.join(", ")}`
6455
6867
  );
6456
6868
  }
6869
+ if (params.termMatch !== null) {
6870
+ validateTermMatchParam(params.termMatch);
6871
+ }
6872
+ if (params.prefixLength !== null) {
6873
+ validatePrefixLengthParam(params.prefixLength);
6874
+ }
6875
+ if (params.prefix !== null) {
6876
+ validateBooleanParam(params.prefix, "prefix");
6877
+ }
6878
+ if (params.exact !== null) {
6879
+ validateBooleanParam(params.exact, "exact");
6880
+ }
6881
+ if (params.pinned !== null) {
6882
+ validatePinnedParam(params.pinned);
6883
+ }
6884
+ if (params.mode !== null) {
6885
+ validateModeParam(params.mode);
6886
+ }
6457
6887
  if (params.vector !== null) {
6458
6888
  validateVectorParams(params.vector);
6459
6889
  }
@@ -6704,6 +7134,7 @@ var MAX_SORT_VALUES = 8;
6704
7134
  var MAX_FACET_FIELDS = 64;
6705
7135
  var MAX_FACET_BUCKETS = 1e4;
6706
7136
  var MAX_FACET_VALUE_LENGTH = 1024;
7137
+ var MAX_GROUPS_PER_RESPONSE = 65536;
6707
7138
  function validateScoredEntry(value, fieldLabel) {
6708
7139
  if (!isRecord2(value)) {
6709
7140
  throwInvalid(CONFIG_INVALID, `Invalid SearchResultPayload: "${fieldLabel}" must be an object`);
@@ -6846,6 +7277,43 @@ function validateFacetErrorBounds(value) {
6846
7277
  }
6847
7278
  }
6848
7279
  }
7280
+ function validateGroupEntries(value) {
7281
+ if (!Array.isArray(value)) {
7282
+ throwInvalid(CONFIG_INVALID, 'Invalid SearchResultPayload: "groups" must be an array or null');
7283
+ }
7284
+ if (value.length > MAX_GROUPS_PER_RESPONSE) {
7285
+ throwInvalid(
7286
+ CONFIG_INVALID,
7287
+ `Invalid SearchResultPayload: "groups" exceeds maximum length of ${MAX_GROUPS_PER_RESPONSE}`,
7288
+ { length: value.length, limit: MAX_GROUPS_PER_RESPONSE }
7289
+ );
7290
+ }
7291
+ for (let i = 0; i < value.length; i++) {
7292
+ const entry = value[i];
7293
+ if (!isRecord2(entry) || !isRecord2(entry.values)) {
7294
+ throwInvalid(CONFIG_INVALID, `Invalid SearchResultPayload: "groups[${i}]" must hold a values object`);
7295
+ }
7296
+ if (Object.keys(entry.values).length > MAX_FACET_FIELDS) {
7297
+ throwInvalid(
7298
+ CONFIG_INVALID,
7299
+ `Invalid SearchResultPayload: "groups[${i}].values" exceeds maximum field count of ${MAX_FACET_FIELDS}`
7300
+ );
7301
+ }
7302
+ if (!Array.isArray(entry.scored)) {
7303
+ throwInvalid(CONFIG_INVALID, `Invalid SearchResultPayload: "groups[${i}].scored" must be an array`);
7304
+ }
7305
+ if (entry.scored.length > MAX_RESULTS_PER_PARTITION) {
7306
+ throwInvalid(
7307
+ CONFIG_INVALID,
7308
+ `Invalid SearchResultPayload: "groups[${i}].scored" exceeds maximum length of ${MAX_RESULTS_PER_PARTITION}`,
7309
+ { length: entry.scored.length, limit: MAX_RESULTS_PER_PARTITION }
7310
+ );
7311
+ }
7312
+ for (let j = 0; j < entry.scored.length; j++) {
7313
+ validateScoredEntry(entry.scored[j], `groups[${i}].scored[${j}]`);
7314
+ }
7315
+ }
7316
+ }
6849
7317
  function validateSearchResultPayload(decoded) {
6850
7318
  if (!isRecord2(decoded)) {
6851
7319
  throwInvalid(CONFIG_INVALID, "Invalid SearchResultPayload: expected an object");
@@ -6862,6 +7330,9 @@ function validateSearchResultPayload(decoded) {
6862
7330
  if (decoded.facetErrorBounds !== null && decoded.facetErrorBounds !== void 0) {
6863
7331
  validateFacetErrorBounds(decoded.facetErrorBounds);
6864
7332
  }
7333
+ if (decoded.groups !== null && decoded.groups !== void 0) {
7334
+ validateGroupEntries(decoded.groups);
7335
+ }
6865
7336
  return decoded;
6866
7337
  }
6867
7338
 
@@ -7008,21 +7479,52 @@ function wireParamsToLocal(wire, facetShardSize) {
7008
7479
  tolerance: wire.tolerance ?? void 0,
7009
7480
  minScore: wire.threshold ?? void 0,
7010
7481
  includeScores: wire.includeScores ?? void 0,
7482
+ termMatch: wire.termMatch ?? void 0,
7483
+ prefixLength: wire.prefixLength ?? void 0,
7484
+ prefix: wire.prefix ?? void 0,
7485
+ exact: wire.exact ?? void 0,
7486
+ pinned: wire.pinned !== null ? wire.pinned.map((entry) => ({ docId: entry.docId, position: entry.position })) : void 0,
7487
+ mode: wire.mode ?? void 0,
7011
7488
  limit: wire.limit,
7012
7489
  offset: wire.offset,
7013
7490
  searchAfter: wire.searchAfter ?? void 0,
7014
7491
  sort: convertWireSortToLocal(wire.sort),
7015
- group: wire.group !== null ? { fields: [wire.group.field], maxPerGroup: wire.group.maxPerGroup } : void 0,
7492
+ group: convertWireGroupToLocal(wire.group),
7016
7493
  facets: convertWireFacetConfigToLocal(wire.facets, facetShardSize ?? wire.facetSize),
7017
- vector: wire.vector !== null ? {
7018
- field: wire.vector.field,
7019
- value: wire.vector.value ?? void 0,
7020
- text: wire.vector.text ?? void 0,
7021
- similarity: wire.vector.similarity ?? void 0
7022
- } : void 0,
7023
- hybrid: wire.hybrid !== null ? { strategy: wire.hybrid.strategy, k: wire.hybrid.k, alpha: wire.hybrid.alpha } : void 0
7494
+ vector: convertWireVectorToLocal(wire.vector),
7495
+ hybrid: convertWireHybridToLocal(wire.hybrid)
7024
7496
  };
7025
7497
  }
7498
+ function convertWireGroupToLocal(group) {
7499
+ if (group === null) {
7500
+ return void 0;
7501
+ }
7502
+ const local = { fields: [...group.fields], maxPerGroup: group.maxPerGroup };
7503
+ if (group.limit !== null) local.limit = group.limit;
7504
+ return local;
7505
+ }
7506
+ function convertWireVectorToLocal(vector) {
7507
+ if (vector === null) {
7508
+ return void 0;
7509
+ }
7510
+ const local = { field: vector.field };
7511
+ if (vector.value !== null) local.value = vector.value;
7512
+ if (vector.text !== null) local.text = vector.text;
7513
+ if (vector.similarity !== null) local.similarity = vector.similarity;
7514
+ if (vector.metric !== null) local.metric = vector.metric;
7515
+ if (vector.efSearch !== null) local.efSearch = vector.efSearch;
7516
+ return local;
7517
+ }
7518
+ function convertWireHybridToLocal(hybrid) {
7519
+ if (hybrid === null) {
7520
+ return void 0;
7521
+ }
7522
+ const local = {};
7523
+ if (hybrid.strategy !== null) local.strategy = hybrid.strategy;
7524
+ if (hybrid.k !== null) local.k = hybrid.k;
7525
+ if (hybrid.alpha !== null) local.alpha = hybrid.alpha;
7526
+ return local;
7527
+ }
7026
7528
  function convertWireSortToLocal(wireSort) {
7027
7529
  if (wireSort === null || wireSort.length === 0) {
7028
7530
  return void 0;
@@ -7056,6 +7558,12 @@ function localParamsToWire(params) {
7056
7558
  threshold: params.minScore ?? null,
7057
7559
  includeScores: params.includeScores ?? null,
7058
7560
  scoring: params.scoring ?? "local",
7561
+ termMatch: params.termMatch ?? null,
7562
+ prefixLength: params.prefixLength ?? null,
7563
+ prefix: params.prefix ?? null,
7564
+ exact: params.exact ?? null,
7565
+ pinned: params.pinned !== void 0 ? params.pinned.map((entry) => ({ docId: entry.docId, position: entry.position })) : null,
7566
+ mode: params.mode ?? null,
7059
7567
  vector: convertLocalVectorToWire(params.vector),
7060
7568
  hybrid: convertLocalHybridToWire(params.hybrid)
7061
7569
  };
@@ -7083,8 +7591,9 @@ function convertLocalGroupToWire(group) {
7083
7591
  return null;
7084
7592
  }
7085
7593
  return {
7086
- field: group.fields[0],
7087
- maxPerGroup: group.maxPerGroup ?? 1
7594
+ fields: [...group.fields],
7595
+ maxPerGroup: group.maxPerGroup ?? 1,
7596
+ limit: group.limit ?? null
7088
7597
  };
7089
7598
  }
7090
7599
  function convertLocalFacetsToWire(facets) {
@@ -7101,7 +7610,9 @@ function convertLocalVectorToWire(vector) {
7101
7610
  field: vector.field,
7102
7611
  value: vector.value === void 0 ? null : Array.from(vector.value),
7103
7612
  text: vector.text ?? null,
7104
- similarity: vector.similarity ?? null
7613
+ similarity: vector.similarity ?? null,
7614
+ metric: vector.metric ?? null,
7615
+ efSearch: vector.efSearch ?? null
7105
7616
  };
7106
7617
  }
7107
7618
  function convertLocalHybridToWire(hybrid) {
@@ -7109,9 +7620,9 @@ function convertLocalHybridToWire(hybrid) {
7109
7620
  return null;
7110
7621
  }
7111
7622
  return {
7112
- strategy: hybrid.strategy ?? "rrf",
7113
- k: hybrid.k ?? 60,
7114
- alpha: hybrid.alpha ?? 0.5
7623
+ strategy: hybrid.strategy ?? null,
7624
+ k: hybrid.k ?? null,
7625
+ alpha: hybrid.alpha ?? null
7115
7626
  };
7116
7627
  }
7117
7628
  function convertWireFacetsToLocal(wireFacets, errorBounds) {
@@ -7133,6 +7644,9 @@ async function handleSearch(message, respond, deps) {
7133
7644
  const decoded = decode(message.payload);
7134
7645
  const payload = validateSearchPayload(decoded);
7135
7646
  const queryParams = wireParamsToLocal(payload.params, payload.facetShardSize);
7647
+ if (queryParams.group?.limit !== void 0) {
7648
+ queryParams.group = { ...queryParams.group, limit: oversampledShardSize(queryParams.group.limit) };
7649
+ }
7136
7650
  const queryResult = await deps.engine.queryPartitions(
7137
7651
  payload.indexName,
7138
7652
  queryParams,
@@ -7153,7 +7667,8 @@ async function handleSearch(message, respond, deps) {
7153
7667
  const resultPayload = {
7154
7668
  results,
7155
7669
  facets: convertLocalFacetsToWire2(queryResult.facets),
7156
- facetErrorBounds: convertLocalFacetBoundsToWire(queryResult.facets)
7670
+ facetErrorBounds: convertLocalFacetBoundsToWire(queryResult.facets),
7671
+ groups: await convertLocalGroupsToWire(queryResult.groups, sortFields, deps, payload.indexName)
7157
7672
  };
7158
7673
  await respond({
7159
7674
  type: QueryMessageTypes.SEARCH_RESULT,
@@ -7162,6 +7677,25 @@ async function handleSearch(message, respond, deps) {
7162
7677
  payload: encode(resultPayload)
7163
7678
  });
7164
7679
  }
7680
+ async function convertLocalGroupsToWire(groups, sortFields, deps, indexName) {
7681
+ if (groups === void 0) {
7682
+ return null;
7683
+ }
7684
+ const wireGroups = [];
7685
+ for (const group of groups) {
7686
+ const scored = [];
7687
+ for (const hit of group.hits) {
7688
+ let sortValues = null;
7689
+ if (sortFields !== null) {
7690
+ const document = hit.document ?? await deps.engine.get(indexName, hit.id);
7691
+ sortValues = readSortValues(document, sortFields).map(toWireSortValue);
7692
+ }
7693
+ scored.push({ docId: hit.id, score: hit.score ?? null, sortValues });
7694
+ }
7695
+ wireGroups.push({ values: group.values, scored });
7696
+ }
7697
+ return wireGroups;
7698
+ }
7165
7699
  async function handleFetch(message, respond, deps) {
7166
7700
  const decoded = decode(message.payload);
7167
7701
  const payload = validateFetchPayload(decoded);
@@ -7188,7 +7722,7 @@ async function handleFetch(message, respond, deps) {
7188
7722
  async function handleStats(message, respond, deps) {
7189
7723
  const decoded = decode(message.payload);
7190
7724
  const payload = validateStatsPayload(decoded);
7191
- const resultPayload = deps.engine.collectQueryStats(
7725
+ const resultPayload = await deps.engine.collectQueryStats(
7192
7726
  payload.indexName,
7193
7727
  payload.terms,
7194
7728
  payload.partitionIds
@@ -7229,7 +7763,7 @@ function convertLocalFacetsToWire2(facets) {
7229
7763
  async function handleCount(message, respond, deps) {
7230
7764
  const payload = validateCountPayload(decode(message.payload));
7231
7765
  const requested = new Set(payload.partitionIds);
7232
- const partitions = deps.engine.getPartitionStats(payload.indexName).filter((partition) => requested.has(partition.partitionId)).map((partition) => ({
7766
+ const partitions = (await deps.engine.partitionStatsForRead(payload.indexName)).filter((partition) => requested.has(partition.partitionId)).map((partition) => ({
7233
7767
  partitionId: partition.partitionId,
7234
7768
  documentCount: partition.documentCount,
7235
7769
  estimatedMemoryBytes: partition.estimatedMemoryBytes
@@ -7379,59 +7913,65 @@ async function handleSyncRequestMessage(message, respond, deps) {
7379
7913
  }
7380
7914
  );
7381
7915
  }
7382
- if (!deps.engine.listIndexes().some((index) => index.name === request.indexName)) {
7916
+ const localIndex = deps.engine.listIndexes().find((index) => index.name === request.indexName);
7917
+ if (localIndex === void 0) {
7383
7918
  throw new NarsilError(
7384
7919
  ErrorCodes.SNAPSHOT_SYNC_INDEX_NOT_FOUND,
7385
7920
  `Node '${deps.nodeId}' holds no copy of index '${request.indexName}' yet, so it cannot serve a sync for partition ${request.partitionId}`,
7386
7921
  { indexName: request.indexName, partitionId: request.partitionId, primaryNodeId: deps.nodeId }
7387
7922
  );
7388
7923
  }
7389
- if (message.sourceId !== deps.nodeId) {
7390
- recordReplicaPosition(
7391
- deps.writeDeps.catchUp,
7392
- request.indexName,
7393
- request.partitionId,
7394
- message.sourceId,
7395
- request.lastSeqNo
7396
- );
7397
- }
7398
- const log = deps.writeDeps.getReplicationLog(request.indexName, request.partitionId);
7399
- const tier = decideSyncTier(log, request.lastSeqNo);
7400
- if (tier === "incremental") {
7401
- await sendSyncEntriesResponse(message, respond, deps, log.getEntriesFrom(request.lastSeqNo + 1));
7402
- return;
7403
- }
7404
- const snapshotSeqNo = log.localLogEnd;
7405
- const sink = createSingleResponseSink(respond);
7406
- await streamValidatedSnapshotRequest(
7407
- message,
7408
- sink,
7409
- { indexName: request.indexName, partitionId: request.partitionId },
7410
- {
7411
- nodeId: deps.nodeId,
7412
- engine: deps.engine,
7413
- coordinator: deps.coordinator,
7414
- state: deps.snapshotSyncState,
7415
- resolveHeaderMetadata: deps.resolveHeaderMetadata
7416
- },
7417
- {
7418
- metadata: {
7419
- partitionId: request.partitionId,
7420
- lastSeqNo: snapshotSeqNo,
7421
- primaryTerm: assignment.primaryTerm
7422
- },
7423
- closeOnEnd: false,
7424
- disableBuildCache: true,
7425
- buildSnapshot: async () => {
7426
- const bytes = await deps.engine.serializeReplicationPartition(request.indexName, request.partitionId);
7427
- return { bytes, checksum: crc32(bytes) };
7924
+ const release = await deps.engine.acquireIndexForReplication(request.indexName);
7925
+ try {
7926
+ if (message.sourceId !== deps.nodeId) {
7927
+ recordReplicaPosition(
7928
+ deps.writeDeps.catchUp,
7929
+ request.indexName,
7930
+ request.partitionId,
7931
+ message.sourceId,
7932
+ request.lastSeqNo
7933
+ );
7934
+ }
7935
+ const log = deps.writeDeps.getReplicationLog(request.indexName, request.partitionId);
7936
+ const tier = decideSyncTier(log, request.lastSeqNo);
7937
+ if (tier === "incremental") {
7938
+ await sendSyncEntriesResponse(message, respond, deps, log.getEntriesFrom(request.lastSeqNo + 1));
7939
+ return;
7940
+ }
7941
+ const snapshotSeqNo = log.localLogEnd;
7942
+ const sink = createSingleResponseSink(respond);
7943
+ await streamValidatedSnapshotRequest(
7944
+ message,
7945
+ sink,
7946
+ { indexName: request.indexName, partitionId: request.partitionId },
7947
+ {
7948
+ nodeId: deps.nodeId,
7949
+ engine: deps.engine,
7950
+ coordinator: deps.coordinator,
7951
+ state: deps.snapshotSyncState,
7952
+ resolveHeaderMetadata: deps.resolveHeaderMetadata
7428
7953
  },
7429
- afterSnapshot: async (trailingSink) => {
7430
- const entries = log.getEntriesFrom(snapshotSeqNo + 1);
7431
- await sendSyncEntriesResponse(message, trailingSink, deps, entries);
7954
+ {
7955
+ metadata: {
7956
+ partitionId: request.partitionId,
7957
+ lastSeqNo: snapshotSeqNo,
7958
+ primaryTerm: assignment.primaryTerm
7959
+ },
7960
+ closeOnEnd: false,
7961
+ disableBuildCache: true,
7962
+ buildSnapshot: async () => {
7963
+ const bytes = await deps.engine.serializeReplicationPartition(request.indexName, request.partitionId);
7964
+ return { bytes, checksum: crc32(bytes) };
7965
+ },
7966
+ afterSnapshot: async (trailingSink) => {
7967
+ const entries = log.getEntriesFrom(snapshotSeqNo + 1);
7968
+ await sendSyncEntriesResponse(message, trailingSink, deps, entries);
7969
+ }
7432
7970
  }
7433
- }
7434
- );
7971
+ );
7972
+ } finally {
7973
+ release();
7974
+ }
7435
7975
  }
7436
7976
  async function sendSyncEntriesResponse(message, respond, deps, entries) {
7437
7977
  const payload = {
@@ -7503,6 +8043,37 @@ var DEFAULT_CAPACITY = {
7503
8043
  cpuCores: 4,
7504
8044
  diskBytes: null
7505
8045
  };
8046
+
8047
+ // src/distribution/cluster-node/transport-failure.ts
8048
+ var FAILURE_CODE_BY_TRANSPORT_CODE = {
8049
+ [TransportErrorCodes.CONNECT_FAILED]: ErrorCodes.QUERY_NO_ACTIVE_REPLICA,
8050
+ [TransportErrorCodes.PEER_UNAVAILABLE]: ErrorCodes.QUERY_NO_ACTIVE_REPLICA,
8051
+ [TransportErrorCodes.TIMEOUT]: ErrorCodes.QUERY_NODE_TIMEOUT,
8052
+ [TransportErrorCodes.MESSAGE_TOO_LARGE]: ErrorCodes.QUERY_ROUTING_FAILED,
8053
+ [TransportErrorCodes.DECODE_FAILED]: ErrorCodes.QUERY_ROUTING_FAILED
8054
+ };
8055
+ function narsilErrorFromTransportFailure(error, message, details) {
8056
+ return new NarsilError(FAILURE_CODE_BY_TRANSPORT_CODE[error.code], message, {
8057
+ ...details,
8058
+ transportCode: error.code
8059
+ });
8060
+ }
8061
+ async function sendThroughTargets(targets, send, failure) {
8062
+ let lastError;
8063
+ for (const target of targets) {
8064
+ try {
8065
+ return await send(target);
8066
+ } catch (error) {
8067
+ lastError = error;
8068
+ }
8069
+ }
8070
+ if (lastError instanceof TransportError) {
8071
+ throw narsilErrorFromTransportFailure(lastError, failure.message, failure.details ?? {});
8072
+ }
8073
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
8074
+ }
8075
+
8076
+ // src/distribution/cluster-node/write-routing/forwarding.ts
7506
8077
  function assertForwardResponse(response, indexName, primaryNodeId) {
7507
8078
  const decoded = decode(response.payload);
7508
8079
  if (response.type.endsWith(".error") || response.type === "error" || decoded.error === true) {
@@ -7560,15 +8131,10 @@ async function forwardRemoveToRemote(indexName, docId, primaryNodeId, deps) {
7560
8131
  }
7561
8132
  async function sendToNode(nodeId, message, deps) {
7562
8133
  const targets = await resolveNodeTargets(nodeId, deps);
7563
- let lastError;
7564
- for (const target of targets) {
7565
- try {
7566
- return await deps.transport.send(target, message);
7567
- } catch (error) {
7568
- lastError = error;
7569
- }
7570
- }
7571
- throw lastError instanceof Error ? lastError : new Error(String(lastError));
8134
+ return sendThroughTargets(targets, (target) => deps.transport.send(target, message), {
8135
+ message: `Node '${nodeId}' did not answer a forwarded write`,
8136
+ details: { targetNodeId: nodeId }
8137
+ });
7572
8138
  }
7573
8139
  async function assertPrimaryWriteAuthority(entry, deps) {
7574
8140
  const table = await deps.coordinator.getAllocation(entry.indexName);
@@ -7645,24 +8211,15 @@ async function removeFailedReplicasFromInsync(entry, failedReplicas, deps) {
7645
8211
  }
7646
8212
  async function requestInsyncRemovalWithTargets(indexName, partitionId, replicaNodeId, primaryTerm, controllerNodeId, deps) {
7647
8213
  const targets = await resolveNodeTargets(controllerNodeId, deps);
7648
- let lastError;
7649
- for (const target of targets) {
7650
- try {
7651
- const result = await requestInsyncRemoval(
7652
- indexName,
7653
- partitionId,
7654
- replicaNodeId,
7655
- primaryTerm,
7656
- target,
7657
- deps.transport,
7658
- deps.nodeId
7659
- );
7660
- return result.accepted;
7661
- } catch (error) {
7662
- lastError = error;
8214
+ const result = await sendThroughTargets(
8215
+ targets,
8216
+ (target) => requestInsyncRemoval(indexName, partitionId, replicaNodeId, primaryTerm, target, deps.transport, deps.nodeId),
8217
+ {
8218
+ message: `Controller '${controllerNodeId}' did not answer an in-sync removal request for index '${indexName}' partition ${partitionId}`,
8219
+ details: { indexName, partitionId, replicaNodeId, controllerNodeId }
7663
8220
  }
7664
- }
7665
- throw lastError instanceof Error ? lastError : new Error(String(lastError));
8221
+ );
8222
+ return result.accepted;
7666
8223
  }
7667
8224
  async function replicateEntry(entry, assignment, deps) {
7668
8225
  const pending = getPendingAdmissions(deps.catchUp, entry.indexName, entry.partitionId);
@@ -8908,17 +9465,12 @@ async function resolveNodeTargets2(config, targetNodeId) {
8908
9465
  }
8909
9466
  return targets;
8910
9467
  }
8911
- async function sendToNode2(config, targetNodeId, message) {
9468
+ async function sendToNode2(config, targetNodeId, message, failure) {
8912
9469
  const targets = await resolveNodeTargets2(config, targetNodeId);
8913
- let lastError;
8914
- for (const target of targets) {
8915
- try {
8916
- return await config.transport.send(target, message);
8917
- } catch (error) {
8918
- lastError = error;
8919
- }
8920
- }
8921
- throw lastError instanceof Error ? lastError : new Error(String(lastError));
9470
+ return sendThroughTargets(targets, (target) => config.transport.send(target, message), {
9471
+ message: failure?.message ?? `Node '${targetNodeId}' did not answer a cluster request`,
9472
+ details: { targetNodeId, ...failure?.details }
9473
+ });
8922
9474
  }
8923
9475
  function fetchFieldsFor(projection) {
8924
9476
  if (projection.kind !== "fields" || projection.include === null) return null;
@@ -8966,7 +9518,10 @@ async function fetchDistributedDocuments(config, nodeId, engine, indexName, resu
8966
9518
  },
8967
9519
  nodeId
8968
9520
  );
8969
- const response = await sendToNode2(config, targetNodeId, fetchMessage);
9521
+ const response = await sendToNode2(config, targetNodeId, fetchMessage, {
9522
+ message: `Node '${targetNodeId}' did not answer a document fetch for index '${indexName}'`,
9523
+ details: { indexName }
9524
+ });
8970
9525
  const decoded = decode(response.payload);
8971
9526
  const payload = validateFetchResultPayload(decoded);
8972
9527
  for (const fetched of payload.documents) {
@@ -9014,7 +9569,10 @@ async function readDistributedDocuments(config, nodeId, engine, indexName, docId
9014
9569
  continue;
9015
9570
  }
9016
9571
  const fetchMessage = createFetchMessage({ indexName, documentIds, fields: null, highlight: null }, nodeId);
9017
- const response = await sendToNode2(config, targetNodeId, fetchMessage);
9572
+ const response = await sendToNode2(config, targetNodeId, fetchMessage, {
9573
+ message: `Node '${targetNodeId}' did not answer a document fetch for index '${indexName}'`,
9574
+ details: { indexName }
9575
+ });
9018
9576
  const payload = validateFetchResultPayload(decode(response.payload));
9019
9577
  for (const fetched of payload.documents) {
9020
9578
  documents.set(fetched.docId, fetched.document);
@@ -9162,79 +9720,6 @@ function isTimeoutError(error) {
9162
9720
  return error instanceof TransportError && error.code === "TRANSPORT_TIMEOUT";
9163
9721
  }
9164
9722
 
9165
- // src/distribution/query/fusion.ts
9166
- function clampAlpha(alpha) {
9167
- if (!Number.isFinite(alpha)) return 0.5;
9168
- if (alpha < 0) return 0;
9169
- if (alpha > 1) return 1;
9170
- return alpha;
9171
- }
9172
- function compareFusedEntries(a, b) {
9173
- const aScore = a.score ?? 0;
9174
- const bScore = b.score ?? 0;
9175
- if (aScore !== bScore) {
9176
- return bScore - aScore;
9177
- }
9178
- return compareCodePoints(a.docId, b.docId);
9179
- }
9180
- function distributedRRF(lists, options) {
9181
- const k = options.k > 0 ? options.k : 60;
9182
- const scores = /* @__PURE__ */ new Map();
9183
- for (const list of lists) {
9184
- for (let rank = 0; rank < list.length; rank++) {
9185
- const entry = list[rank];
9186
- const rrfContribution = 1 / (k + rank + 1);
9187
- scores.set(entry.docId, (scores.get(entry.docId) ?? 0) + rrfContribution);
9188
- }
9189
- }
9190
- const result = [];
9191
- for (const [docId, score] of scores) {
9192
- result.push({ docId, score, sortValues: null });
9193
- }
9194
- result.sort(compareFusedEntries);
9195
- return result;
9196
- }
9197
- function distributedLinearCombination(textResults, vectorResults, options) {
9198
- const { alpha } = options;
9199
- const normalizedText = minMaxNormalizeScoredEntries(textResults);
9200
- const normalizedVector = minMaxNormalizeScoredEntries(vectorResults);
9201
- const textScoreMap = /* @__PURE__ */ new Map();
9202
- for (const entry of normalizedText) {
9203
- textScoreMap.set(entry.docId, entry.score);
9204
- }
9205
- const vectorScoreMap = /* @__PURE__ */ new Map();
9206
- for (const entry of normalizedVector) {
9207
- vectorScoreMap.set(entry.docId, entry.score);
9208
- }
9209
- const allDocIds = /* @__PURE__ */ new Set();
9210
- for (const entry of textResults) allDocIds.add(entry.docId);
9211
- for (const entry of vectorResults) allDocIds.add(entry.docId);
9212
- const result = [];
9213
- for (const docId of allDocIds) {
9214
- const tScore = textScoreMap.get(docId) ?? 0;
9215
- const vScore = vectorScoreMap.get(docId) ?? 0;
9216
- const combined = alpha * vScore + (1 - alpha) * tScore;
9217
- result.push({ docId, score: combined, sortValues: null });
9218
- }
9219
- result.sort(compareFusedEntries);
9220
- return result;
9221
- }
9222
- function minMaxNormalizeScoredEntries(entries) {
9223
- if (entries.length === 0) return [];
9224
- let min = entries[0].score ?? 0;
9225
- let max = entries[0].score ?? 0;
9226
- for (let i = 1; i < entries.length; i++) {
9227
- const score = entries[i].score ?? 0;
9228
- if (score < min) min = score;
9229
- if (score > max) max = score;
9230
- }
9231
- const range = max - min;
9232
- if (range === 0) {
9233
- return entries.map((e) => ({ docId: e.docId, score: 1 }));
9234
- }
9235
- return entries.map((e) => ({ docId: e.docId, score: ((e.score ?? 0) - min) / range }));
9236
- }
9237
-
9238
9723
  // src/distribution/query/merge.ts
9239
9724
  function compareByScoreThenId(a, b) {
9240
9725
  const aScore = a.score ?? Number.NEGATIVE_INFINITY;
@@ -9396,6 +9881,241 @@ function mergeDistributedFacets(allFacets, allBounds, maxBuckets = DEFAULT_MAX_F
9396
9881
  return { facets, errorBounds };
9397
9882
  }
9398
9883
 
9884
+ // src/distribution/query/group-merge.ts
9885
+ function mergeDistributedGroups(perNode, fields, maxPerGroup, limit, sortDirections) {
9886
+ const byKey = /* @__PURE__ */ new Map();
9887
+ for (const groups of perNode) {
9888
+ for (const group of groups) {
9889
+ const key = JSON.stringify(fields.map((field) => group.values[field]));
9890
+ const existing = byKey.get(key);
9891
+ if (existing === void 0) {
9892
+ byKey.set(key, { values: group.values, lists: [group.scored] });
9893
+ } else {
9894
+ existing.lists.push(group.scored);
9895
+ }
9896
+ }
9897
+ }
9898
+ const merged = [];
9899
+ for (const entry of byKey.values()) {
9900
+ const scored = sortDirections !== null ? mergeAndTruncateSortedEntries(entry.lists, maxPerGroup, sortDirections) : mergeAndTruncateScoredEntries(entry.lists, maxPerGroup);
9901
+ merged.push({ values: entry.values, scored });
9902
+ }
9903
+ merged.sort((a, b) => {
9904
+ const aScore = a.scored.length > 0 ? a.scored[0].score ?? 0 : 0;
9905
+ const bScore = b.scored.length > 0 ? b.scored[0].score ?? 0 : 0;
9906
+ if (aScore !== bScore) return bScore - aScore;
9907
+ const aId = a.scored.length > 0 ? a.scored[0].docId : "";
9908
+ const bId = b.scored.length > 0 ? b.scored[0].docId : "";
9909
+ return compareCodePoints(aId, bId);
9910
+ });
9911
+ if (limit !== null && merged.length > limit) {
9912
+ merged.length = limit;
9913
+ }
9914
+ return merged;
9915
+ }
9916
+ function mergeGroupsFor(params, allGroups, sortFields) {
9917
+ if (params.group === null || allGroups.length === 0) {
9918
+ return null;
9919
+ }
9920
+ return mergeDistributedGroups(
9921
+ allGroups,
9922
+ params.group.fields,
9923
+ params.group.maxPerGroup,
9924
+ params.group.limit,
9925
+ sortFields !== null ? sortFields.map((field) => field.direction) : null
9926
+ );
9927
+ }
9928
+
9929
+ // src/distribution/query/fusion.ts
9930
+ function clampAlpha(alpha) {
9931
+ if (!Number.isFinite(alpha)) return 0.5;
9932
+ if (alpha < 0) return 0;
9933
+ if (alpha > 1) return 1;
9934
+ return alpha;
9935
+ }
9936
+ function compareFusedEntries(a, b) {
9937
+ const aScore = a.score ?? 0;
9938
+ const bScore = b.score ?? 0;
9939
+ if (aScore !== bScore) {
9940
+ return bScore - aScore;
9941
+ }
9942
+ return compareCodePoints(a.docId, b.docId);
9943
+ }
9944
+ function distributedRRF(lists, options) {
9945
+ const k = options.k > 0 ? options.k : 60;
9946
+ const scores = /* @__PURE__ */ new Map();
9947
+ for (const list of lists) {
9948
+ for (let rank = 0; rank < list.length; rank++) {
9949
+ const entry = list[rank];
9950
+ const rrfContribution = 1 / (k + rank + 1);
9951
+ scores.set(entry.docId, (scores.get(entry.docId) ?? 0) + rrfContribution);
9952
+ }
9953
+ }
9954
+ const result = [];
9955
+ for (const [docId, score] of scores) {
9956
+ result.push({ docId, score, sortValues: null });
9957
+ }
9958
+ result.sort(compareFusedEntries);
9959
+ return result;
9960
+ }
9961
+ function distributedLinearCombination(textResults, vectorResults, options) {
9962
+ const { alpha } = options;
9963
+ const normalizedText = minMaxNormalizeScoredEntries(textResults);
9964
+ const normalizedVector = minMaxNormalizeScoredEntries(vectorResults);
9965
+ const textScoreMap = /* @__PURE__ */ new Map();
9966
+ for (const entry of normalizedText) {
9967
+ textScoreMap.set(entry.docId, entry.score);
9968
+ }
9969
+ const vectorScoreMap = /* @__PURE__ */ new Map();
9970
+ for (const entry of normalizedVector) {
9971
+ vectorScoreMap.set(entry.docId, entry.score);
9972
+ }
9973
+ const allDocIds = /* @__PURE__ */ new Set();
9974
+ for (const entry of textResults) allDocIds.add(entry.docId);
9975
+ for (const entry of vectorResults) allDocIds.add(entry.docId);
9976
+ const result = [];
9977
+ for (const docId of allDocIds) {
9978
+ const tScore = textScoreMap.get(docId) ?? 0;
9979
+ const vScore = vectorScoreMap.get(docId) ?? 0;
9980
+ const combined = alpha * vScore + (1 - alpha) * tScore;
9981
+ result.push({ docId, score: combined, sortValues: null });
9982
+ }
9983
+ result.sort(compareFusedEntries);
9984
+ return result;
9985
+ }
9986
+ function minMaxNormalizeScoredEntries(entries) {
9987
+ if (entries.length === 0) return [];
9988
+ let min = entries[0].score ?? 0;
9989
+ let max = entries[0].score ?? 0;
9990
+ for (let i = 1; i < entries.length; i++) {
9991
+ const score = entries[i].score ?? 0;
9992
+ if (score < min) min = score;
9993
+ if (score > max) max = score;
9994
+ }
9995
+ const range = max - min;
9996
+ if (range === 0) {
9997
+ return entries.map((e) => ({ docId: e.docId, score: 1 }));
9998
+ }
9999
+ return entries.map((e) => ({ docId: e.docId, score: ((e.score ?? 0) - min) / range }));
10000
+ }
10001
+
10002
+ // src/distribution/query/pinning.ts
10003
+ function lastOrganicEntry(page, pinned) {
10004
+ if (pinned === null) {
10005
+ return page.length > 0 ? page[page.length - 1] : void 0;
10006
+ }
10007
+ const pinnedIds = new Set(pinned.map((entry) => entry.docId));
10008
+ for (let i = page.length - 1; i >= 0; i--) {
10009
+ if (!pinnedIds.has(page[i].docId)) return page[i];
10010
+ }
10011
+ return void 0;
10012
+ }
10013
+ function placePinnedEntries(merged, pinned, depth, allMatchesPresent) {
10014
+ const deduped = dedupePinnedEntries(pinned);
10015
+ const pinnedIds = new Set(deduped.map((entry) => entry.docId));
10016
+ const removed = /* @__PURE__ */ new Map();
10017
+ const result = [];
10018
+ for (const entry of merged) {
10019
+ if (pinnedIds.has(entry.docId)) {
10020
+ removed.set(entry.docId, entry);
10021
+ } else {
10022
+ result.push(entry);
10023
+ }
10024
+ }
10025
+ const ordered = deduped.sort((a, b) => a.position - b.position);
10026
+ for (const entry of ordered) {
10027
+ if (!allMatchesPresent && entry.position >= depth) continue;
10028
+ const organic = removed.get(entry.docId);
10029
+ const placed = { docId: entry.docId, score: 0, sortValues: organic?.sortValues ?? null };
10030
+ result.splice(Math.max(0, Math.min(entry.position, result.length)), 0, placed);
10031
+ }
10032
+ return result;
10033
+ }
10034
+
10035
+ // src/distribution/query/hybrid.ts
10036
+ async function executeHybridQuery(indexName, params, limit, offset, facetShardSize, facetSize, totalPartitions, routing, deps, config) {
10037
+ const depth = limit + offset;
10038
+ const textParams = { ...params, vector: null, hybrid: null, mode: null, limit: depth, offset: 0 };
10039
+ const vectorParams = {
10040
+ ...params,
10041
+ term: null,
10042
+ hybrid: null,
10043
+ mode: null,
10044
+ group: null,
10045
+ limit: depth,
10046
+ offset: 0
10047
+ };
10048
+ let textGlobalStats = null;
10049
+ if (params.scoring === "dfs") {
10050
+ textGlobalStats = await collectDistributedStats(indexName, textParams, routing.nodeToPartitions, deps, config);
10051
+ }
10052
+ const [textOutcomes, vectorOutcomes] = await Promise.all([
10053
+ fanOutSearch(indexName, textParams, textGlobalStats, facetShardSize, routing.nodeToPartitions, deps, config),
10054
+ fanOutSearch(indexName, vectorParams, null, null, routing.nodeToPartitions, deps, config)
10055
+ ]);
10056
+ const coverage = buildCoverage(totalPartitions, routing.unavailablePartitions, textOutcomes, vectorOutcomes);
10057
+ if (!config.allowPartialResults) {
10058
+ const failedCount = coverage.timedOutPartitions + coverage.failedPartitions;
10059
+ if (failedCount > 0) {
10060
+ throw new NarsilError(ErrorCodes.QUERY_PARTIAL_FAILURE, "One or more partitions failed during hybrid query", {
10061
+ coverage
10062
+ });
10063
+ }
10064
+ }
10065
+ const textScored = [];
10066
+ const vectorScored = [];
10067
+ const allFacets = [];
10068
+ const allFacetBounds = [];
10069
+ const allGroups = [];
10070
+ let totalHits = 0;
10071
+ for (const outcome of textOutcomes) {
10072
+ if (outcome.status !== "success" || outcome.results === null) continue;
10073
+ for (const partitionResult of outcome.results.results) {
10074
+ totalHits += partitionResult.totalHits;
10075
+ if (partitionResult.scored.length > 0) {
10076
+ textScored.push(partitionResult.scored);
10077
+ }
10078
+ }
10079
+ if (outcome.results.facets !== null) {
10080
+ allFacets.push(outcome.results.facets);
10081
+ allFacetBounds.push(outcome.results.facetErrorBounds);
10082
+ }
10083
+ if (outcome.results.groups !== null && outcome.results.groups !== void 0) {
10084
+ allGroups.push(outcome.results.groups);
10085
+ }
10086
+ }
10087
+ for (const outcome of vectorOutcomes) {
10088
+ if (outcome.status !== "success" || outcome.results === null) continue;
10089
+ for (const partitionResult of outcome.results.results) {
10090
+ if (partitionResult.scored.length > 0) {
10091
+ vectorScored.push(partitionResult.scored);
10092
+ }
10093
+ }
10094
+ }
10095
+ const mergedText = mergeAndTruncateScoredEntries(textScored, depth);
10096
+ const mergedVector = mergeAndTruncateScoredEntries(vectorScored, depth);
10097
+ const hybrid = params.hybrid;
10098
+ let fused;
10099
+ if ((hybrid?.strategy ?? "rrf") === "rrf") {
10100
+ fused = distributedRRF([mergedText, mergedVector], { k: hybrid?.k ?? 60 });
10101
+ } else {
10102
+ fused = distributedLinearCombination(mergedText, mergedVector, { alpha: clampAlpha(hybrid?.alpha ?? 0.5) });
10103
+ }
10104
+ const allMatchesPresent = coverage.queriedPartitions === coverage.totalPartitions && totalHits <= mergedText.length && mergedVector.length < depth;
10105
+ const fusedWithPinned = params.pinned !== null ? placePinnedEntries(fused, params.pinned, depth, allMatchesPresent) : fused;
10106
+ const truncated = fusedWithPinned.slice(offset, depth);
10107
+ const mergedFacets = allFacets.length > 0 ? mergeDistributedFacets(allFacets, allFacetBounds, facetSize) : null;
10108
+ return {
10109
+ scored: truncated,
10110
+ totalHits,
10111
+ facets: mergedFacets?.facets ?? null,
10112
+ facetErrorBounds: mergedFacets?.errorBounds ?? null,
10113
+ groups: mergeGroupsFor(params, allGroups, null),
10114
+ cursor: null,
10115
+ coverage
10116
+ };
10117
+ }
10118
+
9399
10119
  // src/distribution/query/types.ts
9400
10120
  var DEFAULT_QUERY_CONFIG = {
9401
10121
  allowPartialResults: true,
@@ -9418,16 +10138,17 @@ async function distributedQuery(indexName, params, deps, config, selector) {
9418
10138
  ...DEFAULT_QUERY_CONFIG,
9419
10139
  ...config
9420
10140
  };
10141
+ const binding = queryBindingOf(wireParamsToLocal(params));
9421
10142
  if (params.searchAfter !== null) {
9422
10143
  const decoded = decodePageCursor(params.searchAfter);
9423
- requireMatchingCursor(decoded, params.searchAfter, wireSortSignature(params.sort), true);
10144
+ requireMatchingCursor(decoded, params.searchAfter, wireSortSignature(params.sort), true, binding);
9424
10145
  }
9425
10146
  const limit = Math.max(params.limit, 0);
9426
10147
  const offset = Math.max(params.offset, 0);
9427
10148
  requireWithinResultWindow(limit, offset);
9428
10149
  const hasFacets = params.facets !== null && params.facets.length > 0;
9429
10150
  const facetSize = resolveAndClampFacetSize(params.facetSize, resolvedConfig.defaultFacetSize);
9430
- const facetShardSize = hasFacets ? Math.ceil(facetSize * 1.5) + 10 : null;
10151
+ const facetShardSize = hasFacets ? oversampledShardSize(facetSize) : null;
9431
10152
  const allocationTable = await deps.getAllocation(indexName);
9432
10153
  if (allocationTable === null) {
9433
10154
  throw new NarsilError(ErrorCodes.QUERY_ROUTING_FAILED, `No allocation table found for index '${indexName}'`, {
@@ -9440,6 +10161,7 @@ async function distributedQuery(indexName, params, deps, config, selector) {
9440
10161
  totalHits: 0,
9441
10162
  facets: null,
9442
10163
  facetErrorBounds: null,
10164
+ groups: null,
9443
10165
  cursor: null,
9444
10166
  coverage: { totalPartitions: 0, queriedPartitions: 0, timedOutPartitions: 0, failedPartitions: 0 }
9445
10167
  };
@@ -9453,11 +10175,13 @@ async function distributedQuery(indexName, params, deps, config, selector) {
9453
10175
  { unavailablePartitions: routing.unavailablePartitions }
9454
10176
  );
9455
10177
  }
9456
- const isHybrid = params.hybrid !== null && params.term !== null && params.vector !== null;
10178
+ const isHybrid = params.vector !== null && (params.term !== null || params.mode === "hybrid");
9457
10179
  if (isHybrid && params.searchAfter !== null) {
9458
- throw new NarsilError(ErrorCodes.QUERY_ROUTING_FAILED, "Cursor pagination is not supported for hybrid queries", {
9459
- indexName
9460
- });
10180
+ throw new NarsilError(
10181
+ ErrorCodes.SEARCH_INVALID_CURSOR,
10182
+ "Cursor pagination is not supported for hybrid queries, because fused ranks do not seek",
10183
+ { indexName }
10184
+ );
9461
10185
  }
9462
10186
  if (isHybrid && params.sort !== null && params.sort.length > 0) {
9463
10187
  throw new NarsilError(
@@ -9483,6 +10207,7 @@ async function distributedQuery(indexName, params, deps, config, selector) {
9483
10207
  return executeSingleFanOut(
9484
10208
  indexName,
9485
10209
  params,
10210
+ binding,
9486
10211
  limit,
9487
10212
  offset,
9488
10213
  facetShardSize,
@@ -9493,7 +10218,7 @@ async function distributedQuery(indexName, params, deps, config, selector) {
9493
10218
  resolvedConfig
9494
10219
  );
9495
10220
  }
9496
- async function executeSingleFanOut(indexName, params, limit, offset, facetShardSize, facetSize, totalPartitions, routing, deps, config) {
10221
+ async function executeSingleFanOut(indexName, params, binding, limit, offset, facetShardSize, facetSize, totalPartitions, routing, deps, config) {
9497
10222
  let globalStats = null;
9498
10223
  if (params.scoring === "dfs") {
9499
10224
  globalStats = await collectDistributedStats(indexName, params, routing.nodeToPartitions, deps, config);
@@ -9525,6 +10250,7 @@ async function executeSingleFanOut(indexName, params, limit, offset, facetShardS
9525
10250
  const allScored = [];
9526
10251
  const allFacets = [];
9527
10252
  const allFacetBounds = [];
10253
+ const allGroups = [];
9528
10254
  let totalHits = 0;
9529
10255
  for (const outcome of outcomes) {
9530
10256
  if (outcome.status !== "success" || outcome.results === null) continue;
@@ -9538,29 +10264,43 @@ async function executeSingleFanOut(indexName, params, limit, offset, facetShardS
9538
10264
  allFacets.push(outcome.results.facets);
9539
10265
  allFacetBounds.push(outcome.results.facetErrorBounds);
9540
10266
  }
10267
+ if (outcome.results.groups !== null && outcome.results.groups !== void 0) {
10268
+ allGroups.push(outcome.results.groups);
10269
+ }
9541
10270
  }
9542
10271
  const merged = sortFields !== null ? mergeAndTruncateSortedEntries(
9543
10272
  allScored,
9544
10273
  depth,
9545
10274
  sortFields.map((field) => field.direction)
9546
10275
  ) : mergeAndTruncateScoredEntries(allScored, depth);
9547
- const mergedScored = offset > 0 ? merged.slice(offset) : merged;
10276
+ const allMatchesPresent = coverage.queriedPartitions === coverage.totalPartitions && totalHits <= merged.length;
10277
+ const placed = params.pinned !== null && params.searchAfter === null ? placePinnedEntries(merged, params.pinned, depth, allMatchesPresent) : merged;
10278
+ const mergedScored = placed.slice(offset, depth);
9548
10279
  const mergedFacets = allFacets.length > 0 ? mergeDistributedFacets(allFacets, allFacetBounds, facetSize) : null;
10280
+ const mergedGroups = mergeGroupsFor(params, allGroups, sortFields);
9549
10281
  let cursor = null;
9550
- if (mergedScored.length > 0) {
9551
- const lastEntry = mergedScored[mergedScored.length - 1];
10282
+ const lastEntry = lastOrganicEntry(mergedScored, params.searchAfter === null ? params.pinned : null);
10283
+ if (lastEntry !== void 0) {
9552
10284
  cursor = sortFields !== null ? encodePageCursor({
9553
10285
  anchor: lastEntry.docId,
9554
10286
  score: null,
9555
10287
  sortKey: (lastEntry.sortValues ?? []).map(toComparableSortValue),
9556
- sortSignature: wireSortSignature(sortFields)
9557
- }) : encodePageCursor({ anchor: lastEntry.docId, score: lastEntry.score, sortKey: null, sortSignature: null });
10288
+ sortSignature: wireSortSignature(sortFields),
10289
+ binding
10290
+ }) : encodePageCursor({
10291
+ anchor: lastEntry.docId,
10292
+ score: lastEntry.score,
10293
+ sortKey: null,
10294
+ sortSignature: null,
10295
+ binding
10296
+ });
9558
10297
  }
9559
10298
  return {
9560
10299
  scored: mergedScored,
9561
10300
  totalHits,
9562
10301
  facets: mergedFacets?.facets ?? null,
9563
10302
  facetErrorBounds: mergedFacets?.errorBounds ?? null,
10303
+ groups: mergedGroups,
9564
10304
  cursor,
9565
10305
  coverage
9566
10306
  };
@@ -9575,79 +10315,73 @@ function failOutcomesMissingSortValues(outcomes) {
9575
10315
  }
9576
10316
  }
9577
10317
  }
9578
- async function executeHybridQuery(indexName, params, limit, offset, facetShardSize, facetSize, totalPartitions, routing, deps, config) {
9579
- const depth = limit + offset;
9580
- const textParams = { ...params, vector: null, hybrid: null, limit: depth, offset: 0 };
9581
- const vectorParams = { ...params, term: null, hybrid: null, limit: depth, offset: 0 };
9582
- let textGlobalStats = null;
9583
- if (params.scoring === "dfs") {
9584
- textGlobalStats = await collectDistributedStats(indexName, textParams, routing.nodeToPartitions, deps, config);
10318
+
10319
+ // src/distribution/cluster-node/reads/groups.ts
10320
+ async function assembleDistributedGroups(deps, indexName, params, distributed, allocation, projection) {
10321
+ const wireGroups = distributed.groups;
10322
+ const group = params.group;
10323
+ if (wireGroups === null || group === void 0) {
10324
+ return void 0;
9585
10325
  }
9586
- const [textOutcomes, vectorOutcomes] = await Promise.all([
9587
- fanOutSearch(indexName, textParams, textGlobalStats, facetShardSize, routing.nodeToPartitions, deps, config),
9588
- fanOutSearch(indexName, vectorParams, null, null, routing.nodeToPartitions, deps, config)
9589
- ]);
9590
- const coverage = buildCoverage(totalPartitions, routing.unavailablePartitions, textOutcomes, vectorOutcomes);
9591
- if (!config.allowPartialResults) {
9592
- const failedCount = coverage.timedOutPartitions + coverage.failedPartitions;
9593
- if (failedCount > 0) {
9594
- throw new NarsilError(ErrorCodes.QUERY_PARTIAL_FAILURE, "One or more partitions failed during hybrid query", {
9595
- coverage
9596
- });
9597
- }
9598
- }
9599
- const textScored = [];
9600
- const vectorScored = [];
9601
- const allFacets = [];
9602
- const allFacetBounds = [];
9603
- let totalHits = 0;
9604
- for (const outcome of textOutcomes) {
9605
- if (outcome.status !== "success" || outcome.results === null) continue;
9606
- for (const partitionResult of outcome.results.results) {
9607
- totalHits += partitionResult.totalHits;
9608
- if (partitionResult.scored.length > 0) {
9609
- textScored.push(partitionResult.scored);
10326
+ const reduce = group.reduce;
10327
+ const needsDocuments = reduce !== void 0 || projection.kind !== "none";
10328
+ const groupDocIds = [...new Set(wireGroups.flatMap((entry) => entry.scored.map((scored) => scored.docId)))];
10329
+ const documents = needsDocuments && groupDocIds.length > 0 ? await readDistributedDocuments(deps.config, deps.nodeId, deps.engine, indexName, groupDocIds, allocation) : /* @__PURE__ */ new Map();
10330
+ return wireGroups.map((entry) => {
10331
+ const hits = entry.scored.map((scored) => ({
10332
+ id: scored.docId,
10333
+ score: scored.score ?? void 0,
10334
+ document: projectedGroupDocument(documents.get(scored.docId), projection)
10335
+ }));
10336
+ const result = { values: entry.values, hits };
10337
+ if (reduce !== void 0) {
10338
+ const folded = foldGroupReducer(
10339
+ reduce,
10340
+ entry.scored.map((scored) => ({ document: documents.get(scored.docId), score: scored.score ?? 0 }))
10341
+ );
10342
+ if (folded.reducerError !== void 0) {
10343
+ result.reducerError = folded.reducerError;
10344
+ } else {
10345
+ result.reduced = folded.reduced;
9610
10346
  }
9611
10347
  }
9612
- if (outcome.results.facets !== null) {
9613
- allFacets.push(outcome.results.facets);
9614
- allFacetBounds.push(outcome.results.facetErrorBounds);
9615
- }
10348
+ return result;
10349
+ });
10350
+ }
10351
+ function projectedGroupDocument(document, projection) {
10352
+ if (document === void 0 || projection.kind === "none") {
10353
+ return {};
9616
10354
  }
9617
- for (const outcome of vectorOutcomes) {
9618
- if (outcome.status !== "success" || outcome.results === null) continue;
9619
- for (const partitionResult of outcome.results.results) {
9620
- if (partitionResult.scored.length > 0) {
9621
- vectorScored.push(partitionResult.scored);
9622
- }
10355
+ return applyProjection(document, projection);
10356
+ }
10357
+
10358
+ // src/distribution/cluster-node/reads/pinned.ts
10359
+ function splitPinnedByReachability(pinnedIds, allocation) {
10360
+ const partitionCount = allocation.assignments.size;
10361
+ const verifiable = [];
10362
+ const unverifiable = /* @__PURE__ */ new Set();
10363
+ for (const docId of pinnedIds) {
10364
+ const partitionId = resolvePartitionId(docId, partitionCount);
10365
+ const assignment = allocation.assignments.get(partitionId);
10366
+ const nodeId = assignment === void 0 ? null : selectReplica(assignment, void 0, partitionId);
10367
+ if (nodeId === null) {
10368
+ unverifiable.add(docId);
10369
+ } else {
10370
+ verifiable.push(docId);
9623
10371
  }
9624
10372
  }
9625
- const mergedText = mergeAndTruncateScoredEntries(textScored, depth);
9626
- const mergedVector = mergeAndTruncateScoredEntries(vectorScored, depth);
9627
- const hybrid = params.hybrid;
9628
- let fused;
9629
- if (hybrid !== null && hybrid.strategy === "rrf") {
9630
- fused = distributedRRF([mergedText, mergedVector], { k: hybrid.k });
9631
- } else if (hybrid !== null) {
9632
- fused = distributedLinearCombination(mergedText, mergedVector, { alpha: clampAlpha(hybrid.alpha) });
9633
- } else {
9634
- fused = mergedText;
9635
- }
9636
- const truncated = fused.slice(offset, depth);
9637
- const mergedFacets = allFacets.length > 0 ? mergeDistributedFacets(allFacets, allFacetBounds, facetSize) : null;
9638
- let cursor = null;
9639
- if (truncated.length > 0) {
9640
- const lastEntry = truncated[truncated.length - 1];
9641
- cursor = encodePageCursor({ anchor: lastEntry.docId, score: lastEntry.score, sortKey: null, sortSignature: null });
10373
+ return { verifiable, unverifiable };
10374
+ }
10375
+ async function dropUnstoredPinnedEntries(deps, indexName, params, distributed, allocation, projection, documents) {
10376
+ if (params.pinned === void 0 || params.searchAfter !== void 0) {
10377
+ return distributed.scored;
9642
10378
  }
9643
- return {
9644
- scored: truncated,
9645
- totalHits,
9646
- facets: mergedFacets?.facets ?? null,
9647
- facetErrorBounds: mergedFacets?.errorBounds ?? null,
9648
- cursor,
9649
- coverage
9650
- };
10379
+ const pinnedIds = new Set(params.pinned.map((entry) => entry.docId));
10380
+ const { verifiable, unverifiable } = splitPinnedByReachability(pinnedIds, allocation);
10381
+ const stored = projection.kind === "none" ? await readDistributedDocuments(deps.config, deps.nodeId, deps.engine, indexName, verifiable, allocation) : documents;
10382
+ return distributed.scored.filter(
10383
+ (entry) => !pinnedIds.has(entry.docId) || unverifiable.has(entry.docId) || stored.has(entry.docId)
10384
+ );
9651
10385
  }
9652
10386
  function strictScatterGroups(allocation, indexName) {
9653
10387
  const routing = selectReplicasForQuery(allocation, randomSelector);
@@ -9660,29 +10394,10 @@ function strictScatterGroups(allocation, indexName) {
9660
10394
  }
9661
10395
  return Array.from(routing.nodeToPartitions.entries(), ([nodeId, partitionIds]) => ({ nodeId, partitionIds }));
9662
10396
  }
9663
- var READ_FAILURE_BY_TRANSPORT_CODE = {
9664
- [TransportErrorCodes.CONNECT_FAILED]: ErrorCodes.QUERY_NO_ACTIVE_REPLICA,
9665
- [TransportErrorCodes.PEER_UNAVAILABLE]: ErrorCodes.QUERY_NO_ACTIVE_REPLICA,
9666
- [TransportErrorCodes.TIMEOUT]: ErrorCodes.QUERY_NODE_TIMEOUT,
9667
- [TransportErrorCodes.MESSAGE_TOO_LARGE]: ErrorCodes.QUERY_ROUTING_FAILED,
9668
- [TransportErrorCodes.DECODE_FAILED]: ErrorCodes.QUERY_ROUTING_FAILED
9669
- };
9670
- function readFailure(error, targetNodeId, indexName) {
9671
- if (error instanceof NarsilError) {
9672
- return error;
9673
- }
9674
- if (error instanceof TransportError) {
9675
- return new NarsilError(
9676
- READ_FAILURE_BY_TRANSPORT_CODE[error.code],
9677
- `Node '${targetNodeId}' did not answer a read request for index '${indexName}'`,
9678
- { indexName, targetNodeId, transportCode: error.code }
9679
- );
9680
- }
9681
- return error;
9682
- }
9683
10397
  async function sendReadRequest(deps, targetNodeId, message, indexName, validate) {
9684
- const response = await sendToNode2(deps.config, targetNodeId, message).catch((error) => {
9685
- throw readFailure(error, targetNodeId, indexName);
10398
+ const response = await sendToNode2(deps.config, targetNodeId, message, {
10399
+ message: `Node '${targetNodeId}' did not answer a read request for index '${indexName}'`,
10400
+ details: { indexName }
9686
10401
  });
9687
10402
  const decoded = decode(response.payload);
9688
10403
  if (response.type.endsWith(".error") || response.type === "error" || decoded.error === true) {
@@ -9704,7 +10419,7 @@ async function gatherPartitionCounts(deps, indexName, allocation) {
9704
10419
  groups.map(async (group) => {
9705
10420
  if (group.nodeId === deps.nodeId) {
9706
10421
  const requested = new Set(group.partitionIds);
9707
- const partitions = deps.engine.getPartitionStats(indexName).filter((partition) => requested.has(partition.partitionId)).map((partition) => ({
10422
+ const partitions = (await deps.engine.partitionStatsForRead(indexName)).filter((partition) => requested.has(partition.partitionId)).map((partition) => ({
9708
10423
  partitionId: partition.partitionId,
9709
10424
  documentCount: partition.documentCount,
9710
10425
  estimatedMemoryBytes: partition.estimatedMemoryBytes
@@ -9782,7 +10497,7 @@ async function statsCluster(deps, indexName) {
9782
10497
  async function partitionStatsCluster(deps, indexName) {
9783
10498
  const allocation = await routableAllocation(deps.config.coordinator, indexName);
9784
10499
  if (allocation === null) {
9785
- return deps.engine.getPartitionStats(indexName);
10500
+ return deps.engine.partitionStatsForRead(indexName);
9786
10501
  }
9787
10502
  const { countsByPartition } = await gatherPartitionCounts(deps, indexName, allocation);
9788
10503
  return Array.from(countsByPartition.entries()).map(([partitionId, entry]) => ({
@@ -9841,8 +10556,9 @@ async function listCluster(deps, indexName, params) {
9841
10556
  const startTime = now();
9842
10557
  const limit = clampListLimit(params.limit);
9843
10558
  const signature = sortSignatureOf(params.sort);
10559
+ const binding = listBindingOf(params.filters);
9844
10560
  if (params.cursor !== void 0) {
9845
- requireMatchingCursor(decodePageCursor(params.cursor), params.cursor, signature, false);
10561
+ requireMatchingCursor(decodePageCursor(params.cursor), params.cursor, signature, false, binding);
9846
10562
  }
9847
10563
  const normalizedSort = normalizeSort(params.sort);
9848
10564
  const sortFieldNames = normalizedSort.length === 0 ? null : normalizedSort.map((entry) => entry.field);
@@ -9905,7 +10621,8 @@ async function listCluster(deps, indexName, params) {
9905
10621
  anchor: last.entry.docId,
9906
10622
  score: null,
9907
10623
  sortKey: sortFieldNames === null ? null : [...last.key ?? []],
9908
- sortSignature: signature
10624
+ sortSignature: signature,
10625
+ binding
9909
10626
  });
9910
10627
  }
9911
10628
  const documents = page.map((item) => ({
@@ -10032,6 +10749,7 @@ async function queryCluster(deps, indexName, params) {
10032
10749
  resolveNodeTargets: deps.resolveNodeTargets
10033
10750
  };
10034
10751
  const distributed = await distributedQuery(indexName, wireParams, queryDeps, routingConfig(deps.config.query));
10752
+ const projection = resolveProjection(params.document);
10035
10753
  const documents = await fetchDistributedDocuments(
10036
10754
  deps.config,
10037
10755
  deps.nodeId,
@@ -10039,9 +10757,23 @@ async function queryCluster(deps, indexName, params) {
10039
10757
  indexName,
10040
10758
  distributed,
10041
10759
  allocation,
10042
- resolveProjection(params.document)
10760
+ projection
10043
10761
  );
10044
- return distributedResultToLocal(distributed, documents);
10762
+ const scored = await dropUnstoredPinnedEntries(
10763
+ deps,
10764
+ indexName,
10765
+ params,
10766
+ distributed,
10767
+ allocation,
10768
+ projection,
10769
+ documents
10770
+ );
10771
+ const result = distributedResultToLocal({ ...distributed, scored }, documents);
10772
+ const groups = await assembleDistributedGroups(deps, indexName, params, distributed, allocation, projection);
10773
+ if (groups !== void 0) {
10774
+ result.groups = groups;
10775
+ }
10776
+ return result;
10045
10777
  }
10046
10778
 
10047
10779
  // src/distribution/cluster-node/clear.ts
@@ -10053,391 +10785,234 @@ async function clearCluster(readDeps, writeDeps, indexName) {
10053
10785
  }
10054
10786
  for (; ; ) {
10055
10787
  const page = await listCluster(readDeps, indexName, { limit: CLEAR_PAGE_SIZE, document: false });
10056
- if (page.documents.length === 0) {
10057
- return;
10058
- }
10059
- const result = await routeRemoveBatch(
10060
- indexName,
10061
- page.documents.map((listed) => listed.id),
10062
- writeDeps
10063
- );
10064
- if (result.succeeded.length === 0) {
10065
- const firstFailure = result.failed[0];
10066
- throw new NarsilError(
10067
- ErrorCodes.QUERY_ROUTING_FAILED,
10068
- `Clearing index '${indexName}' stalled: no document in the current page could be removed`,
10069
- {
10070
- indexName,
10071
- remaining: page.total,
10072
- firstFailureDocId: firstFailure?.docId,
10073
- firstFailureCode: firstFailure?.error.code,
10074
- firstFailureMessage: firstFailure?.error.message
10075
- }
10076
- );
10077
- }
10078
- }
10079
- }
10080
-
10081
- // src/distribution/cluster-node/node-operations.ts
10082
- function createClusterNodeOperations(deps) {
10083
- const { trackOp, readDeps, writeDeps, engine, coordinator } = deps;
10084
- return {
10085
- async createIndex(name, indexConfig, options) {
10086
- return trackOp(name, () => routeCreateIndex(name, indexConfig, options, coordinator, engine));
10087
- },
10088
- async insert(indexName, document, docId, options) {
10089
- return trackOp(indexName, () => routeInsert(indexName, document, docId, writeDeps, options));
10090
- },
10091
- async insertBatch(indexName, documents, options) {
10092
- return trackOp(indexName, () => routeInsertBatch(indexName, documents, writeDeps, options));
10093
- },
10094
- async remove(indexName, docId) {
10095
- return trackOp(indexName, () => routeRemove(indexName, docId, writeDeps));
10096
- },
10097
- async removeBatch(indexName, docIds) {
10098
- return trackOp(indexName, () => routeRemoveBatch(indexName, docIds, writeDeps));
10099
- },
10100
- async update(indexName, docId, document) {
10101
- return trackOp(indexName, () => routeUpdate(indexName, docId, document, writeDeps));
10102
- },
10103
- async updateBatch(indexName, updates) {
10104
- return trackOp(indexName, () => routeUpdateBatch(indexName, updates, writeDeps));
10105
- },
10106
- async dropIndex(name) {
10107
- return trackOp(null, async () => {
10108
- await routeDropIndex(name, coordinator, engine);
10109
- deps.forgetIndex(name);
10110
- });
10111
- },
10112
- async clear(indexName) {
10113
- return trackOp(indexName, () => clearCluster(readDeps, writeDeps, indexName));
10114
- },
10115
- async countDocuments(indexName) {
10116
- return trackOp(indexName, () => countCluster(readDeps, indexName));
10117
- },
10118
- async listDocuments(indexName, params) {
10119
- return trackOp(indexName, () => listCluster(readDeps, indexName, params ?? {}));
10120
- },
10121
- async suggest(indexName, params) {
10122
- return trackOp(indexName, () => suggestCluster(readDeps, indexName, params));
10123
- },
10124
- async preflight(indexName, params) {
10125
- return trackOp(indexName, () => preflightCluster(readDeps, indexName, params));
10126
- },
10127
- async getStats(indexName) {
10128
- return trackOp(indexName, () => statsCluster(readDeps, indexName));
10129
- },
10130
- async getPartitionStats(indexName) {
10131
- return trackOp(indexName, () => partitionStatsCluster(readDeps, indexName));
10132
- },
10133
- async checkpoint(indexName) {
10134
- return trackOp(indexName, () => engine.checkpoint(indexName));
10135
- },
10136
- async getMemoryStats() {
10137
- return trackOp(null, () => engine.getMemoryStats());
10138
- },
10139
- on(event, handler) {
10140
- engine.on(event, handler);
10141
- },
10142
- off(event, handler) {
10143
- engine.off(event, handler);
10144
- },
10145
- async query(indexName, params) {
10146
- return trackOp(indexName, () => queryCluster(readDeps, indexName, params));
10147
- },
10148
- async get(indexName, docId) {
10149
- return trackOp(indexName, async () => (await readClusterDocuments(readDeps, indexName, [docId])).get(docId));
10150
- },
10151
- async getMultiple(indexName, docIds) {
10152
- return trackOp(indexName, () => readClusterDocuments(readDeps, indexName, docIds));
10153
- },
10154
- async has(indexName, docId) {
10155
- return trackOp(indexName, async () => (await readClusterDocuments(readDeps, indexName, [docId])).has(docId));
10156
- }
10157
- };
10158
- }
10159
-
10160
- // src/distribution/cluster-node/primary-partition.ts
10161
- async function preparePrimaryPartition(indexName, partitionId, deps) {
10162
- const schema = await deps.coordinator.getSchema(indexName);
10163
- if (schema === null) {
10164
- return false;
10165
- }
10166
- const allocation = await deps.coordinator.getAllocation(indexName);
10167
- if (allocation === null || allocation.assignments.size === 0) {
10168
- return false;
10169
- }
10170
- const existing = deps.engine.listIndexes().find((index) => index.name === indexName);
10171
- if (existing === void 0) {
10172
- try {
10173
- await deps.engine.createIndex(indexName, {
10174
- schema,
10175
- partitions: { maxPartitions: allocation.assignments.size }
10176
- });
10177
- } catch (error) {
10178
- if (!(error instanceof NarsilError) || error.code !== ErrorCodes.INDEX_ALREADY_EXISTS) {
10179
- deps.onError(error);
10180
- return false;
10181
- }
10182
- }
10183
- } else {
10184
- const schemaError = validateRestoredSchema(deps.engine, indexName, deps.nodeId, schema);
10185
- if (schemaError !== null) {
10186
- deps.onError(schemaError);
10187
- return false;
10188
- }
10189
- }
10190
- resumeNumberingAboveOwnCopy(indexName, partitionId, allocation.assignments.get(partitionId), deps);
10191
- return true;
10192
- }
10193
- function resumeNumberingAboveOwnCopy(indexName, partitionId, assignment, deps) {
10194
- if (assignment === void 0) {
10195
- return;
10196
- }
10197
- if (deps.replicationLogPosition(indexName, partitionId) > 0) {
10198
- return;
10199
- }
10200
- const floor = Math.max(deps.engine.highestPersistedSeqNoOf(indexName, partitionId), assignment.commitPoint);
10201
- if (floor <= 0) {
10202
- return;
10203
- }
10204
- deps.seedReplicationLog(indexName, partitionId, floor + 1, assignment.primaryTerm);
10205
- }
10206
-
10207
- // src/distribution/replication/types.ts
10208
- var DEFAULT_LOG_RETENTION_BYTES = 268435456;
10209
-
10210
- // src/distribution/replication/log.ts
10211
- var ENTRY_FIXED_OVERHEAD_BYTES2 = 40;
10212
- function estimateEntrySize(entry) {
10213
- return ENTRY_FIXED_OVERHEAD_BYTES2 + entry.indexName.length + entry.documentId.length + (entry.document?.byteLength ?? 0);
10214
- }
10215
- var computeChecksum = computeEntryChecksum;
10216
- function createReplicationLog(partitionId, config) {
10217
- const retentionBytes = config?.logRetentionBytes ?? DEFAULT_LOG_RETENTION_BYTES;
10218
- let nextSeqNo = config?.startSeqNo ?? 1;
10219
- let localLogEnd = nextSeqNo - 1;
10220
- let localLogEndPrimaryTerm = config?.lastPrimaryTerm ?? 0;
10221
- let commitPoint = nextSeqNo - 1;
10222
- let entries = [];
10223
- let headIndex = 0;
10224
- let totalSizeBytes = 0;
10225
- function liveCount() {
10226
- return entries.length - headIndex;
10227
- }
10228
- function compact() {
10229
- if (headIndex > 0) {
10230
- entries = entries.slice(headIndex);
10231
- headIndex = 0;
10232
- }
10233
- }
10234
- function findIndexBySeqNo(seqNo) {
10235
- let low = headIndex;
10236
- let high = entries.length - 1;
10237
- while (low <= high) {
10238
- const mid = low + high >>> 1;
10239
- const midSeqNo = entries[mid].seqNo;
10240
- if (midSeqNo < seqNo) {
10241
- low = mid + 1;
10242
- } else if (midSeqNo > seqNo) {
10243
- high = mid - 1;
10244
- } else {
10245
- return mid;
10246
- }
10247
- }
10248
- return low;
10249
- }
10250
- function evictOldEntries() {
10251
- while (liveCount() > 0 && totalSizeBytes > retentionBytes) {
10252
- totalSizeBytes -= estimateEntrySize(entries[headIndex]);
10253
- entries[headIndex] = null;
10254
- headIndex += 1;
10255
- }
10256
- if (totalSizeBytes < 0) {
10257
- totalSizeBytes = 0;
10258
- }
10259
- if (headIndex >= 256 || headIndex > liveCount()) {
10260
- compact();
10261
- }
10262
- }
10263
- function assertEntryFitsRetention(entry) {
10264
- const entrySize = ENTRY_FIXED_OVERHEAD_BYTES2 + entry.indexName.length + entry.documentId.length + (entry.document?.byteLength ?? 0);
10265
- if (entrySize > retentionBytes) {
10266
- throw new NarsilError(
10267
- ErrorCodes.REPLICATION_LOG_FULL,
10268
- `Replication log entry of ${entrySize} bytes exceeds the log retention limit of ${retentionBytes} bytes and cannot be buffered`,
10269
- { partitionId, entrySize, retentionBytes, seqNo: entry.seqNo }
10270
- );
10271
- }
10272
- }
10273
- function storeEntry(entry) {
10274
- entries.push(entry);
10275
- if (entry.seqNo >= localLogEnd) {
10276
- localLogEnd = entry.seqNo;
10277
- localLogEndPrimaryTerm = entry.primaryTerm;
10278
- }
10279
- totalSizeBytes += estimateEntrySize(entry);
10280
- evictOldEntries();
10281
- return { ...entry };
10282
- }
10283
- function readEntry(seqNo) {
10284
- if (liveCount() === 0) return void 0;
10285
- const idx = findIndexBySeqNo(seqNo);
10286
- if (idx < entries.length && entries[idx].seqNo === seqNo) {
10287
- return { ...entries[idx] };
10288
- }
10289
- return void 0;
10290
- }
10291
- function verifyEntryChecksum(entry) {
10292
- const expected = computeChecksum({
10293
- seqNo: entry.seqNo,
10294
- primaryTerm: entry.primaryTerm,
10295
- operation: entry.operation,
10296
- partitionId: entry.partitionId,
10297
- indexName: entry.indexName,
10298
- documentId: entry.documentId,
10299
- document: entry.document
10300
- });
10301
- return expected === entry.checksum;
10302
- }
10303
- return {
10304
- append(partial) {
10305
- assertEntryFitsRetention(partial);
10306
- const seqNo = nextSeqNo;
10307
- nextSeqNo += 1;
10308
- const entryWithoutChecksum = {
10309
- seqNo,
10310
- primaryTerm: partial.primaryTerm,
10311
- operation: partial.operation,
10312
- partitionId,
10313
- indexName: partial.indexName,
10314
- documentId: partial.documentId,
10315
- document: partial.document
10316
- };
10317
- const checksum = computeChecksum(entryWithoutChecksum);
10318
- const entry = { ...entryWithoutChecksum, checksum };
10319
- return storeEntry(entry);
10320
- },
10321
- appendCommitted(entry) {
10322
- if (!Number.isSafeInteger(entry.seqNo) || entry.seqNo < 1) {
10323
- throw new NarsilError(
10324
- ErrorCodes.REPLICATION_ENTRY_INVALID,
10325
- `Invalid replication sequence number ${entry.seqNo}`,
10326
- {
10327
- seqNo: entry.seqNo,
10328
- partitionId
10329
- }
10330
- );
10331
- }
10332
- if (entry.partitionId !== partitionId) {
10333
- throw new NarsilError(
10334
- ErrorCodes.REPLICATION_ENTRY_INVALID,
10335
- `Replication entry partition ${entry.partitionId} does not match log partition ${partitionId}`,
10336
- { entryPartitionId: entry.partitionId, logPartitionId: partitionId }
10337
- );
10338
- }
10339
- const existing = readEntry(entry.seqNo);
10340
- if (existing !== void 0) {
10341
- if (existing.checksum !== entry.checksum) {
10342
- throw new NarsilError(
10343
- ErrorCodes.REPLICATION_ENTRY_INVALID,
10344
- `Conflicting replication entry at sequence number ${entry.seqNo}`,
10345
- { seqNo: entry.seqNo, partitionId }
10346
- );
10788
+ if (page.documents.length === 0) {
10789
+ return;
10790
+ }
10791
+ const result = await routeRemoveBatch(
10792
+ indexName,
10793
+ page.documents.map((listed) => listed.id),
10794
+ writeDeps
10795
+ );
10796
+ if (result.succeeded.length === 0) {
10797
+ const firstFailure = result.failed[0];
10798
+ throw new NarsilError(
10799
+ ErrorCodes.QUERY_ROUTING_FAILED,
10800
+ `Clearing index '${indexName}' stalled: no document in the current page could be removed`,
10801
+ {
10802
+ indexName,
10803
+ remaining: page.total,
10804
+ firstFailureDocId: firstFailure?.docId,
10805
+ firstFailureCode: firstFailure?.error.code,
10806
+ firstFailureMessage: firstFailure?.error.message
10347
10807
  }
10348
- return existing;
10349
- }
10350
- if (entry.seqNo !== nextSeqNo) {
10351
- throw new NarsilError(
10352
- ErrorCodes.REPLICATION_ENTRY_INVALID,
10353
- `Out-of-order replication entry ${entry.seqNo}; expected ${nextSeqNo}`,
10354
- { seqNo: entry.seqNo, expectedSeqNo: nextSeqNo, partitionId }
10355
- );
10356
- }
10357
- if (!verifyEntryChecksum(entry)) {
10358
- throw new NarsilError(
10359
- ErrorCodes.REPLICATION_ENTRY_CORRUPT,
10360
- `Invalid checksum for replication entry ${entry.seqNo}`,
10361
- { seqNo: entry.seqNo, partitionId }
10362
- );
10363
- }
10364
- assertEntryFitsRetention(entry);
10365
- nextSeqNo = entry.seqNo + 1;
10366
- return storeEntry({ ...entry });
10808
+ );
10809
+ }
10810
+ }
10811
+ }
10812
+
10813
+ // src/distribution/cluster-node/node-operations.ts
10814
+ function createClusterNodeOperations(deps) {
10815
+ const { trackOp, readDeps, writeDeps, engine, coordinator } = deps;
10816
+ return {
10817
+ async createIndex(name, indexConfig, options) {
10818
+ return trackOp(name, () => routeCreateIndex(name, indexConfig, options, coordinator, engine));
10367
10819
  },
10368
- getEntriesFrom(fromSeqNo) {
10369
- if (liveCount() === 0) return [];
10370
- const idx = findIndexBySeqNo(fromSeqNo);
10371
- if (idx >= entries.length) return [];
10372
- return entries.slice(idx).map((e) => ({ ...e }));
10820
+ async insert(indexName, document, docId, options) {
10821
+ return trackOp(indexName, () => routeInsert(indexName, document, docId, writeDeps, options));
10373
10822
  },
10374
- getEntry(seqNo) {
10375
- return readEntry(seqNo);
10823
+ async insertBatch(indexName, documents, options) {
10824
+ return trackOp(indexName, () => routeInsertBatch(indexName, documents, writeDeps, options));
10376
10825
  },
10377
- verifyChecksum(entry) {
10378
- return verifyEntryChecksum(entry);
10826
+ async remove(indexName, docId) {
10827
+ return trackOp(indexName, () => routeRemove(indexName, docId, writeDeps));
10379
10828
  },
10380
- get localLogEnd() {
10381
- return localLogEnd;
10829
+ async removeBatch(indexName, docIds) {
10830
+ return trackOp(indexName, () => routeRemoveBatch(indexName, docIds, writeDeps));
10382
10831
  },
10383
- get localLogEndPrimaryTerm() {
10384
- return localLogEndPrimaryTerm;
10832
+ async update(indexName, docId, document) {
10833
+ return trackOp(indexName, () => routeUpdate(indexName, docId, document, writeDeps));
10385
10834
  },
10386
- get commitPoint() {
10387
- return commitPoint;
10835
+ async updateBatch(indexName, updates) {
10836
+ return trackOp(indexName, () => routeUpdateBatch(indexName, updates, writeDeps));
10388
10837
  },
10389
- advanceCommitPoint(seqNo) {
10390
- const bounded = Math.min(seqNo, localLogEnd);
10391
- if (bounded > commitPoint) {
10392
- commitPoint = bounded;
10393
- }
10838
+ async dropIndex(name) {
10839
+ return deps.transitionIndex(name, async () => {
10840
+ await routeDropIndex(name, coordinator, engine);
10841
+ deps.forgetIndex(name);
10842
+ });
10394
10843
  },
10395
- get oldestSeqNo() {
10396
- if (liveCount() === 0) return void 0;
10397
- return entries[headIndex].seqNo;
10844
+ async clear(indexName) {
10845
+ return trackOp(indexName, () => clearCluster(readDeps, writeDeps, indexName));
10398
10846
  },
10399
- get newestSeqNo() {
10400
- if (liveCount() === 0) return void 0;
10401
- return entries[entries.length - 1].seqNo;
10847
+ async countDocuments(indexName) {
10848
+ return trackOp(indexName, () => countCluster(readDeps, indexName));
10402
10849
  },
10403
- get entryCount() {
10404
- return liveCount();
10850
+ async listDocuments(indexName, params) {
10851
+ return trackOp(indexName, () => listCluster(readDeps, indexName, params ?? {}));
10405
10852
  },
10406
- get sizeBytes() {
10407
- return totalSizeBytes;
10853
+ async suggest(indexName, params) {
10854
+ return trackOp(indexName, () => suggestCluster(readDeps, indexName, params));
10408
10855
  },
10409
- clear() {
10410
- entries = [];
10411
- headIndex = 0;
10412
- totalSizeBytes = 0;
10413
- localLogEnd = nextSeqNo - 1;
10414
- commitPoint = nextSeqNo - 1;
10856
+ async preflight(indexName, params) {
10857
+ return trackOp(indexName, () => preflightCluster(readDeps, indexName, params));
10858
+ },
10859
+ async getStats(indexName) {
10860
+ return trackOp(indexName, () => statsCluster(readDeps, indexName));
10861
+ },
10862
+ async getPartitionStats(indexName) {
10863
+ return trackOp(indexName, () => partitionStatsCluster(readDeps, indexName));
10864
+ },
10865
+ async checkpoint(indexName) {
10866
+ return trackOp(indexName, () => engine.checkpoint(indexName));
10867
+ },
10868
+ async getMemoryStats() {
10869
+ return trackOp(null, () => engine.getMemoryStats());
10870
+ },
10871
+ on(event, handler) {
10872
+ engine.on(event, handler);
10873
+ },
10874
+ off(event, handler) {
10875
+ engine.off(event, handler);
10876
+ },
10877
+ async query(indexName, params) {
10878
+ return trackOp(indexName, () => queryCluster(readDeps, indexName, params));
10879
+ },
10880
+ async get(indexName, docId) {
10881
+ return trackOp(indexName, async () => (await readClusterDocuments(readDeps, indexName, [docId])).get(docId));
10882
+ },
10883
+ async getMultiple(indexName, docIds) {
10884
+ return trackOp(indexName, () => readClusterDocuments(readDeps, indexName, docIds));
10885
+ },
10886
+ async has(indexName, docId) {
10887
+ return trackOp(indexName, async () => (await readClusterDocuments(readDeps, indexName, [docId])).has(docId));
10415
10888
  }
10416
10889
  };
10417
10890
  }
10418
10891
 
10419
- // src/distribution/cluster-node/replication-logs.ts
10420
- function replicationLogKey(indexName, partitionId) {
10421
- return `${indexName}:${partitionId}`;
10892
+ // src/distribution/cluster-node/operation-tracker.ts
10893
+ function createClusterOperationTracker(config) {
10894
+ let activeOperations = 0;
10895
+ let drainResolve = null;
10896
+ const activeByIndex = /* @__PURE__ */ new Map();
10897
+ const indexDrainResolvers = /* @__PURE__ */ new Map();
10898
+ const transitions = /* @__PURE__ */ new Map();
10899
+ function releaseIndex(indexName) {
10900
+ const remaining = (activeByIndex.get(indexName) ?? 1) - 1;
10901
+ if (remaining > 0) {
10902
+ activeByIndex.set(indexName, remaining);
10903
+ return;
10904
+ }
10905
+ activeByIndex.delete(indexName);
10906
+ for (const resolve of indexDrainResolvers.get(indexName) ?? []) resolve();
10907
+ indexDrainResolvers.delete(indexName);
10908
+ }
10909
+ async function track(indexName, action) {
10910
+ config.guard();
10911
+ if (indexName !== null) config.assertIndex(indexName);
10912
+ activeOperations += 1;
10913
+ let indexCounted = false;
10914
+ try {
10915
+ if (indexName !== null) {
10916
+ const transition2 = transitions.get(indexName);
10917
+ if (transition2 !== void 0) await transition2;
10918
+ config.assertIndex(indexName);
10919
+ activeByIndex.set(indexName, (activeByIndex.get(indexName) ?? 0) + 1);
10920
+ indexCounted = true;
10921
+ }
10922
+ return await action();
10923
+ } finally {
10924
+ if (indexName !== null && indexCounted) releaseIndex(indexName);
10925
+ activeOperations -= 1;
10926
+ if (activeOperations === 0 && drainResolve !== null) {
10927
+ drainResolve();
10928
+ drainResolve = null;
10929
+ }
10930
+ }
10931
+ }
10932
+ async function waitForIndexDrain(indexName) {
10933
+ if ((activeByIndex.get(indexName) ?? 0) === 0) return;
10934
+ await new Promise((resolve) => {
10935
+ let resolvers = indexDrainResolvers.get(indexName);
10936
+ if (resolvers === void 0) {
10937
+ resolvers = /* @__PURE__ */ new Set();
10938
+ indexDrainResolvers.set(indexName, resolvers);
10939
+ }
10940
+ resolvers.add(resolve);
10941
+ });
10942
+ }
10943
+ function transition(indexName, action) {
10944
+ return track(null, async () => {
10945
+ const previous = transitions.get(indexName) ?? Promise.resolve();
10946
+ const run = previous.catch(() => void 0).then(async () => {
10947
+ await waitForIndexDrain(indexName);
10948
+ return action();
10949
+ });
10950
+ const tail = run.then(
10951
+ () => void 0,
10952
+ () => void 0
10953
+ );
10954
+ transitions.set(indexName, tail);
10955
+ try {
10956
+ return await run;
10957
+ } finally {
10958
+ if (transitions.get(indexName) === tail) transitions.delete(indexName);
10959
+ }
10960
+ });
10961
+ }
10962
+ async function drain() {
10963
+ if (activeOperations === 0) return;
10964
+ await new Promise((resolve) => {
10965
+ drainResolve = resolve;
10966
+ });
10967
+ }
10968
+ return { track, transition, drain };
10422
10969
  }
10423
- function getReplicationLog(replicationLogs, indexName, partitionId, replicationConfig) {
10424
- const key = replicationLogKey(indexName, partitionId);
10425
- let log = replicationLogs.get(key);
10426
- if (log === void 0) {
10427
- log = createReplicationLog(partitionId, { logRetentionBytes: replicationConfig?.logRetentionBytes });
10428
- replicationLogs.set(key, log);
10970
+
10971
+ // src/distribution/cluster-node/primary-partition.ts
10972
+ async function preparePrimaryPartition(indexName, partitionId, deps) {
10973
+ const schema = await deps.coordinator.getSchema(indexName);
10974
+ if (schema === null) {
10975
+ return false;
10429
10976
  }
10430
- return log;
10977
+ const allocation = await deps.coordinator.getAllocation(indexName);
10978
+ if (allocation === null || allocation.assignments.size === 0) {
10979
+ return false;
10980
+ }
10981
+ const existing = deps.engine.listIndexes().find((index) => index.name === indexName);
10982
+ if (existing === void 0) {
10983
+ try {
10984
+ await deps.engine.createIndex(indexName, {
10985
+ schema,
10986
+ partitions: { maxPartitions: allocation.assignments.size }
10987
+ });
10988
+ } catch (error) {
10989
+ if (!(error instanceof NarsilError) || error.code !== ErrorCodes.INDEX_ALREADY_EXISTS) {
10990
+ deps.onError(error);
10991
+ return false;
10992
+ }
10993
+ }
10994
+ } else {
10995
+ const schemaError = validateRestoredSchema(deps.engine, indexName, deps.nodeId, schema);
10996
+ if (schemaError !== null) {
10997
+ deps.onError(schemaError);
10998
+ return false;
10999
+ }
11000
+ }
11001
+ resumeNumberingAboveOwnCopy(indexName, partitionId, allocation.assignments.get(partitionId), deps);
11002
+ return true;
10431
11003
  }
10432
- function seedReplicationLog(replicationLogs, indexName, partitionId, startSeqNo, lastPrimaryTerm = 0, replicationConfig) {
10433
- replicationLogs.set(
10434
- replicationLogKey(indexName, partitionId),
10435
- createReplicationLog(partitionId, {
10436
- startSeqNo,
10437
- lastPrimaryTerm,
10438
- logRetentionBytes: replicationConfig?.logRetentionBytes
10439
- })
10440
- );
11004
+ function resumeNumberingAboveOwnCopy(indexName, partitionId, assignment, deps) {
11005
+ if (assignment === void 0) {
11006
+ return;
11007
+ }
11008
+ if (deps.replicationLogPosition(indexName, partitionId) > 0) {
11009
+ return;
11010
+ }
11011
+ const floor = Math.max(deps.engine.highestPersistedSeqNoOf(indexName, partitionId), assignment.commitPoint);
11012
+ if (floor <= 0) {
11013
+ return;
11014
+ }
11015
+ deps.seedReplicationLog(indexName, partitionId, floor + 1, assignment.primaryTerm);
10441
11016
  }
10442
11017
  var CONTROLLER_MESSAGE_TYPES = /* @__PURE__ */ new Set([
10443
11018
  ReplicationMessageTypes.INSYNC_ADD,
@@ -10608,10 +11183,25 @@ async function createClusterNode(config) {
10608
11183
  const nodeId = config.nodeId ?? generateId();
10609
11184
  const roles = config.roles ?? ["data", "coordinator", "controller"];
10610
11185
  const capacity = config.capacity ?? DEFAULT_CAPACITY;
10611
- const engine = await createClusterLocalEngine(config.engine);
10612
11186
  const bootstrapSyncState = createBootstrapSyncState();
10613
11187
  const snapshotSyncHandlerState = createSnapshotSyncHandlerState();
10614
11188
  const replicationLogs = /* @__PURE__ */ new Map();
11189
+ let localEngine = null;
11190
+ const engine = await createClusterLocalEngine(config.engine, {
11191
+ async onIndexOpen(indexName) {
11192
+ if (localEngine === null) return;
11193
+ await seedReopenedPrimaryLogs({
11194
+ indexName,
11195
+ nodeId,
11196
+ engine: localEngine,
11197
+ coordinator: config.coordinator,
11198
+ replicationLogs,
11199
+ replicationConfig: config.replication
11200
+ });
11201
+ },
11202
+ onIndexClose: (indexName) => deleteIndexReplicationLogs(replicationLogs, indexName)
11203
+ });
11204
+ localEngine = engine;
10615
11205
  function replicationLogKey2(indexName, partitionId) {
10616
11206
  return replicationLogKey(indexName, partitionId);
10617
11207
  }
@@ -10661,8 +11251,6 @@ async function createClusterNode(config) {
10661
11251
  let unregisterNotReadyHandler = null;
10662
11252
  let serving = false;
10663
11253
  let isShutdown = false;
10664
- let activeOps = 0;
10665
- let drainResolve = null;
10666
11254
  const bootstrapSyncDeps = {
10667
11255
  engine,
10668
11256
  coordinator: config.coordinator,
@@ -10735,22 +11323,14 @@ async function createClusterNode(config) {
10735
11323
  throw new NarsilError(ErrorCodes.NODE_NOT_JOINED, `Cluster node '${nodeId}' has been shut down`, { nodeId });
10736
11324
  }
10737
11325
  }
10738
- async function trackOp(indexName, fn) {
10739
- guardShutdown();
10740
- if (indexName !== null && orphanedIndexes.has(indexName)) {
10741
- throw orphanedIndexError(nodeId, indexName);
10742
- }
10743
- activeOps++;
10744
- try {
10745
- return await fn();
10746
- } finally {
10747
- activeOps--;
10748
- if (activeOps === 0 && drainResolve !== null) {
10749
- drainResolve();
10750
- drainResolve = null;
10751
- }
11326
+ const operationTracker = createClusterOperationTracker({
11327
+ guard: guardShutdown,
11328
+ assertIndex(indexName) {
11329
+ if (orphanedIndexes.has(indexName)) throw orphanedIndexError(nodeId, indexName);
10752
11330
  }
10753
- }
11331
+ });
11332
+ const trackOp = operationTracker.track;
11333
+ const transitionIndex = operationTracker.transition;
10754
11334
  const readDeps = { config, nodeId, engine, resolveNodeTargets: resolveNodeTargets3 };
10755
11335
  const cluster = createClusterNamespace({
10756
11336
  nodeId,
@@ -10769,13 +11349,17 @@ async function createClusterNode(config) {
10769
11349
  get roles() {
10770
11350
  return roles;
10771
11351
  },
11352
+ open: (indexName) => trackOp(indexName, () => engine.open(indexName)),
11353
+ close: (indexName) => transitionIndex(indexName, () => engine.close(indexName)),
10772
11354
  ...createClusterNodeOperations({
10773
11355
  trackOp,
11356
+ transitionIndex,
10774
11357
  readDeps,
10775
11358
  writeDeps,
10776
11359
  engine,
10777
11360
  coordinator: config.coordinator,
10778
11361
  forgetIndex: (indexName) => {
11362
+ deleteIndexReplicationLogs(replicationLogs, indexName);
10779
11363
  if (engine.listIndexes().every((index) => index.name !== indexName)) {
10780
11364
  orphanedIndexes.delete(indexName);
10781
11365
  }
@@ -10825,11 +11409,7 @@ async function createClusterNode(config) {
10825
11409
  }
10826
11410
  isShutdown = true;
10827
11411
  serving = false;
10828
- if (activeOps > 0) {
10829
- await new Promise((resolve) => {
10830
- drainResolve = resolve;
10831
- });
10832
- }
11412
+ await operationTracker.drain();
10833
11413
  if (unregisterHandler !== null) {
10834
11414
  unregisterHandler();
10835
11415
  unregisterHandler = null;
@@ -10852,14 +11432,20 @@ async function createClusterNode(config) {
10852
11432
  if (partitionId === null) {
10853
11433
  return fallback;
10854
11434
  }
10855
- const allocation = await config.coordinator.getAllocation(indexName);
10856
- const assignment = allocation?.assignments.get(partitionId);
10857
- const log = getReplicationLog2(indexName, partitionId);
10858
- return {
10859
- partitionId,
10860
- primaryTerm: assignment?.primaryTerm ?? fallback.primaryTerm,
10861
- lastSeqNo: log.newestSeqNo ?? 0
10862
- };
11435
+ const localIndex = engine.listIndexes().find((index) => index.name === indexName);
11436
+ const release = localIndex !== void 0 ? await engine.acquireIndexForReplication(indexName) : null;
11437
+ try {
11438
+ const allocation = await config.coordinator.getAllocation(indexName);
11439
+ const assignment = allocation?.assignments.get(partitionId);
11440
+ const log = getReplicationLog2(indexName, partitionId);
11441
+ return {
11442
+ partitionId,
11443
+ primaryTerm: assignment?.primaryTerm ?? fallback.primaryTerm,
11444
+ lastSeqNo: log.newestSeqNo ?? 0
11445
+ };
11446
+ } finally {
11447
+ release?.();
11448
+ }
10863
11449
  }
10864
11450
  }
10865
11451
 
@@ -10873,6 +11459,12 @@ function unsupported(operation) {
10873
11459
  }
10874
11460
  function clusterNodeEngine(node, options) {
10875
11461
  return {
11462
+ open(indexName) {
11463
+ return node.open(indexName);
11464
+ },
11465
+ close(indexName) {
11466
+ return node.close(indexName);
11467
+ },
10876
11468
  async createIndex(name, config) {
10877
11469
  return node.createIndex(name, config, options?.createIndex);
10878
11470
  },