@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.
- package/dist/adapters/filesystem.mjs +1 -1
- package/dist/adapters/indexeddb.mjs +1 -1
- package/dist/{chunk-GB2IIJJG.mjs → chunk-P4ELXVJQ.mjs} +2 -4
- package/dist/{chunk-MMSWEEZT.mjs → chunk-WASNWIEK.mjs} +1452 -373
- package/dist/{chunk-GRWFY2HI.mjs → chunk-XNCJFU2E.mjs} +239 -61
- package/dist/{chunk-WU2UZEVD.mjs → chunk-XWICBGYT.mjs} +1 -1
- package/dist/{client-BobVnoIK.d.ts → client-C5BkUqB8.d.ts} +26 -3
- package/dist/client.d.ts +8 -8
- package/dist/client.mjs +12 -6
- package/dist/{config-fLC4lbmg.d.ts → config-B-ZI-ft5.d.ts} +2 -2
- package/dist/{config-Byyl1QqH.d.ts → config-BZRExglL.d.ts} +20 -3
- package/dist/{direct-executor-MSJEVPZU.mjs → direct-executor-IMXNRS7O.mjs} +2 -2
- package/dist/distribution/coordinator/etcd.d.ts +2 -2
- package/dist/distribution/coordinator/etcd.mjs +1 -1
- package/dist/distribution/coordinator/in-memory.d.ts +2 -2
- package/dist/distribution/transport/grpc.mjs +1 -1
- package/dist/distribution.d.ts +24 -10
- package/dist/distribution.mjs +1721 -1129
- package/dist/embeddings/openai.mjs +1 -1
- package/dist/{engine-BE45D5ZG.d.ts → engine-Cjr1uBLa.d.ts} +47 -24
- package/dist/index.browser.mjs +1668 -416
- package/dist/index.d.ts +9 -9
- package/dist/index.mjs +4 -4
- package/dist/{options-EXYFcem-.d.ts → options-BC68VPoE.d.ts} +10 -3
- package/dist/persistence/durability/checkpoint-worker.d.ts +3 -1
- package/dist/persistence/durability/checkpoint-worker.mjs +4 -4
- package/dist/react.d.ts +6 -6
- package/dist/react.mjs +2 -4
- package/dist/{schema-ZRbk8BgH.d.ts → schema-B7juWg8Q.d.ts} +17 -6
- package/dist/{search-DHuXwJYk.d.ts → search-DDHjfXhJ.d.ts} +35 -11
- package/dist/server.d.ts +10 -10
- package/dist/server.mjs +30 -12
- package/dist/{tasks-CnMLpfk-.d.ts → tasks-Bn4R1NPN.d.ts} +1 -1
- package/dist/{types-B5Tsaalz.d.ts → types-DsSSjY4d.d.ts} +1 -1
- package/dist/workers/entry.mjs +2 -2
- package/package.json +1 -1
package/dist/distribution.mjs
CHANGED
|
@@ -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,
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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/
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
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
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
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
|
-
|
|
4984
|
-
if (
|
|
4985
|
-
|
|
4986
|
-
|
|
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
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
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
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
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
|
-
|
|
5035
|
-
|
|
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
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
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
|
-
|
|
5053
|
-
if (
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
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
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
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
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
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
|
-
|
|
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/
|
|
5108
|
-
|
|
5109
|
-
|
|
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
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
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
|
-
|
|
5070
|
+
return log;
|
|
5125
5071
|
}
|
|
5126
|
-
function
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
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
|
-
|
|
5134
|
-
const
|
|
5135
|
-
|
|
5136
|
-
|
|
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
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
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
|
|
5157
|
-
|
|
5158
|
-
|
|
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/
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
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 (
|
|
5184
|
-
|
|
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
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
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
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
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
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
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
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
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
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
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.
|
|
5255
|
-
`
|
|
5256
|
-
{
|
|
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
|
-
|
|
5260
|
-
|
|
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
|
|
5263
|
-
if (
|
|
5264
|
-
|
|
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
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
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 (
|
|
5272
|
-
|
|
5273
|
-
|
|
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
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
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
|
|
5290
|
-
|
|
5291
|
-
|
|
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
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
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
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
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
|
-
|
|
5303
|
-
}
|
|
5304
|
-
|
|
5305
|
-
|
|
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
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
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/
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6311
|
-
|
|
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
|
|
7492
|
+
group: convertWireGroupToLocal(wire.group),
|
|
7016
7493
|
facets: convertWireFacetConfigToLocal(wire.facets, facetShardSize ?? wire.facetSize),
|
|
7017
|
-
vector: wire.vector
|
|
7018
|
-
|
|
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
|
-
|
|
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 ??
|
|
7113
|
-
k: hybrid.k ??
|
|
7114
|
-
alpha: hybrid.alpha ??
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
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
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
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
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
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
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
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 ?
|
|
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.
|
|
10178
|
+
const isHybrid = params.vector !== null && (params.term !== null || params.mode === "hybrid");
|
|
9457
10179
|
if (isHybrid && params.searchAfter !== null) {
|
|
9458
|
-
throw new NarsilError(
|
|
9459
|
-
|
|
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
|
|
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
|
-
|
|
9551
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
const
|
|
9582
|
-
|
|
9583
|
-
if (
|
|
9584
|
-
|
|
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
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
|
|
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
|
-
|
|
9613
|
-
|
|
9614
|
-
|
|
9615
|
-
|
|
10348
|
+
return result;
|
|
10349
|
+
});
|
|
10350
|
+
}
|
|
10351
|
+
function projectedGroupDocument(document, projection) {
|
|
10352
|
+
if (document === void 0 || projection.kind === "none") {
|
|
10353
|
+
return {};
|
|
9616
10354
|
}
|
|
9617
|
-
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
|
|
9621
|
-
|
|
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
|
-
|
|
9626
|
-
|
|
9627
|
-
|
|
9628
|
-
|
|
9629
|
-
|
|
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
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
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
|
|
9685
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
10760
|
+
projection
|
|
10043
10761
|
);
|
|
10044
|
-
|
|
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
|
-
|
|
10349
|
-
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
|
|
10356
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
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
|
-
|
|
10369
|
-
|
|
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
|
-
|
|
10375
|
-
return
|
|
10823
|
+
async insertBatch(indexName, documents, options) {
|
|
10824
|
+
return trackOp(indexName, () => routeInsertBatch(indexName, documents, writeDeps, options));
|
|
10376
10825
|
},
|
|
10377
|
-
|
|
10378
|
-
return
|
|
10826
|
+
async remove(indexName, docId) {
|
|
10827
|
+
return trackOp(indexName, () => routeRemove(indexName, docId, writeDeps));
|
|
10379
10828
|
},
|
|
10380
|
-
|
|
10381
|
-
return
|
|
10829
|
+
async removeBatch(indexName, docIds) {
|
|
10830
|
+
return trackOp(indexName, () => routeRemoveBatch(indexName, docIds, writeDeps));
|
|
10382
10831
|
},
|
|
10383
|
-
|
|
10384
|
-
return
|
|
10832
|
+
async update(indexName, docId, document) {
|
|
10833
|
+
return trackOp(indexName, () => routeUpdate(indexName, docId, document, writeDeps));
|
|
10385
10834
|
},
|
|
10386
|
-
|
|
10387
|
-
return
|
|
10835
|
+
async updateBatch(indexName, updates) {
|
|
10836
|
+
return trackOp(indexName, () => routeUpdateBatch(indexName, updates, writeDeps));
|
|
10388
10837
|
},
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
|
|
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
|
-
|
|
10396
|
-
|
|
10397
|
-
return entries[headIndex].seqNo;
|
|
10844
|
+
async clear(indexName) {
|
|
10845
|
+
return trackOp(indexName, () => clearCluster(readDeps, writeDeps, indexName));
|
|
10398
10846
|
},
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
return entries[entries.length - 1].seqNo;
|
|
10847
|
+
async countDocuments(indexName) {
|
|
10848
|
+
return trackOp(indexName, () => countCluster(readDeps, indexName));
|
|
10402
10849
|
},
|
|
10403
|
-
|
|
10404
|
-
return
|
|
10850
|
+
async listDocuments(indexName, params) {
|
|
10851
|
+
return trackOp(indexName, () => listCluster(readDeps, indexName, params ?? {}));
|
|
10405
10852
|
},
|
|
10406
|
-
|
|
10407
|
-
return
|
|
10853
|
+
async suggest(indexName, params) {
|
|
10854
|
+
return trackOp(indexName, () => suggestCluster(readDeps, indexName, params));
|
|
10408
10855
|
},
|
|
10409
|
-
|
|
10410
|
-
|
|
10411
|
-
|
|
10412
|
-
|
|
10413
|
-
|
|
10414
|
-
|
|
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/
|
|
10420
|
-
function
|
|
10421
|
-
|
|
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
|
-
|
|
10424
|
-
|
|
10425
|
-
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
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
|
-
|
|
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
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
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
|
-
|
|
10739
|
-
guardShutdown
|
|
10740
|
-
|
|
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
|
-
|
|
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
|
|
10856
|
-
const
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
partitionId
|
|
10860
|
-
|
|
10861
|
-
|
|
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
|
},
|