@delali/narsil 0.1.15 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +54 -0
- package/README.md +38 -982
- package/dist/adapters/filesystem.mjs +1 -1
- package/dist/adapters/indexeddb.mjs +1 -1
- package/dist/{chunk-WISGW24C.mjs → chunk-AUMKFYTG.mjs} +1 -1
- package/dist/{chunk-HRABJLPB.mjs → chunk-BKAI2WXI.mjs} +1 -0
- package/dist/chunk-GHJWC2E5.mjs +9 -0
- package/dist/chunk-KH3YGMVT.mjs +15 -0
- package/dist/{chunk-ALB35PRQ.mjs → chunk-M2EDG55Y.mjs} +8 -0
- package/dist/{chunk-HMO6OMZ3.mjs → chunk-S6IAXOBA.mjs} +1073 -506
- package/dist/chunk-TGP5M5R7.mjs +880 -0
- package/dist/chunk-U2BKYEC7.mjs +254 -0
- package/dist/{chunk-MEW7HD75.mjs → chunk-XAKNSWIK.mjs} +2543 -1154
- package/dist/{config-CojEPOgS.d.ts → config-P27AtSn4.d.ts} +28 -7
- package/dist/direct-executor-2FV3MEDD.mjs +5 -0
- package/dist/distribution/coordinator/etcd.d.ts +2 -2
- package/dist/distribution/coordinator/etcd.mjs +2 -2
- package/dist/distribution/coordinator/in-memory.d.ts +2 -2
- package/dist/distribution.d.ts +10 -4
- package/dist/distribution.mjs +1875 -1772
- package/dist/embeddings/openai.mjs +1 -1
- package/dist/index.browser.mjs +4566 -1783
- package/dist/index.d.ts +14 -5
- package/dist/index.mjs +6 -5
- package/dist/{language-ONQ3iVDf.d.ts → language-CmJBHf1j.d.ts} +4 -1
- package/dist/languages/albanian.d.ts +5 -0
- package/dist/languages/albanian.mjs +103 -0
- package/dist/languages/amharic.d.ts +5 -0
- package/dist/languages/amharic.mjs +240 -0
- package/dist/languages/arabic.d.ts +1 -1
- package/dist/languages/arabic.mjs +939 -154
- package/dist/languages/armenian.d.ts +1 -1
- package/dist/languages/armenian.mjs +275 -243
- package/dist/languages/azerbaijani.d.ts +5 -0
- package/dist/languages/azerbaijani.mjs +152 -0
- package/dist/languages/bambara.d.ts +5 -0
- package/dist/languages/bambara.mjs +60 -0
- package/dist/languages/basque.d.ts +5 -0
- package/dist/languages/basque.mjs +726 -0
- package/dist/languages/belarusian.d.ts +5 -0
- package/dist/languages/belarusian.mjs +100 -0
- package/dist/languages/bengali.d.ts +5 -0
- package/dist/languages/bengali.mjs +128 -0
- package/dist/languages/bosnian.d.ts +5 -0
- package/dist/languages/bosnian.mjs +197 -0
- package/dist/languages/breton.d.ts +5 -0
- package/dist/languages/breton.mjs +96 -0
- package/dist/languages/bulgarian.d.ts +1 -1
- package/dist/languages/bulgarian.mjs +11 -2
- package/dist/languages/burmese.d.ts +5 -0
- package/dist/languages/burmese.mjs +57 -0
- package/dist/languages/catalan.d.ts +5 -0
- package/dist/languages/catalan.mjs +987 -0
- package/dist/languages/chinese.d.ts +1 -1
- package/dist/languages/chinese.mjs +4 -3
- package/dist/languages/croatian.d.ts +5 -0
- package/dist/languages/croatian.mjs +191 -0
- package/dist/languages/czech.d.ts +5 -0
- package/dist/languages/czech.mjs +628 -0
- package/dist/languages/dagbani.d.ts +1 -1
- package/dist/languages/dagbani.mjs +11 -2
- package/dist/languages/danish.d.ts +1 -1
- package/dist/languages/danish.mjs +215 -110
- package/dist/languages/dutch.d.ts +1 -1
- package/dist/languages/dutch.mjs +1248 -163
- package/dist/languages/english.d.ts +1 -1
- package/dist/languages/english.mjs +2 -1
- package/dist/languages/esperanto.d.ts +5 -0
- package/dist/languages/esperanto.mjs +689 -0
- package/dist/languages/estonian.d.ts +5 -0
- package/dist/languages/estonian.mjs +2160 -0
- package/dist/languages/ewe.d.ts +1 -1
- package/dist/languages/ewe.mjs +11 -2
- package/dist/languages/faroese.d.ts +5 -0
- package/dist/languages/faroese.mjs +93 -0
- package/dist/languages/fijian.d.ts +5 -0
- package/dist/languages/fijian.mjs +55 -0
- package/dist/languages/finnish.d.ts +1 -1
- package/dist/languages/finnish.mjs +528 -263
- package/dist/languages/french.d.ts +1 -1
- package/dist/languages/french.mjs +950 -437
- package/dist/languages/ga.d.ts +1 -1
- package/dist/languages/ga.mjs +2 -1
- package/dist/languages/galician.d.ts +5 -0
- package/dist/languages/galician.mjs +172 -0
- package/dist/languages/georgian.d.ts +5 -0
- package/dist/languages/georgian.mjs +91 -0
- package/dist/languages/german.d.ts +1 -1
- package/dist/languages/german.mjs +432 -162
- package/dist/languages/greek.d.ts +1 -1
- package/dist/languages/greek.mjs +1970 -281
- package/dist/languages/guarani.d.ts +5 -0
- package/dist/languages/guarani.mjs +132 -0
- package/dist/languages/gujarati.d.ts +5 -0
- package/dist/languages/gujarati.mjs +234 -0
- package/dist/languages/haitian-creole.d.ts +5 -0
- package/dist/languages/haitian-creole.mjs +106 -0
- package/dist/languages/hausa.d.ts +1 -1
- package/dist/languages/hausa.mjs +2 -1
- package/dist/languages/hawaiian.d.ts +5 -0
- package/dist/languages/hawaiian.mjs +69 -0
- package/dist/languages/hebrew.d.ts +5 -0
- package/dist/languages/hebrew.mjs +213 -0
- package/dist/languages/hindi.d.ts +1 -1
- package/dist/languages/hindi.mjs +175 -97
- package/dist/languages/hungarian.d.ts +1 -1
- package/dist/languages/hungarian.mjs +412 -326
- package/dist/languages/icelandic.d.ts +5 -0
- package/dist/languages/icelandic.mjs +161 -0
- package/dist/languages/igbo.d.ts +1 -1
- package/dist/languages/igbo.mjs +2 -1
- package/dist/languages/indonesian.d.ts +1 -1
- package/dist/languages/indonesian.mjs +329 -137
- package/dist/languages/irish.d.ts +1 -1
- package/dist/languages/irish.mjs +223 -139
- package/dist/languages/italian.d.ts +1 -1
- package/dist/languages/italian.mjs +703 -406
- package/dist/languages/japanese.d.ts +1 -1
- package/dist/languages/japanese.mjs +5 -108
- package/dist/languages/kannada.d.ts +5 -0
- package/dist/languages/kannada.mjs +94 -0
- package/dist/languages/kazakh.d.ts +5 -0
- package/dist/languages/kazakh.mjs +90 -0
- package/dist/languages/khmer.d.ts +5 -0
- package/dist/languages/khmer.mjs +181 -0
- package/dist/languages/kinyarwanda.d.ts +5 -0
- package/dist/languages/kinyarwanda.mjs +99 -0
- package/dist/languages/kirundi.d.ts +5 -0
- package/dist/languages/kirundi.mjs +92 -0
- package/dist/languages/korean.d.ts +5 -0
- package/dist/languages/korean.mjs +252 -0
- package/dist/languages/kurmanji.d.ts +5 -0
- package/dist/languages/kurmanji.mjs +50 -0
- package/dist/languages/kyrgyz.d.ts +5 -0
- package/dist/languages/kyrgyz.mjs +108 -0
- package/dist/languages/lao.d.ts +5 -0
- package/dist/languages/lao.mjs +63 -0
- package/dist/languages/latin.d.ts +5 -0
- package/dist/languages/latin.mjs +69 -0
- package/dist/languages/latvian.d.ts +5 -0
- package/dist/languages/latvian.mjs +173 -0
- package/dist/languages/lingala.d.ts +5 -0
- package/dist/languages/lingala.mjs +61 -0
- package/dist/languages/lithuanian.d.ts +5 -0
- package/dist/languages/lithuanian.mjs +571 -0
- package/dist/languages/luxembourgish.d.ts +5 -0
- package/dist/languages/luxembourgish.mjs +214 -0
- package/dist/languages/macedonian.d.ts +5 -0
- package/dist/languages/macedonian.mjs +808 -0
- package/dist/languages/malagasy.d.ts +5 -0
- package/dist/languages/malagasy.mjs +67 -0
- package/dist/languages/malay.d.ts +5 -0
- package/dist/languages/malay.mjs +482 -0
- package/dist/languages/malayalam.d.ts +5 -0
- package/dist/languages/malayalam.mjs +122 -0
- package/dist/languages/maltese.d.ts +5 -0
- package/dist/languages/maltese.mjs +139 -0
- package/dist/languages/maori.d.ts +5 -0
- package/dist/languages/maori.mjs +79 -0
- package/dist/languages/marathi.d.ts +5 -0
- package/dist/languages/marathi.mjs +111 -0
- package/dist/languages/nepali.d.ts +1 -1
- package/dist/languages/nepali.mjs +209 -99
- package/dist/languages/norwegian.d.ts +1 -1
- package/dist/languages/norwegian.mjs +218 -85
- package/dist/languages/oromo.d.ts +5 -0
- package/dist/languages/oromo.mjs +78 -0
- package/dist/languages/persian.d.ts +5 -0
- package/dist/languages/persian.mjs +710 -0
- package/dist/languages/polish.d.ts +5 -0
- package/dist/languages/polish.mjs +614 -0
- package/dist/languages/portuguese.d.ts +1 -1
- package/dist/languages/portuguese.mjs +613 -380
- package/dist/languages/punjabi.d.ts +5 -0
- package/dist/languages/punjabi.mjs +197 -0
- package/dist/languages/romanian.d.ts +1 -1
- package/dist/languages/romanian.mjs +561 -382
- package/dist/languages/russian.d.ts +1 -1
- package/dist/languages/russian.mjs +373 -244
- package/dist/languages/samoan.d.ts +5 -0
- package/dist/languages/samoan.mjs +63 -0
- package/dist/languages/sanskrit.d.ts +1 -1
- package/dist/languages/sanskrit.mjs +2 -2
- package/dist/languages/scottish-gaelic.d.ts +5 -0
- package/dist/languages/scottish-gaelic.mjs +370 -0
- package/dist/languages/serbian.d.ts +1 -1
- package/dist/languages/serbian.mjs +3635 -155
- package/dist/languages/shona.d.ts +5 -0
- package/dist/languages/shona.mjs +91 -0
- package/dist/languages/sinhala.d.ts +5 -0
- package/dist/languages/sinhala.mjs +182 -0
- package/dist/languages/slovak.d.ts +5 -0
- package/dist/languages/slovak.mjs +430 -0
- package/dist/languages/slovenian.d.ts +1 -1
- package/dist/languages/slovenian.mjs +11 -2
- package/dist/languages/sorani.d.ts +5 -0
- package/dist/languages/sorani.mjs +156 -0
- package/dist/languages/spanish.d.ts +1 -1
- package/dist/languages/spanish.mjs +676 -374
- package/dist/languages/swahili.d.ts +1 -1
- package/dist/languages/swahili.mjs +2 -1
- package/dist/languages/swedish.d.ts +1 -1
- package/dist/languages/swedish.mjs +237 -95
- package/dist/languages/tagalog.d.ts +5 -0
- package/dist/languages/tagalog.mjs +159 -0
- package/dist/languages/tamil.d.ts +1 -1
- package/dist/languages/tamil.mjs +836 -64
- package/dist/languages/tatar.d.ts +5 -0
- package/dist/languages/tatar.mjs +1017 -0
- package/dist/languages/telugu.d.ts +5 -0
- package/dist/languages/telugu.mjs +62 -0
- package/dist/languages/thai.d.ts +5 -0
- package/dist/languages/thai.mjs +65 -0
- package/dist/languages/tibetan.d.ts +5 -0
- package/dist/languages/tibetan.mjs +119 -0
- package/dist/languages/tigrinya.d.ts +5 -0
- package/dist/languages/tigrinya.mjs +194 -0
- package/dist/languages/tongan.d.ts +5 -0
- package/dist/languages/tongan.mjs +56 -0
- package/dist/languages/turkish.d.ts +1 -1
- package/dist/languages/turkish.mjs +1246 -393
- package/dist/languages/twi.d.ts +1 -1
- package/dist/languages/twi.mjs +13 -2
- package/dist/languages/ukrainian.d.ts +1 -1
- package/dist/languages/ukrainian.mjs +125 -47
- package/dist/languages/urdu.d.ts +5 -0
- package/dist/languages/urdu.mjs +528 -0
- package/dist/languages/vietnamese.d.ts +5 -0
- package/dist/languages/vietnamese.mjs +277 -0
- package/dist/languages/welsh.d.ts +5 -0
- package/dist/languages/welsh.mjs +446 -0
- package/dist/languages/wolof.d.ts +5 -0
- package/dist/languages/wolof.mjs +79 -0
- package/dist/languages/xhosa.d.ts +5 -0
- package/dist/languages/xhosa.mjs +95 -0
- package/dist/languages/yoruba.d.ts +1 -1
- package/dist/languages/yoruba.mjs +1 -0
- package/dist/languages/zulu.d.ts +1 -1
- package/dist/languages/zulu.mjs +2 -1
- package/dist/narsil-CMZRPDlQ.d.ts +159 -0
- package/dist/persistence/durability/checkpoint-worker.d.ts +13 -0
- package/dist/persistence/durability/checkpoint-worker.mjs +6 -5
- package/dist/{schema-DCgkO1j4.d.ts → schema-BnvLvjRp.d.ts} +5 -3
- package/dist/server.d.ts +4 -3
- package/dist/server.mjs +13 -2
- package/dist/{types-DHX6zGeD.d.ts → types-BRTcBgAp.d.ts} +1 -1
- package/dist/workers/entry.mjs +2 -2
- package/package.json +10 -3
- package/dist/chunk-TW6BXTOL.mjs +0 -338
- package/dist/direct-executor-E52FFGDE.mjs +0 -4
- package/dist/narsil-D-84Nvm4.d.ts +0 -72
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { getNestedValue, getLanguage, extractVectorFieldsFromSchema, createDirectExecutor, createPartitionRouter, createPartitionManager, createVectorIndex, validateSchema, deserializePayloadV2, deserializePayloadV1, tokenize, fanOutQuery, validateRequiredFields,
|
|
1
|
+
import { getNestedValue, getLanguage, extractVectorFieldsFromSchema, createDirectExecutor, createPartitionRouter, createPartitionManager, createVectorIndex, validateSchema, validateVectorPromotion, resolveIndexAnalysis, deserializePayloadV2, deserializePayloadV1, tokenize, fanOutQuery, validateRequiredFields, mergePartitionStats, createPartitionIndex, flattenSchema, pruneStatsToQueryTerms, fnv1a, detectRuntime, serializeMetadata, isNodeMainThread, spawnNodeWorker, deserializeMetadata, kWayMerge } from './chunk-S6IAXOBA.mjs';
|
|
2
2
|
import { crc32, crc32Init, crc32Update, crc32Final } from './chunk-KMMJMDUA.mjs';
|
|
3
|
-
import { ErrorCodes, NarsilError } from './chunk-
|
|
4
|
-
import { generateId, createRequestId, isValidWorkerResponse } from './chunk-
|
|
3
|
+
import { ErrorCodes, NarsilError } from './chunk-M2EDG55Y.mjs';
|
|
4
|
+
import { generateId, createRequestId, isValidWorkerResponse } from './chunk-BKAI2WXI.mjs';
|
|
5
5
|
import { encode, decode } from '@msgpack/msgpack';
|
|
6
6
|
|
|
7
7
|
// src/partitioning/rebalancer.ts
|
|
8
8
|
var CHUNK_SIZE = 1e3;
|
|
9
9
|
function createRebalancer() {
|
|
10
|
-
|
|
10
|
+
const activeManagers = /* @__PURE__ */ new WeakSet();
|
|
11
|
+
let activeCount = 0;
|
|
11
12
|
async function rebalance(manager, newPartitionCount, router, onProgress) {
|
|
12
|
-
if (
|
|
13
|
+
if (activeManagers.has(manager)) {
|
|
13
14
|
throw new NarsilError(
|
|
14
15
|
ErrorCodes.PARTITION_REBALANCING_BACKPRESSURE,
|
|
15
16
|
"A rebalance operation is already in progress"
|
|
@@ -29,19 +30,14 @@ function createRebalancer() {
|
|
|
29
30
|
{ newPartitionCount, currentPartitionCount: manager.partitionCount }
|
|
30
31
|
);
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
activeManagers.add(manager);
|
|
34
|
+
activeCount++;
|
|
33
35
|
try {
|
|
34
|
-
const collectedDocs = [];
|
|
35
36
|
const currentPartitions = manager.getAllPartitions();
|
|
37
|
+
let documentsTotal = 0;
|
|
36
38
|
for (const partition of currentPartitions) {
|
|
37
|
-
|
|
38
|
-
const document = partition.get(docId);
|
|
39
|
-
if (document) {
|
|
40
|
-
collectedDocs.push({ docId, document });
|
|
41
|
-
}
|
|
42
|
-
}
|
|
39
|
+
documentsTotal += partition.count();
|
|
43
40
|
}
|
|
44
|
-
const documentsTotal = collectedDocs.length;
|
|
45
41
|
onProgress?.({
|
|
46
42
|
phase: "scanning",
|
|
47
43
|
documentsProcessed: 0,
|
|
@@ -52,24 +48,38 @@ function createRebalancer() {
|
|
|
52
48
|
newPartitions.push(createPartitionIndex(i, manager.config.trackPositions ?? true));
|
|
53
49
|
}
|
|
54
50
|
let documentsProcessed = 0;
|
|
51
|
+
let chunkFill = 0;
|
|
55
52
|
const insertOptions = {
|
|
56
53
|
validate: false,
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
skipClone: true,
|
|
55
|
+
stopWordOverride: manager.analysis.stopWords,
|
|
56
|
+
customTokenizer: manager.analysis.customTokenizer,
|
|
59
57
|
collectSurfaces: manager.config.surfaceForms === true
|
|
60
58
|
};
|
|
61
|
-
for (
|
|
62
|
-
const
|
|
63
|
-
for (
|
|
64
|
-
const
|
|
59
|
+
for (const partition of currentPartitions) {
|
|
60
|
+
const docIds = [...partition.docIds()];
|
|
61
|
+
for (const docId of docIds) {
|
|
62
|
+
const document = partition.getRef(docId);
|
|
63
|
+
if (!document) continue;
|
|
65
64
|
const targetPartitionId = router.route(docId, newPartitionCount);
|
|
66
65
|
const targetPartition = newPartitions[targetPartitionId];
|
|
67
|
-
if (targetPartition.has(docId)) {
|
|
68
|
-
|
|
66
|
+
if (!targetPartition.has(docId)) {
|
|
67
|
+
targetPartition.insert(docId, document, manager.schema, manager.language, insertOptions);
|
|
68
|
+
documentsProcessed++;
|
|
69
|
+
}
|
|
70
|
+
chunkFill++;
|
|
71
|
+
if (chunkFill >= CHUNK_SIZE) {
|
|
72
|
+
chunkFill = 0;
|
|
73
|
+
onProgress?.({
|
|
74
|
+
phase: "moving",
|
|
75
|
+
documentsProcessed,
|
|
76
|
+
documentsTotal
|
|
77
|
+
});
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
69
79
|
}
|
|
70
|
-
targetPartition.insert(docId, document, manager.schema, manager.language, insertOptions);
|
|
71
|
-
documentsProcessed++;
|
|
72
80
|
}
|
|
81
|
+
}
|
|
82
|
+
if (chunkFill > 0) {
|
|
73
83
|
onProgress?.({
|
|
74
84
|
phase: "moving",
|
|
75
85
|
documentsProcessed,
|
|
@@ -89,13 +99,14 @@ function createRebalancer() {
|
|
|
89
99
|
documentsTotal
|
|
90
100
|
});
|
|
91
101
|
} finally {
|
|
92
|
-
|
|
102
|
+
activeManagers.delete(manager);
|
|
103
|
+
activeCount--;
|
|
93
104
|
}
|
|
94
105
|
}
|
|
95
106
|
return {
|
|
96
107
|
rebalance,
|
|
97
108
|
isRebalancing() {
|
|
98
|
-
return
|
|
109
|
+
return activeCount > 0;
|
|
99
110
|
}
|
|
100
111
|
};
|
|
101
112
|
}
|
|
@@ -293,6 +304,221 @@ function createExecutionPromoter(config) {
|
|
|
293
304
|
return { check, markPromoted, isPromoted };
|
|
294
305
|
}
|
|
295
306
|
|
|
307
|
+
// src/engine/analysis-rebuild.ts
|
|
308
|
+
function yieldToEventLoop() {
|
|
309
|
+
return new Promise((resolve) => {
|
|
310
|
+
setTimeout(resolve, 0);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function toError(value) {
|
|
314
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
315
|
+
}
|
|
316
|
+
function wireAnalysisRebuild(wiring) {
|
|
317
|
+
return createAnalysisRebuildCoordinator(wiring.config, {
|
|
318
|
+
getManager: wiring.getManager,
|
|
319
|
+
desyncIndex: wiring.desyncIndex,
|
|
320
|
+
resyncIndex: wiring.resyncIndex,
|
|
321
|
+
async persistAnalysisRevision(indexName) {
|
|
322
|
+
const durability = wiring.durabilityManager;
|
|
323
|
+
if (durability === null) return;
|
|
324
|
+
await durability.persistMetadata(indexName);
|
|
325
|
+
await (durability.checkpointFromMemory?.(indexName) ?? durability.checkpoint(indexName));
|
|
326
|
+
},
|
|
327
|
+
emit(payload) {
|
|
328
|
+
const handlers = wiring.eventHandlers.get("analysisRebuild");
|
|
329
|
+
if (!handlers) return;
|
|
330
|
+
for (const handler of handlers) {
|
|
331
|
+
try {
|
|
332
|
+
handler(payload);
|
|
333
|
+
} catch (err) {
|
|
334
|
+
console.warn("analysisRebuild handler error:", err instanceof Error ? err.message : String(err));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function createAnalysisRebuildCoordinator(config, deps) {
|
|
341
|
+
const stale = /* @__PURE__ */ new Map();
|
|
342
|
+
const rebuildMode = config?.rebuild ?? "auto";
|
|
343
|
+
let queue = Promise.resolve();
|
|
344
|
+
async function rebuildNow(indexName, entry) {
|
|
345
|
+
const manager = deps.getManager(indexName);
|
|
346
|
+
if (manager === void 0) {
|
|
347
|
+
stale.delete(indexName);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const partitionCount = manager.partitionCount;
|
|
351
|
+
deps.emit({ indexName, status: "started", partitionsRebuilt: 0, partitionCount });
|
|
352
|
+
const wasPromoted = deps.desyncIndex(indexName);
|
|
353
|
+
try {
|
|
354
|
+
for (let partitionId = 0; partitionId < manager.partitionCount; partitionId += 1) {
|
|
355
|
+
manager.rebuildTextIndex(partitionId);
|
|
356
|
+
entry.partitionsRebuilt = partitionId + 1;
|
|
357
|
+
await yieldToEventLoop();
|
|
358
|
+
}
|
|
359
|
+
stale.delete(indexName);
|
|
360
|
+
await deps.persistAnalysisRevision(indexName);
|
|
361
|
+
await deps.resyncIndex(indexName, wasPromoted);
|
|
362
|
+
deps.emit({
|
|
363
|
+
indexName,
|
|
364
|
+
status: "completed",
|
|
365
|
+
partitionsRebuilt: entry.partitionsRebuilt,
|
|
366
|
+
partitionCount: manager.partitionCount
|
|
367
|
+
});
|
|
368
|
+
} catch (err) {
|
|
369
|
+
const error = toError(err);
|
|
370
|
+
stale.set(indexName, entry);
|
|
371
|
+
entry.run = null;
|
|
372
|
+
await deps.resyncIndex(indexName, wasPromoted).catch(() => void 0);
|
|
373
|
+
deps.emit({
|
|
374
|
+
indexName,
|
|
375
|
+
status: "failed",
|
|
376
|
+
partitionsRebuilt: entry.partitionsRebuilt,
|
|
377
|
+
partitionCount,
|
|
378
|
+
error
|
|
379
|
+
});
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function enqueue(indexName, entry) {
|
|
384
|
+
if (entry.run !== null) {
|
|
385
|
+
return entry.run;
|
|
386
|
+
}
|
|
387
|
+
const run = queue.then(() => rebuildNow(indexName, entry));
|
|
388
|
+
entry.run = run;
|
|
389
|
+
queue = run.then(
|
|
390
|
+
() => void 0,
|
|
391
|
+
() => void 0
|
|
392
|
+
);
|
|
393
|
+
return run;
|
|
394
|
+
}
|
|
395
|
+
async function announce(indexName, entry) {
|
|
396
|
+
const documentCount = deps.getManager(indexName)?.countDocuments() ?? 0;
|
|
397
|
+
await config?.onStaleAnalysis?.({ ...entry.index, documentCount }, () => enqueue(indexName, entry));
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
markStale(index) {
|
|
401
|
+
const existing = stale.get(index.indexName);
|
|
402
|
+
if (existing !== void 0 && existing.run !== null) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
stale.set(index.indexName, { index, partitionsRebuilt: 0, run: null });
|
|
406
|
+
},
|
|
407
|
+
clearStale(indexName) {
|
|
408
|
+
stale.delete(indexName);
|
|
409
|
+
},
|
|
410
|
+
isStale(indexName) {
|
|
411
|
+
return stale.has(indexName);
|
|
412
|
+
},
|
|
413
|
+
async reviewStaleIndexes() {
|
|
414
|
+
for (const [indexName, entry] of [...stale]) {
|
|
415
|
+
if (entry.run !== null) continue;
|
|
416
|
+
try {
|
|
417
|
+
await announce(indexName, entry);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
deps.emit({
|
|
420
|
+
indexName,
|
|
421
|
+
status: "failed",
|
|
422
|
+
partitionsRebuilt: 0,
|
|
423
|
+
partitionCount: deps.getManager(indexName)?.partitionCount ?? 0,
|
|
424
|
+
error: toError(err)
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
if (rebuildMode === "auto") {
|
|
428
|
+
void enqueue(indexName, entry).catch(() => void 0);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
rebuild(indexName) {
|
|
433
|
+
const entry = stale.get(indexName);
|
|
434
|
+
if (entry === void 0) {
|
|
435
|
+
return Promise.resolve();
|
|
436
|
+
}
|
|
437
|
+
return enqueue(indexName, entry);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/engine/durability-config.ts
|
|
443
|
+
var WAL_ONLY_FIELDS = ["directory", "mode", "flushIntervalMs", "segmentMaxBytes", "compactionThreshold"];
|
|
444
|
+
function configError(message) {
|
|
445
|
+
return new NarsilError(ErrorCodes.CONFIG_INVALID, message);
|
|
446
|
+
}
|
|
447
|
+
function requireKnob(name, value, minimum) {
|
|
448
|
+
if (value === void 0) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < minimum) {
|
|
452
|
+
throw configError(`durability.${name} must be a finite number of at least ${minimum}, got ${String(value)}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function requireValidDurabilityConfig(durability) {
|
|
456
|
+
if (durability.tier !== void 0 && durability.tier !== "wal" && durability.tier !== "snapshot") {
|
|
457
|
+
throw configError(`durability.tier must be "wal" or "snapshot", got "${String(durability.tier)}"`);
|
|
458
|
+
}
|
|
459
|
+
if (durability.mode !== void 0 && durability.mode !== "sync" && durability.mode !== "async") {
|
|
460
|
+
throw configError(`durability.mode must be "sync" or "async", got "${String(durability.mode)}"`);
|
|
461
|
+
}
|
|
462
|
+
requireKnob("checkpointIntervalMs", durability.checkpointIntervalMs, 0);
|
|
463
|
+
requireKnob("checkpointMutationThreshold", durability.checkpointMutationThreshold, 1);
|
|
464
|
+
requireKnob("flushIntervalMs", durability.flushIntervalMs, 0);
|
|
465
|
+
requireKnob("segmentMaxBytes", durability.segmentMaxBytes, 1);
|
|
466
|
+
requireKnob("compactionThreshold", durability.compactionThreshold, 1);
|
|
467
|
+
if (durability.tier === "snapshot") {
|
|
468
|
+
for (const field of WAL_ONLY_FIELDS) {
|
|
469
|
+
if (durability[field] !== void 0) {
|
|
470
|
+
throw configError(
|
|
471
|
+
`durability.${field} applies to the write-ahead log tier only and cannot combine with durability.tier "snapshot"`
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function filesystemBackedDirectory(config) {
|
|
478
|
+
const adapterDirectory = config.persistence?.directory;
|
|
479
|
+
if (adapterDirectory !== void 0 && adapterDirectory.trim().length > 0) {
|
|
480
|
+
return adapterDirectory;
|
|
481
|
+
}
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
function resolveDurabilityTier(config) {
|
|
485
|
+
const filesystemDirectory = filesystemBackedDirectory(config);
|
|
486
|
+
if (config.durability) {
|
|
487
|
+
requireValidDurabilityConfig(config.durability);
|
|
488
|
+
}
|
|
489
|
+
if (config.durability?.tier === "snapshot") {
|
|
490
|
+
if (config.persistence === void 0) {
|
|
491
|
+
throw configError(
|
|
492
|
+
"Snapshot durability persists through a persistence adapter. Configure persistence, or remove durability.tier"
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
return { kind: "snapshot", config: { ...config.durability }, adapter: config.persistence };
|
|
496
|
+
}
|
|
497
|
+
if (config.durability) {
|
|
498
|
+
if (config.durability.directory !== void 0 && config.durability.directory.trim().length > 0) {
|
|
499
|
+
return { kind: "wal", config: { ...config.durability } };
|
|
500
|
+
}
|
|
501
|
+
if (filesystemDirectory !== null) {
|
|
502
|
+
return { kind: "wal", config: { ...config.durability, directory: filesystemDirectory } };
|
|
503
|
+
}
|
|
504
|
+
if (config.persistence !== void 0) {
|
|
505
|
+
throw configError(
|
|
506
|
+
"WAL durability requires a filesystem directory, but the configured persistence adapter is not filesystem-backed. Use a filesystem persistence adapter or set durability.directory"
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
throw configError(
|
|
510
|
+
"Durability requires a directory. Set durability.directory explicitly, or configure a filesystem persistence adapter"
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
if (config.persistence === void 0) {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
if (filesystemDirectory !== null) {
|
|
517
|
+
return { kind: "wal", config: { directory: filesystemDirectory } };
|
|
518
|
+
}
|
|
519
|
+
return { kind: "snapshot", config: {}, adapter: config.persistence };
|
|
520
|
+
}
|
|
521
|
+
|
|
296
522
|
// src/persistence/durability/node-fs.ts
|
|
297
523
|
var cachedFs = null;
|
|
298
524
|
var cachedPath = null;
|
|
@@ -597,7 +823,7 @@ async function spawnWorker() {
|
|
|
597
823
|
return null;
|
|
598
824
|
}
|
|
599
825
|
}
|
|
600
|
-
function
|
|
826
|
+
function yieldToEventLoop2() {
|
|
601
827
|
if (typeof setImmediate === "function") {
|
|
602
828
|
return new Promise((resolve) => setImmediate(resolve));
|
|
603
829
|
}
|
|
@@ -609,7 +835,7 @@ async function chunkedChecksum(payload) {
|
|
|
609
835
|
const end = Math.min(offset + YIELD_CHUNK_BYTES, payload.length);
|
|
610
836
|
state = crc32Update(state, payload.subarray(offset, end));
|
|
611
837
|
if (end < payload.length) {
|
|
612
|
-
await
|
|
838
|
+
await yieldToEventLoop2();
|
|
613
839
|
}
|
|
614
840
|
}
|
|
615
841
|
return crc32Final(state);
|
|
@@ -978,6 +1204,10 @@ async function encodeSnapshotBundle(bundle) {
|
|
|
978
1204
|
version: 1,
|
|
979
1205
|
schema: bundle.schema,
|
|
980
1206
|
language: bundle.language,
|
|
1207
|
+
...bundle.analysisRevision !== void 0 ? { analysis_revision: bundle.analysisRevision } : {},
|
|
1208
|
+
...bundle.tokenizer !== void 0 ? { tokenizer: bundle.tokenizer } : {},
|
|
1209
|
+
...bundle.stopWords !== void 0 ? { stop_words: bundle.stopWords } : {},
|
|
1210
|
+
...bundle.stopWordList !== void 0 ? { stop_word_list: bundle.stopWordList } : {},
|
|
981
1211
|
partitions: bundle.partitions,
|
|
982
1212
|
vectorIndexes: bundle.vectorIndexes,
|
|
983
1213
|
checkpoint: bundle.checkpoint.map((c) => ({
|
|
@@ -1007,15 +1237,38 @@ async function decodeSnapshotBundle(data) {
|
|
|
1007
1237
|
if (!Array.isArray(raw.partitions)) {
|
|
1008
1238
|
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Snapshot bundle missing partitions");
|
|
1009
1239
|
}
|
|
1240
|
+
if (raw.analysis_revision !== void 0 && typeof raw.analysis_revision !== "string") {
|
|
1241
|
+
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Snapshot bundle analysis_revision must be a revision");
|
|
1242
|
+
}
|
|
1243
|
+
if (raw.tokenizer !== void 0 && typeof raw.tokenizer !== "string") {
|
|
1244
|
+
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Snapshot bundle tokenizer must be a name");
|
|
1245
|
+
}
|
|
1246
|
+
if (raw.stop_words !== void 0 && typeof raw.stop_words !== "string") {
|
|
1247
|
+
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Snapshot bundle stop_words must be a name");
|
|
1248
|
+
}
|
|
1249
|
+
const stopWordList = normalizeStopWordList(raw.stop_word_list);
|
|
1010
1250
|
return {
|
|
1011
1251
|
version: 1,
|
|
1012
1252
|
schema: raw.schema,
|
|
1013
1253
|
language: raw.language,
|
|
1254
|
+
...typeof raw.analysis_revision === "string" ? { analysisRevision: raw.analysis_revision } : {},
|
|
1255
|
+
...typeof raw.tokenizer === "string" ? { tokenizer: raw.tokenizer } : {},
|
|
1256
|
+
...typeof raw.stop_words === "string" ? { stopWords: raw.stop_words } : {},
|
|
1257
|
+
...stopWordList !== void 0 ? { stopWordList } : {},
|
|
1014
1258
|
partitions: raw.partitions,
|
|
1015
1259
|
vectorIndexes: raw.vectorIndexes ?? {},
|
|
1016
1260
|
checkpoint: normalizeCheckpoint(raw.checkpoint)
|
|
1017
1261
|
};
|
|
1018
1262
|
}
|
|
1263
|
+
function normalizeStopWordList(raw) {
|
|
1264
|
+
if (raw === void 0) {
|
|
1265
|
+
return void 0;
|
|
1266
|
+
}
|
|
1267
|
+
if (!Array.isArray(raw) || !raw.every((word) => typeof word === "string")) {
|
|
1268
|
+
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Snapshot bundle stop_word_list must be a list of words");
|
|
1269
|
+
}
|
|
1270
|
+
return raw;
|
|
1271
|
+
}
|
|
1019
1272
|
function normalizeCheckpoint(raw) {
|
|
1020
1273
|
if (!Array.isArray(raw)) {
|
|
1021
1274
|
return [];
|
|
@@ -1064,6 +1317,10 @@ async function buildSnapshotBundleBytes(input) {
|
|
|
1064
1317
|
version: 1,
|
|
1065
1318
|
schema: input.schema,
|
|
1066
1319
|
language: input.language,
|
|
1320
|
+
...input.analysisRevision !== void 0 ? { analysisRevision: input.analysisRevision } : {},
|
|
1321
|
+
...input.tokenizer !== void 0 ? { tokenizer: input.tokenizer } : {},
|
|
1322
|
+
...input.stopWords !== void 0 ? { stopWords: input.stopWords } : {},
|
|
1323
|
+
...input.stopWordList !== void 0 ? { stopWordList: input.stopWordList } : {},
|
|
1067
1324
|
partitions: partitionBuffers,
|
|
1068
1325
|
vectorIndexes: vectorPayloads,
|
|
1069
1326
|
checkpoint
|
|
@@ -1104,6 +1361,36 @@ async function truncateCoveredSegments(directory, indexName, checkpoint) {
|
|
|
1104
1361
|
}
|
|
1105
1362
|
}
|
|
1106
1363
|
}
|
|
1364
|
+
async function reclaimWalBeyondCount(directory, indexName, partitionCount, partitions, onCloseError) {
|
|
1365
|
+
for (const [partitionId, partition] of [...partitions]) {
|
|
1366
|
+
if (partitionId < partitionCount) continue;
|
|
1367
|
+
try {
|
|
1368
|
+
await partition.walWriter.close();
|
|
1369
|
+
} catch (err) {
|
|
1370
|
+
onCloseError(err instanceof Error ? err : new Error(String(err)));
|
|
1371
|
+
}
|
|
1372
|
+
partitions.delete(partitionId);
|
|
1373
|
+
}
|
|
1374
|
+
const walRoot = `${indexName}/wal/`;
|
|
1375
|
+
for (const key of await directory.list(walRoot)) {
|
|
1376
|
+
const partitionId = walKeyPartitionId(key, walRoot);
|
|
1377
|
+
if (partitionId === null || partitionId < partitionCount) continue;
|
|
1378
|
+
await directory.remove(key);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
function walKeyPartitionId(key, walRoot) {
|
|
1382
|
+
const tail = key.slice(walRoot.length);
|
|
1383
|
+
const separator = tail.indexOf("/");
|
|
1384
|
+
if (separator <= 0) {
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|
|
1387
|
+
const segment = tail.slice(0, separator);
|
|
1388
|
+
if (!/^\d+$/.test(segment)) {
|
|
1389
|
+
return null;
|
|
1390
|
+
}
|
|
1391
|
+
const value = Number.parseInt(segment, 10);
|
|
1392
|
+
return Number.isSafeInteger(value) ? value : null;
|
|
1393
|
+
}
|
|
1107
1394
|
function parseSegmentStartSeqNo(key, prefix) {
|
|
1108
1395
|
if (!key.startsWith(prefix)) {
|
|
1109
1396
|
return null;
|
|
@@ -1253,454 +1540,198 @@ function terminateCheckpointWorker() {
|
|
|
1253
1540
|
}
|
|
1254
1541
|
}
|
|
1255
1542
|
|
|
1256
|
-
// src/
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
if (!Array.isArray(value)) return null;
|
|
1262
|
-
const arr = new Float32Array(value.length);
|
|
1263
|
-
for (let i = 0; i < value.length; i++) {
|
|
1264
|
-
const n = value[i];
|
|
1265
|
-
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
|
1266
|
-
arr[i] = n;
|
|
1267
|
-
}
|
|
1268
|
-
return arr;
|
|
1543
|
+
// src/persistence/durability/segment/layout.ts
|
|
1544
|
+
var DEFAULT_COMPACTION_THRESHOLD = 12;
|
|
1545
|
+
var SEGMENT_ID_WIDTH = 16;
|
|
1546
|
+
function manifestKey(indexName) {
|
|
1547
|
+
return `${indexName}/manifest`;
|
|
1269
1548
|
}
|
|
1270
|
-
function
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1549
|
+
function legacySnapshotKey(indexName) {
|
|
1550
|
+
return `${indexName}/snapshot`;
|
|
1551
|
+
}
|
|
1552
|
+
function segmentPrefix(indexName, partitionId) {
|
|
1553
|
+
return `${indexName}/segments/${partitionId}/`;
|
|
1554
|
+
}
|
|
1555
|
+
function segmentKey(indexName, partitionId, segmentId) {
|
|
1556
|
+
return `${segmentPrefix(indexName, partitionId)}s${formatSegmentId(segmentId)}`;
|
|
1557
|
+
}
|
|
1558
|
+
function vectorSegmentKey(indexName, partitionId, fieldPath, generation) {
|
|
1559
|
+
return `${segmentPrefix(indexName, partitionId)}vec-${encodeFieldPath(fieldPath)}-g${generation}`;
|
|
1560
|
+
}
|
|
1561
|
+
function formatSegmentId(segmentId) {
|
|
1562
|
+
if (!Number.isInteger(segmentId) || segmentId < 0) {
|
|
1563
|
+
throw new NarsilError(
|
|
1564
|
+
ErrorCodes.PERSISTENCE_SAVE_FAILED,
|
|
1565
|
+
`Segment id ${segmentId} must be a non-negative integer`,
|
|
1566
|
+
{ segmentId }
|
|
1567
|
+
);
|
|
1274
1568
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1569
|
+
return segmentId.toString().padStart(SEGMENT_ID_WIDTH, "0");
|
|
1570
|
+
}
|
|
1571
|
+
var FIELD_PATH_PATTERN = /^[A-Za-z0-9_.]+$/;
|
|
1572
|
+
function encodeFieldPath(fieldPath) {
|
|
1573
|
+
if (!FIELD_PATH_PATTERN.test(fieldPath)) {
|
|
1574
|
+
throw new NarsilError(
|
|
1575
|
+
ErrorCodes.PERSISTENCE_SAVE_FAILED,
|
|
1576
|
+
`Vector field path "${fieldPath}" contains characters that cannot be encoded in a segment key`,
|
|
1577
|
+
{ fieldPath }
|
|
1578
|
+
);
|
|
1281
1579
|
}
|
|
1282
|
-
|
|
1580
|
+
const fingerprint = fnv1a(fieldPath).toString(36);
|
|
1581
|
+
return `${fieldPath.replace(/\./g, "_")}-${fingerprint}`;
|
|
1283
1582
|
}
|
|
1284
|
-
function
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1583
|
+
async function decodeSegmentManifest(data) {
|
|
1584
|
+
const { payloadBytes } = await unpackEnvelopeBytes(data);
|
|
1585
|
+
const raw = decode(payloadBytes);
|
|
1586
|
+
if (raw.version !== SEGMENT_MANIFEST_VERSION) {
|
|
1587
|
+
throw new NarsilError(
|
|
1588
|
+
ErrorCodes.PERSISTENCE_LOAD_FAILED,
|
|
1589
|
+
`Unsupported segment manifest version ${raw.version}; expected ${SEGMENT_MANIFEST_VERSION}`,
|
|
1590
|
+
{ version: raw.version }
|
|
1591
|
+
);
|
|
1289
1592
|
}
|
|
1290
|
-
|
|
1593
|
+
const base = validateManifestCommon(raw);
|
|
1594
|
+
return {
|
|
1595
|
+
version: SEGMENT_MANIFEST_VERSION,
|
|
1596
|
+
schema: base.schema,
|
|
1597
|
+
language: base.language,
|
|
1598
|
+
checkpoint: normalizeCheckpoint2(raw.checkpoint),
|
|
1599
|
+
partitions: normalizePartitions(raw.partitions)
|
|
1600
|
+
};
|
|
1291
1601
|
}
|
|
1292
|
-
function
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
ErrorCodes.VECTOR_DIMENSION_MISMATCH,
|
|
1299
|
-
`Vector dimension mismatch for field "${fieldPath}": expected ${vecIndex.dimension}, got ${vector.length}`,
|
|
1300
|
-
{ field: fieldPath, expected: vecIndex.dimension, received: vector.length }
|
|
1301
|
-
);
|
|
1302
|
-
}
|
|
1602
|
+
function validateManifestCommon(raw) {
|
|
1603
|
+
if (raw.schema === void 0 || typeof raw.schema !== "object") {
|
|
1604
|
+
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest is missing schema");
|
|
1605
|
+
}
|
|
1606
|
+
if (typeof raw.language !== "string") {
|
|
1607
|
+
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest is missing language");
|
|
1303
1608
|
}
|
|
1609
|
+
return { schema: raw.schema, language: raw.language };
|
|
1304
1610
|
}
|
|
1305
|
-
function
|
|
1306
|
-
if (
|
|
1307
|
-
return
|
|
1611
|
+
function normalizeCheckpoint2(raw) {
|
|
1612
|
+
if (!Array.isArray(raw)) {
|
|
1613
|
+
return [];
|
|
1308
1614
|
}
|
|
1309
|
-
const
|
|
1310
|
-
for (const
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
extractedVectors.set(fieldPath, vec);
|
|
1615
|
+
const result = [];
|
|
1616
|
+
for (const entry of raw) {
|
|
1617
|
+
if (typeof entry?.partitionId !== "number" || typeof entry.lastSeqNo !== "number" || typeof entry.primaryTerm !== "number") {
|
|
1618
|
+
continue;
|
|
1314
1619
|
}
|
|
1620
|
+
result.push({ partitionId: entry.partitionId, lastSeqNo: entry.lastSeqNo, primaryTerm: entry.primaryTerm });
|
|
1315
1621
|
}
|
|
1316
|
-
|
|
1317
|
-
return { partitionDoc: document, extractedVectors };
|
|
1318
|
-
}
|
|
1319
|
-
const partitionDoc = shallowCopyExcluding(document, extractedVectors.keys());
|
|
1320
|
-
return { partitionDoc, extractedVectors };
|
|
1622
|
+
return result;
|
|
1321
1623
|
}
|
|
1322
|
-
function
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
for (const path of fieldPaths) {
|
|
1326
|
-
if (!path.includes(".")) {
|
|
1327
|
-
topLevelExcludes.add(path);
|
|
1328
|
-
} else {
|
|
1329
|
-
nestedPaths.push(path);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
const result = {};
|
|
1333
|
-
for (const key of Object.keys(document)) {
|
|
1334
|
-
if (topLevelExcludes.has(key)) continue;
|
|
1335
|
-
result[key] = document[key];
|
|
1624
|
+
function normalizePartitions(raw) {
|
|
1625
|
+
if (!Array.isArray(raw)) {
|
|
1626
|
+
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest is missing partition entries");
|
|
1336
1627
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
result[topKey] = target;
|
|
1344
|
-
for (let i = 1; i < segments.length - 1; i++) {
|
|
1345
|
-
const seg = segments[i];
|
|
1346
|
-
const next = target[seg];
|
|
1347
|
-
if (next === null || next === void 0 || typeof next !== "object") break;
|
|
1348
|
-
const copy = { ...next };
|
|
1349
|
-
target[seg] = copy;
|
|
1350
|
-
target = copy;
|
|
1628
|
+
const result = [];
|
|
1629
|
+
for (const entry of raw) {
|
|
1630
|
+
if (!isNonNegativeInteger(entry?.partitionId)) {
|
|
1631
|
+
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest has a partition without a valid id", {
|
|
1632
|
+
partitionId: entry?.partitionId
|
|
1633
|
+
});
|
|
1351
1634
|
}
|
|
1352
|
-
|
|
1635
|
+
const segments = normalizeSegments(entry.segments, entry.partitionId);
|
|
1636
|
+
const nextSegmentId = readNextSegmentId(entry.nextSegmentId, segments, entry.partitionId);
|
|
1637
|
+
result.push({
|
|
1638
|
+
partitionId: entry.partitionId,
|
|
1639
|
+
nextSegmentId,
|
|
1640
|
+
segments,
|
|
1641
|
+
vectors: normalizeVectors(entry.vectors, entry.partitionId)
|
|
1642
|
+
});
|
|
1353
1643
|
}
|
|
1354
1644
|
return result;
|
|
1355
1645
|
}
|
|
1356
|
-
function
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
for (const [fieldPath, vector] of vectors) {
|
|
1360
|
-
const vecIndex = vecIndexes.get(fieldPath);
|
|
1361
|
-
if (!vecIndex) continue;
|
|
1362
|
-
vecIndex.insert(docId, vector);
|
|
1363
|
-
insertedFields.push(fieldPath);
|
|
1364
|
-
}
|
|
1365
|
-
} catch (err) {
|
|
1366
|
-
for (const fieldPath of insertedFields) {
|
|
1367
|
-
const vecIndex = vecIndexes.get(fieldPath);
|
|
1368
|
-
if (vecIndex) {
|
|
1369
|
-
vecIndex.remove(docId);
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
throw err;
|
|
1646
|
+
function normalizeSegments(raw, partitionId) {
|
|
1647
|
+
if (raw === void 0) {
|
|
1648
|
+
return [];
|
|
1373
1649
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
vecIndex.remove(docId);
|
|
1650
|
+
if (!Array.isArray(raw)) {
|
|
1651
|
+
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest has malformed segment references", {
|
|
1652
|
+
partitionId
|
|
1653
|
+
});
|
|
1379
1654
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
}
|
|
1395
|
-
const oldVec = vecIndex.getVector(docId);
|
|
1396
|
-
if (vectorsEqual(oldVec, newVec)) continue;
|
|
1397
|
-
vecIndex.remove(docId);
|
|
1398
|
-
try {
|
|
1399
|
-
vecIndex.insert(docId, newVec);
|
|
1400
|
-
updatedFields.push({ fieldPath, oldVec });
|
|
1401
|
-
} catch (err) {
|
|
1402
|
-
if (oldVec) {
|
|
1403
|
-
vecIndex.insert(docId, oldVec);
|
|
1404
|
-
}
|
|
1405
|
-
throw err;
|
|
1406
|
-
}
|
|
1655
|
+
if (raw.length > MAX_SEGMENTS_PER_PARTITION) {
|
|
1656
|
+
throw new NarsilError(
|
|
1657
|
+
ErrorCodes.PERSISTENCE_LOAD_FAILED,
|
|
1658
|
+
`Segment manifest lists ${raw.length} segments for partition ${partitionId}, exceeding the maximum of ${MAX_SEGMENTS_PER_PARTITION}`,
|
|
1659
|
+
{ partitionId, segmentCount: raw.length, maximum: MAX_SEGMENTS_PER_PARTITION }
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1663
|
+
const result = [];
|
|
1664
|
+
for (const segment of raw) {
|
|
1665
|
+
if (!isSegmentId(segment?.id) || !isNonNegativeInteger(segment.docCount) || !isNonNegativeInteger(segment.tombstoneCount) || !isKey(segment.key)) {
|
|
1666
|
+
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest has an invalid segment reference", {
|
|
1667
|
+
partitionId,
|
|
1668
|
+
segment
|
|
1669
|
+
});
|
|
1407
1670
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
vecIndex.insert(docId, oldVec);
|
|
1415
|
-
}
|
|
1671
|
+
if (seenIds.has(segment.id)) {
|
|
1672
|
+
throw new NarsilError(
|
|
1673
|
+
ErrorCodes.PERSISTENCE_LOAD_FAILED,
|
|
1674
|
+
`Segment manifest reuses segment id ${segment.id} in partition ${partitionId}`,
|
|
1675
|
+
{ partitionId, segmentId: segment.id }
|
|
1676
|
+
);
|
|
1416
1677
|
}
|
|
1417
|
-
|
|
1678
|
+
seenIds.add(segment.id);
|
|
1679
|
+
result.push({
|
|
1680
|
+
id: segment.id,
|
|
1681
|
+
key: segment.key,
|
|
1682
|
+
docCount: segment.docCount,
|
|
1683
|
+
tombstoneCount: segment.tombstoneCount
|
|
1684
|
+
});
|
|
1418
1685
|
}
|
|
1686
|
+
return result;
|
|
1419
1687
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1688
|
+
function readNextSegmentId(value, segments, partitionId) {
|
|
1689
|
+
if (!isSegmentId(value)) {
|
|
1690
|
+
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest has an invalid next segment id", {
|
|
1691
|
+
partitionId,
|
|
1692
|
+
nextSegmentId: value
|
|
1693
|
+
});
|
|
1425
1694
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1695
|
+
for (const segment of segments) {
|
|
1696
|
+
if (segment.id >= value) {
|
|
1697
|
+
throw new NarsilError(
|
|
1698
|
+
ErrorCodes.PERSISTENCE_LOAD_FAILED,
|
|
1699
|
+
`Segment manifest next segment id ${value} does not exceed an existing segment id ${segment.id}`,
|
|
1700
|
+
{ partitionId, nextSegmentId: value, segmentId: segment.id }
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1428
1703
|
}
|
|
1429
|
-
return
|
|
1704
|
+
return value;
|
|
1430
1705
|
}
|
|
1431
|
-
function
|
|
1432
|
-
if (
|
|
1433
|
-
return;
|
|
1434
|
-
}
|
|
1435
|
-
const document = decode(entry.document);
|
|
1436
|
-
restoreVectorFields(document, vectorFieldPaths);
|
|
1437
|
-
if (manager.has(entry.documentId)) {
|
|
1438
|
-
removeDocumentVectors(entry.documentId, vecIndexes);
|
|
1439
|
-
manager.remove(entry.documentId);
|
|
1706
|
+
function normalizeVectors(raw, partitionId) {
|
|
1707
|
+
if (raw === void 0) {
|
|
1708
|
+
return [];
|
|
1440
1709
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
} catch (err) {
|
|
1446
|
-
manager.remove(entry.documentId);
|
|
1447
|
-
throw err;
|
|
1710
|
+
if (!Array.isArray(raw)) {
|
|
1711
|
+
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest has malformed vector references", {
|
|
1712
|
+
partitionId
|
|
1713
|
+
});
|
|
1448
1714
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
if (
|
|
1452
|
-
|
|
1715
|
+
const result = [];
|
|
1716
|
+
for (const vector of raw) {
|
|
1717
|
+
if (typeof vector?.fieldPath !== "string" || !isNonNegativeInteger(vector.generation) || !isKey(vector.key)) {
|
|
1718
|
+
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest has an invalid vector reference", {
|
|
1719
|
+
partitionId,
|
|
1720
|
+
vector
|
|
1721
|
+
});
|
|
1453
1722
|
}
|
|
1723
|
+
result.push({ fieldPath: vector.fieldPath, generation: vector.generation, key: vector.key });
|
|
1454
1724
|
}
|
|
1725
|
+
return result;
|
|
1455
1726
|
}
|
|
1456
|
-
function
|
|
1457
|
-
|
|
1458
|
-
return;
|
|
1459
|
-
}
|
|
1460
|
-
removeDocumentVectors(entry.documentId, vecIndexes);
|
|
1461
|
-
manager.remove(entry.documentId);
|
|
1727
|
+
function isNonNegativeInteger(value) {
|
|
1728
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
1462
1729
|
}
|
|
1463
|
-
function
|
|
1464
|
-
|
|
1465
|
-
const value = getNestedValue(document, fieldPath);
|
|
1466
|
-
if (value instanceof Uint8Array && !(value instanceof Float32Array)) {
|
|
1467
|
-
if (value.byteLength % 4 !== 0) {
|
|
1468
|
-
throw new Error(
|
|
1469
|
-
`Vector field "${fieldPath}" has invalid byte length ${value.byteLength} (must be divisible by 4)`
|
|
1470
|
-
);
|
|
1471
|
-
}
|
|
1472
|
-
const elementCount = value.byteLength / 4;
|
|
1473
|
-
const float32 = new Float32Array(elementCount);
|
|
1474
|
-
const dataView = new DataView(value.buffer, value.byteOffset, value.byteLength);
|
|
1475
|
-
for (let i = 0; i < elementCount; i += 1) {
|
|
1476
|
-
float32[i] = dataView.getFloat32(i * 4, true);
|
|
1477
|
-
}
|
|
1478
|
-
setNestedValue(document, fieldPath, float32);
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1730
|
+
function isSegmentId(value) {
|
|
1731
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= MAX_SEGMENT_ID;
|
|
1481
1732
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
if (!path.includes(".")) {
|
|
1485
|
-
if (FORBIDDEN_KEYS.has(path)) {
|
|
1486
|
-
return;
|
|
1487
|
-
}
|
|
1488
|
-
obj[path] = value;
|
|
1489
|
-
return;
|
|
1490
|
-
}
|
|
1491
|
-
const segments = path.split(".");
|
|
1492
|
-
let current = obj;
|
|
1493
|
-
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
1494
|
-
const segment = segments[i];
|
|
1495
|
-
if (segment === void 0 || FORBIDDEN_KEYS.has(segment)) {
|
|
1496
|
-
return;
|
|
1497
|
-
}
|
|
1498
|
-
let next = current[segment];
|
|
1499
|
-
if (next === null || next === void 0 || typeof next !== "object") {
|
|
1500
|
-
next = {};
|
|
1501
|
-
current[segment] = next;
|
|
1502
|
-
}
|
|
1503
|
-
current = next;
|
|
1504
|
-
}
|
|
1505
|
-
const finalSegment = segments.at(-1);
|
|
1506
|
-
if (finalSegment === void 0 || FORBIDDEN_KEYS.has(finalSegment)) {
|
|
1507
|
-
return;
|
|
1508
|
-
}
|
|
1509
|
-
current[finalSegment] = value;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
// src/persistence/durability/segment/layout.ts
|
|
1513
|
-
var DEFAULT_COMPACTION_THRESHOLD = 12;
|
|
1514
|
-
var SEGMENT_ID_WIDTH = 16;
|
|
1515
|
-
function manifestKey(indexName) {
|
|
1516
|
-
return `${indexName}/manifest`;
|
|
1517
|
-
}
|
|
1518
|
-
function legacySnapshotKey(indexName) {
|
|
1519
|
-
return `${indexName}/snapshot`;
|
|
1520
|
-
}
|
|
1521
|
-
function segmentPrefix(indexName, partitionId) {
|
|
1522
|
-
return `${indexName}/segments/${partitionId}/`;
|
|
1523
|
-
}
|
|
1524
|
-
function segmentKey(indexName, partitionId, segmentId) {
|
|
1525
|
-
return `${segmentPrefix(indexName, partitionId)}s${formatSegmentId(segmentId)}`;
|
|
1526
|
-
}
|
|
1527
|
-
function vectorSegmentKey(indexName, partitionId, fieldPath, generation) {
|
|
1528
|
-
return `${segmentPrefix(indexName, partitionId)}vec-${encodeFieldPath(fieldPath)}-g${generation}`;
|
|
1529
|
-
}
|
|
1530
|
-
function formatSegmentId(segmentId) {
|
|
1531
|
-
if (!Number.isInteger(segmentId) || segmentId < 0) {
|
|
1532
|
-
throw new NarsilError(
|
|
1533
|
-
ErrorCodes.PERSISTENCE_SAVE_FAILED,
|
|
1534
|
-
`Segment id ${segmentId} must be a non-negative integer`,
|
|
1535
|
-
{ segmentId }
|
|
1536
|
-
);
|
|
1537
|
-
}
|
|
1538
|
-
return segmentId.toString().padStart(SEGMENT_ID_WIDTH, "0");
|
|
1539
|
-
}
|
|
1540
|
-
var FIELD_PATH_PATTERN = /^[A-Za-z0-9_.]+$/;
|
|
1541
|
-
function encodeFieldPath(fieldPath) {
|
|
1542
|
-
if (!FIELD_PATH_PATTERN.test(fieldPath)) {
|
|
1543
|
-
throw new NarsilError(
|
|
1544
|
-
ErrorCodes.PERSISTENCE_SAVE_FAILED,
|
|
1545
|
-
`Vector field path "${fieldPath}" contains characters that cannot be encoded in a segment key`,
|
|
1546
|
-
{ fieldPath }
|
|
1547
|
-
);
|
|
1548
|
-
}
|
|
1549
|
-
const fingerprint = fnv1a(fieldPath).toString(36);
|
|
1550
|
-
return `${fieldPath.replace(/\./g, "_")}-${fingerprint}`;
|
|
1551
|
-
}
|
|
1552
|
-
async function decodeSegmentManifest(data) {
|
|
1553
|
-
const { payloadBytes } = await unpackEnvelopeBytes(data);
|
|
1554
|
-
const raw = decode(payloadBytes);
|
|
1555
|
-
if (raw.version !== SEGMENT_MANIFEST_VERSION) {
|
|
1556
|
-
throw new NarsilError(
|
|
1557
|
-
ErrorCodes.PERSISTENCE_LOAD_FAILED,
|
|
1558
|
-
`Unsupported segment manifest version ${raw.version}; expected ${SEGMENT_MANIFEST_VERSION}`,
|
|
1559
|
-
{ version: raw.version }
|
|
1560
|
-
);
|
|
1561
|
-
}
|
|
1562
|
-
const base = validateManifestCommon(raw);
|
|
1563
|
-
return {
|
|
1564
|
-
version: SEGMENT_MANIFEST_VERSION,
|
|
1565
|
-
schema: base.schema,
|
|
1566
|
-
language: base.language,
|
|
1567
|
-
checkpoint: normalizeCheckpoint2(raw.checkpoint),
|
|
1568
|
-
partitions: normalizePartitions(raw.partitions)
|
|
1569
|
-
};
|
|
1570
|
-
}
|
|
1571
|
-
function validateManifestCommon(raw) {
|
|
1572
|
-
if (raw.schema === void 0 || typeof raw.schema !== "object") {
|
|
1573
|
-
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest is missing schema");
|
|
1574
|
-
}
|
|
1575
|
-
if (typeof raw.language !== "string") {
|
|
1576
|
-
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest is missing language");
|
|
1577
|
-
}
|
|
1578
|
-
return { schema: raw.schema, language: raw.language };
|
|
1579
|
-
}
|
|
1580
|
-
function normalizeCheckpoint2(raw) {
|
|
1581
|
-
if (!Array.isArray(raw)) {
|
|
1582
|
-
return [];
|
|
1583
|
-
}
|
|
1584
|
-
const result = [];
|
|
1585
|
-
for (const entry of raw) {
|
|
1586
|
-
if (typeof entry?.partitionId !== "number" || typeof entry.lastSeqNo !== "number" || typeof entry.primaryTerm !== "number") {
|
|
1587
|
-
continue;
|
|
1588
|
-
}
|
|
1589
|
-
result.push({ partitionId: entry.partitionId, lastSeqNo: entry.lastSeqNo, primaryTerm: entry.primaryTerm });
|
|
1590
|
-
}
|
|
1591
|
-
return result;
|
|
1592
|
-
}
|
|
1593
|
-
function normalizePartitions(raw) {
|
|
1594
|
-
if (!Array.isArray(raw)) {
|
|
1595
|
-
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest is missing partition entries");
|
|
1596
|
-
}
|
|
1597
|
-
const result = [];
|
|
1598
|
-
for (const entry of raw) {
|
|
1599
|
-
if (!isNonNegativeInteger(entry?.partitionId)) {
|
|
1600
|
-
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest has a partition without a valid id", {
|
|
1601
|
-
partitionId: entry?.partitionId
|
|
1602
|
-
});
|
|
1603
|
-
}
|
|
1604
|
-
const segments = normalizeSegments(entry.segments, entry.partitionId);
|
|
1605
|
-
const nextSegmentId = readNextSegmentId(entry.nextSegmentId, segments, entry.partitionId);
|
|
1606
|
-
result.push({
|
|
1607
|
-
partitionId: entry.partitionId,
|
|
1608
|
-
nextSegmentId,
|
|
1609
|
-
segments,
|
|
1610
|
-
vectors: normalizeVectors(entry.vectors, entry.partitionId)
|
|
1611
|
-
});
|
|
1612
|
-
}
|
|
1613
|
-
return result;
|
|
1614
|
-
}
|
|
1615
|
-
function normalizeSegments(raw, partitionId) {
|
|
1616
|
-
if (raw === void 0) {
|
|
1617
|
-
return [];
|
|
1618
|
-
}
|
|
1619
|
-
if (!Array.isArray(raw)) {
|
|
1620
|
-
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest has malformed segment references", {
|
|
1621
|
-
partitionId
|
|
1622
|
-
});
|
|
1623
|
-
}
|
|
1624
|
-
if (raw.length > MAX_SEGMENTS_PER_PARTITION) {
|
|
1625
|
-
throw new NarsilError(
|
|
1626
|
-
ErrorCodes.PERSISTENCE_LOAD_FAILED,
|
|
1627
|
-
`Segment manifest lists ${raw.length} segments for partition ${partitionId}, exceeding the maximum of ${MAX_SEGMENTS_PER_PARTITION}`,
|
|
1628
|
-
{ partitionId, segmentCount: raw.length, maximum: MAX_SEGMENTS_PER_PARTITION }
|
|
1629
|
-
);
|
|
1630
|
-
}
|
|
1631
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
1632
|
-
const result = [];
|
|
1633
|
-
for (const segment of raw) {
|
|
1634
|
-
if (!isSegmentId(segment?.id) || !isNonNegativeInteger(segment.docCount) || !isNonNegativeInteger(segment.tombstoneCount) || !isKey(segment.key)) {
|
|
1635
|
-
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest has an invalid segment reference", {
|
|
1636
|
-
partitionId,
|
|
1637
|
-
segment
|
|
1638
|
-
});
|
|
1639
|
-
}
|
|
1640
|
-
if (seenIds.has(segment.id)) {
|
|
1641
|
-
throw new NarsilError(
|
|
1642
|
-
ErrorCodes.PERSISTENCE_LOAD_FAILED,
|
|
1643
|
-
`Segment manifest reuses segment id ${segment.id} in partition ${partitionId}`,
|
|
1644
|
-
{ partitionId, segmentId: segment.id }
|
|
1645
|
-
);
|
|
1646
|
-
}
|
|
1647
|
-
seenIds.add(segment.id);
|
|
1648
|
-
result.push({
|
|
1649
|
-
id: segment.id,
|
|
1650
|
-
key: segment.key,
|
|
1651
|
-
docCount: segment.docCount,
|
|
1652
|
-
tombstoneCount: segment.tombstoneCount
|
|
1653
|
-
});
|
|
1654
|
-
}
|
|
1655
|
-
return result;
|
|
1656
|
-
}
|
|
1657
|
-
function readNextSegmentId(value, segments, partitionId) {
|
|
1658
|
-
if (!isSegmentId(value)) {
|
|
1659
|
-
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest has an invalid next segment id", {
|
|
1660
|
-
partitionId,
|
|
1661
|
-
nextSegmentId: value
|
|
1662
|
-
});
|
|
1663
|
-
}
|
|
1664
|
-
for (const segment of segments) {
|
|
1665
|
-
if (segment.id >= value) {
|
|
1666
|
-
throw new NarsilError(
|
|
1667
|
-
ErrorCodes.PERSISTENCE_LOAD_FAILED,
|
|
1668
|
-
`Segment manifest next segment id ${value} does not exceed an existing segment id ${segment.id}`,
|
|
1669
|
-
{ partitionId, nextSegmentId: value, segmentId: segment.id }
|
|
1670
|
-
);
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
return value;
|
|
1674
|
-
}
|
|
1675
|
-
function normalizeVectors(raw, partitionId) {
|
|
1676
|
-
if (raw === void 0) {
|
|
1677
|
-
return [];
|
|
1678
|
-
}
|
|
1679
|
-
if (!Array.isArray(raw)) {
|
|
1680
|
-
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest has malformed vector references", {
|
|
1681
|
-
partitionId
|
|
1682
|
-
});
|
|
1683
|
-
}
|
|
1684
|
-
const result = [];
|
|
1685
|
-
for (const vector of raw) {
|
|
1686
|
-
if (typeof vector?.fieldPath !== "string" || !isNonNegativeInteger(vector.generation) || !isKey(vector.key)) {
|
|
1687
|
-
throw new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, "Segment manifest has an invalid vector reference", {
|
|
1688
|
-
partitionId,
|
|
1689
|
-
vector
|
|
1690
|
-
});
|
|
1691
|
-
}
|
|
1692
|
-
result.push({ fieldPath: vector.fieldPath, generation: vector.generation, key: vector.key });
|
|
1693
|
-
}
|
|
1694
|
-
return result;
|
|
1695
|
-
}
|
|
1696
|
-
function isNonNegativeInteger(value) {
|
|
1697
|
-
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
1698
|
-
}
|
|
1699
|
-
function isSegmentId(value) {
|
|
1700
|
-
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= MAX_SEGMENT_ID;
|
|
1701
|
-
}
|
|
1702
|
-
function isKey(value) {
|
|
1703
|
-
return typeof value === "string" && value.length > 0 && !value.includes("\0");
|
|
1733
|
+
function isKey(value) {
|
|
1734
|
+
return typeof value === "string" && value.length > 0 && !value.includes("\0");
|
|
1704
1735
|
}
|
|
1705
1736
|
|
|
1706
1737
|
// src/persistence/durability/segment/manifest.ts
|
|
@@ -2045,6 +2076,7 @@ async function loadSegmentedSnapshot(directory, indexName, manifest, deps) {
|
|
|
2045
2076
|
break;
|
|
2046
2077
|
}
|
|
2047
2078
|
}
|
|
2079
|
+
const payloadsByField = /* @__PURE__ */ new Map();
|
|
2048
2080
|
for (const partition of manifest.partitions) {
|
|
2049
2081
|
if (partition.partitionId >= deps.manager.partitionCount) {
|
|
2050
2082
|
throw new NarsilError(
|
|
@@ -2053,11 +2085,12 @@ async function loadSegmentedSnapshot(directory, indexName, manifest, deps) {
|
|
|
2053
2085
|
{ indexName, partitionId: partition.partitionId, partitionCount: deps.manager.partitionCount }
|
|
2054
2086
|
);
|
|
2055
2087
|
}
|
|
2056
|
-
await loadPartition(directory, indexName, partition, deps);
|
|
2088
|
+
await loadPartition(directory, indexName, partition, deps, payloadsByField);
|
|
2057
2089
|
}
|
|
2090
|
+
restoreVectorIndexes(payloadsByField, deps);
|
|
2058
2091
|
return manifest.checkpoint;
|
|
2059
2092
|
}
|
|
2060
|
-
async function loadPartition(directory, indexName, partition, deps) {
|
|
2093
|
+
async function loadPartition(directory, indexName, partition, deps, payloadsByField) {
|
|
2061
2094
|
const ordered = [];
|
|
2062
2095
|
for (const segment of partition.segments) {
|
|
2063
2096
|
ordered.push(await readSegmentContents(directory, segment.key));
|
|
@@ -2070,14 +2103,19 @@ async function loadPartition(directory, indexName, partition, deps) {
|
|
|
2070
2103
|
});
|
|
2071
2104
|
deps.manager.deserializePartition(partition.partitionId, merged);
|
|
2072
2105
|
for (const vector of partition.vectors) {
|
|
2073
|
-
|
|
2106
|
+
if (!deps.vectorIndexes.has(vector.fieldPath)) {
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
const payload = await readVectorSegment(directory, indexName, partition, vector.fieldPath, vector.key);
|
|
2110
|
+
const collected = payloadsByField.get(vector.fieldPath);
|
|
2111
|
+
if (collected === void 0) {
|
|
2112
|
+
payloadsByField.set(vector.fieldPath, [payload]);
|
|
2113
|
+
} else {
|
|
2114
|
+
collected.push(payload);
|
|
2115
|
+
}
|
|
2074
2116
|
}
|
|
2075
2117
|
}
|
|
2076
|
-
async function
|
|
2077
|
-
const vecIndex = deps.vectorIndexes.get(fieldPath);
|
|
2078
|
-
if (vecIndex === void 0) {
|
|
2079
|
-
return;
|
|
2080
|
-
}
|
|
2118
|
+
async function readVectorSegment(directory, indexName, partition, fieldPath, key) {
|
|
2081
2119
|
const bytes = await directory.read(key);
|
|
2082
2120
|
if (bytes === null) {
|
|
2083
2121
|
throw new NarsilError(
|
|
@@ -2086,12 +2124,31 @@ async function loadVectorSegment(directory, indexName, partition, fieldPath, key
|
|
|
2086
2124
|
{ indexName, partitionId: partition.partitionId, fieldPath, key }
|
|
2087
2125
|
);
|
|
2088
2126
|
}
|
|
2089
|
-
vecIndex.deserialize(await decodeVectorSegment(bytes));
|
|
2090
|
-
}
|
|
2091
|
-
async function decodeVectorSegment(bytes) {
|
|
2092
2127
|
const { payloadBytes } = await unpackEnvelopeBytes(bytes);
|
|
2093
2128
|
return decode(payloadBytes);
|
|
2094
2129
|
}
|
|
2130
|
+
function restoreVectorIndexes(payloadsByField, deps) {
|
|
2131
|
+
for (const [fieldPath, payloads] of payloadsByField) {
|
|
2132
|
+
const vecIndex = deps.vectorIndexes.get(fieldPath);
|
|
2133
|
+
if (vecIndex === void 0) {
|
|
2134
|
+
continue;
|
|
2135
|
+
}
|
|
2136
|
+
vecIndex.deserialize(payloads.length === 1 ? payloads[0] : mergePartitionPayloads(payloads));
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
function mergePartitionPayloads(payloads) {
|
|
2140
|
+
const vectors = [];
|
|
2141
|
+
const graphs = [];
|
|
2142
|
+
for (const payload of payloads) {
|
|
2143
|
+
for (const entry of payload.vectors) {
|
|
2144
|
+
vectors.push(entry);
|
|
2145
|
+
}
|
|
2146
|
+
for (const graph of payload.graphs) {
|
|
2147
|
+
graphs.push(graph);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
return { fieldName: payloads[0].fieldName, dimension: payloads[0].dimension, vectors, graphs, sq8: null };
|
|
2151
|
+
}
|
|
2095
2152
|
async function reclaimOrphanedSegments(directory, indexName, manifest) {
|
|
2096
2153
|
const referenced = manifestReferencedKeys(manifest);
|
|
2097
2154
|
for (const partition of manifest.partitions) {
|
|
@@ -2139,298 +2196,289 @@ function reconstructSchemaFromMetadata(metadata) {
|
|
|
2139
2196
|
if (metadata.surfaceForms === true) {
|
|
2140
2197
|
config.surfaceForms = true;
|
|
2141
2198
|
}
|
|
2199
|
+
if (metadata.tokenizer !== void 0) {
|
|
2200
|
+
config.tokenizer = metadata.tokenizer;
|
|
2201
|
+
}
|
|
2202
|
+
if (metadata.stopWords !== void 0) {
|
|
2203
|
+
config.stopWords = metadata.stopWords;
|
|
2204
|
+
} else if (metadata.stopWordList !== void 0) {
|
|
2205
|
+
config.stopWords = new Set(metadata.stopWordList);
|
|
2206
|
+
}
|
|
2207
|
+
if (metadata.partitionLimits !== void 0) {
|
|
2208
|
+
config.partitions = metadata.partitionLimits;
|
|
2209
|
+
}
|
|
2210
|
+
if (metadata.defaultScoring !== void 0) {
|
|
2211
|
+
config.defaultScoring = metadata.defaultScoring;
|
|
2212
|
+
}
|
|
2213
|
+
if (metadata.trackPositions !== void 0) {
|
|
2214
|
+
config.trackPositions = metadata.trackPositions;
|
|
2215
|
+
}
|
|
2216
|
+
if (metadata.strict !== void 0) {
|
|
2217
|
+
config.strict = metadata.strict;
|
|
2218
|
+
}
|
|
2219
|
+
if (metadata.required !== void 0) {
|
|
2220
|
+
config.required = metadata.required;
|
|
2221
|
+
}
|
|
2222
|
+
if (metadata.vectorPromotion !== void 0) {
|
|
2223
|
+
config.vectorPromotion = metadata.vectorPromotion;
|
|
2224
|
+
}
|
|
2142
2225
|
return config;
|
|
2143
2226
|
}
|
|
2144
2227
|
|
|
2145
|
-
// src/
|
|
2146
|
-
function
|
|
2147
|
-
const
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
applyIndexEntry(entry, manager, input.vectorFieldPaths, vectorSink);
|
|
2157
|
-
}
|
|
2228
|
+
// src/engine/vector-coordinator.ts
|
|
2229
|
+
function extractVectorFromDoc(document, fieldPath) {
|
|
2230
|
+
const value = getNestedValue(document, fieldPath);
|
|
2231
|
+
if (value === void 0 || value === null) return null;
|
|
2232
|
+
if (value instanceof Float32Array) return value;
|
|
2233
|
+
if (!Array.isArray(value)) return null;
|
|
2234
|
+
const arr = new Float32Array(value.length);
|
|
2235
|
+
for (let i = 0; i < value.length; i++) {
|
|
2236
|
+
const n = value[i];
|
|
2237
|
+
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
|
2238
|
+
arr[i] = n;
|
|
2158
2239
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2240
|
+
return arr;
|
|
2241
|
+
}
|
|
2242
|
+
function deleteNestedValue(obj, path) {
|
|
2243
|
+
if (!path.includes(".")) {
|
|
2244
|
+
delete obj[path];
|
|
2245
|
+
return;
|
|
2165
2246
|
}
|
|
2166
|
-
|
|
2167
|
-
|
|
2247
|
+
const segments = path.split(".");
|
|
2248
|
+
let current = obj;
|
|
2249
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
2250
|
+
const next = current[segments[i]];
|
|
2251
|
+
if (next === null || next === void 0 || typeof next !== "object") return;
|
|
2252
|
+
current = next;
|
|
2168
2253
|
}
|
|
2169
|
-
|
|
2170
|
-
payload: manager.serializePartitionToBytes(0),
|
|
2171
|
-
tombstones,
|
|
2172
|
-
docCount: liveDocIds.size
|
|
2173
|
-
};
|
|
2174
|
-
}
|
|
2175
|
-
function collectLiveDocIds(manager) {
|
|
2176
|
-
return new Set(manager.getPartition(0).docIds());
|
|
2254
|
+
delete current[segments[segments.length - 1]];
|
|
2177
2255
|
}
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
return { segments, nextSegmentId: input.nextSegmentId };
|
|
2184
|
-
}
|
|
2185
|
-
const windowLength = segments.length - compactionThreshold + 1;
|
|
2186
|
-
const start = chooseCheapestWindow(segments, windowLength);
|
|
2187
|
-
const window = segments.slice(start, start + windowLength);
|
|
2188
|
-
const contents = [];
|
|
2189
|
-
for (const ref of window) {
|
|
2190
|
-
contents.push(await readSegmentContents(input.directory, ref.key));
|
|
2256
|
+
function vectorsEqual(a, b) {
|
|
2257
|
+
if (!a) return false;
|
|
2258
|
+
if (a.length !== b.length) return false;
|
|
2259
|
+
for (let i = 0; i < a.length; i++) {
|
|
2260
|
+
if (a[i] !== b[i]) return false;
|
|
2191
2261
|
}
|
|
2192
|
-
|
|
2193
|
-
indexName: input.indexName,
|
|
2194
|
-
partitionId: input.partitionId,
|
|
2195
|
-
totalPartitions: 1,
|
|
2196
|
-
language: input.language.name
|
|
2197
|
-
});
|
|
2198
|
-
const liveDocIds = new Set(Object.keys(merged.documents));
|
|
2199
|
-
const retainedTombstones = collectRetainedTombstones(contents, liveDocIds);
|
|
2200
|
-
const payload = reserialize(input, merged);
|
|
2201
|
-
const id = input.nextSegmentId;
|
|
2202
|
-
const key = segmentKey(input.indexName, input.partitionId, id);
|
|
2203
|
-
await persistSegmentFile(input.directory, key, payload, retainedTombstones);
|
|
2204
|
-
const mergedRef = {
|
|
2205
|
-
id,
|
|
2206
|
-
key,
|
|
2207
|
-
docCount: liveDocIds.size,
|
|
2208
|
-
tombstoneCount: retainedTombstones.length
|
|
2209
|
-
};
|
|
2210
|
-
const nextSegments = [...segments.slice(0, start), mergedRef, ...segments.slice(start + windowLength)];
|
|
2211
|
-
return { segments: nextSegments, nextSegmentId: id + 1 };
|
|
2262
|
+
return true;
|
|
2212
2263
|
}
|
|
2213
|
-
function
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
bestCost = prefix;
|
|
2224
|
-
bestStart = start;
|
|
2264
|
+
function validateVectorDimensions(vectors, vecIndexes) {
|
|
2265
|
+
for (const [fieldPath, vector] of vectors) {
|
|
2266
|
+
const vecIndex = vecIndexes.get(fieldPath);
|
|
2267
|
+
if (!vecIndex) continue;
|
|
2268
|
+
if (vector.length !== vecIndex.dimension) {
|
|
2269
|
+
throw new NarsilError(
|
|
2270
|
+
ErrorCodes.VECTOR_DIMENSION_MISMATCH,
|
|
2271
|
+
`Vector dimension mismatch for field "${fieldPath}": expected ${vecIndex.dimension}, got ${vector.length}`,
|
|
2272
|
+
{ field: fieldPath, expected: vecIndex.dimension, received: vector.length }
|
|
2273
|
+
);
|
|
2225
2274
|
}
|
|
2226
2275
|
}
|
|
2227
|
-
return bestStart;
|
|
2228
2276
|
}
|
|
2229
|
-
function
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2277
|
+
function prepareDocumentVectors(document, vectorFieldPaths, vecIndexes) {
|
|
2278
|
+
if (vecIndexes.size === 0) {
|
|
2279
|
+
return { partitionDoc: document, extractedVectors: /* @__PURE__ */ new Map() };
|
|
2280
|
+
}
|
|
2281
|
+
const extractedVectors = /* @__PURE__ */ new Map();
|
|
2282
|
+
for (const fieldPath of vectorFieldPaths) {
|
|
2283
|
+
const vec = extractVectorFromDoc(document, fieldPath);
|
|
2284
|
+
if (vec) {
|
|
2285
|
+
extractedVectors.set(fieldPath, vec);
|
|
2236
2286
|
}
|
|
2237
2287
|
}
|
|
2238
|
-
|
|
2239
|
-
}
|
|
2240
|
-
function reserialize(input, merged) {
|
|
2241
|
-
const router = createPartitionRouter();
|
|
2242
|
-
const vectorSink = /* @__PURE__ */ new Map();
|
|
2243
|
-
const manager = createPartitionManager(input.indexName, input.config, input.language, router, 1, vectorSink);
|
|
2244
|
-
manager.deserializePartition(0, merged);
|
|
2245
|
-
return manager.serializePartitionToBytes(0);
|
|
2246
|
-
}
|
|
2247
|
-
async function writePartitionVectors(input) {
|
|
2248
|
-
if (input.vectorFields.size === 0) {
|
|
2249
|
-
return [];
|
|
2288
|
+
if (extractedVectors.size === 0) {
|
|
2289
|
+
return { partitionDoc: document, extractedVectors };
|
|
2250
2290
|
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2291
|
+
const partitionDoc = shallowCopyExcluding(document, extractedVectors.keys());
|
|
2292
|
+
return { partitionDoc, extractedVectors };
|
|
2293
|
+
}
|
|
2294
|
+
function shallowCopyExcluding(document, fieldPaths) {
|
|
2295
|
+
const topLevelExcludes = /* @__PURE__ */ new Set();
|
|
2296
|
+
const nestedPaths = [];
|
|
2297
|
+
for (const path of fieldPaths) {
|
|
2298
|
+
if (!path.includes(".")) {
|
|
2299
|
+
topLevelExcludes.add(path);
|
|
2300
|
+
} else {
|
|
2301
|
+
nestedPaths.push(path);
|
|
2302
|
+
}
|
|
2253
2303
|
}
|
|
2254
|
-
const
|
|
2255
|
-
for (const
|
|
2256
|
-
|
|
2304
|
+
const result = {};
|
|
2305
|
+
for (const key of Object.keys(document)) {
|
|
2306
|
+
if (topLevelExcludes.has(key)) continue;
|
|
2307
|
+
result[key] = document[key];
|
|
2257
2308
|
}
|
|
2258
|
-
const
|
|
2259
|
-
|
|
2260
|
-
|
|
2309
|
+
for (const path of nestedPaths) {
|
|
2310
|
+
const segments = path.split(".");
|
|
2311
|
+
const topKey = segments[0];
|
|
2312
|
+
if (!(topKey in result) || result[topKey] === null || typeof result[topKey] !== "object") continue;
|
|
2313
|
+
const source = document[topKey];
|
|
2314
|
+
let target = { ...source };
|
|
2315
|
+
result[topKey] = target;
|
|
2316
|
+
for (let i = 1; i < segments.length - 1; i++) {
|
|
2317
|
+
const seg = segments[i];
|
|
2318
|
+
const next = target[seg];
|
|
2319
|
+
if (next === null || next === void 0 || typeof next !== "object") break;
|
|
2320
|
+
const copy = { ...next };
|
|
2321
|
+
target[seg] = copy;
|
|
2322
|
+
target = copy;
|
|
2323
|
+
}
|
|
2324
|
+
delete target[segments[segments.length - 1]];
|
|
2261
2325
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2326
|
+
return result;
|
|
2327
|
+
}
|
|
2328
|
+
function insertDocumentVectors(docId, vectors, vecIndexes) {
|
|
2329
|
+
const insertedFields = [];
|
|
2330
|
+
try {
|
|
2331
|
+
for (const [fieldPath, vector] of vectors) {
|
|
2332
|
+
const vecIndex = vecIndexes.get(fieldPath);
|
|
2333
|
+
if (!vecIndex) continue;
|
|
2334
|
+
vecIndex.insert(docId, vector);
|
|
2335
|
+
insertedFields.push(fieldPath);
|
|
2268
2336
|
}
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
const
|
|
2272
|
-
vecIndex
|
|
2337
|
+
} catch (err) {
|
|
2338
|
+
for (const fieldPath of insertedFields) {
|
|
2339
|
+
const vecIndex = vecIndexes.get(fieldPath);
|
|
2340
|
+
if (vecIndex) {
|
|
2341
|
+
vecIndex.remove(docId);
|
|
2342
|
+
}
|
|
2273
2343
|
}
|
|
2344
|
+
throw err;
|
|
2274
2345
|
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
}
|
|
2346
|
+
return insertedFields;
|
|
2347
|
+
}
|
|
2348
|
+
function removeDocumentVectors(docId, vecIndexes) {
|
|
2349
|
+
for (const [, vecIndex] of vecIndexes) {
|
|
2350
|
+
vecIndex.remove(docId);
|
|
2281
2351
|
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
const
|
|
2287
|
-
|
|
2288
|
-
|
|
2352
|
+
}
|
|
2353
|
+
function updateDocumentVectors(docId, vectors, vecIndexes) {
|
|
2354
|
+
const updatedFields = [];
|
|
2355
|
+
try {
|
|
2356
|
+
for (const [fieldPath, newVec] of vectors) {
|
|
2357
|
+
const vecIndex = vecIndexes.get(fieldPath);
|
|
2358
|
+
if (!vecIndex) continue;
|
|
2359
|
+
if (newVec === null) {
|
|
2360
|
+
if (vecIndex.has(docId)) {
|
|
2361
|
+
const oldVec2 = vecIndex.getVector(docId);
|
|
2362
|
+
vecIndex.remove(docId);
|
|
2363
|
+
updatedFields.push({ fieldPath, oldVec: oldVec2 });
|
|
2364
|
+
}
|
|
2365
|
+
continue;
|
|
2366
|
+
}
|
|
2367
|
+
const oldVec = vecIndex.getVector(docId);
|
|
2368
|
+
if (vectorsEqual(oldVec, newVec)) continue;
|
|
2369
|
+
vecIndex.remove(docId);
|
|
2370
|
+
try {
|
|
2371
|
+
vecIndex.insert(docId, newVec);
|
|
2372
|
+
updatedFields.push({ fieldPath, oldVec });
|
|
2373
|
+
} catch (err) {
|
|
2374
|
+
if (oldVec) {
|
|
2375
|
+
vecIndex.insert(docId, oldVec);
|
|
2376
|
+
}
|
|
2377
|
+
throw err;
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
} catch (err) {
|
|
2381
|
+
for (const { fieldPath, oldVec } of updatedFields) {
|
|
2382
|
+
const vecIndex = vecIndexes.get(fieldPath);
|
|
2383
|
+
if (!vecIndex) continue;
|
|
2384
|
+
vecIndex.remove(docId);
|
|
2385
|
+
if (oldVec) {
|
|
2386
|
+
vecIndex.insert(docId, oldVec);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
throw err;
|
|
2289
2390
|
}
|
|
2290
|
-
return result;
|
|
2291
2391
|
}
|
|
2292
2392
|
|
|
2293
|
-
// src/
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
const config = reconstructSchemaFromMetadata(metadata);
|
|
2298
|
-
const language = getLanguage(config.language ?? "english");
|
|
2299
|
-
const vectorFields = extractVectorFieldsFromSchema(config.schema);
|
|
2300
|
-
const compactionThreshold = resolveCompactionThreshold(input.compactionThreshold);
|
|
2301
|
-
const context = {
|
|
2302
|
-
directory,
|
|
2303
|
-
indexName,
|
|
2304
|
-
config,
|
|
2305
|
-
language,
|
|
2306
|
-
vectorFields,
|
|
2307
|
-
vectorFieldPaths: new Set(vectorFields.keys()),
|
|
2308
|
-
compactionThreshold
|
|
2309
|
-
};
|
|
2310
|
-
const priorManifest = await readSegmentManifest(directory, indexName);
|
|
2311
|
-
const checkpointByPartition = /* @__PURE__ */ new Map();
|
|
2312
|
-
for (const target of input.targets) {
|
|
2313
|
-
checkpointByPartition.set(target.partitionId, {
|
|
2314
|
-
partitionId: target.partitionId,
|
|
2315
|
-
lastSeqNo: target.lastSeqNo,
|
|
2316
|
-
primaryTerm: target.primaryTerm
|
|
2317
|
-
});
|
|
2393
|
+
// src/distribution/replication/replica.ts
|
|
2394
|
+
function validateReplicationEntry(entry, localPrimaryTerm, log) {
|
|
2395
|
+
if (!log.verifyChecksum(entry)) {
|
|
2396
|
+
return { valid: false, error: ErrorCodes.REPLICATION_ENTRY_CORRUPT };
|
|
2318
2397
|
}
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
const priorPartition = priorManifest?.partitions.find((p) => p.partitionId === target.partitionId);
|
|
2322
|
-
const priorSeqNo = snapshotCheckpointFor(priorManifest?.checkpoint ?? [], target.partitionId);
|
|
2323
|
-
const entries = await collectWalEntriesInRange(
|
|
2324
|
-
directory,
|
|
2325
|
-
indexName,
|
|
2326
|
-
target.partitionId,
|
|
2327
|
-
priorSeqNo,
|
|
2328
|
-
target.lastSeqNo
|
|
2329
|
-
);
|
|
2330
|
-
partitions.push(await writePartition(context, target.partitionId, priorPartition, entries));
|
|
2398
|
+
if (entry.primaryTerm < localPrimaryTerm) {
|
|
2399
|
+
return { valid: false, error: ErrorCodes.REPLICATION_TERM_MISMATCH };
|
|
2331
2400
|
}
|
|
2332
|
-
|
|
2333
|
-
const manifest = {
|
|
2334
|
-
schema: metadata.schema,
|
|
2335
|
-
language: metadata.language,
|
|
2336
|
-
checkpoint: [...checkpointByPartition.values()],
|
|
2337
|
-
partitions
|
|
2338
|
-
};
|
|
2339
|
-
const parts = await encodeSegmentManifest(manifest);
|
|
2340
|
-
await directory.atomicWrite(manifestKey(indexName), [parts.header, parts.payload]);
|
|
2341
|
-
await collectGarbage(directory, indexName, priorManifest, manifest);
|
|
2401
|
+
return { valid: true };
|
|
2342
2402
|
}
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
const built = buildSegmentFromEntries({
|
|
2347
|
-
indexName: context.indexName,
|
|
2348
|
-
config: context.config,
|
|
2349
|
-
language: context.language,
|
|
2350
|
-
vectorFieldPaths: context.vectorFieldPaths,
|
|
2351
|
-
entries
|
|
2352
|
-
});
|
|
2353
|
-
if (built !== null) {
|
|
2354
|
-
const id = nextSegmentId;
|
|
2355
|
-
const key = segmentKey(context.indexName, partitionId, id);
|
|
2356
|
-
await persistSegmentFile(context.directory, key, built.payload, built.tombstones);
|
|
2357
|
-
segments.push({ id, key, docCount: built.docCount, tombstoneCount: built.tombstones.length });
|
|
2358
|
-
nextSegmentId = id + 1;
|
|
2403
|
+
function applyIndexEntry(entry, manager, vectorFieldPaths, vecIndexes) {
|
|
2404
|
+
if (entry.document === null) {
|
|
2405
|
+
return;
|
|
2359
2406
|
}
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
);
|
|
2407
|
+
const document = decode(entry.document);
|
|
2408
|
+
restoreVectorFields(document, vectorFieldPaths);
|
|
2409
|
+
if (manager.has(entry.documentId)) {
|
|
2410
|
+
removeDocumentVectors(entry.documentId, vecIndexes);
|
|
2411
|
+
manager.remove(entry.documentId);
|
|
2366
2412
|
}
|
|
2367
|
-
const
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
nextSegmentId,
|
|
2375
|
-
compactionThreshold: context.compactionThreshold
|
|
2376
|
-
});
|
|
2377
|
-
segments = compacted.segments;
|
|
2378
|
-
nextSegmentId = compacted.nextSegmentId;
|
|
2379
|
-
const vectors = await writePartitionVectors({
|
|
2380
|
-
directory: context.directory,
|
|
2381
|
-
indexName: context.indexName,
|
|
2382
|
-
partitionId,
|
|
2383
|
-
config: context.config,
|
|
2384
|
-
language: context.language,
|
|
2385
|
-
vectorFields: context.vectorFields,
|
|
2386
|
-
vectorFieldPaths: context.vectorFieldPaths,
|
|
2387
|
-
entries,
|
|
2388
|
-
priorVectors: priorPartition?.vectors ?? []
|
|
2389
|
-
});
|
|
2390
|
-
return { partitionId, nextSegmentId, segments, vectors };
|
|
2391
|
-
}
|
|
2392
|
-
function carryForwardUncheckpointedPartitions(priorManifest, targets, partitions, checkpointByPartition) {
|
|
2393
|
-
if (priorManifest === null) {
|
|
2394
|
-
return;
|
|
2413
|
+
const { partitionDoc, extractedVectors } = prepareDocumentVectors(document, vectorFieldPaths, vecIndexes);
|
|
2414
|
+
manager.insert(entry.documentId, partitionDoc);
|
|
2415
|
+
try {
|
|
2416
|
+
insertDocumentVectors(entry.documentId, extractedVectors, vecIndexes);
|
|
2417
|
+
} catch (err) {
|
|
2418
|
+
manager.remove(entry.documentId);
|
|
2419
|
+
throw err;
|
|
2395
2420
|
}
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
if (
|
|
2399
|
-
|
|
2400
|
-
}
|
|
2401
|
-
partitions.push(priorPartition);
|
|
2402
|
-
const priorCheckpoint = priorManifest.checkpoint.find((c) => c.partitionId === priorPartition.partitionId);
|
|
2403
|
-
if (priorCheckpoint !== void 0 && !checkpointByPartition.has(priorPartition.partitionId)) {
|
|
2404
|
-
checkpointByPartition.set(priorPartition.partitionId, priorCheckpoint);
|
|
2421
|
+
for (const fieldPath of extractedVectors.keys()) {
|
|
2422
|
+
const vecIndex = vecIndexes.get(fieldPath);
|
|
2423
|
+
if (vecIndex) {
|
|
2424
|
+
vecIndex.scheduleBuild();
|
|
2405
2425
|
}
|
|
2406
2426
|
}
|
|
2407
2427
|
}
|
|
2408
|
-
function
|
|
2409
|
-
|
|
2410
|
-
|
|
2428
|
+
function applyDeleteEntry(entry, manager, vecIndexes) {
|
|
2429
|
+
removeDocumentVectors(entry.documentId, vecIndexes);
|
|
2430
|
+
if (!manager.has(entry.documentId)) {
|
|
2431
|
+
return;
|
|
2411
2432
|
}
|
|
2412
|
-
|
|
2433
|
+
manager.remove(entry.documentId);
|
|
2413
2434
|
}
|
|
2414
|
-
|
|
2415
|
-
const
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2435
|
+
function restoreVectorFields(document, vectorFieldPaths) {
|
|
2436
|
+
for (const fieldPath of vectorFieldPaths) {
|
|
2437
|
+
const value = getNestedValue(document, fieldPath);
|
|
2438
|
+
if (value instanceof Uint8Array && !(value instanceof Float32Array)) {
|
|
2439
|
+
if (value.byteLength % 4 !== 0) {
|
|
2440
|
+
throw new Error(
|
|
2441
|
+
`Vector field "${fieldPath}" has invalid byte length ${value.byteLength} (must be divisible by 4)`
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
const elementCount = value.byteLength / 4;
|
|
2445
|
+
const float32 = new Float32Array(elementCount);
|
|
2446
|
+
const dataView = new DataView(value.buffer, value.byteOffset, value.byteLength);
|
|
2447
|
+
for (let i = 0; i < elementCount; i += 1) {
|
|
2448
|
+
float32[i] = dataView.getFloat32(i * 4, true);
|
|
2449
|
+
}
|
|
2450
|
+
setNestedValue(document, fieldPath, float32);
|
|
2451
|
+
}
|
|
2419
2452
|
}
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2453
|
+
}
|
|
2454
|
+
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
2455
|
+
function setNestedValue(obj, path, value) {
|
|
2456
|
+
if (!path.includes(".")) {
|
|
2457
|
+
if (FORBIDDEN_KEYS.has(path)) {
|
|
2458
|
+
return;
|
|
2423
2459
|
}
|
|
2460
|
+
obj[path] = value;
|
|
2461
|
+
return;
|
|
2424
2462
|
}
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2463
|
+
const segments = path.split(".");
|
|
2464
|
+
let current = obj;
|
|
2465
|
+
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
2466
|
+
const segment = segments[i];
|
|
2467
|
+
if (segment === void 0 || FORBIDDEN_KEYS.has(segment)) {
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
let next = current[segment];
|
|
2471
|
+
if (next === null || next === void 0 || typeof next !== "object") {
|
|
2472
|
+
next = {};
|
|
2473
|
+
current[segment] = next;
|
|
2431
2474
|
}
|
|
2475
|
+
current = next;
|
|
2432
2476
|
}
|
|
2433
|
-
|
|
2477
|
+
const finalSegment = segments.at(-1);
|
|
2478
|
+
if (finalSegment === void 0 || FORBIDDEN_KEYS.has(finalSegment)) {
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
current[finalSegment] = value;
|
|
2434
2482
|
}
|
|
2435
2483
|
|
|
2436
2484
|
// src/distribution/transport/types.ts
|
|
@@ -2742,225 +2790,570 @@ function readTailBeyondFrontier(data, durableByteLength, lastDurableSeqNo, maxRe
|
|
|
2742
2790
|
return { entries, cleanEnd };
|
|
2743
2791
|
}
|
|
2744
2792
|
|
|
2745
|
-
// src/persistence/durability/recovery.ts
|
|
2746
|
-
async function listPersistedIndexes(directory) {
|
|
2747
|
-
const metaKeys = await directory.list("");
|
|
2748
|
-
const names = [];
|
|
2749
|
-
for (const key of metaKeys) {
|
|
2750
|
-
if (key.endsWith("/meta")) {
|
|
2751
|
-
names.push(key.slice(0, -"/meta".length));
|
|
2793
|
+
// src/persistence/durability/recovery.ts
|
|
2794
|
+
async function listPersistedIndexes(directory) {
|
|
2795
|
+
const metaKeys = await directory.list("");
|
|
2796
|
+
const names = [];
|
|
2797
|
+
for (const key of metaKeys) {
|
|
2798
|
+
if (key.endsWith("/meta")) {
|
|
2799
|
+
names.push(key.slice(0, -"/meta".length));
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
return names;
|
|
2803
|
+
}
|
|
2804
|
+
async function loadMetadata(directory, indexName) {
|
|
2805
|
+
const bytes = await directory.read(`${indexName}/meta`);
|
|
2806
|
+
if (bytes === null) {
|
|
2807
|
+
return null;
|
|
2808
|
+
}
|
|
2809
|
+
const { metadata } = await readMetadataEnvelope(bytes);
|
|
2810
|
+
return metadata;
|
|
2811
|
+
}
|
|
2812
|
+
async function loadSnapshotBundleBytes(bytes, deps) {
|
|
2813
|
+
const bundle = await decodeSnapshotBundle(bytes);
|
|
2814
|
+
while (deps.manager.partitionCount < bundle.partitions.length) {
|
|
2815
|
+
deps.manager.addPartition();
|
|
2816
|
+
}
|
|
2817
|
+
if (bundle.partitions.length > 0) {
|
|
2818
|
+
deps.manager.trimPartitions(bundle.partitions.length);
|
|
2819
|
+
}
|
|
2820
|
+
for (let i = 0; i < bundle.partitions.length; i += 1) {
|
|
2821
|
+
deps.manager.deserializePartition(i, deserializePayloadV2(bundle.partitions[i]));
|
|
2822
|
+
}
|
|
2823
|
+
for (const [fieldPath, payload] of Object.entries(bundle.vectorIndexes)) {
|
|
2824
|
+
const vecIndex = deps.vectorIndexes.get(fieldPath);
|
|
2825
|
+
if (vecIndex) {
|
|
2826
|
+
vecIndex.deserialize(payload);
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
return bundle.checkpoint;
|
|
2830
|
+
}
|
|
2831
|
+
async function loadSnapshot(directory, indexName, deps) {
|
|
2832
|
+
const manifest = await readSegmentManifest(directory, indexName);
|
|
2833
|
+
if (manifest !== null) {
|
|
2834
|
+
return loadSegmentedSnapshot(directory, indexName, manifest, deps);
|
|
2835
|
+
}
|
|
2836
|
+
const bytes = await directory.read(`${indexName}/snapshot`);
|
|
2837
|
+
if (bytes === null) {
|
|
2838
|
+
return [];
|
|
2839
|
+
}
|
|
2840
|
+
return loadSnapshotBundleBytes(bytes, deps);
|
|
2841
|
+
}
|
|
2842
|
+
function segmentStartSeqNo(key, prefix) {
|
|
2843
|
+
if (!key.startsWith(prefix)) {
|
|
2844
|
+
return null;
|
|
2845
|
+
}
|
|
2846
|
+
const tail = key.slice(prefix.length);
|
|
2847
|
+
if (!/^\d{16}$/.test(tail)) {
|
|
2848
|
+
return null;
|
|
2849
|
+
}
|
|
2850
|
+
const value = Number.parseInt(tail, 10);
|
|
2851
|
+
return Number.isSafeInteger(value) ? value : null;
|
|
2852
|
+
}
|
|
2853
|
+
async function readWalSegments(directory, indexName, partitionId) {
|
|
2854
|
+
const prefix = `${indexName}/wal/${partitionId}/`;
|
|
2855
|
+
const markerBytes = await directory.read(`${prefix}commit`);
|
|
2856
|
+
const marker = markerBytes === null ? null : readCommitMarker(markerBytes);
|
|
2857
|
+
if (marker === null) {
|
|
2858
|
+
return null;
|
|
2859
|
+
}
|
|
2860
|
+
const segments = await collectSegments(directory, prefix);
|
|
2861
|
+
const durableEntries = [];
|
|
2862
|
+
let highestReadFromWal = 0;
|
|
2863
|
+
let activeTail = null;
|
|
2864
|
+
for (const { key, startSeqNo } of segments) {
|
|
2865
|
+
if (startSeqNo > marker.state.activeSegmentSeqNo) {
|
|
2866
|
+
continue;
|
|
2867
|
+
}
|
|
2868
|
+
const bytes = await directory.read(key);
|
|
2869
|
+
if (bytes === null) {
|
|
2870
|
+
continue;
|
|
2871
|
+
}
|
|
2872
|
+
if (startSeqNo < marker.state.activeSegmentSeqNo) {
|
|
2873
|
+
const header = checkSegmentHeader(bytes);
|
|
2874
|
+
if (!header.ok) {
|
|
2875
|
+
throw new NarsilError(
|
|
2876
|
+
ErrorCodes.PERSISTENCE_WAL_CORRUPT,
|
|
2877
|
+
`Sealed WAL segment header invalid: ${header.reason}`,
|
|
2878
|
+
{
|
|
2879
|
+
key,
|
|
2880
|
+
reason: header.reason
|
|
2881
|
+
}
|
|
2882
|
+
);
|
|
2883
|
+
}
|
|
2884
|
+
for (const entry of readDurableRegion(bytes, bytes.length)) {
|
|
2885
|
+
durableEntries.push(entry);
|
|
2886
|
+
if (entry.seqNo > highestReadFromWal) {
|
|
2887
|
+
highestReadFromWal = entry.seqNo;
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
continue;
|
|
2891
|
+
}
|
|
2892
|
+
for (const entry of readDurableRegion(bytes, marker.state.durableByteLength)) {
|
|
2893
|
+
durableEntries.push(entry);
|
|
2894
|
+
if (entry.seqNo > highestReadFromWal) {
|
|
2895
|
+
highestReadFromWal = entry.seqNo;
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
const tail = readTailBeyondFrontier(
|
|
2899
|
+
bytes,
|
|
2900
|
+
marker.state.durableByteLength,
|
|
2901
|
+
Math.max(highestReadFromWal, marker.state.highestDurableSeqNo)
|
|
2902
|
+
);
|
|
2903
|
+
activeTail = { key, entries: tail.entries, cleanEnd: tail.cleanEnd, segmentLength: bytes.length };
|
|
2904
|
+
}
|
|
2905
|
+
return { marker, segments, durableEntries, activeTail, highestReadFromWal };
|
|
2906
|
+
}
|
|
2907
|
+
async function replayWal(directory, indexName, partitionId, fromSeqNoExclusive, deps) {
|
|
2908
|
+
const read = await readWalSegments(directory, indexName, partitionId);
|
|
2909
|
+
if (read === null) {
|
|
2910
|
+
return { highestSeqNo: fromSeqNoExclusive, highestPrimaryTerm: 0 };
|
|
2911
|
+
}
|
|
2912
|
+
const { marker, segments, durableEntries, activeTail, highestReadFromWal } = read;
|
|
2913
|
+
if (Math.max(fromSeqNoExclusive, highestReadFromWal) < marker.state.highestDurableSeqNo) {
|
|
2914
|
+
throw new NarsilError(
|
|
2915
|
+
ErrorCodes.PERSISTENCE_WAL_CORRUPT,
|
|
2916
|
+
"A durable WAL record is missing: the highest recovered seqNo is below the commit marker",
|
|
2917
|
+
{
|
|
2918
|
+
indexName,
|
|
2919
|
+
partitionId,
|
|
2920
|
+
highestRead: highestReadFromWal,
|
|
2921
|
+
highestDurable: marker.state.highestDurableSeqNo
|
|
2922
|
+
}
|
|
2923
|
+
);
|
|
2924
|
+
}
|
|
2925
|
+
let highestSeqNo = fromSeqNoExclusive;
|
|
2926
|
+
let highestPrimaryTerm = 0;
|
|
2927
|
+
const replay = (entries) => {
|
|
2928
|
+
for (const entry of entries) {
|
|
2929
|
+
if (entry.seqNo <= highestSeqNo) {
|
|
2930
|
+
continue;
|
|
2931
|
+
}
|
|
2932
|
+
applyEntry(entry, deps);
|
|
2933
|
+
highestSeqNo = entry.seqNo;
|
|
2934
|
+
if (entry.primaryTerm > highestPrimaryTerm) {
|
|
2935
|
+
highestPrimaryTerm = entry.primaryTerm;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
};
|
|
2939
|
+
replay(durableEntries);
|
|
2940
|
+
if (activeTail !== null) {
|
|
2941
|
+
replay(activeTail.entries);
|
|
2942
|
+
}
|
|
2943
|
+
await deleteOrphanSegments(directory, segments, marker.state.activeSegmentSeqNo);
|
|
2944
|
+
if (activeTail !== null && activeTail.cleanEnd < activeTail.segmentLength) {
|
|
2945
|
+
await truncateSegmentTail(directory, activeTail.key, activeTail.cleanEnd);
|
|
2946
|
+
}
|
|
2947
|
+
return { highestSeqNo, highestPrimaryTerm };
|
|
2948
|
+
}
|
|
2949
|
+
async function collectSegments(directory, prefix) {
|
|
2950
|
+
const keys = await directory.list(prefix);
|
|
2951
|
+
const refs = [];
|
|
2952
|
+
for (const key of keys) {
|
|
2953
|
+
const startSeqNo = segmentStartSeqNo(key, prefix);
|
|
2954
|
+
if (startSeqNo !== null) {
|
|
2955
|
+
refs.push({ key, startSeqNo });
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
refs.sort((a, b) => a.startSeqNo - b.startSeqNo);
|
|
2959
|
+
return refs;
|
|
2960
|
+
}
|
|
2961
|
+
async function deleteOrphanSegments(directory, segments, activeSegmentSeqNo) {
|
|
2962
|
+
for (const { key, startSeqNo } of segments) {
|
|
2963
|
+
if (startSeqNo > activeSegmentSeqNo) {
|
|
2964
|
+
await directory.remove(key);
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
async function truncateSegmentTail(directory, key, durableByteLength) {
|
|
2969
|
+
const cleanLength = Math.max(durableByteLength, SEGMENT_HEADER_SIZE);
|
|
2970
|
+
const handle = await directory.appendHandle(key);
|
|
2971
|
+
try {
|
|
2972
|
+
const currentSize = await handle.size();
|
|
2973
|
+
if (cleanLength < currentSize) {
|
|
2974
|
+
await handle.truncate(cleanLength);
|
|
2975
|
+
}
|
|
2976
|
+
} finally {
|
|
2977
|
+
await handle.close();
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
function applyEntry(entry, deps) {
|
|
2981
|
+
if (entry.operation === "DELETE") {
|
|
2982
|
+
applyDeleteEntry(entry, deps.manager, deps.vectorIndexes);
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
applyIndexEntry(entry, deps.manager, deps.vectorFieldPaths, deps.vectorIndexes);
|
|
2986
|
+
}
|
|
2987
|
+
function snapshotCheckpointFor(checkpoint, partitionId) {
|
|
2988
|
+
return checkpointLastSeqNo(checkpoint, partitionId);
|
|
2989
|
+
}
|
|
2990
|
+
async function collectWalEntriesInRange(directory, indexName, partitionId, fromSeqNoExclusive, upToSeqNoInclusive) {
|
|
2991
|
+
const read = await readWalSegments(directory, indexName, partitionId);
|
|
2992
|
+
if (read === null) {
|
|
2993
|
+
return [];
|
|
2994
|
+
}
|
|
2995
|
+
if (Math.max(fromSeqNoExclusive, read.highestReadFromWal) < read.marker.state.highestDurableSeqNo) {
|
|
2996
|
+
throw new NarsilError(
|
|
2997
|
+
ErrorCodes.PERSISTENCE_WAL_CORRUPT,
|
|
2998
|
+
"A durable WAL record is missing: the highest recovered seqNo is below the commit marker",
|
|
2999
|
+
{
|
|
3000
|
+
indexName,
|
|
3001
|
+
partitionId,
|
|
3002
|
+
highestRead: read.highestReadFromWal,
|
|
3003
|
+
highestDurable: read.marker.state.highestDurableSeqNo
|
|
3004
|
+
}
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
3007
|
+
const entries = [];
|
|
3008
|
+
for (const entry of read.durableEntries) {
|
|
3009
|
+
if (entry.seqNo <= fromSeqNoExclusive || entry.seqNo > upToSeqNoInclusive) {
|
|
3010
|
+
continue;
|
|
3011
|
+
}
|
|
3012
|
+
entries.push(entry);
|
|
3013
|
+
}
|
|
3014
|
+
return entries;
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
// src/persistence/durability/segment/build-segment.ts
|
|
3018
|
+
function buildSegmentFromEntries(input) {
|
|
3019
|
+
const router = createPartitionRouter();
|
|
3020
|
+
const vectorSink = /* @__PURE__ */ new Map();
|
|
3021
|
+
const manager = createPartitionManager(input.indexName, input.config, input.language, router, 1, vectorSink);
|
|
3022
|
+
const deleted = /* @__PURE__ */ new Set();
|
|
3023
|
+
for (const entry of input.entries) {
|
|
3024
|
+
if (entry.operation === "DELETE") {
|
|
3025
|
+
deleted.add(entry.documentId);
|
|
3026
|
+
applyDeleteEntry(entry, manager, vectorSink);
|
|
3027
|
+
} else {
|
|
3028
|
+
applyIndexEntry(entry, manager, input.vectorFieldPaths, vectorSink);
|
|
2752
3029
|
}
|
|
2753
3030
|
}
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
3031
|
+
const liveDocIds = collectLiveDocIds(manager);
|
|
3032
|
+
const tombstones = [];
|
|
3033
|
+
for (const docId of deleted) {
|
|
3034
|
+
if (!liveDocIds.has(docId)) {
|
|
3035
|
+
tombstones.push(docId);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
if (liveDocIds.size === 0 && tombstones.length === 0) {
|
|
2759
3039
|
return null;
|
|
2760
3040
|
}
|
|
2761
|
-
|
|
2762
|
-
|
|
3041
|
+
return {
|
|
3042
|
+
payload: manager.serializePartitionToBytes(0),
|
|
3043
|
+
tombstones,
|
|
3044
|
+
docCount: liveDocIds.size
|
|
3045
|
+
};
|
|
2763
3046
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
3047
|
+
function collectLiveDocIds(manager) {
|
|
3048
|
+
return new Set(manager.getPartition(0).docIds());
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
// src/persistence/durability/segment/compaction.ts
|
|
3052
|
+
async function compactPartitionSegments(input) {
|
|
3053
|
+
const { segments, compactionThreshold } = input;
|
|
3054
|
+
if (segments.length <= compactionThreshold) {
|
|
3055
|
+
return { segments, nextSegmentId: input.nextSegmentId };
|
|
2768
3056
|
}
|
|
2769
|
-
|
|
2770
|
-
|
|
3057
|
+
const windowLength = segments.length - compactionThreshold + 1;
|
|
3058
|
+
const start = chooseCheapestWindow(segments, windowLength);
|
|
3059
|
+
const window = segments.slice(start, start + windowLength);
|
|
3060
|
+
const contents = [];
|
|
3061
|
+
for (const ref of window) {
|
|
3062
|
+
contents.push(await readSegmentContents(input.directory, ref.key));
|
|
2771
3063
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
3064
|
+
const merged = mergeTimeOrderedSegments(contents, {
|
|
3065
|
+
indexName: input.indexName,
|
|
3066
|
+
partitionId: input.partitionId,
|
|
3067
|
+
totalPartitions: 1,
|
|
3068
|
+
language: input.language.name
|
|
3069
|
+
});
|
|
3070
|
+
const liveDocIds = new Set(Object.keys(merged.documents));
|
|
3071
|
+
const retainedTombstones = collectRetainedTombstones(contents, liveDocIds);
|
|
3072
|
+
const payload = reserialize(input, merged);
|
|
3073
|
+
const id = input.nextSegmentId;
|
|
3074
|
+
const key = segmentKey(input.indexName, input.partitionId, id);
|
|
3075
|
+
await persistSegmentFile(input.directory, key, payload, retainedTombstones);
|
|
3076
|
+
const mergedRef = {
|
|
3077
|
+
id,
|
|
3078
|
+
key,
|
|
3079
|
+
docCount: liveDocIds.size,
|
|
3080
|
+
tombstoneCount: retainedTombstones.length
|
|
3081
|
+
};
|
|
3082
|
+
const nextSegments = [...segments.slice(0, start), mergedRef, ...segments.slice(start + windowLength)];
|
|
3083
|
+
return { segments: nextSegments, nextSegmentId: id + 1 };
|
|
3084
|
+
}
|
|
3085
|
+
function chooseCheapestWindow(segments, windowLength) {
|
|
3086
|
+
let prefix = 0;
|
|
3087
|
+
for (let i = 0; i < windowLength; i += 1) {
|
|
3088
|
+
prefix += segments[i].docCount;
|
|
3089
|
+
}
|
|
3090
|
+
let bestStart = 0;
|
|
3091
|
+
let bestCost = prefix;
|
|
3092
|
+
for (let start = 1; start + windowLength <= segments.length; start += 1) {
|
|
3093
|
+
prefix += segments[start + windowLength - 1].docCount - segments[start - 1].docCount;
|
|
3094
|
+
if (prefix < bestCost) {
|
|
3095
|
+
bestCost = prefix;
|
|
3096
|
+
bestStart = start;
|
|
2776
3097
|
}
|
|
2777
3098
|
}
|
|
2778
|
-
return
|
|
3099
|
+
return bestStart;
|
|
2779
3100
|
}
|
|
2780
|
-
|
|
2781
|
-
const
|
|
2782
|
-
|
|
2783
|
-
|
|
3101
|
+
function collectRetainedTombstones(contents, liveDocIds) {
|
|
3102
|
+
const retained = /* @__PURE__ */ new Set();
|
|
3103
|
+
for (const segment of contents) {
|
|
3104
|
+
for (const docId of segment.tombstones) {
|
|
3105
|
+
if (!liveDocIds.has(docId)) {
|
|
3106
|
+
retained.add(docId);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
2784
3109
|
}
|
|
2785
|
-
|
|
2786
|
-
|
|
3110
|
+
return [...retained];
|
|
3111
|
+
}
|
|
3112
|
+
function reserialize(input, merged) {
|
|
3113
|
+
const router = createPartitionRouter();
|
|
3114
|
+
const vectorSink = /* @__PURE__ */ new Map();
|
|
3115
|
+
const manager = createPartitionManager(input.indexName, input.config, input.language, router, 1, vectorSink);
|
|
3116
|
+
manager.deserializePartition(0, merged);
|
|
3117
|
+
return manager.serializePartitionToBytes(0);
|
|
3118
|
+
}
|
|
3119
|
+
async function writePartitionVectors(input) {
|
|
3120
|
+
if (input.vectorFields.size === 0) {
|
|
2787
3121
|
return [];
|
|
2788
3122
|
}
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
function segmentStartSeqNo(key, prefix) {
|
|
2792
|
-
if (!key.startsWith(prefix)) {
|
|
2793
|
-
return null;
|
|
3123
|
+
if (input.entries.length === 0) {
|
|
3124
|
+
return input.priorVectors;
|
|
2794
3125
|
}
|
|
2795
|
-
const
|
|
2796
|
-
|
|
2797
|
-
|
|
3126
|
+
const priorByField = /* @__PURE__ */ new Map();
|
|
3127
|
+
for (const ref of input.priorVectors) {
|
|
3128
|
+
priorByField.set(ref.fieldPath, ref);
|
|
2798
3129
|
}
|
|
2799
|
-
const
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
async function readWalSegments(directory, indexName, partitionId) {
|
|
2803
|
-
const prefix = `${indexName}/wal/${partitionId}/`;
|
|
2804
|
-
const markerBytes = await directory.read(`${prefix}commit`);
|
|
2805
|
-
const marker = markerBytes === null ? null : readCommitMarker(markerBytes);
|
|
2806
|
-
if (marker === null) {
|
|
2807
|
-
return null;
|
|
3130
|
+
const vectorIndexes = /* @__PURE__ */ new Map();
|
|
3131
|
+
for (const [fieldPath, dimension] of input.vectorFields) {
|
|
3132
|
+
vectorIndexes.set(fieldPath, createVectorIndex(fieldPath, dimension, input.config.vectorPromotion));
|
|
2808
3133
|
}
|
|
2809
|
-
const
|
|
2810
|
-
const
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
if (startSeqNo > marker.state.activeSegmentSeqNo) {
|
|
2815
|
-
continue;
|
|
2816
|
-
}
|
|
2817
|
-
const bytes = await directory.read(key);
|
|
2818
|
-
if (bytes === null) {
|
|
3134
|
+
const router = createPartitionRouter();
|
|
3135
|
+
const manager = createPartitionManager(input.indexName, input.config, input.language, router, 1, vectorIndexes);
|
|
3136
|
+
for (const ref of input.priorVectors) {
|
|
3137
|
+
const vecIndex = vectorIndexes.get(ref.fieldPath);
|
|
3138
|
+
if (vecIndex === void 0) {
|
|
2819
3139
|
continue;
|
|
2820
3140
|
}
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
ErrorCodes.PERSISTENCE_WAL_CORRUPT,
|
|
2826
|
-
`Sealed WAL segment header invalid: ${header.reason}`,
|
|
2827
|
-
{
|
|
2828
|
-
key,
|
|
2829
|
-
reason: header.reason
|
|
2830
|
-
}
|
|
2831
|
-
);
|
|
2832
|
-
}
|
|
2833
|
-
for (const entry of readDurableRegion(bytes, bytes.length)) {
|
|
2834
|
-
durableEntries.push(entry);
|
|
2835
|
-
if (entry.seqNo > highestReadFromWal) {
|
|
2836
|
-
highestReadFromWal = entry.seqNo;
|
|
2837
|
-
}
|
|
2838
|
-
}
|
|
2839
|
-
continue;
|
|
3141
|
+
const bytes = await input.directory.read(ref.key);
|
|
3142
|
+
if (bytes !== null) {
|
|
3143
|
+
const { payloadBytes } = await unpackEnvelopeBytes(bytes);
|
|
3144
|
+
vecIndex.deserialize(decode(payloadBytes));
|
|
2840
3145
|
}
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
3146
|
+
}
|
|
3147
|
+
for (const entry of input.entries) {
|
|
3148
|
+
if (entry.operation === "DELETE") {
|
|
3149
|
+
applyDeleteEntry(entry, manager, vectorIndexes);
|
|
3150
|
+
} else {
|
|
3151
|
+
applyIndexEntry(entry, manager, input.vectorFieldPaths, vectorIndexes);
|
|
2846
3152
|
}
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
3153
|
+
}
|
|
3154
|
+
const result = [];
|
|
3155
|
+
for (const [fieldPath, vecIndex] of vectorIndexes) {
|
|
3156
|
+
const generation = (priorByField.get(fieldPath)?.generation ?? 0) + 1;
|
|
3157
|
+
const key = vectorSegmentKey(input.indexName, input.partitionId, fieldPath, generation);
|
|
3158
|
+
const parts = await packSnapshotEnvelopePartsRetrying(() => encode(vecIndex.serialize()));
|
|
3159
|
+
await input.directory.atomicWrite(key, [parts.header, parts.payload]);
|
|
3160
|
+
result.push({ fieldPath, generation, key });
|
|
3161
|
+
}
|
|
3162
|
+
return result;
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
// src/persistence/durability/segment/write.ts
|
|
3166
|
+
async function writeSegmentedCheckpoint(input) {
|
|
3167
|
+
const { directory, metadata } = input;
|
|
3168
|
+
const indexName = metadata.indexName;
|
|
3169
|
+
const config = reconstructSchemaFromMetadata(metadata);
|
|
3170
|
+
const language = getLanguage(config.language ?? "english");
|
|
3171
|
+
const vectorFields = extractVectorFieldsFromSchema(config.schema);
|
|
3172
|
+
const compactionThreshold = resolveCompactionThreshold(input.compactionThreshold);
|
|
3173
|
+
const context = {
|
|
3174
|
+
directory,
|
|
3175
|
+
indexName,
|
|
3176
|
+
config,
|
|
3177
|
+
language,
|
|
3178
|
+
vectorFields,
|
|
3179
|
+
vectorFieldPaths: new Set(vectorFields.keys()),
|
|
3180
|
+
compactionThreshold
|
|
3181
|
+
};
|
|
3182
|
+
const priorManifest = await readSegmentManifest(directory, indexName);
|
|
3183
|
+
const checkpointByPartition = /* @__PURE__ */ new Map();
|
|
3184
|
+
for (const target of input.targets) {
|
|
3185
|
+
checkpointByPartition.set(target.partitionId, {
|
|
3186
|
+
partitionId: target.partitionId,
|
|
3187
|
+
lastSeqNo: target.lastSeqNo,
|
|
3188
|
+
primaryTerm: target.primaryTerm
|
|
3189
|
+
});
|
|
3190
|
+
}
|
|
3191
|
+
const partitions = [];
|
|
3192
|
+
for (const target of input.targets) {
|
|
3193
|
+
const priorPartition = priorManifest?.partitions.find((p) => p.partitionId === target.partitionId);
|
|
3194
|
+
const priorSeqNo = snapshotCheckpointFor(priorManifest?.checkpoint ?? [], target.partitionId);
|
|
3195
|
+
const entries = await collectWalEntriesInRange(
|
|
3196
|
+
directory,
|
|
3197
|
+
indexName,
|
|
3198
|
+
target.partitionId,
|
|
3199
|
+
priorSeqNo,
|
|
3200
|
+
target.lastSeqNo
|
|
3201
|
+
);
|
|
3202
|
+
partitions.push(
|
|
3203
|
+
input.wholePartitionPayload === void 0 ? await writePartition(context, target.partitionId, priorPartition, entries) : await writeWholePartition(
|
|
3204
|
+
context,
|
|
3205
|
+
target.partitionId,
|
|
3206
|
+
priorPartition,
|
|
3207
|
+
input.wholePartitionPayload(target.partitionId),
|
|
3208
|
+
entries
|
|
3209
|
+
)
|
|
2851
3210
|
);
|
|
2852
|
-
activeTail = { key, entries: tail.entries, cleanEnd: tail.cleanEnd, segmentLength: bytes.length };
|
|
2853
3211
|
}
|
|
2854
|
-
|
|
3212
|
+
carryForwardUncheckpointedPartitions(priorManifest, input.targets, partitions, checkpointByPartition);
|
|
3213
|
+
const manifest = {
|
|
3214
|
+
schema: metadata.schema,
|
|
3215
|
+
language: metadata.language,
|
|
3216
|
+
checkpoint: [...checkpointByPartition.values()],
|
|
3217
|
+
partitions
|
|
3218
|
+
};
|
|
3219
|
+
const parts = await encodeSegmentManifest(manifest);
|
|
3220
|
+
await directory.atomicWrite(manifestKey(indexName), [parts.header, parts.payload]);
|
|
3221
|
+
await collectGarbage(directory, indexName, priorManifest, manifest);
|
|
2855
3222
|
}
|
|
2856
|
-
async function
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
3223
|
+
async function writePartition(context, partitionId, priorPartition, entries) {
|
|
3224
|
+
let segments = priorPartition ? [...priorPartition.segments] : [];
|
|
3225
|
+
let nextSegmentId = priorPartition?.nextSegmentId ?? 0;
|
|
3226
|
+
const built = buildSegmentFromEntries({
|
|
3227
|
+
indexName: context.indexName,
|
|
3228
|
+
config: context.config,
|
|
3229
|
+
language: context.language,
|
|
3230
|
+
vectorFieldPaths: context.vectorFieldPaths,
|
|
3231
|
+
entries
|
|
3232
|
+
});
|
|
3233
|
+
if (built !== null) {
|
|
3234
|
+
const id = nextSegmentId;
|
|
3235
|
+
const key = segmentKey(context.indexName, partitionId, id);
|
|
3236
|
+
await persistSegmentFile(context.directory, key, built.payload, built.tombstones);
|
|
3237
|
+
segments.push({ id, key, docCount: built.docCount, tombstoneCount: built.tombstones.length });
|
|
3238
|
+
nextSegmentId = id + 1;
|
|
2860
3239
|
}
|
|
2861
|
-
|
|
2862
|
-
if (Math.max(fromSeqNoExclusive, highestReadFromWal) < marker.state.highestDurableSeqNo) {
|
|
3240
|
+
if (segments.length > MAX_SEGMENTS_PER_PARTITION) {
|
|
2863
3241
|
throw new NarsilError(
|
|
2864
|
-
ErrorCodes.
|
|
2865
|
-
|
|
2866
|
-
{
|
|
2867
|
-
indexName,
|
|
2868
|
-
partitionId,
|
|
2869
|
-
highestRead: highestReadFromWal,
|
|
2870
|
-
highestDurable: marker.state.highestDurableSeqNo
|
|
2871
|
-
}
|
|
3242
|
+
ErrorCodes.PERSISTENCE_SAVE_FAILED,
|
|
3243
|
+
`Partition ${partitionId} would exceed the maximum of ${MAX_SEGMENTS_PER_PARTITION} segments`,
|
|
3244
|
+
{ partitionId, segmentCount: segments.length, maximum: MAX_SEGMENTS_PER_PARTITION }
|
|
2872
3245
|
);
|
|
2873
3246
|
}
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
3247
|
+
const compacted = await compactPartitionSegments({
|
|
3248
|
+
directory: context.directory,
|
|
3249
|
+
indexName: context.indexName,
|
|
3250
|
+
partitionId,
|
|
3251
|
+
config: context.config,
|
|
3252
|
+
language: context.language,
|
|
3253
|
+
segments,
|
|
3254
|
+
nextSegmentId,
|
|
3255
|
+
compactionThreshold: context.compactionThreshold
|
|
3256
|
+
});
|
|
3257
|
+
segments = compacted.segments;
|
|
3258
|
+
nextSegmentId = compacted.nextSegmentId;
|
|
3259
|
+
const vectors = await writePartitionVectors({
|
|
3260
|
+
directory: context.directory,
|
|
3261
|
+
indexName: context.indexName,
|
|
3262
|
+
partitionId,
|
|
3263
|
+
config: context.config,
|
|
3264
|
+
language: context.language,
|
|
3265
|
+
vectorFields: context.vectorFields,
|
|
3266
|
+
vectorFieldPaths: context.vectorFieldPaths,
|
|
3267
|
+
entries,
|
|
3268
|
+
priorVectors: priorPartition?.vectors ?? []
|
|
3269
|
+
});
|
|
3270
|
+
return { partitionId, nextSegmentId, segments, vectors };
|
|
3271
|
+
}
|
|
3272
|
+
async function writeWholePartition(context, partitionId, priorPartition, whole, entries) {
|
|
3273
|
+
const id = priorPartition?.nextSegmentId ?? 0;
|
|
3274
|
+
const key = segmentKey(context.indexName, partitionId, id);
|
|
3275
|
+
await persistSegmentFile(context.directory, key, whole.payload, []);
|
|
3276
|
+
const vectors = await writePartitionVectors({
|
|
3277
|
+
directory: context.directory,
|
|
3278
|
+
indexName: context.indexName,
|
|
3279
|
+
partitionId,
|
|
3280
|
+
config: context.config,
|
|
3281
|
+
language: context.language,
|
|
3282
|
+
vectorFields: context.vectorFields,
|
|
3283
|
+
vectorFieldPaths: context.vectorFieldPaths,
|
|
3284
|
+
entries,
|
|
3285
|
+
priorVectors: priorPartition?.vectors ?? []
|
|
3286
|
+
});
|
|
3287
|
+
return {
|
|
3288
|
+
partitionId,
|
|
3289
|
+
nextSegmentId: id + 1,
|
|
3290
|
+
segments: [{ id, key, docCount: whole.docCount, tombstoneCount: 0 }],
|
|
3291
|
+
vectors
|
|
2887
3292
|
};
|
|
2888
|
-
replay(durableEntries);
|
|
2889
|
-
if (activeTail !== null) {
|
|
2890
|
-
replay(activeTail.entries);
|
|
2891
|
-
}
|
|
2892
|
-
await deleteOrphanSegments(directory, segments, marker.state.activeSegmentSeqNo);
|
|
2893
|
-
if (activeTail !== null && activeTail.cleanEnd < activeTail.segmentLength) {
|
|
2894
|
-
await truncateSegmentTail(directory, activeTail.key, activeTail.cleanEnd);
|
|
2895
|
-
}
|
|
2896
|
-
return { highestSeqNo, highestPrimaryTerm };
|
|
2897
3293
|
}
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
for (const key of keys) {
|
|
2902
|
-
const startSeqNo = segmentStartSeqNo(key, prefix);
|
|
2903
|
-
if (startSeqNo !== null) {
|
|
2904
|
-
refs.push({ key, startSeqNo });
|
|
2905
|
-
}
|
|
3294
|
+
function carryForwardUncheckpointedPartitions(priorManifest, targets, partitions, checkpointByPartition) {
|
|
3295
|
+
if (priorManifest === null) {
|
|
3296
|
+
return;
|
|
2906
3297
|
}
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
for (const { key, startSeqNo } of segments) {
|
|
2912
|
-
if (startSeqNo > activeSegmentSeqNo) {
|
|
2913
|
-
await directory.remove(key);
|
|
3298
|
+
const covered = new Set(targets.map((t) => t.partitionId));
|
|
3299
|
+
for (const priorPartition of priorManifest.partitions) {
|
|
3300
|
+
if (covered.has(priorPartition.partitionId)) {
|
|
3301
|
+
continue;
|
|
2914
3302
|
}
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
const handle = await directory.appendHandle(key);
|
|
2920
|
-
try {
|
|
2921
|
-
const currentSize = await handle.size();
|
|
2922
|
-
if (cleanLength < currentSize) {
|
|
2923
|
-
await handle.truncate(cleanLength);
|
|
3303
|
+
partitions.push(priorPartition);
|
|
3304
|
+
const priorCheckpoint = priorManifest.checkpoint.find((c) => c.partitionId === priorPartition.partitionId);
|
|
3305
|
+
if (priorCheckpoint !== void 0 && !checkpointByPartition.has(priorPartition.partitionId)) {
|
|
3306
|
+
checkpointByPartition.set(priorPartition.partitionId, priorCheckpoint);
|
|
2924
3307
|
}
|
|
2925
|
-
} finally {
|
|
2926
|
-
await handle.close();
|
|
2927
3308
|
}
|
|
2928
3309
|
}
|
|
2929
|
-
function
|
|
2930
|
-
if (
|
|
2931
|
-
|
|
2932
|
-
return;
|
|
3310
|
+
function resolveCompactionThreshold(configured) {
|
|
3311
|
+
if (!Number.isInteger(configured) || configured <= 0 || configured > MAX_SEGMENTS_PER_PARTITION) {
|
|
3312
|
+
return DEFAULT_COMPACTION_THRESHOLD;
|
|
2933
3313
|
}
|
|
2934
|
-
|
|
2935
|
-
}
|
|
2936
|
-
function snapshotCheckpointFor(checkpoint, partitionId) {
|
|
2937
|
-
return checkpointLastSeqNo(checkpoint, partitionId);
|
|
3314
|
+
return configured;
|
|
2938
3315
|
}
|
|
2939
|
-
async function
|
|
2940
|
-
const
|
|
2941
|
-
|
|
2942
|
-
|
|
3316
|
+
async function collectGarbage(directory, indexName, priorManifest, manifest) {
|
|
3317
|
+
const referenced = manifestReferencedKeys(manifest);
|
|
3318
|
+
const partitionIds = /* @__PURE__ */ new Set();
|
|
3319
|
+
for (const partition of manifest.partitions) {
|
|
3320
|
+
partitionIds.add(partition.partitionId);
|
|
2943
3321
|
}
|
|
2944
|
-
if (
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
{
|
|
2949
|
-
indexName,
|
|
2950
|
-
partitionId,
|
|
2951
|
-
highestRead: read.highestReadFromWal,
|
|
2952
|
-
highestDurable: read.marker.state.highestDurableSeqNo
|
|
2953
|
-
}
|
|
2954
|
-
);
|
|
3322
|
+
if (priorManifest !== null) {
|
|
3323
|
+
for (const partition of priorManifest.partitions) {
|
|
3324
|
+
partitionIds.add(partition.partitionId);
|
|
3325
|
+
}
|
|
2955
3326
|
}
|
|
2956
|
-
const
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
3327
|
+
for (const partitionId of partitionIds) {
|
|
3328
|
+
const prefix = segmentPrefix(indexName, partitionId);
|
|
3329
|
+
for (const key of await directory.list(prefix)) {
|
|
3330
|
+
if (!referenced.has(key)) {
|
|
3331
|
+
await directory.remove(key);
|
|
3332
|
+
}
|
|
2960
3333
|
}
|
|
2961
|
-
entries.push(entry);
|
|
2962
3334
|
}
|
|
2963
|
-
|
|
3335
|
+
await directory.remove(legacySnapshotKey(indexName));
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
// src/persistence/durability/checkpoint-write.ts
|
|
3339
|
+
async function writeIndexCheckpoint(input) {
|
|
3340
|
+
const { directory, metadata, targets, compactionThreshold, manager } = input;
|
|
3341
|
+
const offloaded = !input.fromMemory && input.canOffload && metadata.tokenizer === void 0 && metadata.stopWords === void 0 && await runCheckpointOnWorker({ root: directory.root, metadata, targets, compactionThreshold });
|
|
3342
|
+
if (offloaded) {
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3345
|
+
await writeSegmentedCheckpoint({
|
|
3346
|
+
directory,
|
|
3347
|
+
metadata,
|
|
3348
|
+
targets,
|
|
3349
|
+
compactionThreshold,
|
|
3350
|
+
...input.fromMemory ? {
|
|
3351
|
+
wholePartitionPayload: (partitionId) => ({
|
|
3352
|
+
payload: manager.serializePartitionToBytes(partitionId),
|
|
3353
|
+
docCount: manager.getPartition(partitionId).count()
|
|
3354
|
+
})
|
|
3355
|
+
} : {}
|
|
3356
|
+
});
|
|
2964
3357
|
}
|
|
2965
3358
|
|
|
2966
3359
|
// src/persistence/durability/seq-owner.ts
|
|
@@ -3232,6 +3625,7 @@ function createWalWriter(directory, config) {
|
|
|
3232
3625
|
var DEFAULT_ASYNC_FLUSH_INTERVAL_MS = 1e3;
|
|
3233
3626
|
function createDurabilityManager(config, hooks, directoryOverride) {
|
|
3234
3627
|
const directory = createDurableDirectory(config.directory ?? "");
|
|
3628
|
+
const canOffloadCheckpoint = directoryOverride === void 0;
|
|
3235
3629
|
const segmentMaxBytes = config.segmentMaxBytes ?? DEFAULT_SEGMENT_MAX_BYTES;
|
|
3236
3630
|
const checkpointIntervalMs = config.checkpointIntervalMs ?? DEFAULT_CHECKPOINT_INTERVAL_MS;
|
|
3237
3631
|
const checkpointMutationThreshold = config.checkpointMutationThreshold ?? DEFAULT_CHECKPOINT_MUTATION_THRESHOLD;
|
|
@@ -3258,14 +3652,14 @@ function createDurabilityManager(config, hooks, directoryOverride) {
|
|
|
3258
3652
|
}
|
|
3259
3653
|
hooks.onFatalError(error);
|
|
3260
3654
|
}
|
|
3261
|
-
function
|
|
3655
|
+
function toError3(err) {
|
|
3262
3656
|
return err instanceof Error ? err : new Error(String(err));
|
|
3263
3657
|
}
|
|
3264
3658
|
async function closeWriterReportingFailure(closePromise) {
|
|
3265
3659
|
try {
|
|
3266
3660
|
await closePromise;
|
|
3267
3661
|
} catch (err) {
|
|
3268
|
-
markFatal(
|
|
3662
|
+
markFatal(toError3(err));
|
|
3269
3663
|
}
|
|
3270
3664
|
}
|
|
3271
3665
|
function getOrCreateIndexState(indexName) {
|
|
@@ -3325,7 +3719,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
|
|
|
3325
3719
|
try {
|
|
3326
3720
|
await partition.walWriter.commit();
|
|
3327
3721
|
} catch (err) {
|
|
3328
|
-
partition.failed =
|
|
3722
|
+
partition.failed = toError3(err);
|
|
3329
3723
|
markFatal(partition.failed);
|
|
3330
3724
|
}
|
|
3331
3725
|
}
|
|
@@ -3339,7 +3733,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
|
|
|
3339
3733
|
try {
|
|
3340
3734
|
await checkpointIndex(indexName);
|
|
3341
3735
|
} catch (err) {
|
|
3342
|
-
markFatal(
|
|
3736
|
+
markFatal(toError3(err));
|
|
3343
3737
|
}
|
|
3344
3738
|
}
|
|
3345
3739
|
}
|
|
@@ -3354,21 +3748,24 @@ function createDurabilityManager(config, hooks, directoryOverride) {
|
|
|
3354
3748
|
document: record.document
|
|
3355
3749
|
});
|
|
3356
3750
|
}
|
|
3357
|
-
async function checkpointIndex(indexName) {
|
|
3751
|
+
async function checkpointIndex(indexName, fromMemory = false) {
|
|
3358
3752
|
const indexState = indexes.get(indexName);
|
|
3359
3753
|
if (indexState === void 0) {
|
|
3360
3754
|
return;
|
|
3361
3755
|
}
|
|
3362
3756
|
if (indexState.checkpointInFlight !== null) {
|
|
3363
|
-
|
|
3757
|
+
await indexState.checkpointInFlight;
|
|
3758
|
+
if (!fromMemory) {
|
|
3759
|
+
return;
|
|
3760
|
+
}
|
|
3364
3761
|
}
|
|
3365
|
-
const run = performCheckpoint(indexName, indexState).finally(() => {
|
|
3762
|
+
const run = performCheckpoint(indexName, indexState, fromMemory).finally(() => {
|
|
3366
3763
|
indexState.checkpointInFlight = null;
|
|
3367
3764
|
});
|
|
3368
3765
|
indexState.checkpointInFlight = run;
|
|
3369
3766
|
return run;
|
|
3370
3767
|
}
|
|
3371
|
-
async function performCheckpoint(indexName, indexState) {
|
|
3768
|
+
async function performCheckpoint(indexName, indexState, fromMemory = false) {
|
|
3372
3769
|
const manager = hooks.getManager(indexName);
|
|
3373
3770
|
if (manager === void 0) {
|
|
3374
3771
|
return;
|
|
@@ -3389,22 +3786,23 @@ function createDurabilityManager(config, hooks, directoryOverride) {
|
|
|
3389
3786
|
try {
|
|
3390
3787
|
await partition.walWriter.commit();
|
|
3391
3788
|
} catch (err) {
|
|
3392
|
-
partition.failed =
|
|
3789
|
+
partition.failed = toError3(err);
|
|
3393
3790
|
markFatal(partition.failed);
|
|
3394
3791
|
throw partition.failed;
|
|
3395
3792
|
}
|
|
3396
3793
|
}
|
|
3397
3794
|
}
|
|
3398
|
-
|
|
3399
|
-
|
|
3795
|
+
await writeIndexCheckpoint({
|
|
3796
|
+
directory,
|
|
3400
3797
|
metadata,
|
|
3401
3798
|
targets,
|
|
3402
|
-
compactionThreshold
|
|
3799
|
+
compactionThreshold,
|
|
3800
|
+
manager,
|
|
3801
|
+
canOffload: canOffloadCheckpoint,
|
|
3802
|
+
fromMemory
|
|
3403
3803
|
});
|
|
3404
|
-
if (!offloaded) {
|
|
3405
|
-
await writeSegmentedCheckpoint({ directory, metadata, targets, compactionThreshold });
|
|
3406
|
-
}
|
|
3407
3804
|
await truncateCoveredSegments(directory, indexName, targets);
|
|
3805
|
+
await reclaimWalBeyondCount(directory, indexName, manager.partitionCount, indexState.partitions, markFatal);
|
|
3408
3806
|
indexState.mutationsSinceCheckpoint = 0;
|
|
3409
3807
|
}
|
|
3410
3808
|
async function recoverIndex(indexName) {
|
|
@@ -3464,7 +3862,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
|
|
|
3464
3862
|
const entry = buildMutationEntry(record, allocatedSeqNo);
|
|
3465
3863
|
await partition.walWriter.append(entry);
|
|
3466
3864
|
} catch (err) {
|
|
3467
|
-
partition.failed =
|
|
3865
|
+
partition.failed = toError3(err);
|
|
3468
3866
|
markFatal(partition.failed);
|
|
3469
3867
|
throw partition.failed;
|
|
3470
3868
|
}
|
|
@@ -3476,7 +3874,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
|
|
|
3476
3874
|
try {
|
|
3477
3875
|
await partition.walWriter.commit();
|
|
3478
3876
|
} catch (err) {
|
|
3479
|
-
partition.failed =
|
|
3877
|
+
partition.failed = toError3(err);
|
|
3480
3878
|
markFatal(partition.failed);
|
|
3481
3879
|
throw partition.failed;
|
|
3482
3880
|
}
|
|
@@ -3486,7 +3884,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
|
|
|
3486
3884
|
startAsyncFlushTimer();
|
|
3487
3885
|
if (indexState.mutationsSinceCheckpoint >= checkpointMutationThreshold) {
|
|
3488
3886
|
void checkpointIndex(record.indexName).catch((err) => {
|
|
3489
|
-
markFatal(
|
|
3887
|
+
markFatal(toError3(err));
|
|
3490
3888
|
});
|
|
3491
3889
|
}
|
|
3492
3890
|
return allocatedSeqNo;
|
|
@@ -3503,6 +3901,9 @@ function createDurabilityManager(config, hooks, directoryOverride) {
|
|
|
3503
3901
|
checkpoint(indexName) {
|
|
3504
3902
|
return checkpointIndex(indexName);
|
|
3505
3903
|
},
|
|
3904
|
+
checkpointFromMemory(indexName) {
|
|
3905
|
+
return checkpointIndex(indexName, true);
|
|
3906
|
+
},
|
|
3506
3907
|
async checkpointAll() {
|
|
3507
3908
|
for (const indexName of [...indexes.keys()]) {
|
|
3508
3909
|
await checkpointIndex(indexName);
|
|
@@ -3548,7 +3949,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
|
|
|
3548
3949
|
function metadataKey(indexName) {
|
|
3549
3950
|
return `${indexName}/meta`;
|
|
3550
3951
|
}
|
|
3551
|
-
function createSnapshotOnlyManager(adapter, config, hooks) {
|
|
3952
|
+
function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
|
|
3552
3953
|
const checkpointIntervalMs = config.checkpointIntervalMs ?? DEFAULT_CHECKPOINT_INTERVAL_MS;
|
|
3553
3954
|
const checkpointMutationThreshold = config.checkpointMutationThreshold ?? DEFAULT_CHECKPOINT_MUTATION_THRESHOLD;
|
|
3554
3955
|
const indexes = /* @__PURE__ */ new Map();
|
|
@@ -3597,6 +3998,10 @@ function createSnapshotOnlyManager(adapter, config, hooks) {
|
|
|
3597
3998
|
const { parts } = await buildSnapshotBundleBytes({
|
|
3598
3999
|
schema: metadata.schema,
|
|
3599
4000
|
language: metadata.language,
|
|
4001
|
+
...metadata.analysisRevision !== void 0 ? { analysisRevision: metadata.analysisRevision } : {},
|
|
4002
|
+
...metadata.tokenizer !== void 0 ? { tokenizer: metadata.tokenizer } : {},
|
|
4003
|
+
...metadata.stopWords !== void 0 ? { stopWords: metadata.stopWords } : {},
|
|
4004
|
+
...metadata.stopWordList !== void 0 ? { stopWordList: metadata.stopWordList } : {},
|
|
3600
4005
|
manager,
|
|
3601
4006
|
vectorIndexes: hooks.getVectorIndexes(indexName),
|
|
3602
4007
|
seqNoByPartition,
|
|
@@ -3604,6 +4009,13 @@ function createSnapshotOnlyManager(adapter, config, hooks) {
|
|
|
3604
4009
|
});
|
|
3605
4010
|
await adapter.save(snapshotStorageKey(indexName), concatEnvelopeParts(parts));
|
|
3606
4011
|
indexState.mutationsSinceCheckpoint = 0;
|
|
4012
|
+
if (publisher !== void 0) {
|
|
4013
|
+
const partitionIds = [];
|
|
4014
|
+
for (let i = 0; i < manager.partitionCount; i += 1) {
|
|
4015
|
+
partitionIds.push(i);
|
|
4016
|
+
}
|
|
4017
|
+
await publisher.publishPartitions(indexName, partitionIds);
|
|
4018
|
+
}
|
|
3607
4019
|
}
|
|
3608
4020
|
async function checkpointIndex(indexName) {
|
|
3609
4021
|
const indexState = indexes.get(indexName);
|
|
@@ -3690,6 +4102,12 @@ function createSnapshotOnlyManager(adapter, config, hooks) {
|
|
|
3690
4102
|
await adapter.delete(key);
|
|
3691
4103
|
}
|
|
3692
4104
|
},
|
|
4105
|
+
async reloadIndex(indexName) {
|
|
4106
|
+
const indexState = getOrCreateIndexState(indexName);
|
|
4107
|
+
const reload = indexState.applyChain.then(() => recoverIndex(indexName));
|
|
4108
|
+
indexState.applyChain = reload.catch(() => void 0);
|
|
4109
|
+
await reload;
|
|
4110
|
+
},
|
|
3693
4111
|
async shutdown() {
|
|
3694
4112
|
shuttingDown = true;
|
|
3695
4113
|
if (checkpointTimer !== null) {
|
|
@@ -3728,6 +4146,36 @@ function buildMetadata(indexName, hooks) {
|
|
|
3728
4146
|
if (config.surfaceForms) {
|
|
3729
4147
|
metadata.surfaceForms = true;
|
|
3730
4148
|
}
|
|
4149
|
+
if (config.analysisRevision !== void 0) {
|
|
4150
|
+
metadata.analysisRevision = config.analysisRevision;
|
|
4151
|
+
}
|
|
4152
|
+
if (config.tokenizer !== void 0) {
|
|
4153
|
+
metadata.tokenizer = config.tokenizer;
|
|
4154
|
+
}
|
|
4155
|
+
if (config.stopWords !== void 0) {
|
|
4156
|
+
metadata.stopWords = config.stopWords;
|
|
4157
|
+
}
|
|
4158
|
+
if (config.stopWordList !== void 0) {
|
|
4159
|
+
metadata.stopWordList = config.stopWordList;
|
|
4160
|
+
}
|
|
4161
|
+
if (config.partitionLimits !== void 0) {
|
|
4162
|
+
metadata.partitionLimits = config.partitionLimits;
|
|
4163
|
+
}
|
|
4164
|
+
if (config.defaultScoring !== void 0) {
|
|
4165
|
+
metadata.defaultScoring = config.defaultScoring;
|
|
4166
|
+
}
|
|
4167
|
+
if (config.trackPositions !== void 0) {
|
|
4168
|
+
metadata.trackPositions = config.trackPositions;
|
|
4169
|
+
}
|
|
4170
|
+
if (config.strict !== void 0) {
|
|
4171
|
+
metadata.strict = config.strict;
|
|
4172
|
+
}
|
|
4173
|
+
if (config.required !== void 0) {
|
|
4174
|
+
metadata.required = config.required;
|
|
4175
|
+
}
|
|
4176
|
+
if (config.vectorPromotion !== void 0) {
|
|
4177
|
+
metadata.vectorPromotion = config.vectorPromotion;
|
|
4178
|
+
}
|
|
3731
4179
|
return metadata;
|
|
3732
4180
|
}
|
|
3733
4181
|
function createDurabilityIntegration(tier, hooks) {
|
|
@@ -3739,7 +4187,7 @@ function createDurabilityIntegration(tier, hooks) {
|
|
|
3739
4187
|
createIndexFromMetadata: hooks.createIndexFromMetadata,
|
|
3740
4188
|
onFatalError: hooks.onFatalError
|
|
3741
4189
|
};
|
|
3742
|
-
const manager = tier.kind === "wal" ? createDurabilityManager(tier.config, managerHooks) : createSnapshotOnlyManager(tier.adapter, tier.config, managerHooks);
|
|
4190
|
+
const manager = tier.kind === "wal" ? createDurabilityManager(tier.config, managerHooks) : createSnapshotOnlyManager(tier.adapter, tier.config, managerHooks, hooks.checkpointPublisher);
|
|
3743
4191
|
function partitionFor(indexName, docId) {
|
|
3744
4192
|
const partitionManager = hooks.getManager(indexName);
|
|
3745
4193
|
const partitionCount = partitionManager?.partitionCount ?? 1;
|
|
@@ -3758,12 +4206,168 @@ function createDurabilityIntegration(tier, hooks) {
|
|
|
3758
4206
|
return { indexName, partitionId, seqNo };
|
|
3759
4207
|
}
|
|
3760
4208
|
return {
|
|
3761
|
-
manager,
|
|
3762
|
-
recordInsertOrUpdate(indexName, docId, document, apply) {
|
|
3763
|
-
return recordMutation(indexName, docId, "INDEX", encode(document), apply);
|
|
4209
|
+
manager,
|
|
4210
|
+
recordInsertOrUpdate(indexName, docId, document, apply) {
|
|
4211
|
+
return recordMutation(indexName, docId, "INDEX", encode(document), apply);
|
|
4212
|
+
},
|
|
4213
|
+
recordRemove(indexName, docId, apply) {
|
|
4214
|
+
return recordMutation(indexName, docId, "DELETE", null, apply);
|
|
4215
|
+
}
|
|
4216
|
+
};
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
// src/engine/invalidation.ts
|
|
4220
|
+
var STATISTICS_BROADCAST_INTERVAL_MS = 5e3;
|
|
4221
|
+
var FOREIGN_STATS_TTL_MS = 6e4;
|
|
4222
|
+
function createInvalidationFromConfig(config, tierKind, deps) {
|
|
4223
|
+
if (config?.invalidation === void 0) {
|
|
4224
|
+
return null;
|
|
4225
|
+
}
|
|
4226
|
+
if (tierKind !== "snapshot") {
|
|
4227
|
+
throw new NarsilError(
|
|
4228
|
+
ErrorCodes.CONFIG_INVALID,
|
|
4229
|
+
"Invalidation requires a shared persistence adapter. The write-ahead log tier owns its directory exclusively, so configure a non-filesystem persistence adapter to share data between instances"
|
|
4230
|
+
);
|
|
4231
|
+
}
|
|
4232
|
+
return createInvalidationIntegration({
|
|
4233
|
+
adapter: config.invalidation,
|
|
4234
|
+
instanceId: generateId(),
|
|
4235
|
+
...deps
|
|
4236
|
+
});
|
|
4237
|
+
}
|
|
4238
|
+
function createInvalidationIntegration(deps) {
|
|
4239
|
+
const { adapter, instanceId, reloadIndex, getManager, listBroadcastIndexNames, onError } = deps;
|
|
4240
|
+
const foreignStats = /* @__PURE__ */ new Map();
|
|
4241
|
+
const reloadChains = /* @__PURE__ */ new Map();
|
|
4242
|
+
let broadcastTimer = null;
|
|
4243
|
+
let stopped = false;
|
|
4244
|
+
function toError3(err) {
|
|
4245
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
4246
|
+
}
|
|
4247
|
+
function pruneStaleForeignStats(entries) {
|
|
4248
|
+
const cutoff = Date.now() - FOREIGN_STATS_TTL_MS;
|
|
4249
|
+
for (const [sourceInstanceId, entry] of entries) {
|
|
4250
|
+
if (entry.receivedAt < cutoff) {
|
|
4251
|
+
entries.delete(sourceInstanceId);
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
function scheduleReload(indexName) {
|
|
4256
|
+
const chain = reloadChains.get(indexName) ?? Promise.resolve();
|
|
4257
|
+
const next = chain.then(() => reloadIndex(indexName)).catch((err) => onError(toError3(err)));
|
|
4258
|
+
reloadChains.set(indexName, next);
|
|
4259
|
+
}
|
|
4260
|
+
function storeForeignStats(indexName, sourceInstanceId, stats) {
|
|
4261
|
+
let entries = foreignStats.get(indexName);
|
|
4262
|
+
if (entries === void 0) {
|
|
4263
|
+
entries = /* @__PURE__ */ new Map();
|
|
4264
|
+
foreignStats.set(indexName, entries);
|
|
4265
|
+
}
|
|
4266
|
+
entries.set(sourceInstanceId, { stats, receivedAt: Date.now() });
|
|
4267
|
+
pruneStaleForeignStats(entries);
|
|
4268
|
+
}
|
|
4269
|
+
function handleEvent(event) {
|
|
4270
|
+
try {
|
|
4271
|
+
if (event.type === "partition") {
|
|
4272
|
+
if (event.sourceInstanceId === instanceId) {
|
|
4273
|
+
return;
|
|
4274
|
+
}
|
|
4275
|
+
scheduleReload(event.indexName);
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4278
|
+
if (event.instanceId === instanceId) {
|
|
4279
|
+
return;
|
|
4280
|
+
}
|
|
4281
|
+
storeForeignStats(event.indexName, event.instanceId, event.stats);
|
|
4282
|
+
} catch (err) {
|
|
4283
|
+
onError(toError3(err));
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
async function publishOwnStatistics() {
|
|
4287
|
+
for (const indexName of listBroadcastIndexNames()) {
|
|
4288
|
+
const manager = getManager(indexName);
|
|
4289
|
+
if (manager === void 0) {
|
|
4290
|
+
continue;
|
|
4291
|
+
}
|
|
4292
|
+
const aggregate = manager.getAggregateStats();
|
|
4293
|
+
try {
|
|
4294
|
+
await adapter.publish({
|
|
4295
|
+
type: "statistics",
|
|
4296
|
+
indexName,
|
|
4297
|
+
instanceId,
|
|
4298
|
+
stats: {
|
|
4299
|
+
totalDocs: aggregate.totalDocuments,
|
|
4300
|
+
docFrequencies: aggregate.docFrequencies,
|
|
4301
|
+
totalFieldLengths: aggregate.totalFieldLengths
|
|
4302
|
+
}
|
|
4303
|
+
});
|
|
4304
|
+
} catch (err) {
|
|
4305
|
+
onError(toError3(err));
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
return {
|
|
4310
|
+
instanceId,
|
|
4311
|
+
async publishPartitions(indexName, partitions) {
|
|
4312
|
+
if (stopped || partitions.length === 0) {
|
|
4313
|
+
return;
|
|
4314
|
+
}
|
|
4315
|
+
try {
|
|
4316
|
+
await adapter.publish({
|
|
4317
|
+
type: "partition",
|
|
4318
|
+
indexName,
|
|
4319
|
+
partitions,
|
|
4320
|
+
timestamp: Date.now(),
|
|
4321
|
+
sourceInstanceId: instanceId
|
|
4322
|
+
});
|
|
4323
|
+
} catch (err) {
|
|
4324
|
+
onError(toError3(err));
|
|
4325
|
+
}
|
|
3764
4326
|
},
|
|
3765
|
-
|
|
3766
|
-
|
|
4327
|
+
broadcastStats(indexName) {
|
|
4328
|
+
const manager = getManager(indexName);
|
|
4329
|
+
if (manager === void 0) {
|
|
4330
|
+
return void 0;
|
|
4331
|
+
}
|
|
4332
|
+
const own = manager.getAggregateStats();
|
|
4333
|
+
const entries = foreignStats.get(indexName);
|
|
4334
|
+
if (entries === void 0 || entries.size === 0) {
|
|
4335
|
+
return mergePartitionStats([own]);
|
|
4336
|
+
}
|
|
4337
|
+
pruneStaleForeignStats(entries);
|
|
4338
|
+
const combined = [own];
|
|
4339
|
+
for (const [, entry] of entries) {
|
|
4340
|
+
combined.push({
|
|
4341
|
+
totalDocuments: entry.stats.totalDocs,
|
|
4342
|
+
docFrequencies: entry.stats.docFrequencies,
|
|
4343
|
+
totalFieldLengths: entry.stats.totalFieldLengths
|
|
4344
|
+
});
|
|
4345
|
+
}
|
|
4346
|
+
return mergePartitionStats(combined);
|
|
4347
|
+
},
|
|
4348
|
+
async start() {
|
|
4349
|
+
await adapter.subscribe(handleEvent);
|
|
4350
|
+
if (broadcastTimer !== null || stopped) {
|
|
4351
|
+
return;
|
|
4352
|
+
}
|
|
4353
|
+
broadcastTimer = setInterval(() => {
|
|
4354
|
+
void publishOwnStatistics();
|
|
4355
|
+
}, STATISTICS_BROADCAST_INTERVAL_MS);
|
|
4356
|
+
if (typeof broadcastTimer.unref === "function") {
|
|
4357
|
+
broadcastTimer.unref();
|
|
4358
|
+
}
|
|
4359
|
+
},
|
|
4360
|
+
async shutdown() {
|
|
4361
|
+
stopped = true;
|
|
4362
|
+
if (broadcastTimer !== null) {
|
|
4363
|
+
clearInterval(broadcastTimer);
|
|
4364
|
+
broadcastTimer = null;
|
|
4365
|
+
}
|
|
4366
|
+
try {
|
|
4367
|
+
await adapter.shutdown();
|
|
4368
|
+
} catch (err) {
|
|
4369
|
+
onError(toError3(err));
|
|
4370
|
+
}
|
|
3767
4371
|
}
|
|
3768
4372
|
};
|
|
3769
4373
|
}
|
|
@@ -4016,42 +4620,174 @@ function createWorkerPool(config) {
|
|
|
4016
4620
|
};
|
|
4017
4621
|
}
|
|
4018
4622
|
|
|
4623
|
+
// src/engine/worker-resync.ts
|
|
4624
|
+
async function transferIndexToPool(indexName, pool, config, manager) {
|
|
4625
|
+
const allExecutors = pool.getAllExecutors();
|
|
4626
|
+
await Promise.allSettled(
|
|
4627
|
+
allExecutors.map(
|
|
4628
|
+
(workerExecutor) => workerExecutor.execute({ type: "dropIndex", indexName, requestId: `resync-drop-${indexName}` })
|
|
4629
|
+
)
|
|
4630
|
+
);
|
|
4631
|
+
pool.addIndexToAll(indexName);
|
|
4632
|
+
await Promise.all(
|
|
4633
|
+
allExecutors.map(
|
|
4634
|
+
(workerExecutor) => workerExecutor.execute({
|
|
4635
|
+
type: "createIndex",
|
|
4636
|
+
indexName,
|
|
4637
|
+
config,
|
|
4638
|
+
requestId: `resync-create-${indexName}`
|
|
4639
|
+
})
|
|
4640
|
+
)
|
|
4641
|
+
);
|
|
4642
|
+
for (let i = 0; i < manager.partitionCount; i++) {
|
|
4643
|
+
const serialized = manager.serializePartition(i);
|
|
4644
|
+
await Promise.all(
|
|
4645
|
+
allExecutors.map(
|
|
4646
|
+
(workerExecutor) => workerExecutor.execute({
|
|
4647
|
+
type: "deserialize",
|
|
4648
|
+
indexName,
|
|
4649
|
+
partitionId: i,
|
|
4650
|
+
data: serialized,
|
|
4651
|
+
requestId: `resync-${indexName}-${i}`
|
|
4652
|
+
})
|
|
4653
|
+
)
|
|
4654
|
+
);
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
|
|
4019
4658
|
// src/engine/orchestration.ts
|
|
4659
|
+
function toError2(value) {
|
|
4660
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
4661
|
+
}
|
|
4662
|
+
function isDeterministicFailure(error) {
|
|
4663
|
+
return error instanceof NarsilError && error.code === ErrorCodes.CONFIG_INVALID;
|
|
4664
|
+
}
|
|
4665
|
+
function alreadyPresentOnWorker(reason) {
|
|
4666
|
+
return reason instanceof NarsilError && reason.code === ErrorCodes.DOC_ALREADY_EXISTS;
|
|
4667
|
+
}
|
|
4668
|
+
function workerIneligibility(indexName, config, bootstrapModule) {
|
|
4669
|
+
try {
|
|
4670
|
+
assertConfigReachesWorker(indexName, config, bootstrapModule);
|
|
4671
|
+
return null;
|
|
4672
|
+
} catch (err) {
|
|
4673
|
+
if (err instanceof NarsilError && err.code === ErrorCodes.CONFIG_INVALID) {
|
|
4674
|
+
return err;
|
|
4675
|
+
}
|
|
4676
|
+
throw err;
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
function assertConfigReachesWorker(indexName, config, bootstrapModule) {
|
|
4680
|
+
if (config.tokenizer !== void 0 && typeof config.tokenizer !== "string") {
|
|
4681
|
+
throw new NarsilError(
|
|
4682
|
+
ErrorCodes.CONFIG_INVALID,
|
|
4683
|
+
`Index "${indexName}" holds a tokenizer instance, and no worker thread can receive one. Register the tokenizer with registerTokenizer and name it in the index config`,
|
|
4684
|
+
{ indexName }
|
|
4685
|
+
);
|
|
4686
|
+
}
|
|
4687
|
+
if (typeof config.stopWords === "function") {
|
|
4688
|
+
throw new NarsilError(
|
|
4689
|
+
ErrorCodes.CONFIG_INVALID,
|
|
4690
|
+
`Index "${indexName}" holds a stop word function, and no worker thread can receive one. Register the function with registerStopWords and name it in the index config`,
|
|
4691
|
+
{ indexName }
|
|
4692
|
+
);
|
|
4693
|
+
}
|
|
4694
|
+
const language = config.language ?? "english";
|
|
4695
|
+
if (language !== "english" && bootstrapModule === void 0) {
|
|
4696
|
+
throw new NarsilError(
|
|
4697
|
+
ErrorCodes.CONFIG_INVALID,
|
|
4698
|
+
`Index "${indexName}" uses language "${language}", which a worker thread registers only from a bootstrap module. Set workers.bootstrapModule to a module that registers it`,
|
|
4699
|
+
{ indexName, language }
|
|
4700
|
+
);
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4020
4703
|
function createWorkerOrchestrator(config, executor, promoter, indexRegistry, callbacks) {
|
|
4021
4704
|
let workerPool = null;
|
|
4022
4705
|
let promotionInProgress = false;
|
|
4706
|
+
let promotionBlocked = false;
|
|
4707
|
+
let promotionRun = null;
|
|
4023
4708
|
const promotionBuffer = [];
|
|
4709
|
+
const awaitingBufferedWrites = /* @__PURE__ */ new Set();
|
|
4710
|
+
const reportedIneligible = /* @__PURE__ */ new Set();
|
|
4711
|
+
const promotedIndexes = /* @__PURE__ */ new Set();
|
|
4024
4712
|
const workersEnabled = config?.workers?.enabled === true;
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4713
|
+
const bootstrapModule = config?.workers?.bootstrapModule;
|
|
4714
|
+
function reportIneligible(indexName, error) {
|
|
4715
|
+
if (reportedIneligible.has(indexName)) return;
|
|
4716
|
+
reportedIneligible.add(indexName);
|
|
4717
|
+
callbacks?.onPromotionFailure?.("index-excluded", error, false);
|
|
4718
|
+
}
|
|
4719
|
+
function collectEligibleIndexes() {
|
|
4720
|
+
const eligible = /* @__PURE__ */ new Map();
|
|
4721
|
+
for (const [name, entry] of indexRegistry) {
|
|
4722
|
+
const ineligibility = workerIneligibility(name, entry.config, bootstrapModule);
|
|
4723
|
+
if (ineligibility) {
|
|
4724
|
+
reportIneligible(name, ineligibility);
|
|
4725
|
+
continue;
|
|
4726
|
+
}
|
|
4029
4727
|
const mgr = executor.getManager(name);
|
|
4030
|
-
|
|
4728
|
+
eligible.set(name, { documentCount: mgr?.countDocuments() ?? 0 });
|
|
4031
4729
|
}
|
|
4730
|
+
return eligible;
|
|
4731
|
+
}
|
|
4732
|
+
async function checkPromotion() {
|
|
4733
|
+
if (!workersEnabled || promotionInProgress || promotionBlocked || workerPool) return;
|
|
4734
|
+
if (callbacks?.shouldDeferPromotion?.()) return;
|
|
4735
|
+
const indexMap = collectEligibleIndexes();
|
|
4736
|
+
if (indexMap.size === 0) return;
|
|
4032
4737
|
const result = promoter.check(indexMap);
|
|
4033
4738
|
if (result.shouldPromote) {
|
|
4034
4739
|
promotionInProgress = true;
|
|
4035
4740
|
setTimeout(() => {
|
|
4036
|
-
runPromotion(result.reason).catch((err) => {
|
|
4037
|
-
|
|
4741
|
+
const run = runPromotion(result.reason).catch((err) => {
|
|
4742
|
+
const error = toError2(err);
|
|
4743
|
+
promotionBlocked = isDeterministicFailure(error);
|
|
4038
4744
|
promotionInProgress = false;
|
|
4745
|
+
callbacks?.onPromotionFailure?.(result.reason, error, !promotionBlocked);
|
|
4746
|
+
});
|
|
4747
|
+
promotionRun = run.then(() => {
|
|
4748
|
+
promotionRun = null;
|
|
4039
4749
|
});
|
|
4040
4750
|
}, 0);
|
|
4041
4751
|
}
|
|
4042
4752
|
}
|
|
4043
4753
|
async function runPromotion(reason) {
|
|
4044
4754
|
try {
|
|
4755
|
+
const promotable = [];
|
|
4756
|
+
for (const [name, entry] of indexRegistry) {
|
|
4757
|
+
const ineligibility = workerIneligibility(name, entry.config, bootstrapModule);
|
|
4758
|
+
if (ineligibility) {
|
|
4759
|
+
reportIneligible(name, ineligibility);
|
|
4760
|
+
continue;
|
|
4761
|
+
}
|
|
4762
|
+
promotable.push(name);
|
|
4763
|
+
}
|
|
4764
|
+
if (promotable.length === 0) {
|
|
4765
|
+
promotionBuffer.length = 0;
|
|
4766
|
+
return;
|
|
4767
|
+
}
|
|
4045
4768
|
const factory = await createWorkerFactory();
|
|
4046
4769
|
const pool = createWorkerPool({
|
|
4047
4770
|
count: config?.workers?.count,
|
|
4048
4771
|
workerFactory: factory
|
|
4049
4772
|
});
|
|
4050
|
-
for (const
|
|
4773
|
+
for (const name of promotable) {
|
|
4051
4774
|
pool.addIndexToAll(name);
|
|
4052
4775
|
}
|
|
4053
4776
|
const allExecutors = pool.getAllExecutors();
|
|
4054
|
-
|
|
4777
|
+
if (bootstrapModule !== void 0) {
|
|
4778
|
+
await Promise.all(
|
|
4779
|
+
allExecutors.map(
|
|
4780
|
+
(workerExecutor) => workerExecutor.execute({
|
|
4781
|
+
type: "bootstrap",
|
|
4782
|
+
moduleUrl: bootstrapModule,
|
|
4783
|
+
requestId: "promote-bootstrap"
|
|
4784
|
+
})
|
|
4785
|
+
)
|
|
4786
|
+
);
|
|
4787
|
+
}
|
|
4788
|
+
for (const name of promotable) {
|
|
4789
|
+
const entry = indexRegistry.get(name);
|
|
4790
|
+
if (!entry) continue;
|
|
4055
4791
|
await Promise.all(
|
|
4056
4792
|
allExecutors.map(
|
|
4057
4793
|
(workerExecutor) => workerExecutor.execute({
|
|
@@ -4081,38 +4817,64 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
|
|
|
4081
4817
|
}
|
|
4082
4818
|
}
|
|
4083
4819
|
workerPool = pool;
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
promotionBuffer.length = 0;
|
|
4088
|
-
for (const action of buffered) {
|
|
4089
|
-
await replicateToWorkers(action);
|
|
4090
|
-
}
|
|
4820
|
+
for (const name of promotable) {
|
|
4821
|
+
promotedIndexes.add(name);
|
|
4822
|
+
awaitingBufferedWrites.add(name);
|
|
4091
4823
|
}
|
|
4824
|
+
promoter.markPromoted();
|
|
4825
|
+
await drainPromotionBuffer();
|
|
4826
|
+
awaitingBufferedWrites.clear();
|
|
4092
4827
|
callbacks?.onPromotion?.(pool.workerCount, reason);
|
|
4093
4828
|
} catch (err) {
|
|
4094
4829
|
promotionBuffer.length = 0;
|
|
4095
4830
|
throw err;
|
|
4096
4831
|
} finally {
|
|
4832
|
+
awaitingBufferedWrites.clear();
|
|
4097
4833
|
promotionInProgress = false;
|
|
4098
4834
|
}
|
|
4099
4835
|
}
|
|
4836
|
+
async function drainPromotionBuffer() {
|
|
4837
|
+
while (promotionBuffer.length > 0) {
|
|
4838
|
+
const action = promotionBuffer.shift();
|
|
4839
|
+
if (action === void 0) return;
|
|
4840
|
+
await dispatchToWorkers(action);
|
|
4841
|
+
}
|
|
4842
|
+
}
|
|
4100
4843
|
async function replicateToWorkers(action) {
|
|
4101
4844
|
if (promotionInProgress) {
|
|
4102
4845
|
promotionBuffer.push(action);
|
|
4103
4846
|
return;
|
|
4104
4847
|
}
|
|
4848
|
+
await dispatchToWorkers(action);
|
|
4849
|
+
}
|
|
4850
|
+
async function dispatchToWorkers(action) {
|
|
4105
4851
|
if (!workerPool) return;
|
|
4852
|
+
if (action.type === "createIndex") {
|
|
4853
|
+
const ineligibility = workerIneligibility(action.indexName, action.config, bootstrapModule);
|
|
4854
|
+
if (ineligibility) {
|
|
4855
|
+
reportIneligible(action.indexName, ineligibility);
|
|
4856
|
+
return;
|
|
4857
|
+
}
|
|
4858
|
+
workerPool.addIndexToAll(action.indexName);
|
|
4859
|
+
promotedIndexes.add(action.indexName);
|
|
4860
|
+
} else if ("indexName" in action && !promotedIndexes.has(action.indexName)) {
|
|
4861
|
+
return;
|
|
4862
|
+
}
|
|
4106
4863
|
const allExecutors = workerPool.getAllExecutors();
|
|
4107
4864
|
const results = await Promise.allSettled(allExecutors.map((workerExecutor) => workerExecutor.execute(action)));
|
|
4108
4865
|
for (const result of results) {
|
|
4109
4866
|
if (result.status === "rejected") {
|
|
4867
|
+
if (action.type === "insert" && alreadyPresentOnWorker(result.reason)) {
|
|
4868
|
+
continue;
|
|
4869
|
+
}
|
|
4110
4870
|
console.warn("Worker replication failed:", result.reason);
|
|
4111
4871
|
}
|
|
4112
4872
|
}
|
|
4113
4873
|
}
|
|
4114
|
-
async function searchViaWorker(indexName, params) {
|
|
4874
|
+
async function searchViaWorker(indexName, params, globalStats) {
|
|
4115
4875
|
if (!workerPool) return null;
|
|
4876
|
+
if (!promotedIndexes.has(indexName)) return null;
|
|
4877
|
+
if (awaitingBufferedWrites.has(indexName)) return null;
|
|
4116
4878
|
const manager = executor.getManager(indexName);
|
|
4117
4879
|
if (!manager) return null;
|
|
4118
4880
|
const allExecutors = workerPool.getAllExecutors();
|
|
@@ -4125,7 +4887,8 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
|
|
|
4125
4887
|
type: "query",
|
|
4126
4888
|
indexName,
|
|
4127
4889
|
params,
|
|
4128
|
-
requestId: createRequestId()
|
|
4890
|
+
requestId: createRequestId(),
|
|
4891
|
+
...globalStats !== void 0 ? { globalStats } : {}
|
|
4129
4892
|
});
|
|
4130
4893
|
} catch (err) {
|
|
4131
4894
|
console.warn("Worker search failed, falling back to local:", err);
|
|
@@ -4144,7 +4907,8 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
|
|
|
4144
4907
|
indexName,
|
|
4145
4908
|
params,
|
|
4146
4909
|
requestId: createRequestId(),
|
|
4147
|
-
partitionIds: workerAssignments[idx]
|
|
4910
|
+
partitionIds: workerAssignments[idx],
|
|
4911
|
+
...globalStats !== void 0 ? { globalStats } : {}
|
|
4148
4912
|
})
|
|
4149
4913
|
)
|
|
4150
4914
|
);
|
|
@@ -4163,6 +4927,24 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
|
|
|
4163
4927
|
function isPromoted() {
|
|
4164
4928
|
return workerPool !== null;
|
|
4165
4929
|
}
|
|
4930
|
+
function desyncIndex(indexName) {
|
|
4931
|
+
return promotedIndexes.delete(indexName);
|
|
4932
|
+
}
|
|
4933
|
+
async function resyncIndex(indexName, wasPromoted) {
|
|
4934
|
+
if (promotionRun) {
|
|
4935
|
+
await promotionRun.catch(() => void 0);
|
|
4936
|
+
}
|
|
4937
|
+
if (!workerPool) return;
|
|
4938
|
+
if (!wasPromoted && !promotedIndexes.has(indexName)) return;
|
|
4939
|
+
const entry = indexRegistry.get(indexName);
|
|
4940
|
+
if (!entry) return;
|
|
4941
|
+
if (workerIneligibility(indexName, entry.config, bootstrapModule)) return;
|
|
4942
|
+
const manager = executor.getManager(indexName);
|
|
4943
|
+
if (!manager) return;
|
|
4944
|
+
promotedIndexes.delete(indexName);
|
|
4945
|
+
await transferIndexToPool(indexName, workerPool, entry.config, manager);
|
|
4946
|
+
promotedIndexes.add(indexName);
|
|
4947
|
+
}
|
|
4166
4948
|
async function getWorkerMemoryStats() {
|
|
4167
4949
|
if (!workerPool) return [];
|
|
4168
4950
|
const reports = await workerPool.getMemoryStats();
|
|
@@ -4177,59 +4959,65 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
|
|
|
4177
4959
|
if (workerPool) {
|
|
4178
4960
|
await workerPool.shutdown();
|
|
4179
4961
|
workerPool = null;
|
|
4962
|
+
promotedIndexes.clear();
|
|
4180
4963
|
}
|
|
4181
4964
|
}
|
|
4182
|
-
return {
|
|
4965
|
+
return {
|
|
4966
|
+
checkPromotion,
|
|
4967
|
+
replicateToWorkers,
|
|
4968
|
+
searchViaWorker,
|
|
4969
|
+
isPromoted,
|
|
4970
|
+
desyncIndex,
|
|
4971
|
+
resyncIndex,
|
|
4972
|
+
getWorkerMemoryStats,
|
|
4973
|
+
shutdown
|
|
4974
|
+
};
|
|
4183
4975
|
}
|
|
4184
4976
|
|
|
4185
|
-
// src/engine/
|
|
4186
|
-
function
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
}
|
|
4196
|
-
function resolveDurabilityTier(config) {
|
|
4197
|
-
const filesystemDirectory = filesystemBackedDirectory(config);
|
|
4198
|
-
if (config.durability) {
|
|
4199
|
-
if (config.durability.directory !== void 0 && config.durability.directory.trim().length > 0) {
|
|
4200
|
-
return { kind: "wal", config: { ...config.durability } };
|
|
4977
|
+
// src/engine/watermark.ts
|
|
4978
|
+
function createWatermarkNotifier(deps) {
|
|
4979
|
+
const latchedCapacity = /* @__PURE__ */ new Map();
|
|
4980
|
+
function check(indexName) {
|
|
4981
|
+
const partitionConfig = deps.getPartitionConfig(indexName);
|
|
4982
|
+
const maxDocs = partitionConfig?.maxDocsPerPartition;
|
|
4983
|
+
const watermark = partitionConfig?.watermark;
|
|
4984
|
+
if (maxDocs === void 0 || watermark === void 0) {
|
|
4985
|
+
latchedCapacity.delete(indexName);
|
|
4986
|
+
return;
|
|
4201
4987
|
}
|
|
4202
|
-
|
|
4203
|
-
|
|
4988
|
+
const manager = deps.getManager(indexName);
|
|
4989
|
+
if (!manager) {
|
|
4990
|
+
latchedCapacity.delete(indexName);
|
|
4991
|
+
return;
|
|
4204
4992
|
}
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4993
|
+
const partitionCount = manager.partitionCount;
|
|
4994
|
+
const capacity = maxDocs * partitionCount;
|
|
4995
|
+
const documentCount = manager.countDocuments();
|
|
4996
|
+
const threshold = watermark * capacity;
|
|
4997
|
+
if (documentCount < threshold) {
|
|
4998
|
+
latchedCapacity.delete(indexName);
|
|
4999
|
+
return;
|
|
4210
5000
|
}
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
);
|
|
4215
|
-
}
|
|
4216
|
-
if (config.persistence === void 0) {
|
|
4217
|
-
return null;
|
|
5001
|
+
if (latchedCapacity.get(indexName) === capacity) return;
|
|
5002
|
+
latchedCapacity.set(indexName, capacity);
|
|
5003
|
+
deps.emit({ indexName, documentCount, capacity, partitionCount });
|
|
4218
5004
|
}
|
|
4219
|
-
|
|
4220
|
-
|
|
5005
|
+
function forget(indexName) {
|
|
5006
|
+
latchedCapacity.delete(indexName);
|
|
4221
5007
|
}
|
|
4222
|
-
return {
|
|
5008
|
+
return { check, forget };
|
|
4223
5009
|
}
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
5010
|
+
|
|
5011
|
+
// src/engine/core.ts
|
|
5012
|
+
function getVectorFieldPaths(schema) {
|
|
5013
|
+
return new Set(extractVectorFieldsFromSchema(schema).keys());
|
|
5014
|
+
}
|
|
5015
|
+
function createDurabilityFromTier(tier, wiring) {
|
|
4229
5016
|
if (tier === null) {
|
|
4230
5017
|
return null;
|
|
4231
5018
|
}
|
|
4232
5019
|
return createDurabilityIntegration(tier, {
|
|
5020
|
+
checkpointPublisher: { publishPartitions: wiring.publishCheckpointedPartitions },
|
|
4233
5021
|
getManager: (indexName) => wiring.indexRegistry.has(indexName) ? wiring.requireManager(indexName) : void 0,
|
|
4234
5022
|
getVectorFieldPaths: (indexName) => wiring.indexRegistry.get(indexName)?.vectorFieldPaths ?? /* @__PURE__ */ new Set(),
|
|
4235
5023
|
getVectorIndexes: (indexName) => wiring.indexRegistry.has(indexName) ? wiring.requireManager(indexName).getVectorIndexes() : /* @__PURE__ */ new Map(),
|
|
@@ -4248,7 +5036,17 @@ function createDurabilityFromConfig(config, wiring) {
|
|
|
4248
5036
|
k1: entry.config.bm25?.k1 ?? 1.2,
|
|
4249
5037
|
b: entry.config.bm25?.b ?? 0.75,
|
|
4250
5038
|
...embedding !== void 0 ? { embedding } : {},
|
|
4251
|
-
...entry.config.surfaceForms === true ? { surfaceForms: true } : {}
|
|
5039
|
+
...entry.config.surfaceForms === true ? { surfaceForms: true } : {},
|
|
5040
|
+
analysisRevision: entry.language.revision,
|
|
5041
|
+
...typeof entry.config.tokenizer === "string" ? { tokenizer: entry.config.tokenizer } : {},
|
|
5042
|
+
...typeof entry.config.stopWords === "string" ? { stopWords: entry.config.stopWords } : {},
|
|
5043
|
+
...entry.config.stopWords instanceof Set ? { stopWordList: [...entry.config.stopWords].sort() } : {},
|
|
5044
|
+
...entry.config.partitions !== void 0 ? { partitionLimits: entry.config.partitions } : {},
|
|
5045
|
+
...entry.config.defaultScoring !== void 0 ? { defaultScoring: entry.config.defaultScoring } : {},
|
|
5046
|
+
...entry.config.trackPositions !== void 0 ? { trackPositions: entry.config.trackPositions } : {},
|
|
5047
|
+
...entry.config.strict !== void 0 ? { strict: entry.config.strict } : {},
|
|
5048
|
+
...entry.config.required !== void 0 ? { required: entry.config.required } : {},
|
|
5049
|
+
...entry.config.vectorPromotion !== void 0 ? { vectorPromotion: entry.config.vectorPromotion } : {}
|
|
4252
5050
|
};
|
|
4253
5051
|
},
|
|
4254
5052
|
createIndexFromMetadata: wiring.createIndexFromMetadata,
|
|
@@ -4277,19 +5075,30 @@ function createEngineCore(config) {
|
|
|
4277
5075
|
const eventHandlers = /* @__PURE__ */ new Map();
|
|
4278
5076
|
const shutdownState = { isShutdown: false };
|
|
4279
5077
|
const abortController = new AbortController();
|
|
5078
|
+
const rebalancingIndexes = /* @__PURE__ */ new Set();
|
|
4280
5079
|
const orchestrator = createWorkerOrchestrator(config, executor, promoter, indexRegistry, {
|
|
5080
|
+
shouldDeferPromotion() {
|
|
5081
|
+
return rebalancingIndexes.size > 0;
|
|
5082
|
+
},
|
|
4281
5083
|
onPromotion(workerCount, reason) {
|
|
4282
5084
|
const handlers = eventHandlers.get("workerPromote");
|
|
4283
5085
|
if (handlers) {
|
|
4284
5086
|
for (const handler of handlers) handler({ workerCount, reason });
|
|
4285
5087
|
}
|
|
5088
|
+
},
|
|
5089
|
+
onPromotionFailure(reason, error, retryable) {
|
|
5090
|
+
const handlers = eventHandlers.get("workerPromoteFailure");
|
|
5091
|
+
if (!handlers || handlers.size === 0) {
|
|
5092
|
+
console.warn(`Worker promotion failed (${reason}):`, error);
|
|
5093
|
+
return;
|
|
5094
|
+
}
|
|
5095
|
+
for (const handler of handlers) handler({ reason, error, retryable });
|
|
4286
5096
|
}
|
|
4287
5097
|
});
|
|
4288
5098
|
const rebalancer = createRebalancer();
|
|
4289
5099
|
const rebalanceRouter = createPartitionRouter();
|
|
4290
|
-
const rebalancingIndexes = /* @__PURE__ */ new Set();
|
|
4291
5100
|
const waqMap = /* @__PURE__ */ new Map();
|
|
4292
|
-
const
|
|
5101
|
+
const rebalanceTargets = /* @__PURE__ */ new Map();
|
|
4293
5102
|
function guardShutdown() {
|
|
4294
5103
|
if (shutdownState.isShutdown) {
|
|
4295
5104
|
throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, "This Narsil instance has been shut down");
|
|
@@ -4346,7 +5155,18 @@ function createEngineCore(config) {
|
|
|
4346
5155
|
embeddingAdapter = candidate;
|
|
4347
5156
|
}
|
|
4348
5157
|
}
|
|
4349
|
-
|
|
5158
|
+
try {
|
|
5159
|
+
executor.createIndex(metadata.indexName, indexConfig, language);
|
|
5160
|
+
} catch (err) {
|
|
5161
|
+
if (err instanceof NarsilError && err.code === ErrorCodes.CONFIG_INVALID) {
|
|
5162
|
+
throw new NarsilError(err.code, `Recovery of index "${metadata.indexName}" failed: ${err.message}`, {
|
|
5163
|
+
indexName: metadata.indexName,
|
|
5164
|
+
tokenizer: metadata.tokenizer,
|
|
5165
|
+
stopWords: metadata.stopWords
|
|
5166
|
+
});
|
|
5167
|
+
}
|
|
5168
|
+
throw err;
|
|
5169
|
+
}
|
|
4350
5170
|
indexRegistry.set(metadata.indexName, {
|
|
4351
5171
|
config: indexConfig,
|
|
4352
5172
|
language,
|
|
@@ -4354,8 +5174,18 @@ function createEngineCore(config) {
|
|
|
4354
5174
|
embeddingAdapterName: adapterName,
|
|
4355
5175
|
vectorFieldPaths: getVectorFieldPaths(indexConfig.schema)
|
|
4356
5176
|
});
|
|
5177
|
+
if (metadata.analysisRevision !== language.revision) {
|
|
5178
|
+
analysisRebuild.markStale({
|
|
5179
|
+
indexName: metadata.indexName,
|
|
5180
|
+
language: language.name,
|
|
5181
|
+
storedRevision: metadata.analysisRevision ?? null,
|
|
5182
|
+
currentRevision: language.revision
|
|
5183
|
+
});
|
|
5184
|
+
}
|
|
4357
5185
|
}
|
|
4358
|
-
const
|
|
5186
|
+
const durabilityTier = config !== void 0 ? resolveDurabilityTier(config) : null;
|
|
5187
|
+
let invalidation = null;
|
|
5188
|
+
const durability = createDurabilityFromTier(durabilityTier, {
|
|
4359
5189
|
requireManager,
|
|
4360
5190
|
indexRegistry,
|
|
4361
5191
|
createIndexFromMetadata,
|
|
@@ -4364,8 +5194,53 @@ function createEngineCore(config) {
|
|
|
4364
5194
|
if (handlers) {
|
|
4365
5195
|
for (const handler of handlers) handler({ error });
|
|
4366
5196
|
}
|
|
5197
|
+
},
|
|
5198
|
+
publishCheckpointedPartitions: (indexName, partitions) => invalidation?.publishPartitions(indexName, partitions) ?? Promise.resolve()
|
|
5199
|
+
});
|
|
5200
|
+
invalidation = createInvalidationFromConfig(config, durabilityTier?.kind ?? null, {
|
|
5201
|
+
getManager: (indexName) => executor.getManager(indexName) ?? void 0,
|
|
5202
|
+
listBroadcastIndexNames: () => {
|
|
5203
|
+
const names = [];
|
|
5204
|
+
for (const [name, entry] of indexRegistry) {
|
|
5205
|
+
if (entry.config.defaultScoring === "broadcast") {
|
|
5206
|
+
names.push(name);
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
return names;
|
|
5210
|
+
},
|
|
5211
|
+
reloadIndex: (indexName) => durability?.manager.reloadIndex?.(indexName) ?? Promise.resolve(),
|
|
5212
|
+
onError(error) {
|
|
5213
|
+
const handlers = eventHandlers.get("invalidationError");
|
|
5214
|
+
if (!handlers || handlers.size === 0) {
|
|
5215
|
+
console.warn("Invalidation error:", error);
|
|
5216
|
+
return;
|
|
5217
|
+
}
|
|
5218
|
+
for (const handler of handlers) handler({ error });
|
|
5219
|
+
}
|
|
5220
|
+
});
|
|
5221
|
+
const watermarkNotifier = createWatermarkNotifier({
|
|
5222
|
+
getManager: (indexName) => executor.getManager(indexName),
|
|
5223
|
+
getPartitionConfig: (indexName) => indexRegistry.get(indexName)?.config.partitions,
|
|
5224
|
+
emit(payload) {
|
|
5225
|
+
const handlers = eventHandlers.get("partitionWatermark");
|
|
5226
|
+
if (!handlers) return;
|
|
5227
|
+
for (const handler of handlers) {
|
|
5228
|
+
try {
|
|
5229
|
+
handler(payload);
|
|
5230
|
+
} catch (err) {
|
|
5231
|
+
console.warn("partitionWatermark handler error:", err instanceof Error ? err.message : String(err));
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
4367
5234
|
}
|
|
4368
5235
|
});
|
|
5236
|
+
const analysisRebuild = wireAnalysisRebuild({
|
|
5237
|
+
config: config?.analysis,
|
|
5238
|
+
eventHandlers,
|
|
5239
|
+
getManager: (indexName) => executor.getManager(indexName),
|
|
5240
|
+
desyncIndex: (indexName) => orchestrator.desyncIndex(indexName),
|
|
5241
|
+
resyncIndex: (indexName, wasPromoted) => orchestrator.resyncIndex(indexName, wasPromoted),
|
|
5242
|
+
durabilityManager: durability?.manager ?? null
|
|
5243
|
+
});
|
|
4369
5244
|
const mutationCtx = {
|
|
4370
5245
|
executor,
|
|
4371
5246
|
pluginRegistry,
|
|
@@ -4376,15 +5251,24 @@ function createEngineCore(config) {
|
|
|
4376
5251
|
guardShutdown,
|
|
4377
5252
|
requireIndex,
|
|
4378
5253
|
requireManager,
|
|
4379
|
-
bufferIfRebalancing
|
|
5254
|
+
bufferIfRebalancing,
|
|
5255
|
+
isRebalancing: (indexName) => rebalancingIndexes.has(indexName),
|
|
5256
|
+
pendingRebalanceWrites: (indexName) => waqMap.get(indexName)?.size ?? 0,
|
|
5257
|
+
rebalanceTargetPartitionCount: (indexName) => rebalanceTargets.get(indexName),
|
|
5258
|
+
bufferedDocState: (indexName, docId) => waqMap.get(indexName)?.bufferedDocState(docId),
|
|
5259
|
+
checkWatermark: watermarkNotifier.check
|
|
4380
5260
|
};
|
|
4381
5261
|
const rebalanceCtx = {
|
|
4382
5262
|
rebalancer,
|
|
4383
5263
|
router: rebalanceRouter,
|
|
4384
5264
|
waqMap,
|
|
4385
5265
|
rebalancingIndexes,
|
|
4386
|
-
|
|
5266
|
+
rebalanceTargets,
|
|
4387
5267
|
eventHandlers,
|
|
5268
|
+
pluginRegistry,
|
|
5269
|
+
orchestrator,
|
|
5270
|
+
durabilityManager: durability?.manager ?? null,
|
|
5271
|
+
checkWatermark: watermarkNotifier.check,
|
|
4388
5272
|
requireIndex
|
|
4389
5273
|
};
|
|
4390
5274
|
return {
|
|
@@ -4392,6 +5276,7 @@ function createEngineCore(config) {
|
|
|
4392
5276
|
promoter,
|
|
4393
5277
|
pluginRegistry,
|
|
4394
5278
|
durability,
|
|
5279
|
+
invalidation,
|
|
4395
5280
|
idGenerator,
|
|
4396
5281
|
indexRegistry,
|
|
4397
5282
|
embeddingAdapters,
|
|
@@ -4403,11 +5288,12 @@ function createEngineCore(config) {
|
|
|
4403
5288
|
rebalanceRouter,
|
|
4404
5289
|
rebalancingIndexes,
|
|
4405
5290
|
waqMap,
|
|
4406
|
-
lastAppliedSeqMap,
|
|
4407
5291
|
guardShutdown,
|
|
4408
5292
|
requireIndex,
|
|
4409
5293
|
requireManager,
|
|
4410
5294
|
bufferIfRebalancing,
|
|
5295
|
+
watermarkNotifier,
|
|
5296
|
+
analysisRebuild,
|
|
4411
5297
|
mutationCtx,
|
|
4412
5298
|
rebalanceCtx
|
|
4413
5299
|
};
|
|
@@ -4587,6 +5473,24 @@ function validateDocId(docId) {
|
|
|
4587
5473
|
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not contain null bytes", { docId });
|
|
4588
5474
|
}
|
|
4589
5475
|
}
|
|
5476
|
+
function validatePartitionConfig(partitions) {
|
|
5477
|
+
const { maxDocsPerPartition, maxPartitions, watermark } = partitions;
|
|
5478
|
+
if (maxDocsPerPartition !== void 0 && (!Number.isInteger(maxDocsPerPartition) || maxDocsPerPartition < 1)) {
|
|
5479
|
+
throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.maxDocsPerPartition must be a positive integer", {
|
|
5480
|
+
maxDocsPerPartition
|
|
5481
|
+
});
|
|
5482
|
+
}
|
|
5483
|
+
if (maxPartitions !== void 0 && (!Number.isInteger(maxPartitions) || maxPartitions < 1)) {
|
|
5484
|
+
throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.maxPartitions must be a positive integer", {
|
|
5485
|
+
maxPartitions
|
|
5486
|
+
});
|
|
5487
|
+
}
|
|
5488
|
+
if (watermark !== void 0 && (typeof watermark !== "number" || !(watermark > 0) || watermark > 1)) {
|
|
5489
|
+
throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.watermark must be above 0 and at most 1", {
|
|
5490
|
+
watermark
|
|
5491
|
+
});
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
4590
5494
|
function clampLimit(limit) {
|
|
4591
5495
|
if (limit === void 0) return DEFAULT_LIMIT;
|
|
4592
5496
|
return Math.max(0, Math.min(limit, MAX_LIMIT));
|
|
@@ -4605,6 +5509,25 @@ async function createEngineIndex(core, config, name, indexConfig) {
|
|
|
4605
5509
|
}
|
|
4606
5510
|
validateSchema(indexConfig.schema);
|
|
4607
5511
|
validateVectorPromotion(indexConfig.vectorPromotion);
|
|
5512
|
+
if (indexConfig.partitions) {
|
|
5513
|
+
validatePartitionConfig(indexConfig.partitions);
|
|
5514
|
+
}
|
|
5515
|
+
if (core.durability) {
|
|
5516
|
+
if (indexConfig.tokenizer !== void 0 && typeof indexConfig.tokenizer !== "string") {
|
|
5517
|
+
throw new NarsilError(
|
|
5518
|
+
ErrorCodes.CONFIG_INVALID,
|
|
5519
|
+
`Index "${name}" cannot use a tokenizer instance while durability is configured, because a checkpoint persists no code. Register it with registerTokenizer and pass its name`,
|
|
5520
|
+
{ indexName: name }
|
|
5521
|
+
);
|
|
5522
|
+
}
|
|
5523
|
+
if (typeof indexConfig.stopWords === "function") {
|
|
5524
|
+
throw new NarsilError(
|
|
5525
|
+
ErrorCodes.CONFIG_INVALID,
|
|
5526
|
+
`Index "${name}" cannot use a stop word function while durability is configured, because a checkpoint persists no code. Register it with registerStopWords and pass its name`,
|
|
5527
|
+
{ indexName: name }
|
|
5528
|
+
);
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
4608
5531
|
let resolvedEmbeddingAdapter = null;
|
|
4609
5532
|
let embeddingAdapterName = null;
|
|
4610
5533
|
if (indexConfig.embedding) {
|
|
@@ -4649,6 +5572,8 @@ async function dropEngineIndex(core, name) {
|
|
|
4649
5572
|
const entry = core.requireIndex(name);
|
|
4650
5573
|
core.executor.dropIndex(name);
|
|
4651
5574
|
core.indexRegistry.delete(name);
|
|
5575
|
+
core.watermarkNotifier.forget(name);
|
|
5576
|
+
core.analysisRebuild.clearStale(name);
|
|
4652
5577
|
if (core.durability) {
|
|
4653
5578
|
await core.durability.manager.removeIndex(name);
|
|
4654
5579
|
}
|
|
@@ -4697,6 +5622,9 @@ async function shutdownEngine(core) {
|
|
|
4697
5622
|
if (durability) {
|
|
4698
5623
|
await durability.manager.shutdown();
|
|
4699
5624
|
}
|
|
5625
|
+
if (core.invalidation) {
|
|
5626
|
+
await core.invalidation.shutdown();
|
|
5627
|
+
}
|
|
4700
5628
|
const adaptersToShutdown = /* @__PURE__ */ new Set();
|
|
4701
5629
|
for (const [, entry] of indexRegistry) {
|
|
4702
5630
|
if (entry.embeddingAdapter?.shutdown) {
|
|
@@ -4979,14 +5907,23 @@ function providedDocId(document) {
|
|
|
4979
5907
|
const id = document.id;
|
|
4980
5908
|
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
4981
5909
|
}
|
|
5910
|
+
function admitInsert(ctx, indexName, manager, docId) {
|
|
5911
|
+
if (!ctx.isRebalancing(indexName)) {
|
|
5912
|
+
manager.assertCapacity();
|
|
5913
|
+
return;
|
|
5914
|
+
}
|
|
5915
|
+
const bufferedState = ctx.bufferedDocState(indexName, docId);
|
|
5916
|
+
const exists = bufferedState !== void 0 ? bufferedState === "present" : manager.has(docId);
|
|
5917
|
+
if (exists) {
|
|
5918
|
+
throw new NarsilError(ErrorCodes.DOC_ALREADY_EXISTS, `Document "${docId}" already exists`, { docId });
|
|
5919
|
+
}
|
|
5920
|
+
manager.assertCapacity(ctx.pendingRebalanceWrites(indexName), ctx.rebalanceTargetPartitionCount(indexName));
|
|
5921
|
+
}
|
|
4982
5922
|
async function insertDocument(ctx, indexName, document, docId, options) {
|
|
4983
5923
|
ctx.guardShutdown();
|
|
4984
5924
|
const entry = ctx.requireIndex(indexName);
|
|
4985
5925
|
const resolvedDocId = docId ?? providedDocId(document) ?? ctx.idGenerator();
|
|
4986
5926
|
validateDocId(resolvedDocId);
|
|
4987
|
-
if (ctx.bufferIfRebalancing(indexName, { action: "insert", docId: resolvedDocId, document, indexName })) {
|
|
4988
|
-
return resolvedDocId;
|
|
4989
|
-
}
|
|
4990
5927
|
await ctx.pluginRegistry.runHook("beforeInsert", { indexName, docId: resolvedDocId, document });
|
|
4991
5928
|
if (entry.config.required && entry.config.required.length > 0) {
|
|
4992
5929
|
validateRequiredFields(document, entry.config.required);
|
|
@@ -5008,6 +5945,7 @@ async function insertDocument(ctx, indexName, document, docId, options) {
|
|
|
5008
5945
|
}
|
|
5009
5946
|
}
|
|
5010
5947
|
const insertManager = ctx.requireManager(indexName);
|
|
5948
|
+
admitInsert(ctx, indexName, insertManager, resolvedDocId);
|
|
5011
5949
|
const insertVecIndexes = insertManager.getVectorIndexes();
|
|
5012
5950
|
const { partitionDoc, extractedVectors } = prepareDocumentVectors(
|
|
5013
5951
|
document,
|
|
@@ -5018,7 +5956,13 @@ async function insertDocument(ctx, indexName, document, docId, options) {
|
|
|
5018
5956
|
validateVectorDimensions(extractedVectors, insertVecIndexes);
|
|
5019
5957
|
}
|
|
5020
5958
|
let inserted = false;
|
|
5959
|
+
let buffered = false;
|
|
5021
5960
|
const applyInsert = async () => {
|
|
5961
|
+
admitInsert(ctx, indexName, insertManager, resolvedDocId);
|
|
5962
|
+
if (ctx.bufferIfRebalancing(indexName, { action: "insert", docId: resolvedDocId, document, indexName })) {
|
|
5963
|
+
buffered = true;
|
|
5964
|
+
return;
|
|
5965
|
+
}
|
|
5022
5966
|
await ctx.executor.execute({
|
|
5023
5967
|
type: "insert",
|
|
5024
5968
|
indexName,
|
|
@@ -5053,6 +5997,10 @@ async function insertDocument(ctx, indexName, document, docId, options) {
|
|
|
5053
5997
|
} else {
|
|
5054
5998
|
await applyInsert();
|
|
5055
5999
|
}
|
|
6000
|
+
if (buffered) {
|
|
6001
|
+
ctx.checkWatermark(indexName);
|
|
6002
|
+
return resolvedDocId;
|
|
6003
|
+
}
|
|
5056
6004
|
try {
|
|
5057
6005
|
await ctx.pluginRegistry.runHook("afterInsert", { indexName, docId: resolvedDocId, document });
|
|
5058
6006
|
} catch (err) {
|
|
@@ -5072,6 +6020,7 @@ async function insertDocument(ctx, indexName, document, docId, options) {
|
|
|
5072
6020
|
vecIndex.scheduleBuild();
|
|
5073
6021
|
}
|
|
5074
6022
|
}
|
|
6023
|
+
ctx.checkWatermark(indexName);
|
|
5075
6024
|
await ctx.orchestrator.checkPromotion();
|
|
5076
6025
|
return resolvedDocId;
|
|
5077
6026
|
}
|
|
@@ -5090,6 +6039,7 @@ async function insertDocumentBatch(ctx, indexName, documents, options) {
|
|
|
5090
6039
|
const batchVecIndexes = batchManager.getVectorIndexes();
|
|
5091
6040
|
const batchVectorFieldPaths = batchVecIndexes.size > 0 ? entry.vectorFieldPaths : /* @__PURE__ */ new Set();
|
|
5092
6041
|
const touchedVectorFields = /* @__PURE__ */ new Set();
|
|
6042
|
+
const bufferedDocIds = /* @__PURE__ */ new Set();
|
|
5093
6043
|
for (let chunkStart = 0; chunkStart < documents.length; chunkStart += BATCH_CHUNK_SIZE) {
|
|
5094
6044
|
if (ctx.abortController.signal.aborted) break;
|
|
5095
6045
|
const chunkEnd = Math.min(chunkStart + BATCH_CHUNK_SIZE, documents.length);
|
|
@@ -5171,8 +6121,20 @@ async function insertDocumentBatch(ctx, indexName, documents, options) {
|
|
|
5171
6121
|
if (extractedVectors.size > 0) {
|
|
5172
6122
|
validateVectorDimensions(extractedVectors, batchVecIndexes);
|
|
5173
6123
|
}
|
|
6124
|
+
admitInsert(ctx, indexName, batchManager, batchDocId);
|
|
5174
6125
|
let batchInserted = false;
|
|
6126
|
+
let batchBuffered = false;
|
|
5175
6127
|
const applyBatchInsert = async () => {
|
|
6128
|
+
admitInsert(ctx, indexName, batchManager, batchDocId);
|
|
6129
|
+
if (ctx.bufferIfRebalancing(indexName, {
|
|
6130
|
+
action: "insert",
|
|
6131
|
+
docId: batchDocId,
|
|
6132
|
+
document: documents[i],
|
|
6133
|
+
indexName
|
|
6134
|
+
})) {
|
|
6135
|
+
batchBuffered = true;
|
|
6136
|
+
return;
|
|
6137
|
+
}
|
|
5176
6138
|
const result = ctx.executor.execute({
|
|
5177
6139
|
type: "insert",
|
|
5178
6140
|
indexName,
|
|
@@ -5210,6 +6172,12 @@ async function insertDocumentBatch(ctx, indexName, documents, options) {
|
|
|
5210
6172
|
} else {
|
|
5211
6173
|
await applyBatchInsert();
|
|
5212
6174
|
}
|
|
6175
|
+
if (batchBuffered) {
|
|
6176
|
+
bufferedDocIds.add(batchDocId);
|
|
6177
|
+
succeeded.push(batchDocId);
|
|
6178
|
+
succeededDocs.push(documents[i]);
|
|
6179
|
+
continue;
|
|
6180
|
+
}
|
|
5213
6181
|
for (const fieldPath of extractedVectors.keys()) {
|
|
5214
6182
|
touchedVectorFields.add(fieldPath);
|
|
5215
6183
|
}
|
|
@@ -5234,6 +6202,7 @@ async function insertDocumentBatch(ctx, indexName, documents, options) {
|
|
|
5234
6202
|
}
|
|
5235
6203
|
}
|
|
5236
6204
|
for (let i = 0; i < succeeded.length; i++) {
|
|
6205
|
+
if (bufferedDocIds.has(succeeded[i])) continue;
|
|
5237
6206
|
await ctx.orchestrator.replicateToWorkers({
|
|
5238
6207
|
type: "insert",
|
|
5239
6208
|
indexName,
|
|
@@ -5249,6 +6218,7 @@ async function insertDocumentBatch(ctx, indexName, documents, options) {
|
|
|
5249
6218
|
vecIndex.scheduleBuild();
|
|
5250
6219
|
}
|
|
5251
6220
|
}
|
|
6221
|
+
ctx.checkWatermark(indexName);
|
|
5252
6222
|
await ctx.orchestrator.checkPromotion();
|
|
5253
6223
|
return { succeeded, failed };
|
|
5254
6224
|
}
|
|
@@ -5258,16 +6228,26 @@ async function removeDocument(ctx, indexName, docId) {
|
|
|
5258
6228
|
ctx.guardShutdown();
|
|
5259
6229
|
ctx.requireIndex(indexName);
|
|
5260
6230
|
validateDocId(docId);
|
|
5261
|
-
if (ctx.bufferIfRebalancing(indexName, { action: "remove", docId, indexName })) {
|
|
5262
|
-
return;
|
|
5263
|
-
}
|
|
5264
6231
|
await ctx.pluginRegistry.runHook("beforeRemove", { indexName, docId });
|
|
5265
|
-
|
|
6232
|
+
const rebalancing = ctx.isRebalancing(indexName);
|
|
6233
|
+
if (ctx.durability && !rebalancing && !ctx.requireManager(indexName).has(docId)) {
|
|
5266
6234
|
throw new NarsilError(ErrorCodes.DOC_NOT_FOUND, `Document "${docId}" not found in any partition`, { docId });
|
|
5267
6235
|
}
|
|
5268
|
-
const restoreRef = ctx.durability ? ctx.requireManager(indexName).getRef(docId) : void 0;
|
|
6236
|
+
const restoreRef = ctx.durability && !rebalancing ? ctx.requireManager(indexName).getRef(docId) : void 0;
|
|
5269
6237
|
const restoreDoc = restoreRef ? structuredClone(restoreRef) : void 0;
|
|
6238
|
+
let buffered = false;
|
|
5270
6239
|
const applyRemove = async () => {
|
|
6240
|
+
if (ctx.isRebalancing(indexName)) {
|
|
6241
|
+
const bufferedState = ctx.bufferedDocState(indexName, docId);
|
|
6242
|
+
const exists = bufferedState !== void 0 ? bufferedState === "present" : ctx.requireManager(indexName).has(docId);
|
|
6243
|
+
if (!exists) {
|
|
6244
|
+
throw new NarsilError(ErrorCodes.DOC_NOT_FOUND, `Document "${docId}" not found in any partition`, { docId });
|
|
6245
|
+
}
|
|
6246
|
+
}
|
|
6247
|
+
if (ctx.bufferIfRebalancing(indexName, { action: "remove", docId, indexName })) {
|
|
6248
|
+
buffered = true;
|
|
6249
|
+
return;
|
|
6250
|
+
}
|
|
5271
6251
|
await ctx.executor.execute({ type: "remove", indexName, docId, requestId: docId });
|
|
5272
6252
|
const removeVecIndexes = ctx.requireManager(indexName).getVectorIndexes();
|
|
5273
6253
|
removeDocumentVectors(docId, removeVecIndexes);
|
|
@@ -5282,6 +6262,9 @@ async function removeDocument(ctx, indexName, docId) {
|
|
|
5282
6262
|
} else {
|
|
5283
6263
|
await applyRemove();
|
|
5284
6264
|
}
|
|
6265
|
+
if (buffered) {
|
|
6266
|
+
return;
|
|
6267
|
+
}
|
|
5285
6268
|
try {
|
|
5286
6269
|
await ctx.pluginRegistry.runHook("afterRemove", { indexName, docId });
|
|
5287
6270
|
} catch (err) {
|
|
@@ -5343,9 +6326,6 @@ async function updateDocument(ctx, indexName, docId, document) {
|
|
|
5343
6326
|
ctx.guardShutdown();
|
|
5344
6327
|
const entry = ctx.requireIndex(indexName);
|
|
5345
6328
|
validateDocId(docId);
|
|
5346
|
-
if (ctx.bufferIfRebalancing(indexName, { action: "update", docId, document, indexName })) {
|
|
5347
|
-
return;
|
|
5348
|
-
}
|
|
5349
6329
|
if (entry.config.embedding) {
|
|
5350
6330
|
if (entry.embeddingAdapter) {
|
|
5351
6331
|
await embedDocumentFields(
|
|
@@ -5388,7 +6368,22 @@ async function updateDocument(ctx, indexName, docId, document) {
|
|
|
5388
6368
|
}
|
|
5389
6369
|
}
|
|
5390
6370
|
const { partitionDoc } = prepareUpdatePartitionDoc(document, updateExtractedVectors);
|
|
6371
|
+
let buffered = false;
|
|
5391
6372
|
const applyUpdate = async () => {
|
|
6373
|
+
if (ctx.isRebalancing(indexName)) {
|
|
6374
|
+
const bufferedState = ctx.bufferedDocState(indexName, docId);
|
|
6375
|
+
const exists = bufferedState !== void 0 ? bufferedState === "present" : updateManager.has(docId);
|
|
6376
|
+
if (!exists) {
|
|
6377
|
+
updateManager.assertCapacity(
|
|
6378
|
+
ctx.pendingRebalanceWrites(indexName),
|
|
6379
|
+
ctx.rebalanceTargetPartitionCount(indexName)
|
|
6380
|
+
);
|
|
6381
|
+
}
|
|
6382
|
+
}
|
|
6383
|
+
if (ctx.bufferIfRebalancing(indexName, { action: "update", docId, document, indexName })) {
|
|
6384
|
+
buffered = true;
|
|
6385
|
+
return;
|
|
6386
|
+
}
|
|
5392
6387
|
await ctx.executor.execute({
|
|
5393
6388
|
type: "update",
|
|
5394
6389
|
indexName,
|
|
@@ -5428,6 +6423,9 @@ async function updateDocument(ctx, indexName, docId, document) {
|
|
|
5428
6423
|
} else {
|
|
5429
6424
|
await applyUpdate();
|
|
5430
6425
|
}
|
|
6426
|
+
if (buffered) {
|
|
6427
|
+
return;
|
|
6428
|
+
}
|
|
5431
6429
|
try {
|
|
5432
6430
|
await ctx.pluginRegistry.runHook("afterUpdate", {
|
|
5433
6431
|
indexName,
|
|
@@ -5892,13 +6890,13 @@ function highlightField(text, queryTokens, language, options) {
|
|
|
5892
6890
|
}
|
|
5893
6891
|
|
|
5894
6892
|
// src/engine/query/highlight.ts
|
|
5895
|
-
function applyHighlights(hits, params, language,
|
|
6893
|
+
function applyHighlights(hits, params, language, analysis) {
|
|
5896
6894
|
if (!params.highlight) return;
|
|
5897
6895
|
const queryTokenResult = tokenize(params.term ?? "", language, {
|
|
5898
6896
|
stem: true,
|
|
5899
6897
|
removeStopWords: true,
|
|
5900
|
-
customTokenizer:
|
|
5901
|
-
stopWordOverride:
|
|
6898
|
+
customTokenizer: analysis.customTokenizer,
|
|
6899
|
+
stopWordOverride: analysis.stopWords
|
|
5902
6900
|
});
|
|
5903
6901
|
let prefixToken;
|
|
5904
6902
|
if (params.prefix === true && params.exact !== true && queryTokenResult.tokens.length > 0) {
|
|
@@ -5906,8 +6904,8 @@ function applyHighlights(hits, params, language, config) {
|
|
|
5906
6904
|
const unstemmed = tokenize(params.term ?? "", language, {
|
|
5907
6905
|
stem: false,
|
|
5908
6906
|
removeStopWords: true,
|
|
5909
|
-
customTokenizer:
|
|
5910
|
-
stopWordOverride:
|
|
6907
|
+
customTokenizer: analysis.customTokenizer,
|
|
6908
|
+
stopWordOverride: analysis.stopWords
|
|
5911
6909
|
});
|
|
5912
6910
|
for (const t of unstemmed.tokens) {
|
|
5913
6911
|
if (t.position === lastPosition) {
|
|
@@ -5930,8 +6928,64 @@ function applyHighlights(hits, params, language, config) {
|
|
|
5930
6928
|
});
|
|
5931
6929
|
}
|
|
5932
6930
|
}
|
|
5933
|
-
hit.highlights = highlights;
|
|
6931
|
+
hit.highlights = highlights;
|
|
6932
|
+
}
|
|
6933
|
+
}
|
|
6934
|
+
|
|
6935
|
+
// src/engine/query/shared.ts
|
|
6936
|
+
function scoringConfigFor(params, context) {
|
|
6937
|
+
const scoringMode = params.scoring ?? context.config.defaultScoring ?? "local";
|
|
6938
|
+
if (scoringMode !== "broadcast") {
|
|
6939
|
+
return { scoringMode };
|
|
6940
|
+
}
|
|
6941
|
+
return { scoringMode, globalStats: context.broadcastStats?.(context.indexName) };
|
|
6942
|
+
}
|
|
6943
|
+
function broadcastStatsForWorker(params, context, scoring) {
|
|
6944
|
+
if (scoring.scoringMode !== "broadcast" || scoring.globalStats === void 0) {
|
|
6945
|
+
return void 0;
|
|
6946
|
+
}
|
|
6947
|
+
const term = params.term;
|
|
6948
|
+
if (term === void 0 || term.trim().length === 0) {
|
|
6949
|
+
return void 0;
|
|
6950
|
+
}
|
|
6951
|
+
return pruneStatsToQueryTerms(scoring.globalStats, term, context.language, context.manager.analysis);
|
|
6952
|
+
}
|
|
6953
|
+
function searchOptionsFor(manager) {
|
|
6954
|
+
return {
|
|
6955
|
+
bm25Params: manager.config.bm25,
|
|
6956
|
+
stopWords: manager.analysis.stopWords,
|
|
6957
|
+
customTokenizer: manager.analysis.customTokenizer
|
|
6958
|
+
};
|
|
6959
|
+
}
|
|
6960
|
+
function collectFilterDocIds(manager, params, schema) {
|
|
6961
|
+
const filterDocIds = /* @__PURE__ */ new Set();
|
|
6962
|
+
if (!params.filters) return filterDocIds;
|
|
6963
|
+
for (const partition of manager.getAllPartitions()) {
|
|
6964
|
+
const partitionFiltered = partition.applyFilters(params.filters, schema);
|
|
6965
|
+
for (const docId of partitionFiltered) {
|
|
6966
|
+
filterDocIds.add(docId);
|
|
6967
|
+
}
|
|
5934
6968
|
}
|
|
6969
|
+
return filterDocIds;
|
|
6970
|
+
}
|
|
6971
|
+
function vectorResultsToScored(results) {
|
|
6972
|
+
return results.map((r) => ({
|
|
6973
|
+
docId: r.docId,
|
|
6974
|
+
score: r.score,
|
|
6975
|
+
termFrequencies: {},
|
|
6976
|
+
fieldLengths: {},
|
|
6977
|
+
idf: {}
|
|
6978
|
+
}));
|
|
6979
|
+
}
|
|
6980
|
+
function resolveVectorIndex(manager, fieldName) {
|
|
6981
|
+
return manager.getVectorIndexes().get(fieldName);
|
|
6982
|
+
}
|
|
6983
|
+
function clampAlpha(alpha) {
|
|
6984
|
+
if (alpha === void 0) return 0.5;
|
|
6985
|
+
if (!Number.isFinite(alpha)) return 0.5;
|
|
6986
|
+
if (alpha < 0) return 0;
|
|
6987
|
+
if (alpha > 1) return 1;
|
|
6988
|
+
return alpha;
|
|
5935
6989
|
}
|
|
5936
6990
|
|
|
5937
6991
|
// src/search/fusion.ts
|
|
@@ -6015,38 +7069,6 @@ function minMaxNormalize(docs) {
|
|
|
6015
7069
|
return docs.map((d) => ({ docId: d.docId, score: (d.score - min) / range }));
|
|
6016
7070
|
}
|
|
6017
7071
|
|
|
6018
|
-
// src/engine/query/shared.ts
|
|
6019
|
-
function collectFilterDocIds(manager, params, schema) {
|
|
6020
|
-
const filterDocIds = /* @__PURE__ */ new Set();
|
|
6021
|
-
if (!params.filters) return filterDocIds;
|
|
6022
|
-
for (const partition of manager.getAllPartitions()) {
|
|
6023
|
-
const partitionFiltered = partition.applyFilters(params.filters, schema);
|
|
6024
|
-
for (const docId of partitionFiltered) {
|
|
6025
|
-
filterDocIds.add(docId);
|
|
6026
|
-
}
|
|
6027
|
-
}
|
|
6028
|
-
return filterDocIds;
|
|
6029
|
-
}
|
|
6030
|
-
function vectorResultsToScored(results) {
|
|
6031
|
-
return results.map((r) => ({
|
|
6032
|
-
docId: r.docId,
|
|
6033
|
-
score: r.score,
|
|
6034
|
-
termFrequencies: {},
|
|
6035
|
-
fieldLengths: {},
|
|
6036
|
-
idf: {}
|
|
6037
|
-
}));
|
|
6038
|
-
}
|
|
6039
|
-
function resolveVectorIndex(manager, fieldName) {
|
|
6040
|
-
return manager.getVectorIndexes().get(fieldName);
|
|
6041
|
-
}
|
|
6042
|
-
function clampAlpha(alpha) {
|
|
6043
|
-
if (alpha === void 0) return 0.5;
|
|
6044
|
-
if (!Number.isFinite(alpha)) return 0.5;
|
|
6045
|
-
if (alpha < 0) return 0;
|
|
6046
|
-
if (alpha > 1) return 1;
|
|
6047
|
-
return alpha;
|
|
6048
|
-
}
|
|
6049
|
-
|
|
6050
7072
|
// src/engine/query/vector.ts
|
|
6051
7073
|
function executeVectorSearch(params, manager, config, limit, offset) {
|
|
6052
7074
|
const vectorConfig = params.vector;
|
|
@@ -6075,7 +7097,8 @@ function executeVectorSearch(params, manager, config, limit, offset) {
|
|
|
6075
7097
|
const scored = vectorResultsToScored(results);
|
|
6076
7098
|
return { scored, totalMatched: scored.length };
|
|
6077
7099
|
}
|
|
6078
|
-
async function executeHybridSearch(params,
|
|
7100
|
+
async function executeHybridSearch(params, context, limit, offset) {
|
|
7101
|
+
const { manager, language, config, workerSearch, indexName } = context;
|
|
6079
7102
|
const { vector: _vector, mode: _mode, hybrid: _hybrid, ...textOnlyParams } = params;
|
|
6080
7103
|
let filterDocIds;
|
|
6081
7104
|
if (params.filters) {
|
|
@@ -6085,22 +7108,18 @@ async function executeHybridSearch(params, manager, language, config, workerSear
|
|
|
6085
7108
|
}
|
|
6086
7109
|
}
|
|
6087
7110
|
let textFanOutResult;
|
|
6088
|
-
const
|
|
7111
|
+
const scoring = scoringConfigFor(params, context);
|
|
7112
|
+
const textWorkerResult = workerSearch ? await workerSearch(indexName, textOnlyParams, broadcastStatsForWorker(textOnlyParams, context, scoring)) : null;
|
|
6089
7113
|
if (textWorkerResult) {
|
|
6090
7114
|
textFanOutResult = textWorkerResult;
|
|
6091
7115
|
} else {
|
|
6092
|
-
const searchOptions = {
|
|
6093
|
-
bm25Params: config.bm25,
|
|
6094
|
-
stopWords: config.stopWords,
|
|
6095
|
-
customTokenizer: config.tokenizer
|
|
6096
|
-
};
|
|
6097
7116
|
textFanOutResult = await fanOutQuery(
|
|
6098
7117
|
manager,
|
|
6099
7118
|
textOnlyParams,
|
|
6100
7119
|
language,
|
|
6101
7120
|
config.schema,
|
|
6102
|
-
|
|
6103
|
-
|
|
7121
|
+
scoring,
|
|
7122
|
+
searchOptionsFor(manager)
|
|
6104
7123
|
);
|
|
6105
7124
|
}
|
|
6106
7125
|
const vectorConfig = params.vector;
|
|
@@ -6164,25 +7183,14 @@ async function executeQuery(params, context) {
|
|
|
6164
7183
|
if (isVectorOnly && hasGlobalVectorIndex) {
|
|
6165
7184
|
fanOutResult = executeVectorSearch(params, manager, config, limit, offset);
|
|
6166
7185
|
} else if (isHybridMode && hasGlobalVectorIndex) {
|
|
6167
|
-
fanOutResult = await executeHybridSearch(params,
|
|
7186
|
+
fanOutResult = await executeHybridSearch(params, context, limit, offset);
|
|
6168
7187
|
} else {
|
|
6169
|
-
const
|
|
7188
|
+
const scoring = scoringConfigFor(params, context);
|
|
7189
|
+
const workerResult = workerSearch ? await workerSearch(indexName, params, broadcastStatsForWorker(params, context, scoring)) : null;
|
|
6170
7190
|
if (workerResult) {
|
|
6171
7191
|
fanOutResult = workerResult;
|
|
6172
7192
|
} else {
|
|
6173
|
-
|
|
6174
|
-
bm25Params: config.bm25,
|
|
6175
|
-
stopWords: config.stopWords,
|
|
6176
|
-
customTokenizer: config.tokenizer
|
|
6177
|
-
};
|
|
6178
|
-
fanOutResult = await fanOutQuery(
|
|
6179
|
-
manager,
|
|
6180
|
-
params,
|
|
6181
|
-
language,
|
|
6182
|
-
config.schema,
|
|
6183
|
-
{ scoringMode: params.scoring ?? config.defaultScoring ?? "local" },
|
|
6184
|
-
searchOptions
|
|
6185
|
-
);
|
|
7193
|
+
fanOutResult = await fanOutQuery(manager, params, language, config.schema, scoring, searchOptionsFor(manager));
|
|
6186
7194
|
}
|
|
6187
7195
|
}
|
|
6188
7196
|
const needsFullHits = params.sort !== void 0 || params.group !== void 0 || params.pinned !== void 0 || params.searchAfter !== void 0;
|
|
@@ -6233,7 +7241,7 @@ async function executeQuery(params, context) {
|
|
|
6233
7241
|
}
|
|
6234
7242
|
}
|
|
6235
7243
|
if (params.highlight) {
|
|
6236
|
-
applyHighlights(paginated, params, language,
|
|
7244
|
+
applyHighlights(paginated, params, language, manager.analysis);
|
|
6237
7245
|
}
|
|
6238
7246
|
const elapsed = now() - startTime;
|
|
6239
7247
|
return {
|
|
@@ -6261,34 +7269,21 @@ async function executePreflight(params, context) {
|
|
|
6261
7269
|
const result = executeVectorSearch(params, manager, config, preflightLimit, preflightOffset);
|
|
6262
7270
|
totalMatched = result.totalMatched;
|
|
6263
7271
|
} else if (isHybridMode && hasGlobalVectorIndex) {
|
|
6264
|
-
const result = await executeHybridSearch(
|
|
6265
|
-
params,
|
|
6266
|
-
manager,
|
|
6267
|
-
language,
|
|
6268
|
-
config,
|
|
6269
|
-
workerSearch,
|
|
6270
|
-
indexName,
|
|
6271
|
-
preflightLimit,
|
|
6272
|
-
preflightOffset
|
|
6273
|
-
);
|
|
7272
|
+
const result = await executeHybridSearch(params, context, preflightLimit, preflightOffset);
|
|
6274
7273
|
totalMatched = result.totalMatched;
|
|
6275
7274
|
} else {
|
|
6276
|
-
const
|
|
7275
|
+
const scoring = scoringConfigFor(params, context);
|
|
7276
|
+
const workerResult = workerSearch ? await workerSearch(indexName, params, broadcastStatsForWorker(params, context, scoring)) : null;
|
|
6277
7277
|
if (workerResult) {
|
|
6278
7278
|
totalMatched = workerResult.totalMatched;
|
|
6279
7279
|
} else {
|
|
6280
|
-
const searchOptions = {
|
|
6281
|
-
bm25Params: config.bm25,
|
|
6282
|
-
stopWords: config.stopWords,
|
|
6283
|
-
customTokenizer: config.tokenizer
|
|
6284
|
-
};
|
|
6285
7280
|
const fanOutResult = await fanOutQuery(
|
|
6286
7281
|
manager,
|
|
6287
7282
|
params,
|
|
6288
7283
|
language,
|
|
6289
7284
|
config.schema,
|
|
6290
|
-
|
|
6291
|
-
|
|
7285
|
+
scoring,
|
|
7286
|
+
searchOptionsFor(manager)
|
|
6292
7287
|
);
|
|
6293
7288
|
totalMatched = fanOutResult.totalMatched;
|
|
6294
7289
|
}
|
|
@@ -6300,6 +7295,7 @@ async function executePreflight(params, context) {
|
|
|
6300
7295
|
// src/partitioning/write-ahead-queue.ts
|
|
6301
7296
|
function createWriteAheadQueue(maxSize = 1e4) {
|
|
6302
7297
|
const entries = [];
|
|
7298
|
+
const docStates = /* @__PURE__ */ new Map();
|
|
6303
7299
|
let nextSequence = 1;
|
|
6304
7300
|
return {
|
|
6305
7301
|
push(entry) {
|
|
@@ -6312,6 +7308,7 @@ function createWriteAheadQueue(maxSize = 1e4) {
|
|
|
6312
7308
|
}
|
|
6313
7309
|
const seq = nextSequence++;
|
|
6314
7310
|
entries.push({ ...entry, sequenceNumber: seq });
|
|
7311
|
+
docStates.set(entry.docId, entry.action === "remove" ? "absent" : "present");
|
|
6315
7312
|
return seq;
|
|
6316
7313
|
},
|
|
6317
7314
|
drain() {
|
|
@@ -6321,6 +7318,10 @@ function createWriteAheadQueue(maxSize = 1e4) {
|
|
|
6321
7318
|
},
|
|
6322
7319
|
clear() {
|
|
6323
7320
|
entries.length = 0;
|
|
7321
|
+
docStates.clear();
|
|
7322
|
+
},
|
|
7323
|
+
bufferedDocState(docId) {
|
|
7324
|
+
return docStates.get(docId);
|
|
6324
7325
|
},
|
|
6325
7326
|
get size() {
|
|
6326
7327
|
return entries.length;
|
|
@@ -6332,6 +7333,135 @@ function createWriteAheadQueue(maxSize = 1e4) {
|
|
|
6332
7333
|
}
|
|
6333
7334
|
|
|
6334
7335
|
// src/engine/rebalance-executor.ts
|
|
7336
|
+
function warnHookError(hook, err) {
|
|
7337
|
+
console.warn(`${hook} plugin hook error:`, err instanceof Error ? err.message : String(err));
|
|
7338
|
+
}
|
|
7339
|
+
function applyReplayUpdateVectors(entry, vectorFieldPaths) {
|
|
7340
|
+
const updateVectors = /* @__PURE__ */ new Map();
|
|
7341
|
+
for (const fieldPath of vectorFieldPaths) {
|
|
7342
|
+
updateVectors.set(fieldPath, extractVectorFromDoc(entry.document, fieldPath));
|
|
7343
|
+
}
|
|
7344
|
+
let updateDoc = entry.document;
|
|
7345
|
+
if (updateVectors.size > 0) {
|
|
7346
|
+
updateDoc = structuredClone(entry.document);
|
|
7347
|
+
for (const fieldPath of updateVectors.keys()) {
|
|
7348
|
+
deleteNestedValue(updateDoc, fieldPath);
|
|
7349
|
+
}
|
|
7350
|
+
}
|
|
7351
|
+
return { updateDoc, updateVectors };
|
|
7352
|
+
}
|
|
7353
|
+
async function replayEntry(manager, indexName, entry, ctx) {
|
|
7354
|
+
const vecIndexes = manager.getVectorIndexes();
|
|
7355
|
+
const vectorFieldPaths = vecIndexes.size > 0 ? ctx.requireIndex(indexName).vectorFieldPaths : /* @__PURE__ */ new Set();
|
|
7356
|
+
if (entry.action === "insert" && entry.document) {
|
|
7357
|
+
const { partitionDoc, extractedVectors } = prepareDocumentVectors(
|
|
7358
|
+
entry.document,
|
|
7359
|
+
vectorFieldPaths,
|
|
7360
|
+
vecIndexes
|
|
7361
|
+
);
|
|
7362
|
+
const overwrote = manager.has(entry.docId);
|
|
7363
|
+
if (overwrote) {
|
|
7364
|
+
removeDocumentVectors(entry.docId, vecIndexes);
|
|
7365
|
+
manager.remove(entry.docId);
|
|
7366
|
+
}
|
|
7367
|
+
manager.insert(entry.docId, partitionDoc);
|
|
7368
|
+
if (extractedVectors.size > 0) {
|
|
7369
|
+
insertDocumentVectors(entry.docId, extractedVectors, vecIndexes);
|
|
7370
|
+
for (const fieldPath of extractedVectors.keys()) {
|
|
7371
|
+
vecIndexes.get(fieldPath)?.scheduleBuild();
|
|
7372
|
+
}
|
|
7373
|
+
}
|
|
7374
|
+
try {
|
|
7375
|
+
await ctx.pluginRegistry.runHook("afterInsert", { indexName, docId: entry.docId, document: entry.document });
|
|
7376
|
+
} catch (err) {
|
|
7377
|
+
warnHookError("afterInsert", err);
|
|
7378
|
+
}
|
|
7379
|
+
await ctx.orchestrator.replicateToWorkers({
|
|
7380
|
+
type: overwrote ? "update" : "insert",
|
|
7381
|
+
indexName,
|
|
7382
|
+
docId: entry.docId,
|
|
7383
|
+
document: entry.document,
|
|
7384
|
+
requestId: `replicate-insert-${entry.docId}`
|
|
7385
|
+
});
|
|
7386
|
+
return;
|
|
7387
|
+
}
|
|
7388
|
+
if (entry.action === "remove") {
|
|
7389
|
+
manager.remove(entry.docId);
|
|
7390
|
+
removeDocumentVectors(entry.docId, vecIndexes);
|
|
7391
|
+
try {
|
|
7392
|
+
await ctx.pluginRegistry.runHook("afterRemove", { indexName, docId: entry.docId });
|
|
7393
|
+
} catch (err) {
|
|
7394
|
+
warnHookError("afterRemove", err);
|
|
7395
|
+
}
|
|
7396
|
+
await ctx.orchestrator.replicateToWorkers({
|
|
7397
|
+
type: "remove",
|
|
7398
|
+
indexName,
|
|
7399
|
+
docId: entry.docId,
|
|
7400
|
+
requestId: `replicate-remove-${entry.docId}`
|
|
7401
|
+
});
|
|
7402
|
+
return;
|
|
7403
|
+
}
|
|
7404
|
+
if (entry.action === "update" && entry.document) {
|
|
7405
|
+
const oldDocument = manager.get(entry.docId);
|
|
7406
|
+
const { updateDoc, updateVectors } = applyReplayUpdateVectors(entry, vectorFieldPaths);
|
|
7407
|
+
if (manager.has(entry.docId)) {
|
|
7408
|
+
manager.update(entry.docId, updateDoc);
|
|
7409
|
+
} else {
|
|
7410
|
+
manager.insert(entry.docId, updateDoc);
|
|
7411
|
+
}
|
|
7412
|
+
for (const [fieldPath, newVec] of updateVectors) {
|
|
7413
|
+
const vecIndex = vecIndexes.get(fieldPath);
|
|
7414
|
+
if (!vecIndex) continue;
|
|
7415
|
+
if (newVec === null) {
|
|
7416
|
+
if (vecIndex.has(entry.docId)) {
|
|
7417
|
+
vecIndex.remove(entry.docId);
|
|
7418
|
+
}
|
|
7419
|
+
} else {
|
|
7420
|
+
const oldVec = vecIndex.getVector(entry.docId);
|
|
7421
|
+
if (!vectorsEqual(oldVec, newVec)) {
|
|
7422
|
+
if (vecIndex.has(entry.docId)) {
|
|
7423
|
+
vecIndex.remove(entry.docId);
|
|
7424
|
+
}
|
|
7425
|
+
vecIndex.insert(entry.docId, newVec);
|
|
7426
|
+
vecIndex.scheduleBuild();
|
|
7427
|
+
}
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
7430
|
+
try {
|
|
7431
|
+
await ctx.pluginRegistry.runHook("afterUpdate", {
|
|
7432
|
+
indexName,
|
|
7433
|
+
docId: entry.docId,
|
|
7434
|
+
oldDocument: oldDocument ?? {},
|
|
7435
|
+
newDocument: entry.document
|
|
7436
|
+
});
|
|
7437
|
+
} catch (err) {
|
|
7438
|
+
warnHookError("afterUpdate", err);
|
|
7439
|
+
}
|
|
7440
|
+
await ctx.orchestrator.replicateToWorkers({
|
|
7441
|
+
type: "update",
|
|
7442
|
+
indexName,
|
|
7443
|
+
docId: entry.docId,
|
|
7444
|
+
document: entry.document,
|
|
7445
|
+
requestId: `replicate-update-${entry.docId}`
|
|
7446
|
+
});
|
|
7447
|
+
}
|
|
7448
|
+
}
|
|
7449
|
+
async function replayQueued(manager, indexName, waq, ctx) {
|
|
7450
|
+
while (true) {
|
|
7451
|
+
const entries = waq.drain();
|
|
7452
|
+
if (entries.length === 0) break;
|
|
7453
|
+
for (const entry of entries) {
|
|
7454
|
+
try {
|
|
7455
|
+
await replayEntry(manager, indexName, entry, ctx);
|
|
7456
|
+
} catch (replayErr) {
|
|
7457
|
+
const isMissing = replayErr instanceof NarsilError && replayErr.code === ErrorCodes.DOC_NOT_FOUND;
|
|
7458
|
+
if (!isMissing) {
|
|
7459
|
+
console.warn(`WAQ replay failed for ${entry.action} on doc "${entry.docId}":`, replayErr);
|
|
7460
|
+
}
|
|
7461
|
+
}
|
|
7462
|
+
}
|
|
7463
|
+
}
|
|
7464
|
+
}
|
|
6335
7465
|
async function executeRebalance(manager, indexName, targetPartitionCount, ctx) {
|
|
6336
7466
|
if (targetPartitionCount <= 0 || !Number.isInteger(targetPartitionCount)) {
|
|
6337
7467
|
throw new NarsilError(
|
|
@@ -6340,6 +7470,15 @@ async function executeRebalance(manager, indexName, targetPartitionCount, ctx) {
|
|
|
6340
7470
|
{ targetPartitionCount }
|
|
6341
7471
|
);
|
|
6342
7472
|
}
|
|
7473
|
+
const entry = ctx.requireIndex(indexName);
|
|
7474
|
+
const maxPartitions = entry.config.partitions?.maxPartitions;
|
|
7475
|
+
if (maxPartitions !== void 0 && targetPartitionCount > maxPartitions) {
|
|
7476
|
+
throw new NarsilError(
|
|
7477
|
+
ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
|
|
7478
|
+
`Target partition count (${targetPartitionCount}) is above the maximum of ${maxPartitions} partitions`,
|
|
7479
|
+
{ targetPartitionCount, maxPartitions }
|
|
7480
|
+
);
|
|
7481
|
+
}
|
|
6343
7482
|
if (ctx.rebalancingIndexes.has(indexName)) {
|
|
6344
7483
|
throw new NarsilError(
|
|
6345
7484
|
ErrorCodes.PARTITION_REBALANCING_BACKPRESSURE,
|
|
@@ -6352,6 +7491,8 @@ async function executeRebalance(manager, indexName, targetPartitionCount, ctx) {
|
|
|
6352
7491
|
const waq = createWriteAheadQueue();
|
|
6353
7492
|
ctx.waqMap.set(indexName, waq);
|
|
6354
7493
|
ctx.rebalancingIndexes.add(indexName);
|
|
7494
|
+
ctx.rebalanceTargets.set(indexName, targetPartitionCount);
|
|
7495
|
+
const wasPromoted = ctx.orchestrator.desyncIndex(indexName);
|
|
6355
7496
|
try {
|
|
6356
7497
|
const oldCount = manager.partitionCount;
|
|
6357
7498
|
await ctx.rebalancer.rebalance(manager, targetPartitionCount, ctx.router, (progress) => {
|
|
@@ -6364,71 +7505,36 @@ async function executeRebalance(manager, indexName, targetPartitionCount, ctx) {
|
|
|
6364
7505
|
}
|
|
6365
7506
|
}
|
|
6366
7507
|
});
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
} else if (entry.action === "update" && entry.document) {
|
|
6390
|
-
const replayUpdateVectors = /* @__PURE__ */ new Map();
|
|
6391
|
-
for (const fieldPath of rebalanceVectorFieldPaths) {
|
|
6392
|
-
const newVec = extractVectorFromDoc(entry.document, fieldPath);
|
|
6393
|
-
replayUpdateVectors.set(fieldPath, newVec);
|
|
6394
|
-
}
|
|
6395
|
-
const replayUpdateDoc = replayUpdateVectors.size > 0 ? structuredClone(entry.document) : entry.document;
|
|
6396
|
-
if (replayUpdateVectors.size > 0) {
|
|
6397
|
-
for (const fieldPath of replayUpdateVectors.keys()) {
|
|
6398
|
-
deleteNestedValue(replayUpdateDoc, fieldPath);
|
|
6399
|
-
}
|
|
6400
|
-
}
|
|
6401
|
-
manager.update(entry.docId, replayUpdateDoc);
|
|
6402
|
-
for (const [fieldPath, newVec] of replayUpdateVectors) {
|
|
6403
|
-
const vecIndex = rebalanceVecIndexes.get(fieldPath);
|
|
6404
|
-
if (!vecIndex) continue;
|
|
6405
|
-
if (newVec === null) {
|
|
6406
|
-
if (vecIndex.has(entry.docId)) {
|
|
6407
|
-
vecIndex.remove(entry.docId);
|
|
6408
|
-
}
|
|
6409
|
-
} else {
|
|
6410
|
-
const oldVec = vecIndex.getVector(entry.docId);
|
|
6411
|
-
if (!vectorsEqual(oldVec, newVec)) {
|
|
6412
|
-
vecIndex.remove(entry.docId);
|
|
6413
|
-
vecIndex.insert(entry.docId, newVec);
|
|
6414
|
-
}
|
|
6415
|
-
}
|
|
6416
|
-
}
|
|
6417
|
-
}
|
|
6418
|
-
} catch (replayErr) {
|
|
6419
|
-
const isDuplicate = replayErr instanceof NarsilError && replayErr.code === ErrorCodes.DOC_ALREADY_EXISTS;
|
|
6420
|
-
const isMissing = replayErr instanceof NarsilError && replayErr.code === ErrorCodes.DOC_NOT_FOUND;
|
|
6421
|
-
if (!isDuplicate && !isMissing) {
|
|
6422
|
-
console.warn(`WAQ replay failed for ${entry.action} on doc "${entry.docId}":`, replayErr);
|
|
6423
|
-
}
|
|
6424
|
-
}
|
|
6425
|
-
appliedSeqs.set(0, entry.sequenceNumber);
|
|
7508
|
+
await replayQueued(manager, indexName, waq, ctx);
|
|
7509
|
+
if (ctx.durabilityManager) {
|
|
7510
|
+
await ctx.durabilityManager.persistMetadata(indexName);
|
|
7511
|
+
await ctx.durabilityManager.checkpoint(indexName);
|
|
7512
|
+
await replayQueued(manager, indexName, waq, ctx);
|
|
7513
|
+
}
|
|
7514
|
+
await ctx.orchestrator.resyncIndex(indexName, wasPromoted);
|
|
7515
|
+
do {
|
|
7516
|
+
await replayQueued(manager, indexName, waq, ctx);
|
|
7517
|
+
} while (waq.size > 0);
|
|
7518
|
+
} catch (err) {
|
|
7519
|
+
try {
|
|
7520
|
+
await replayQueued(manager, indexName, waq, ctx);
|
|
7521
|
+
await ctx.orchestrator.resyncIndex(indexName, wasPromoted);
|
|
7522
|
+
do {
|
|
7523
|
+
await replayQueued(manager, indexName, waq, ctx);
|
|
7524
|
+
} while (waq.size > 0);
|
|
7525
|
+
} catch (recoveryErr) {
|
|
7526
|
+
console.warn(
|
|
7527
|
+
`Rebalance failure recovery for index "${indexName}" did not complete:`,
|
|
7528
|
+
recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr)
|
|
7529
|
+
);
|
|
6426
7530
|
}
|
|
6427
|
-
|
|
7531
|
+
throw err;
|
|
6428
7532
|
} finally {
|
|
6429
7533
|
ctx.rebalancingIndexes.delete(indexName);
|
|
7534
|
+
ctx.rebalanceTargets.delete(indexName);
|
|
6430
7535
|
ctx.waqMap.delete(indexName);
|
|
6431
7536
|
}
|
|
7537
|
+
ctx.checkWatermark(indexName);
|
|
6432
7538
|
}
|
|
6433
7539
|
|
|
6434
7540
|
// src/engine/resolve-vector-text.ts
|
|
@@ -6474,8 +7580,176 @@ async function resolveVectorText(params, embeddingAdapter, signal, embeddingAdap
|
|
|
6474
7580
|
return { ...params, vector: resolved };
|
|
6475
7581
|
}
|
|
6476
7582
|
|
|
6477
|
-
// src/engine/snapshot.ts
|
|
7583
|
+
// src/engine/snapshot/restore-config.ts
|
|
7584
|
+
function invalid(message) {
|
|
7585
|
+
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, `Invalid snapshot: ${message}`);
|
|
7586
|
+
}
|
|
7587
|
+
function isPlainObject(value) {
|
|
7588
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7589
|
+
}
|
|
7590
|
+
function restoredBm25(raw) {
|
|
7591
|
+
if (raw === void 0) {
|
|
7592
|
+
return void 0;
|
|
7593
|
+
}
|
|
7594
|
+
if (!isPlainObject(raw)) {
|
|
7595
|
+
invalid("bm25 must be an object");
|
|
7596
|
+
}
|
|
7597
|
+
const { k1, b } = raw;
|
|
7598
|
+
if (k1 !== void 0 && (typeof k1 !== "number" || !Number.isFinite(k1))) {
|
|
7599
|
+
invalid("bm25 k1 must be a finite number");
|
|
7600
|
+
}
|
|
7601
|
+
if (b !== void 0 && (typeof b !== "number" || !Number.isFinite(b))) {
|
|
7602
|
+
invalid("bm25 b must be a finite number");
|
|
7603
|
+
}
|
|
7604
|
+
return {
|
|
7605
|
+
...k1 !== void 0 ? { k1 } : {},
|
|
7606
|
+
...b !== void 0 ? { b } : {}
|
|
7607
|
+
};
|
|
7608
|
+
}
|
|
7609
|
+
function restoredStopWords(name, list) {
|
|
7610
|
+
if (name !== void 0 && typeof name !== "string") {
|
|
7611
|
+
invalid("stopWords must be a name");
|
|
7612
|
+
}
|
|
7613
|
+
if (list !== void 0 && (!Array.isArray(list) || !list.every((word) => typeof word === "string"))) {
|
|
7614
|
+
invalid("stopWordList must be a list of words");
|
|
7615
|
+
}
|
|
7616
|
+
if (typeof name === "string" && Array.isArray(list)) {
|
|
7617
|
+
invalid("stopWords and stopWordList cannot both be present");
|
|
7618
|
+
}
|
|
7619
|
+
if (typeof name === "string") {
|
|
7620
|
+
return name;
|
|
7621
|
+
}
|
|
7622
|
+
if (Array.isArray(list)) {
|
|
7623
|
+
return new Set(list);
|
|
7624
|
+
}
|
|
7625
|
+
return void 0;
|
|
7626
|
+
}
|
|
7627
|
+
function restoredPartitionConfig(raw) {
|
|
7628
|
+
if (raw === void 0) {
|
|
7629
|
+
return void 0;
|
|
7630
|
+
}
|
|
7631
|
+
if (!isPlainObject(raw)) {
|
|
7632
|
+
invalid("partitionConfig must be an object");
|
|
7633
|
+
}
|
|
7634
|
+
const { maxDocsPerPartition, maxPartitions, watermark } = raw;
|
|
7635
|
+
if (maxDocsPerPartition !== void 0 && (typeof maxDocsPerPartition !== "number" || !Number.isInteger(maxDocsPerPartition) || maxDocsPerPartition < 1)) {
|
|
7636
|
+
invalid("partitionConfig maxDocsPerPartition must be a positive integer");
|
|
7637
|
+
}
|
|
7638
|
+
if (maxPartitions !== void 0 && (typeof maxPartitions !== "number" || !Number.isInteger(maxPartitions) || maxPartitions < 1)) {
|
|
7639
|
+
invalid("partitionConfig maxPartitions must be a positive integer");
|
|
7640
|
+
}
|
|
7641
|
+
if (watermark !== void 0 && (typeof watermark !== "number" || !(watermark > 0) || watermark > 1)) {
|
|
7642
|
+
invalid("partitionConfig watermark must be above 0 and at most 1");
|
|
7643
|
+
}
|
|
7644
|
+
return {
|
|
7645
|
+
...typeof maxDocsPerPartition === "number" ? { maxDocsPerPartition } : {},
|
|
7646
|
+
...typeof maxPartitions === "number" ? { maxPartitions } : {},
|
|
7647
|
+
...typeof watermark === "number" ? { watermark } : {}
|
|
7648
|
+
};
|
|
7649
|
+
}
|
|
7650
|
+
function restoredScoring(raw) {
|
|
7651
|
+
if (raw === void 0) {
|
|
7652
|
+
return void 0;
|
|
7653
|
+
}
|
|
7654
|
+
if (raw !== "local" && raw !== "dfs" && raw !== "broadcast") {
|
|
7655
|
+
invalid("defaultScoring must be 'local', 'dfs', or 'broadcast'");
|
|
7656
|
+
}
|
|
7657
|
+
return raw;
|
|
7658
|
+
}
|
|
7659
|
+
function restoredBoolean(raw, field) {
|
|
7660
|
+
if (raw === void 0) {
|
|
7661
|
+
return void 0;
|
|
7662
|
+
}
|
|
7663
|
+
if (typeof raw !== "boolean") {
|
|
7664
|
+
invalid(`${field} must be a boolean`);
|
|
7665
|
+
}
|
|
7666
|
+
return raw;
|
|
7667
|
+
}
|
|
7668
|
+
function restoredRequired(raw) {
|
|
7669
|
+
if (raw === void 0) {
|
|
7670
|
+
return void 0;
|
|
7671
|
+
}
|
|
7672
|
+
if (!Array.isArray(raw) || !raw.every((field) => typeof field === "string")) {
|
|
7673
|
+
invalid("required must be a list of field paths");
|
|
7674
|
+
}
|
|
7675
|
+
return raw;
|
|
7676
|
+
}
|
|
7677
|
+
function restoredVectorPromotion(raw) {
|
|
7678
|
+
if (raw === void 0) {
|
|
7679
|
+
return void 0;
|
|
7680
|
+
}
|
|
7681
|
+
if (!isPlainObject(raw) || raw.hnswConfig !== void 0 && !isPlainObject(raw.hnswConfig)) {
|
|
7682
|
+
invalid("vectorPromotion must be an object");
|
|
7683
|
+
}
|
|
7684
|
+
return raw;
|
|
7685
|
+
}
|
|
7686
|
+
function restoredEmbedding(raw) {
|
|
7687
|
+
if (raw === void 0) {
|
|
7688
|
+
return void 0;
|
|
7689
|
+
}
|
|
7690
|
+
if (!isPlainObject(raw)) {
|
|
7691
|
+
invalid("embedding must be an object");
|
|
7692
|
+
}
|
|
7693
|
+
const { fields, adapter } = raw;
|
|
7694
|
+
if (adapter !== void 0 && typeof adapter !== "string") {
|
|
7695
|
+
invalid("embedding adapter must be a name");
|
|
7696
|
+
}
|
|
7697
|
+
if (!isPlainObject(fields)) {
|
|
7698
|
+
invalid("embedding fields must be an object");
|
|
7699
|
+
}
|
|
7700
|
+
for (const value of Object.values(fields)) {
|
|
7701
|
+
const isPath = typeof value === "string";
|
|
7702
|
+
const isPathList = Array.isArray(value) && value.every((path) => typeof path === "string");
|
|
7703
|
+
if (!isPath && !isPathList) {
|
|
7704
|
+
invalid("embedding fields must map vector fields to source paths");
|
|
7705
|
+
}
|
|
7706
|
+
}
|
|
7707
|
+
return {
|
|
7708
|
+
fields,
|
|
7709
|
+
...adapter !== void 0 ? { adapter } : {}
|
|
7710
|
+
};
|
|
7711
|
+
}
|
|
7712
|
+
function restoredConfigFields(envelope) {
|
|
7713
|
+
if (envelope.tokenizer !== void 0 && typeof envelope.tokenizer !== "string") {
|
|
7714
|
+
invalid("tokenizer must be a name");
|
|
7715
|
+
}
|
|
7716
|
+
const stopWords = restoredStopWords(envelope.stopWords, envelope.stopWordList);
|
|
7717
|
+
const bm25 = restoredBm25(envelope.bm25);
|
|
7718
|
+
const surfaceForms = restoredBoolean(envelope.surfaceForms, "surfaceForms");
|
|
7719
|
+
const partitions = restoredPartitionConfig(envelope.partitionConfig);
|
|
7720
|
+
const defaultScoring = restoredScoring(envelope.defaultScoring);
|
|
7721
|
+
const trackPositions = restoredBoolean(envelope.trackPositions, "trackPositions");
|
|
7722
|
+
const strict = restoredBoolean(envelope.strict, "strict");
|
|
7723
|
+
const required = restoredRequired(envelope.required);
|
|
7724
|
+
const vectorPromotion = restoredVectorPromotion(envelope.vectorPromotion);
|
|
7725
|
+
return {
|
|
7726
|
+
...typeof envelope.tokenizer === "string" ? { tokenizer: envelope.tokenizer } : {},
|
|
7727
|
+
...stopWords !== void 0 ? { stopWords } : {},
|
|
7728
|
+
...bm25 !== void 0 ? { bm25 } : {},
|
|
7729
|
+
...surfaceForms !== void 0 ? { surfaceForms } : {},
|
|
7730
|
+
...partitions !== void 0 ? { partitions } : {},
|
|
7731
|
+
...defaultScoring !== void 0 ? { defaultScoring } : {},
|
|
7732
|
+
...trackPositions !== void 0 ? { trackPositions } : {},
|
|
7733
|
+
...strict !== void 0 ? { strict } : {},
|
|
7734
|
+
...required !== void 0 ? { required } : {},
|
|
7735
|
+
...vectorPromotion !== void 0 ? { vectorPromotion } : {}
|
|
7736
|
+
};
|
|
7737
|
+
}
|
|
7738
|
+
|
|
7739
|
+
// src/engine/snapshot/index.ts
|
|
6478
7740
|
async function createSnapshot(manager, entry) {
|
|
7741
|
+
if (entry.config.tokenizer !== void 0 && typeof entry.config.tokenizer !== "string") {
|
|
7742
|
+
throw new NarsilError(
|
|
7743
|
+
ErrorCodes.CONFIG_INVALID,
|
|
7744
|
+
"A snapshot persists no code, so the index tokenizer must be a name registered with registerTokenizer"
|
|
7745
|
+
);
|
|
7746
|
+
}
|
|
7747
|
+
if (typeof entry.config.stopWords === "function") {
|
|
7748
|
+
throw new NarsilError(
|
|
7749
|
+
ErrorCodes.CONFIG_INVALID,
|
|
7750
|
+
"A snapshot persists no code, so the index stop words must be a name registered with registerStopWords"
|
|
7751
|
+
);
|
|
7752
|
+
}
|
|
6479
7753
|
const partitionBuffers = [];
|
|
6480
7754
|
for (let i = 0; i < manager.partitionCount; i++) {
|
|
6481
7755
|
partitionBuffers.push(manager.serializePartitionToBytes(i));
|
|
@@ -6485,16 +7759,41 @@ async function createSnapshot(manager, entry) {
|
|
|
6485
7759
|
for (const [fieldPath, vecIndex] of snapshotVecIndexes) {
|
|
6486
7760
|
vectorPayloads[fieldPath] = vecIndex.serialize();
|
|
6487
7761
|
}
|
|
7762
|
+
const config = entry.config;
|
|
7763
|
+
const bm25 = config.bm25;
|
|
6488
7764
|
const { encode: encode10 } = await import('@msgpack/msgpack');
|
|
6489
7765
|
return encode10({
|
|
6490
7766
|
version: 2,
|
|
6491
|
-
schema:
|
|
7767
|
+
schema: config.schema,
|
|
6492
7768
|
language: entry.language.name,
|
|
7769
|
+
analysisRevision: entry.language.revision,
|
|
7770
|
+
...typeof config.tokenizer === "string" ? { tokenizer: config.tokenizer } : {},
|
|
7771
|
+
...typeof config.stopWords === "string" ? { stopWords: config.stopWords } : {},
|
|
7772
|
+
...config.stopWords instanceof Set ? { stopWordList: [...config.stopWords].sort() } : {},
|
|
7773
|
+
...bm25 !== void 0 ? {
|
|
7774
|
+
bm25: {
|
|
7775
|
+
...bm25.k1 !== void 0 ? { k1: bm25.k1 } : {},
|
|
7776
|
+
...bm25.b !== void 0 ? { b: bm25.b } : {}
|
|
7777
|
+
}
|
|
7778
|
+
} : {},
|
|
7779
|
+
...config.surfaceForms === true ? { surfaceForms: true } : {},
|
|
7780
|
+
...config.partitions !== void 0 ? { partitionConfig: config.partitions } : {},
|
|
7781
|
+
...config.defaultScoring !== void 0 ? { defaultScoring: config.defaultScoring } : {},
|
|
7782
|
+
...config.trackPositions !== void 0 ? { trackPositions: config.trackPositions } : {},
|
|
7783
|
+
...config.strict !== void 0 ? { strict: config.strict } : {},
|
|
7784
|
+
...config.required !== void 0 ? { required: config.required } : {},
|
|
7785
|
+
...config.vectorPromotion !== void 0 ? { vectorPromotion: config.vectorPromotion } : {},
|
|
7786
|
+
...config.embedding !== void 0 ? {
|
|
7787
|
+
embedding: {
|
|
7788
|
+
fields: config.embedding.fields,
|
|
7789
|
+
...entry.embeddingAdapterName !== null ? { adapter: entry.embeddingAdapterName } : {}
|
|
7790
|
+
}
|
|
7791
|
+
} : {},
|
|
6493
7792
|
partitions: partitionBuffers,
|
|
6494
7793
|
vectorIndexes: vectorPayloads
|
|
6495
7794
|
});
|
|
6496
7795
|
}
|
|
6497
|
-
async function restoreFromSnapshot(indexName, data,
|
|
7796
|
+
async function restoreFromSnapshot(indexName, data, deps) {
|
|
6498
7797
|
if (!(data instanceof Uint8Array)) {
|
|
6499
7798
|
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Snapshot data must be a Uint8Array");
|
|
6500
7799
|
}
|
|
@@ -6516,24 +7815,60 @@ async function restoreFromSnapshot(indexName, data, executor, indexRegistry, get
|
|
|
6516
7815
|
if (!Array.isArray(envelope.partitions)) {
|
|
6517
7816
|
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Invalid snapshot: missing partitions");
|
|
6518
7817
|
}
|
|
7818
|
+
const configFields = restoredConfigFields(envelope);
|
|
7819
|
+
const embedding = restoredEmbedding(envelope.embedding);
|
|
6519
7820
|
const language = getLanguage(envelope.language);
|
|
6520
7821
|
const schema = envelope.schema;
|
|
6521
7822
|
validateSchema(schema);
|
|
6522
|
-
|
|
6523
|
-
|
|
7823
|
+
validateVectorPromotion(configFields.vectorPromotion);
|
|
7824
|
+
if (configFields.required !== void 0 && configFields.required.length > 0) {
|
|
7825
|
+
validateRequiredFieldsInSchema(configFields.required, schema);
|
|
7826
|
+
}
|
|
7827
|
+
const indexConfig = {
|
|
7828
|
+
schema,
|
|
7829
|
+
language: envelope.language,
|
|
7830
|
+
...configFields,
|
|
7831
|
+
...embedding !== void 0 ? {
|
|
7832
|
+
embedding: {
|
|
7833
|
+
fields: embedding.fields,
|
|
7834
|
+
...embedding.adapter !== void 0 ? { adapter: embedding.adapter } : {}
|
|
7835
|
+
}
|
|
7836
|
+
} : {}
|
|
7837
|
+
};
|
|
7838
|
+
resolveIndexAnalysis(indexConfig);
|
|
7839
|
+
const adapterName = embedding?.adapter ?? null;
|
|
7840
|
+
let embeddingAdapter = null;
|
|
7841
|
+
if (embedding !== void 0) {
|
|
7842
|
+
const candidate = adapterName !== null ? deps.embeddingAdapters.get(adapterName) ?? null : deps.defaultEmbeddingAdapter;
|
|
7843
|
+
if (candidate) {
|
|
7844
|
+
validateEmbeddingConfig({ fields: embedding.fields, adapter: candidate }, schema, void 0);
|
|
7845
|
+
embeddingAdapter = candidate;
|
|
7846
|
+
}
|
|
7847
|
+
}
|
|
7848
|
+
if (deps.indexRegistry.has(indexName)) {
|
|
7849
|
+
await deps.dropIndex(indexName);
|
|
7850
|
+
}
|
|
7851
|
+
const storedRevision = typeof envelope.analysisRevision === "string" ? envelope.analysisRevision : null;
|
|
7852
|
+
deps.clearAnalysisStale(indexName);
|
|
7853
|
+
if (storedRevision !== language.revision) {
|
|
7854
|
+
deps.markAnalysisStale({
|
|
7855
|
+
indexName,
|
|
7856
|
+
language: language.name,
|
|
7857
|
+
storedRevision,
|
|
7858
|
+
currentRevision: language.revision
|
|
7859
|
+
});
|
|
6524
7860
|
}
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
indexRegistry.set(indexName, {
|
|
7861
|
+
deps.executor.createIndex(indexName, indexConfig, language);
|
|
7862
|
+
const vectorFieldPaths = deps.getVectorFieldPaths(schema);
|
|
7863
|
+
deps.indexRegistry.set(indexName, {
|
|
6529
7864
|
config: indexConfig,
|
|
6530
7865
|
language,
|
|
6531
|
-
embeddingAdapter
|
|
6532
|
-
embeddingAdapterName:
|
|
7866
|
+
embeddingAdapter,
|
|
7867
|
+
embeddingAdapterName: adapterName,
|
|
6533
7868
|
vectorFieldPaths
|
|
6534
7869
|
});
|
|
6535
7870
|
try {
|
|
6536
|
-
const manager = requireManager(indexName);
|
|
7871
|
+
const manager = deps.requireManager(indexName);
|
|
6537
7872
|
while (manager.partitionCount < envelope.partitions.length) {
|
|
6538
7873
|
manager.addPartition();
|
|
6539
7874
|
}
|
|
@@ -6553,12 +7888,27 @@ async function restoreFromSnapshot(indexName, data, executor, indexRegistry, get
|
|
|
6553
7888
|
}
|
|
6554
7889
|
} catch (err) {
|
|
6555
7890
|
try {
|
|
6556
|
-
executor.dropIndex(indexName);
|
|
6557
|
-
indexRegistry.delete(indexName);
|
|
7891
|
+
deps.executor.dropIndex(indexName);
|
|
7892
|
+
deps.indexRegistry.delete(indexName);
|
|
6558
7893
|
} catch (_) {
|
|
6559
7894
|
}
|
|
6560
7895
|
throw err;
|
|
6561
7896
|
}
|
|
7897
|
+
if (deps.durability) {
|
|
7898
|
+
const manager = deps.requireManager(indexName);
|
|
7899
|
+
for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) {
|
|
7900
|
+
for (const docId of manager.getPartition(partitionId).docIds()) {
|
|
7901
|
+
const document = manager.get(docId);
|
|
7902
|
+
if (document !== void 0) {
|
|
7903
|
+
await deps.durability.recordInsertOrUpdate(indexName, docId, document, noopApply);
|
|
7904
|
+
}
|
|
7905
|
+
}
|
|
7906
|
+
}
|
|
7907
|
+
await deps.durability.manager.persistMetadata(indexName);
|
|
7908
|
+
await deps.durability.manager.checkpoint(indexName);
|
|
7909
|
+
}
|
|
7910
|
+
}
|
|
7911
|
+
function noopApply() {
|
|
6562
7912
|
}
|
|
6563
7913
|
|
|
6564
7914
|
// src/engine/suggest.ts
|
|
@@ -6679,6 +8029,10 @@ async function createNarsil(config) {
|
|
|
6679
8029
|
if (core.durability) {
|
|
6680
8030
|
await core.durability.manager.recover();
|
|
6681
8031
|
}
|
|
8032
|
+
if (core.invalidation) {
|
|
8033
|
+
await core.invalidation.start();
|
|
8034
|
+
}
|
|
8035
|
+
await core.analysisRebuild.reviewStaleIndexes();
|
|
6682
8036
|
return createNarsilFromCore(core, config);
|
|
6683
8037
|
}
|
|
6684
8038
|
function createNarsilFromCore(core, config) {
|
|
@@ -6698,6 +8052,8 @@ function createNarsilFromCore(core, config) {
|
|
|
6698
8052
|
mutationCtx,
|
|
6699
8053
|
rebalanceCtx
|
|
6700
8054
|
} = core;
|
|
8055
|
+
const invalidation = core.invalidation;
|
|
8056
|
+
const broadcastStats = invalidation === null ? void 0 : (name) => invalidation.broadcastStats(name);
|
|
6701
8057
|
const narsil = {
|
|
6702
8058
|
createIndex(name, indexConfig) {
|
|
6703
8059
|
return createEngineIndex(core, config, name, indexConfig);
|
|
@@ -6716,7 +8072,8 @@ function createNarsilFromCore(core, config) {
|
|
|
6716
8072
|
name,
|
|
6717
8073
|
documentCount: manager?.countDocuments() ?? 0,
|
|
6718
8074
|
partitionCount: manager?.partitionCount ?? 0,
|
|
6719
|
-
language: entry.language.name
|
|
8075
|
+
language: entry.language.name,
|
|
8076
|
+
...core.analysisRebuild.isStale(name) ? { analysisStale: true } : {}
|
|
6720
8077
|
});
|
|
6721
8078
|
}
|
|
6722
8079
|
return infos;
|
|
@@ -6798,7 +8155,8 @@ function createNarsilFromCore(core, config) {
|
|
|
6798
8155
|
language: entry.language,
|
|
6799
8156
|
config: entry.config,
|
|
6800
8157
|
workerSearch,
|
|
6801
|
-
indexName
|
|
8158
|
+
indexName,
|
|
8159
|
+
broadcastStats
|
|
6802
8160
|
});
|
|
6803
8161
|
try {
|
|
6804
8162
|
await pluginRegistry.runHook("afterSearch", {
|
|
@@ -6809,6 +8167,9 @@ function createNarsilFromCore(core, config) {
|
|
|
6809
8167
|
} catch (err) {
|
|
6810
8168
|
console.warn("afterSearch plugin hook error:", err);
|
|
6811
8169
|
}
|
|
8170
|
+
if (core.analysisRebuild.isStale(indexName)) {
|
|
8171
|
+
result.analysisStale = true;
|
|
8172
|
+
}
|
|
6812
8173
|
return result;
|
|
6813
8174
|
},
|
|
6814
8175
|
async preflight(indexName, params) {
|
|
@@ -6822,17 +8183,31 @@ function createNarsilFromCore(core, config) {
|
|
|
6822
8183
|
entry.embeddingAdapterName
|
|
6823
8184
|
);
|
|
6824
8185
|
const workerSearch = orchestrator.isPromoted() ? orchestrator.searchViaWorker.bind(orchestrator) : void 0;
|
|
6825
|
-
|
|
8186
|
+
const result = await executePreflight(resolvedParams, {
|
|
6826
8187
|
manager,
|
|
6827
8188
|
language: entry.language,
|
|
6828
8189
|
config: entry.config,
|
|
6829
8190
|
workerSearch,
|
|
6830
|
-
indexName
|
|
8191
|
+
indexName,
|
|
8192
|
+
broadcastStats
|
|
6831
8193
|
});
|
|
8194
|
+
if (core.analysisRebuild.isStale(indexName)) {
|
|
8195
|
+
result.analysisStale = true;
|
|
8196
|
+
}
|
|
8197
|
+
return result;
|
|
6832
8198
|
},
|
|
6833
8199
|
async suggest(indexName, params) {
|
|
6834
8200
|
guardShutdown();
|
|
6835
|
-
|
|
8201
|
+
const result = await executeSuggest(requireManager(indexName), requireIndex(indexName).language, params);
|
|
8202
|
+
if (core.analysisRebuild.isStale(indexName)) {
|
|
8203
|
+
result.analysisStale = true;
|
|
8204
|
+
}
|
|
8205
|
+
return result;
|
|
8206
|
+
},
|
|
8207
|
+
async rebuildAnalysis(indexName) {
|
|
8208
|
+
guardShutdown();
|
|
8209
|
+
requireIndex(indexName);
|
|
8210
|
+
await core.analysisRebuild.rebuild(indexName);
|
|
6836
8211
|
},
|
|
6837
8212
|
async snapshot(indexName) {
|
|
6838
8213
|
guardShutdown();
|
|
@@ -6840,15 +8215,18 @@ function createNarsilFromCore(core, config) {
|
|
|
6840
8215
|
},
|
|
6841
8216
|
async restore(indexName, data) {
|
|
6842
8217
|
guardShutdown();
|
|
6843
|
-
return restoreFromSnapshot(
|
|
6844
|
-
indexName,
|
|
6845
|
-
data,
|
|
8218
|
+
return restoreFromSnapshot(indexName, data, {
|
|
6846
8219
|
executor,
|
|
6847
8220
|
indexRegistry,
|
|
6848
8221
|
getVectorFieldPaths,
|
|
6849
|
-
narsil.dropIndex.bind(narsil),
|
|
6850
|
-
requireManager
|
|
6851
|
-
|
|
8222
|
+
dropIndex: narsil.dropIndex.bind(narsil),
|
|
8223
|
+
requireManager,
|
|
8224
|
+
durability,
|
|
8225
|
+
embeddingAdapters: core.embeddingAdapters,
|
|
8226
|
+
defaultEmbeddingAdapter: config?.embedding ?? null,
|
|
8227
|
+
markAnalysisStale: core.analysisRebuild.markStale,
|
|
8228
|
+
clearAnalysisStale: core.analysisRebuild.clearStale
|
|
8229
|
+
});
|
|
6852
8230
|
},
|
|
6853
8231
|
async checkpoint(indexName) {
|
|
6854
8232
|
guardShutdown();
|
|
@@ -6862,6 +8240,7 @@ function createNarsilFromCore(core, config) {
|
|
|
6862
8240
|
requireIndex(indexName);
|
|
6863
8241
|
await executor.execute({ type: "clear", indexName, requestId: indexName });
|
|
6864
8242
|
await orchestrator.replicateToWorkers({ type: "clear", indexName, requestId: `replicate-clear-${indexName}` });
|
|
8243
|
+
core.watermarkNotifier.forget(indexName);
|
|
6865
8244
|
},
|
|
6866
8245
|
async rebalance(indexName, targetPartitionCount) {
|
|
6867
8246
|
guardShutdown();
|
|
@@ -6878,11 +8257,18 @@ function createNarsilFromCore(core, config) {
|
|
|
6878
8257
|
`Index "${indexName}" is currently being rebalanced`
|
|
6879
8258
|
);
|
|
6880
8259
|
}
|
|
8260
|
+
validatePartitionConfig(partitionConfig);
|
|
8261
|
+
if (partitionConfig.maxPartitions !== void 0 && partitionConfig.maxPartitions < manager.partitionCount) {
|
|
8262
|
+
throw new NarsilError(
|
|
8263
|
+
ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
|
|
8264
|
+
`maxPartitions (${partitionConfig.maxPartitions}) is less than the current partition count (${manager.partitionCount})`,
|
|
8265
|
+
{ maxPartitions: partitionConfig.maxPartitions, partitionCount: manager.partitionCount }
|
|
8266
|
+
);
|
|
8267
|
+
}
|
|
6881
8268
|
const currentDocCount = manager.countDocuments();
|
|
6882
8269
|
const newMaxDocs = partitionConfig.maxDocsPerPartition ?? entry.config.partitions?.maxDocsPerPartition;
|
|
6883
|
-
const newMaxPartitions = partitionConfig.maxPartitions ?? entry.config.partitions?.maxPartitions ?? manager.partitionCount;
|
|
6884
8270
|
if (newMaxDocs !== void 0) {
|
|
6885
|
-
const newTotalCapacity = newMaxDocs *
|
|
8271
|
+
const newTotalCapacity = newMaxDocs * manager.partitionCount;
|
|
6886
8272
|
if (newTotalCapacity < currentDocCount) {
|
|
6887
8273
|
throw new NarsilError(
|
|
6888
8274
|
ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
|
|
@@ -6896,6 +8282,9 @@ function createNarsilFromCore(core, config) {
|
|
|
6896
8282
|
entry.config.partitions.maxDocsPerPartition = partitionConfig.maxDocsPerPartition;
|
|
6897
8283
|
if (partitionConfig.maxPartitions !== void 0)
|
|
6898
8284
|
entry.config.partitions.maxPartitions = partitionConfig.maxPartitions;
|
|
8285
|
+
if (partitionConfig.watermark !== void 0) entry.config.partitions.watermark = partitionConfig.watermark;
|
|
8286
|
+
if (durability) await durability.manager.persistMetadata(indexName);
|
|
8287
|
+
core.watermarkNotifier.check(indexName);
|
|
6899
8288
|
},
|
|
6900
8289
|
async getMemoryStats() {
|
|
6901
8290
|
let estimatedIndexBytes = 0;
|