@delali/narsil 0.2.4-next.48 → 0.2.4-next.50
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/README.md +2 -2
- package/dist/adapters/indexeddb.mjs +15 -10
- package/dist/{options-BC68VPoE.d.ts → capabilities-JrVQOwi4.d.ts} +55 -47
- package/dist/{chunk-BFIH7OV5.mjs → chunk-2GWVQWGA.mjs} +3 -1
- package/dist/chunk-AV4IJKD2.mjs +35 -0
- package/dist/{chunk-EV45L35L.mjs → chunk-BEIK3EOZ.mjs} +845 -2407
- package/dist/chunk-CBLS46IE.mjs +10 -0
- package/dist/chunk-CMVG2UEF.mjs +2038 -0
- package/dist/chunk-CV5H7XL7.mjs +876 -0
- package/dist/{chunk-JQUL2T6I.mjs → chunk-GCM3JFVA.mjs} +1 -2
- package/dist/{chunk-XWICBGYT.mjs → chunk-MHE7YWAU.mjs} +3 -5
- package/dist/{chunk-UZTT2BKQ.mjs → chunk-ND34I3SV.mjs} +1202 -1881
- package/dist/{chunk-PM52SMBT.mjs → chunk-OOSIDHA3.mjs} +2 -1
- package/dist/chunk-ORBLD3EL.mjs +6 -0
- package/dist/chunk-PIWKZPP7.mjs +64 -0
- package/dist/{chunk-4COAUBLJ.mjs → chunk-PMBHH44R.mjs} +124 -8
- package/dist/chunk-RP23PWMV.mjs +11 -0
- package/dist/chunk-UNEEFTEU.mjs +7 -0
- package/dist/{chunk-WTONLDU3.mjs → chunk-WYJXSLER.mjs} +9 -19
- package/dist/{chunk-DFZDV5FV.mjs → chunk-X6YZO4MQ.mjs} +19 -15
- package/dist/chunk-XE6XR2OE.mjs +23 -0
- package/dist/chunk-YEO6G2NG.mjs +6 -0
- package/dist/chunk-ZHUQKZBE.mjs +1292 -0
- package/dist/{client-C5BkUqB8.d.ts → client-CBOVHdjp.d.ts} +31 -7
- package/dist/client.d.ts +7 -7
- package/dist/client.mjs +250 -232
- package/dist/{config-B-ZI-ft5.d.ts → config-Czh2YNDi.d.ts} +2 -2
- package/dist/{config-BZRExglL.d.ts → config-OON7IJ_B.d.ts} +50 -13
- package/dist/direct-executor-OREXU72C.mjs +12 -0
- package/dist/distribution/coordinator/etcd.d.ts +2 -2
- package/dist/distribution/coordinator/etcd.mjs +2 -3
- package/dist/distribution/coordinator/in-memory.d.ts +2 -2
- package/dist/distribution/coordinator/in-memory.mjs +2 -2
- package/dist/distribution/transport/grpc.mjs +7 -9
- package/dist/distribution/transport/in-memory.mjs +2 -1
- package/dist/distribution/transport/tcp.mjs +3 -2
- package/dist/distribution.d.ts +8 -8
- package/dist/distribution.mjs +142 -101
- package/dist/embeddings/openai.mjs +5 -3
- package/dist/{engine-Cjr1uBLa.d.ts → engine-9PtFY50q.d.ts} +125 -69
- package/dist/index.browser.mjs +4689 -3330
- package/dist/index.d.ts +7 -7
- package/dist/index.mjs +14 -8
- package/dist/invalidation/broadcast-channel.mjs +2 -1
- package/dist/invalidation/filesystem.mjs +3 -4
- package/dist/{search-DDHjfXhJ.d.ts → memory-Bzd5xcEW.d.ts} +89 -63
- package/dist/persistence/durability/checkpoint-worker.d.ts +1 -1
- package/dist/persistence/durability/checkpoint-worker.mjs +14 -8
- package/dist/react.d.ts +4 -4
- package/dist/react.mjs +7 -5
- package/dist/{schema-B7juWg8Q.d.ts → schema-BqRx9t4E.d.ts} +22 -3
- package/dist/server.d.ts +47 -42
- package/dist/server.mjs +338 -821
- package/dist/thread-PZLEO6JL.mjs +310 -0
- package/dist/{types-DsSSjY4d.d.ts → types-DXPxaXPO.d.ts} +1 -1
- package/dist/vector/hnsw-build-worker.mjs +3 -4
- package/dist/vector/search-worker.mjs +4 -123
- package/dist/workers/entry.mjs +47 -3
- package/package.json +5 -1
- package/dist/direct-executor-EOTFGXTO.mjs +0 -8
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { validateWorkerConfig, validateIndexName, validateBM25Params, validatePartitionConfig, executeQuery, queryBindingOf, executePreflight, executeSuggest, executeListDocuments, validateDocId } from './chunk-ZHUQKZBE.mjs';
|
|
2
|
+
import { ReplicationMessageTypes } from './chunk-GCM3JFVA.mjs';
|
|
3
|
+
import { WIRE_BATCH_BUDGET, resolveVectorText, bindEngineCore } from './chunk-PIWKZPP7.mjs';
|
|
4
|
+
import { STATISTICS_BROADCAST_INTERVAL_MS, MAX_SWEEP_INTERVAL_MS, MIN_SWEEP_INTERVAL_MS, HEAP_PRESSURE_WARNING_FRACTION, FOREIGN_STATS_TTL_MS, HEAP_PRESSURE_REARM_FRACTION, MIN_DOCUMENTS_FOR_SEGMENTS, BATCH_CHUNK_SIZE } from './chunk-XE6XR2OE.mjs';
|
|
5
|
+
import { getNestedValue, generateId, getLanguage, splitWorkerBudget, resolveWorkerCount, createDirectExecutor, createPartitionRouter, createPartitionManager, createVectorIndex, readHeapStatistics, resolveIndexAnalysis, deserializePayloadV2, deserializePayloadV1, resolveRequestThreadCount, createPartitionIndex, createWorkerPool, isNodeMainThread, spawnNodeWorker, CHECKSUM_TIMEOUT_MS, CHECKSUM_YIELD_CHUNK_BYTES, createSharedFrozenSegment, buildCompactedSegmentPayload, freezeSegmentShared, createFrozenSegment, isCompositePartition, WORKER_RESOURCE_LIMITS, deserializeMetadata, DEFAULT_BACKPRESSURE_LIMIT, DEFAULT_REQUEST_TIMEOUT_MS, serializeMetadata, SHUTDOWN_TIMEOUT_MS, CONSECUTIVE_TIMEOUTS_BEFORE_DEATH, encodeSegmentState } from './chunk-BEIK3EOZ.mjs';
|
|
6
|
+
import { extractVectorFieldsFromSchema, validateSchema, validateVectorPromotion, collectQueryTermStats, flattenSchema, isTextFieldType, mergePartitionStats, fnv1a, kWayMerge, mergeFacets, validateRequiredFields, validateDocument, validateDocumentStrict } from './chunk-CMVG2UEF.mjs';
|
|
7
|
+
import { detectRuntime } from './chunk-AV4IJKD2.mjs';
|
|
4
8
|
import { crc32, crc32Init, crc32Update, crc32Final } from './chunk-KMMJMDUA.mjs';
|
|
5
|
-
import { compareCodePoints
|
|
6
|
-
import {
|
|
9
|
+
import { compareCodePoints } from './chunk-2GWVQWGA.mjs';
|
|
10
|
+
import { createRequestId, isValidWorkerResponse } from './chunk-WYJXSLER.mjs';
|
|
7
11
|
import { ErrorCodes, NarsilError } from './chunk-P4ELXVJQ.mjs';
|
|
8
12
|
import { encode, decode } from '@msgpack/msgpack';
|
|
9
13
|
|
|
14
|
+
// src/partitioning/constants.ts
|
|
15
|
+
var REBALANCE_CHUNK_SIZE = 1e3;
|
|
16
|
+
var DEFAULT_WRITE_AHEAD_QUEUE_MAX_SIZE = 1e4;
|
|
17
|
+
|
|
10
18
|
// src/partitioning/rebalancer.ts
|
|
11
|
-
var CHUNK_SIZE = 1e3;
|
|
12
19
|
function createRebalancer() {
|
|
13
20
|
const activeManagers = /* @__PURE__ */ new WeakSet();
|
|
14
21
|
let activeCount = 0;
|
|
@@ -71,7 +78,7 @@ function createRebalancer() {
|
|
|
71
78
|
documentsProcessed++;
|
|
72
79
|
}
|
|
73
80
|
chunkFill++;
|
|
74
|
-
if (chunkFill >=
|
|
81
|
+
if (chunkFill >= REBALANCE_CHUNK_SIZE) {
|
|
75
82
|
chunkFill = 0;
|
|
76
83
|
onProgress?.({
|
|
77
84
|
phase: "moving",
|
|
@@ -269,42 +276,18 @@ function validateRequiredFieldsInSchema(required, schema) {
|
|
|
269
276
|
}
|
|
270
277
|
}
|
|
271
278
|
|
|
272
|
-
// src/
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
return { shouldPromote: false, reason: "" };
|
|
282
|
-
}
|
|
283
|
-
let totalDocuments = 0;
|
|
284
|
-
for (const [indexName, stats] of indexes) {
|
|
285
|
-
if (stats.documentCount >= perIndexThreshold) {
|
|
286
|
-
return {
|
|
287
|
-
shouldPromote: true,
|
|
288
|
-
reason: `Index "${indexName}" has ${stats.documentCount} documents, exceeding the per-index threshold of ${perIndexThreshold}`
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
totalDocuments += stats.documentCount;
|
|
292
|
-
}
|
|
293
|
-
if (totalDocuments >= totalThreshold) {
|
|
294
|
-
return {
|
|
295
|
-
shouldPromote: true,
|
|
296
|
-
reason: `Total document count of ${totalDocuments} exceeds the total threshold of ${totalThreshold}`
|
|
297
|
-
};
|
|
279
|
+
// src/engine/events.ts
|
|
280
|
+
function emitEngineEvent(eventHandlers, event, payload) {
|
|
281
|
+
const handlers = eventHandlers.get(event);
|
|
282
|
+
if (!handlers || handlers.size === 0) return 0;
|
|
283
|
+
for (const handler of handlers) {
|
|
284
|
+
try {
|
|
285
|
+
handler(payload);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
console.warn(`${event} handler error:`, err instanceof Error ? err.message : String(err));
|
|
298
288
|
}
|
|
299
|
-
return { shouldPromote: false, reason: "" };
|
|
300
289
|
}
|
|
301
|
-
|
|
302
|
-
promoted = true;
|
|
303
|
-
}
|
|
304
|
-
function isPromoted() {
|
|
305
|
-
return promoted;
|
|
306
|
-
}
|
|
307
|
-
return { check, markPromoted, isPromoted };
|
|
290
|
+
return handlers.size;
|
|
308
291
|
}
|
|
309
292
|
|
|
310
293
|
// src/engine/analysis-rebuild.ts
|
|
@@ -328,15 +311,7 @@ function wireAnalysisRebuild(wiring) {
|
|
|
328
311
|
await (durability.checkpointFromMemory?.(indexName) ?? durability.checkpoint(indexName));
|
|
329
312
|
},
|
|
330
313
|
emit(payload) {
|
|
331
|
-
|
|
332
|
-
if (!handlers) return;
|
|
333
|
-
for (const handler of handlers) {
|
|
334
|
-
try {
|
|
335
|
-
handler(payload);
|
|
336
|
-
} catch (err) {
|
|
337
|
-
console.warn("analysisRebuild handler error:", err instanceof Error ? err.message : String(err));
|
|
338
|
-
}
|
|
339
|
-
}
|
|
314
|
+
emitEngineEvent(wiring.eventHandlers, "analysisRebuild", payload);
|
|
340
315
|
}
|
|
341
316
|
});
|
|
342
317
|
}
|
|
@@ -383,7 +358,7 @@ function createAnalysisRebuildCoordinator(config, deps) {
|
|
|
383
358
|
throw error;
|
|
384
359
|
}
|
|
385
360
|
}
|
|
386
|
-
function
|
|
361
|
+
function enqueue(indexName, entry) {
|
|
387
362
|
if (entry.run !== null) {
|
|
388
363
|
return entry.run;
|
|
389
364
|
}
|
|
@@ -397,7 +372,7 @@ function createAnalysisRebuildCoordinator(config, deps) {
|
|
|
397
372
|
}
|
|
398
373
|
async function announce(indexName, entry) {
|
|
399
374
|
const documentCount = deps.getManager(indexName)?.countDocuments() ?? 0;
|
|
400
|
-
await config?.onStaleAnalysis?.({ ...entry.index, documentCount }, () =>
|
|
375
|
+
await config?.onStaleAnalysis?.({ ...entry.index, documentCount }, () => enqueue(indexName, entry));
|
|
401
376
|
}
|
|
402
377
|
async function review(indexName, entry) {
|
|
403
378
|
if (entry.run !== null || deps.getManager(indexName) === void 0) return;
|
|
@@ -413,7 +388,7 @@ function createAnalysisRebuildCoordinator(config, deps) {
|
|
|
413
388
|
});
|
|
414
389
|
}
|
|
415
390
|
if (rebuildMode === "auto") {
|
|
416
|
-
void
|
|
391
|
+
void enqueue(indexName, entry).catch(() => void 0);
|
|
417
392
|
}
|
|
418
393
|
}
|
|
419
394
|
return {
|
|
@@ -447,7 +422,7 @@ function createAnalysisRebuildCoordinator(config, deps) {
|
|
|
447
422
|
if (entry === void 0) {
|
|
448
423
|
return Promise.resolve();
|
|
449
424
|
}
|
|
450
|
-
return
|
|
425
|
+
return enqueue(indexName, entry);
|
|
451
426
|
}
|
|
452
427
|
};
|
|
453
428
|
}
|
|
@@ -532,6 +507,15 @@ function resolveDurabilityTier(config) {
|
|
|
532
507
|
return { kind: "snapshot", config: {}, adapter: config.persistence };
|
|
533
508
|
}
|
|
534
509
|
|
|
510
|
+
// src/persistence/durability/constants.ts
|
|
511
|
+
var DEFAULT_CHECKPOINT_INTERVAL_MS = 3e5;
|
|
512
|
+
var DEFAULT_CHECKPOINT_MUTATION_THRESHOLD = 1e5;
|
|
513
|
+
var DEFAULT_ASYNC_FLUSH_INTERVAL_MS = 1e3;
|
|
514
|
+
var DEFAULT_SEGMENT_MAX_BYTES = 67108864;
|
|
515
|
+
var DEFAULT_COMPACTION_THRESHOLD = 12;
|
|
516
|
+
var CHECKPOINT_WORKER_TIMEOUT_MS = 6e5;
|
|
517
|
+
var CHECKPOINT_TIMEOUT_RECOVERY_BACKOFF_MS = 50;
|
|
518
|
+
|
|
535
519
|
// src/persistence/durability/node-fs.ts
|
|
536
520
|
var cachedFs = null;
|
|
537
521
|
var cachedPath = null;
|
|
@@ -809,8 +793,6 @@ function buildEntry(fields) {
|
|
|
809
793
|
}
|
|
810
794
|
|
|
811
795
|
// src/serialization/checksum-dispatch.ts
|
|
812
|
-
var CHECKSUM_TIMEOUT_MS = 12e4;
|
|
813
|
-
var YIELD_CHUNK_BYTES = 4 * 1024 * 1024;
|
|
814
796
|
var workerUsable = true;
|
|
815
797
|
var mainThreadResolved = false;
|
|
816
798
|
var onMainThread = true;
|
|
@@ -844,8 +826,8 @@ function yieldToEventLoop2() {
|
|
|
844
826
|
}
|
|
845
827
|
async function chunkedChecksum(payload) {
|
|
846
828
|
let state = crc32Init();
|
|
847
|
-
for (let offset = 0; offset < payload.length; offset +=
|
|
848
|
-
const end = Math.min(offset +
|
|
829
|
+
for (let offset = 0; offset < payload.length; offset += CHECKSUM_YIELD_CHUNK_BYTES) {
|
|
830
|
+
const end = Math.min(offset + CHECKSUM_YIELD_CHUNK_BYTES, payload.length);
|
|
849
831
|
state = crc32Update(state, payload.subarray(offset, end));
|
|
850
832
|
if (end < payload.length) {
|
|
851
833
|
await yieldToEventLoop2();
|
|
@@ -855,7 +837,7 @@ async function chunkedChecksum(payload) {
|
|
|
855
837
|
}
|
|
856
838
|
function runWorkerChecksum(worker, payload) {
|
|
857
839
|
return new Promise((resolve, reject) => {
|
|
858
|
-
let
|
|
840
|
+
let settled2 = false;
|
|
859
841
|
const timeoutId = setTimeout(
|
|
860
842
|
() => settle(() => reject(new Error(`Checksum worker timed out after ${CHECKSUM_TIMEOUT_MS}ms`))),
|
|
861
843
|
CHECKSUM_TIMEOUT_MS
|
|
@@ -864,8 +846,8 @@ function runWorkerChecksum(worker, payload) {
|
|
|
864
846
|
timeoutId.unref();
|
|
865
847
|
}
|
|
866
848
|
function settle(action) {
|
|
867
|
-
if (
|
|
868
|
-
|
|
849
|
+
if (settled2) return;
|
|
850
|
+
settled2 = true;
|
|
869
851
|
clearTimeout(timeoutId);
|
|
870
852
|
try {
|
|
871
853
|
void worker.terminate();
|
|
@@ -1437,8 +1419,6 @@ function formatSegmentStart(startSeqNo) {
|
|
|
1437
1419
|
}
|
|
1438
1420
|
|
|
1439
1421
|
// src/persistence/durability/checkpoint-worker-dispatch.ts
|
|
1440
|
-
var CHECKPOINT_WORKER_TIMEOUT_MS = 6e5;
|
|
1441
|
-
var TIMEOUT_RECOVERY_BACKOFF_MS = 50;
|
|
1442
1422
|
var workerUsable2 = true;
|
|
1443
1423
|
var pooledWorker = null;
|
|
1444
1424
|
var workerBusy = false;
|
|
@@ -1477,7 +1457,7 @@ function discardWorker(worker) {
|
|
|
1477
1457
|
}
|
|
1478
1458
|
function runWorker(worker, request) {
|
|
1479
1459
|
return new Promise((resolve) => {
|
|
1480
|
-
let
|
|
1460
|
+
let settled2 = false;
|
|
1481
1461
|
const onMessage = (msg) => {
|
|
1482
1462
|
const response = msg;
|
|
1483
1463
|
if (response.type === "success") {
|
|
@@ -1497,10 +1477,10 @@ function runWorker(worker, request) {
|
|
|
1497
1477
|
timeoutId.unref();
|
|
1498
1478
|
}
|
|
1499
1479
|
function settle(outcome, discard) {
|
|
1500
|
-
if (
|
|
1480
|
+
if (settled2) {
|
|
1501
1481
|
return;
|
|
1502
1482
|
}
|
|
1503
|
-
|
|
1483
|
+
settled2 = true;
|
|
1504
1484
|
clearTimeout(timeoutId);
|
|
1505
1485
|
worker.off("message", onMessage);
|
|
1506
1486
|
worker.off("error", onError);
|
|
@@ -1551,7 +1531,7 @@ async function runCheckpointOnWorker(request) {
|
|
|
1551
1531
|
return true;
|
|
1552
1532
|
}
|
|
1553
1533
|
if (outcome.timedOut) {
|
|
1554
|
-
await delay(
|
|
1534
|
+
await delay(CHECKPOINT_TIMEOUT_RECOVERY_BACKOFF_MS);
|
|
1555
1535
|
}
|
|
1556
1536
|
return false;
|
|
1557
1537
|
} finally {
|
|
@@ -1571,7 +1551,6 @@ function terminateCheckpointWorker() {
|
|
|
1571
1551
|
}
|
|
1572
1552
|
|
|
1573
1553
|
// src/persistence/durability/segment/layout.ts
|
|
1574
|
-
var DEFAULT_COMPACTION_THRESHOLD = 12;
|
|
1575
1554
|
var SEGMENT_ID_WIDTH = 16;
|
|
1576
1555
|
function manifestKey(indexName) {
|
|
1577
1556
|
return `${indexName}/manifest`;
|
|
@@ -2513,37 +2492,6 @@ function setNestedValue(obj, path, value) {
|
|
|
2513
2492
|
}
|
|
2514
2493
|
current[finalSegment] = value;
|
|
2515
2494
|
}
|
|
2516
|
-
|
|
2517
|
-
// src/distribution/chunking.ts
|
|
2518
|
-
var WIRE_BATCH_BUDGET = {
|
|
2519
|
-
maxCount: 1e3,
|
|
2520
|
-
maxBytes: 8388608,
|
|
2521
|
-
overheadBytes: 256
|
|
2522
|
-
};
|
|
2523
|
-
function chunkByBudget(items, budget) {
|
|
2524
|
-
const chunks = [];
|
|
2525
|
-
let current = [];
|
|
2526
|
-
let currentBytes = 0;
|
|
2527
|
-
for (const item of items) {
|
|
2528
|
-
const itemBytes = budget.payloadBytesOf(item) + budget.overheadBytes;
|
|
2529
|
-
const previous = current[current.length - 1];
|
|
2530
|
-
const breaksRun = previous !== void 0 && budget.breaksRun?.(item, previous) === true;
|
|
2531
|
-
const overflows = current.length >= budget.maxCount || current.length > 0 && currentBytes + itemBytes > budget.maxBytes;
|
|
2532
|
-
if (breaksRun || overflows) {
|
|
2533
|
-
chunks.push(current);
|
|
2534
|
-
current = [];
|
|
2535
|
-
currentBytes = 0;
|
|
2536
|
-
}
|
|
2537
|
-
current.push(item);
|
|
2538
|
-
currentBytes += itemBytes;
|
|
2539
|
-
}
|
|
2540
|
-
if (current.length > 0) {
|
|
2541
|
-
chunks.push(current);
|
|
2542
|
-
}
|
|
2543
|
-
return chunks;
|
|
2544
|
-
}
|
|
2545
|
-
|
|
2546
|
-
// src/distribution/replication/codec.ts
|
|
2547
2495
|
function createEntryMessage(entry, sourceId) {
|
|
2548
2496
|
const payload = { entry };
|
|
2549
2497
|
return {
|
|
@@ -3266,7 +3214,10 @@ async function writePartitionVectors(input) {
|
|
|
3266
3214
|
}
|
|
3267
3215
|
const vectorIndexes = /* @__PURE__ */ new Map();
|
|
3268
3216
|
for (const [fieldPath, dimension] of input.vectorFields) {
|
|
3269
|
-
vectorIndexes.set(
|
|
3217
|
+
vectorIndexes.set(
|
|
3218
|
+
fieldPath,
|
|
3219
|
+
createVectorIndex(fieldPath, dimension, input.config.vectorPromotion, { enabled: false })
|
|
3220
|
+
);
|
|
3270
3221
|
}
|
|
3271
3222
|
for (const ref of input.priorVectors) {
|
|
3272
3223
|
const vecIndex = vectorIndexes.get(ref.fieldPath);
|
|
@@ -3684,11 +3635,6 @@ async function recoverPersistedIndex(deps, indexName, metadataOnly) {
|
|
|
3684
3635
|
}
|
|
3685
3636
|
}
|
|
3686
3637
|
|
|
3687
|
-
// src/persistence/durability/types.ts
|
|
3688
|
-
var DEFAULT_CHECKPOINT_INTERVAL_MS = 3e5;
|
|
3689
|
-
var DEFAULT_CHECKPOINT_MUTATION_THRESHOLD = 1e5;
|
|
3690
|
-
var DEFAULT_ASYNC_FLUSH_INTERVAL_MS = 1e3;
|
|
3691
|
-
|
|
3692
3638
|
// src/persistence/durability/group-commit.ts
|
|
3693
3639
|
function createGroupCommitCoordinator(sync) {
|
|
3694
3640
|
let inFlight = null;
|
|
@@ -3762,7 +3708,6 @@ async function createMarkerWriter(directory, indexName, partitionId) {
|
|
|
3762
3708
|
}
|
|
3763
3709
|
|
|
3764
3710
|
// src/persistence/durability/wal-writer.ts
|
|
3765
|
-
var DEFAULT_SEGMENT_MAX_BYTES = 67108864;
|
|
3766
3711
|
var SEGMENT_TAIL_PATTERN = /^\d{16}$/;
|
|
3767
3712
|
function segmentKey2(indexName, partitionId, startSeqNo) {
|
|
3768
3713
|
const padded = startSeqNo.toString().padStart(16, "0");
|
|
@@ -4680,11 +4625,11 @@ function createDurabilityFromTier(tier, wiring) {
|
|
|
4680
4625
|
});
|
|
4681
4626
|
}
|
|
4682
4627
|
|
|
4683
|
-
// src/engine/index-state/
|
|
4628
|
+
// src/engine/index-state/constants.ts
|
|
4684
4629
|
var DEFAULT_MAX_REOPEN_WAITERS = 64;
|
|
4685
4630
|
var MAX_REOPEN_FAILURES = 5;
|
|
4686
|
-
|
|
4687
|
-
|
|
4631
|
+
|
|
4632
|
+
// src/engine/index-state/entry.ts
|
|
4688
4633
|
function createLifecycleEntry(state) {
|
|
4689
4634
|
return {
|
|
4690
4635
|
state,
|
|
@@ -4958,7 +4903,10 @@ function createIndexStateCoordinator(config, callbacks) {
|
|
|
4958
4903
|
await reopen(indexName, false);
|
|
4959
4904
|
entry = entryOf(indexName);
|
|
4960
4905
|
}
|
|
4961
|
-
if (markActive)
|
|
4906
|
+
if (markActive) {
|
|
4907
|
+
entry.lastAccessAt = Date.now();
|
|
4908
|
+
callbacks.onAccess?.(indexName);
|
|
4909
|
+
}
|
|
4962
4910
|
entry.activeOperations += 1;
|
|
4963
4911
|
try {
|
|
4964
4912
|
if (opened) await enforceLimits(indexName);
|
|
@@ -5061,6 +5009,9 @@ function wireIndexState(wiring) {
|
|
|
5061
5009
|
}
|
|
5062
5010
|
}
|
|
5063
5011
|
},
|
|
5012
|
+
onAccess(indexName) {
|
|
5013
|
+
wiring.onAccess?.(indexName);
|
|
5014
|
+
},
|
|
5064
5015
|
canCloseAutomatically(indexName) {
|
|
5065
5016
|
return !rebalancingIndexes.has(indexName) && !analysisRebuild.isRunning(indexName) && !orchestrator.isIndexBusy(indexName);
|
|
5066
5017
|
},
|
|
@@ -5071,8 +5022,6 @@ function wireIndexState(wiring) {
|
|
|
5071
5022
|
}
|
|
5072
5023
|
|
|
5073
5024
|
// src/engine/invalidation.ts
|
|
5074
|
-
var STATISTICS_BROADCAST_INTERVAL_MS = 5e3;
|
|
5075
|
-
var FOREIGN_STATS_TTL_MS = 6e4;
|
|
5076
5025
|
function createInvalidationFromConfig(config, tierKind, deps) {
|
|
5077
5026
|
if (config?.invalidation === void 0) {
|
|
5078
5027
|
return null;
|
|
@@ -5226,41 +5175,222 @@ function createInvalidationIntegration(deps) {
|
|
|
5226
5175
|
};
|
|
5227
5176
|
}
|
|
5228
5177
|
|
|
5178
|
+
// src/engine/heap-pressure.ts
|
|
5179
|
+
function createHeapPressureNotifier(deps) {
|
|
5180
|
+
let warned = false;
|
|
5181
|
+
function check(indexName) {
|
|
5182
|
+
const heap = deps.readHeap();
|
|
5183
|
+
if (heap === null) return;
|
|
5184
|
+
const fraction = heap.usedBytes / heap.limitBytes;
|
|
5185
|
+
if (warned) {
|
|
5186
|
+
if (fraction < HEAP_PRESSURE_REARM_FRACTION) warned = false;
|
|
5187
|
+
return;
|
|
5188
|
+
}
|
|
5189
|
+
if (fraction < HEAP_PRESSURE_WARNING_FRACTION) return;
|
|
5190
|
+
warned = true;
|
|
5191
|
+
deps.emit({
|
|
5192
|
+
indexName,
|
|
5193
|
+
heapUsed: heap.usedBytes,
|
|
5194
|
+
heapLimit: heap.limitBytes,
|
|
5195
|
+
estimatedMemoryBytes: deps.estimateIndexBytes(indexName)
|
|
5196
|
+
});
|
|
5197
|
+
}
|
|
5198
|
+
return { check };
|
|
5199
|
+
}
|
|
5200
|
+
|
|
5201
|
+
// src/engine/watermark.ts
|
|
5202
|
+
function createWatermarkNotifier(deps) {
|
|
5203
|
+
const latchedCapacity = /* @__PURE__ */ new Map();
|
|
5204
|
+
function check(indexName) {
|
|
5205
|
+
const partitionConfig = deps.getPartitionConfig(indexName);
|
|
5206
|
+
const maxDocs = partitionConfig?.maxDocsPerPartition;
|
|
5207
|
+
const watermark = partitionConfig?.watermark;
|
|
5208
|
+
if (maxDocs === void 0 || watermark === void 0) {
|
|
5209
|
+
latchedCapacity.delete(indexName);
|
|
5210
|
+
return;
|
|
5211
|
+
}
|
|
5212
|
+
const manager = deps.getManager(indexName);
|
|
5213
|
+
if (!manager) {
|
|
5214
|
+
latchedCapacity.delete(indexName);
|
|
5215
|
+
return;
|
|
5216
|
+
}
|
|
5217
|
+
const partitionCount = manager.partitionCount;
|
|
5218
|
+
const capacity = maxDocs * partitionCount;
|
|
5219
|
+
const documentCount = manager.countDocuments();
|
|
5220
|
+
const threshold = watermark * capacity;
|
|
5221
|
+
if (documentCount < threshold) {
|
|
5222
|
+
latchedCapacity.delete(indexName);
|
|
5223
|
+
return;
|
|
5224
|
+
}
|
|
5225
|
+
if (latchedCapacity.get(indexName) === capacity) return;
|
|
5226
|
+
latchedCapacity.set(indexName, capacity);
|
|
5227
|
+
deps.emit({ indexName, documentCount, capacity, partitionCount });
|
|
5228
|
+
}
|
|
5229
|
+
function forget(indexName) {
|
|
5230
|
+
latchedCapacity.delete(indexName);
|
|
5231
|
+
}
|
|
5232
|
+
return { check, forget };
|
|
5233
|
+
}
|
|
5234
|
+
|
|
5235
|
+
// src/engine/notifiers.ts
|
|
5236
|
+
function wireWatermarkNotifier(wiring) {
|
|
5237
|
+
return createWatermarkNotifier({
|
|
5238
|
+
getManager: wiring.getManager,
|
|
5239
|
+
getPartitionConfig: (indexName) => wiring.indexRegistry.get(indexName)?.config.partitions,
|
|
5240
|
+
emit: (payload) => emitEngineEvent(wiring.eventHandlers, "partitionWatermark", payload)
|
|
5241
|
+
});
|
|
5242
|
+
}
|
|
5243
|
+
function warnUnheardHeapPressure(payload) {
|
|
5244
|
+
console.warn(
|
|
5245
|
+
`Heap at ${payload.heapUsed} of ${payload.heapLimit} bytes after index "${payload.indexName}"; raise --max-old-space-size-percentage or close an idle index`
|
|
5246
|
+
);
|
|
5247
|
+
}
|
|
5248
|
+
function wireHeapPressureNotifier(wiring) {
|
|
5249
|
+
return createHeapPressureNotifier({
|
|
5250
|
+
readHeap: readHeapStatistics,
|
|
5251
|
+
estimateIndexBytes: (indexName) => wiring.getManager(indexName)?.estimateMemoryBytes() ?? 0,
|
|
5252
|
+
emit: (payload) => {
|
|
5253
|
+
if (emitEngineEvent(wiring.eventHandlers, "heapPressure", payload) === 0) warnUnheardHeapPressure(payload);
|
|
5254
|
+
}
|
|
5255
|
+
});
|
|
5256
|
+
}
|
|
5257
|
+
function checkHeapAfterRecovery(core) {
|
|
5258
|
+
const sized = [...core.indexRegistry.keys()].map((indexName) => ({
|
|
5259
|
+
indexName,
|
|
5260
|
+
bytes: core.executor.getManager(indexName)?.estimateMemoryBytes() ?? 0
|
|
5261
|
+
}));
|
|
5262
|
+
sized.sort((a, b) => b.bytes - a.bytes);
|
|
5263
|
+
for (const { indexName } of sized) core.heapPressureNotifier.check(indexName);
|
|
5264
|
+
}
|
|
5265
|
+
|
|
5266
|
+
// src/core/partition/frozen/share.ts
|
|
5267
|
+
function shareFrozenSegment(segment) {
|
|
5268
|
+
if (segment.sharedSnapshot !== null) {
|
|
5269
|
+
return { snapshot: segment.sharedSnapshot, tombstonedDocIds: segment.tombstonedDocIds() };
|
|
5270
|
+
}
|
|
5271
|
+
const payload = encodeSegmentState(segment);
|
|
5272
|
+
const documents = payload.docIds.map((docId) => {
|
|
5273
|
+
const stored = segment.docStore.get(docId);
|
|
5274
|
+
if (stored === void 0) {
|
|
5275
|
+
throw new NarsilError(ErrorCodes.PARTITION_CORRUPTED, `Document "${docId}" vanished while sharing a segment`, {
|
|
5276
|
+
docId
|
|
5277
|
+
});
|
|
5278
|
+
}
|
|
5279
|
+
return stored.fields;
|
|
5280
|
+
});
|
|
5281
|
+
const snapshot = freezeSegmentShared(payload, documents, segment.segmentId);
|
|
5282
|
+
return snapshot === null ? null : { snapshot, tombstonedDocIds: [] };
|
|
5283
|
+
}
|
|
5284
|
+
|
|
5229
5285
|
// src/engine/worker-resync.ts
|
|
5230
|
-
|
|
5231
|
-
const
|
|
5286
|
+
function captureComposite(manager, partitionId) {
|
|
5287
|
+
const partition = manager.getPartition(partitionId);
|
|
5288
|
+
if (!isCompositePartition(partition)) return null;
|
|
5289
|
+
const segmentIds = partition.frozenSegmentSizes().map((size) => size.segmentId);
|
|
5290
|
+
const frozen = [];
|
|
5291
|
+
for (const segment of partition.frozenSegmentsById(segmentIds)) {
|
|
5292
|
+
const shared = shareFrozenSegment(segment);
|
|
5293
|
+
if (shared === null) return null;
|
|
5294
|
+
frozen.push(shared);
|
|
5295
|
+
}
|
|
5296
|
+
const live = partition.live.serialize(
|
|
5297
|
+
manager.indexName,
|
|
5298
|
+
manager.partitionCount,
|
|
5299
|
+
manager.language.name,
|
|
5300
|
+
manager.schema
|
|
5301
|
+
);
|
|
5302
|
+
return { partitionId, live, frozen };
|
|
5303
|
+
}
|
|
5304
|
+
function capturePartitions(manager) {
|
|
5305
|
+
const captured = [];
|
|
5306
|
+
for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) {
|
|
5307
|
+
captured.push(
|
|
5308
|
+
captureComposite(manager, partitionId) ?? {
|
|
5309
|
+
partitionId,
|
|
5310
|
+
live: manager.serializePartition(partitionId),
|
|
5311
|
+
frozen: []
|
|
5312
|
+
}
|
|
5313
|
+
);
|
|
5314
|
+
}
|
|
5315
|
+
return captured;
|
|
5316
|
+
}
|
|
5317
|
+
async function sendPartition(indexName, executors, captured) {
|
|
5318
|
+
await Promise.all(
|
|
5319
|
+
executors.map(
|
|
5320
|
+
(workerExecutor) => workerExecutor.execute({
|
|
5321
|
+
type: "deserialize",
|
|
5322
|
+
indexName,
|
|
5323
|
+
partitionId: captured.partitionId,
|
|
5324
|
+
data: captured.live,
|
|
5325
|
+
requestId: `resync-${indexName}-${captured.partitionId}`
|
|
5326
|
+
})
|
|
5327
|
+
)
|
|
5328
|
+
);
|
|
5329
|
+
if (captured.frozen.length === 0) return;
|
|
5330
|
+
const segments = captured.frozen.map((shared) => ({
|
|
5331
|
+
partitionId: captured.partitionId,
|
|
5332
|
+
snapshot: shared.snapshot,
|
|
5333
|
+
tombstonedDocIds: shared.tombstonedDocIds
|
|
5334
|
+
}));
|
|
5335
|
+
await Promise.all(
|
|
5336
|
+
executors.map(
|
|
5337
|
+
(workerExecutor) => workerExecutor.execute({
|
|
5338
|
+
type: "attachSegments",
|
|
5339
|
+
indexName,
|
|
5340
|
+
segments,
|
|
5341
|
+
requestId: `resync-attach-${indexName}-${captured.partitionId}`
|
|
5342
|
+
})
|
|
5343
|
+
)
|
|
5344
|
+
);
|
|
5345
|
+
}
|
|
5346
|
+
async function transferIndexToPool(indexName, pool, config, manager, analysisStale = false) {
|
|
5347
|
+
await loadIndexOntoWorkers(
|
|
5348
|
+
indexName,
|
|
5349
|
+
pool.getAllExecutors(),
|
|
5350
|
+
config,
|
|
5351
|
+
manager,
|
|
5352
|
+
() => pool.addIndexToAll(indexName),
|
|
5353
|
+
analysisStale
|
|
5354
|
+
);
|
|
5355
|
+
}
|
|
5356
|
+
async function loadIndexOntoWorkers(indexName, executors, config, manager, onDropped, analysisStale = false) {
|
|
5357
|
+
const captured = capturePartitions(manager);
|
|
5232
5358
|
await Promise.allSettled(
|
|
5233
|
-
|
|
5359
|
+
executors.map(
|
|
5234
5360
|
(workerExecutor) => workerExecutor.execute({ type: "dropIndex", indexName, requestId: `resync-drop-${indexName}` })
|
|
5235
5361
|
)
|
|
5236
5362
|
);
|
|
5237
|
-
|
|
5363
|
+
onDropped?.();
|
|
5238
5364
|
await Promise.all(
|
|
5239
|
-
|
|
5365
|
+
executors.map(
|
|
5240
5366
|
(workerExecutor) => workerExecutor.execute({
|
|
5241
5367
|
type: "createIndex",
|
|
5242
5368
|
indexName,
|
|
5243
5369
|
config,
|
|
5244
|
-
requestId: `resync-create-${indexName}
|
|
5370
|
+
requestId: `resync-create-${indexName}`,
|
|
5371
|
+
analysisStale
|
|
5245
5372
|
})
|
|
5246
5373
|
)
|
|
5247
5374
|
);
|
|
5248
|
-
for (
|
|
5249
|
-
|
|
5250
|
-
await Promise.all(
|
|
5251
|
-
allExecutors.map(
|
|
5252
|
-
(workerExecutor) => workerExecutor.execute({
|
|
5253
|
-
type: "deserialize",
|
|
5254
|
-
indexName,
|
|
5255
|
-
partitionId: i,
|
|
5256
|
-
data: serialized,
|
|
5257
|
-
requestId: `resync-${indexName}-${i}`
|
|
5258
|
-
})
|
|
5259
|
-
)
|
|
5260
|
-
);
|
|
5375
|
+
for (const partition of captured) {
|
|
5376
|
+
await sendPartition(indexName, executors, partition);
|
|
5261
5377
|
}
|
|
5262
5378
|
}
|
|
5263
5379
|
|
|
5380
|
+
// src/engine/orchestration/constants.ts
|
|
5381
|
+
var DEFAULT_COPY_THRESHOLD = 1e3;
|
|
5382
|
+
var DEFAULT_COPY_IDLE_TIMEOUT_MS = 3e5;
|
|
5383
|
+
var COMPACTION_SEGMENT_TRIGGER = 8;
|
|
5384
|
+
var IDLE_MERGE_DELAY_MS = 1e3;
|
|
5385
|
+
var LIVE_TAIL_FREEZE_FLOOR = MIN_DOCUMENTS_FOR_SEGMENTS;
|
|
5386
|
+
var LIVE_TAIL_FLUSH_DOCUMENTS = 5e3;
|
|
5387
|
+
var MAX_PENDING_REPLICATION_DOCUMENTS = 2e4;
|
|
5388
|
+
var REPLICATION_WINDOW = 32;
|
|
5389
|
+
var LOAD_BUFFER_YIELD_INTERVAL = 1e3;
|
|
5390
|
+
var MAIN_COPY_LONE_QUERY_DOCUMENTS = 5e4;
|
|
5391
|
+
var POOL_RESTART_DELAY_MS = 1e3;
|
|
5392
|
+
var POOL_RESTART_DELAY_MAX_MS = 6e4;
|
|
5393
|
+
|
|
5264
5394
|
// src/engine/orchestration/eligibility.ts
|
|
5265
5395
|
function toError3(value) {
|
|
5266
5396
|
return value instanceof Error ? value : new Error(String(value));
|
|
@@ -5309,7 +5439,7 @@ function workerIneligibility(indexName, config, bootstrapModule) {
|
|
|
5309
5439
|
function reportIneligible(state, indexName, error) {
|
|
5310
5440
|
if (state.reportedIneligible.has(indexName)) return;
|
|
5311
5441
|
state.reportedIneligible.add(indexName);
|
|
5312
|
-
state.callbacks?.
|
|
5442
|
+
state.callbacks?.onCopyLoadFailure?.("index-excluded", error, false);
|
|
5313
5443
|
}
|
|
5314
5444
|
function eligibleIndexNames(state) {
|
|
5315
5445
|
const names = [];
|
|
@@ -5323,33 +5453,26 @@ function eligibleIndexNames(state) {
|
|
|
5323
5453
|
}
|
|
5324
5454
|
return names;
|
|
5325
5455
|
}
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5456
|
+
|
|
5457
|
+
// src/engine/orchestration/turn.ts
|
|
5458
|
+
function afterCurrentTurn(callback) {
|
|
5459
|
+
if (typeof setImmediate === "function") {
|
|
5460
|
+
setImmediate(callback);
|
|
5461
|
+
return;
|
|
5331
5462
|
}
|
|
5332
|
-
|
|
5463
|
+
setTimeout(callback, 0);
|
|
5464
|
+
}
|
|
5465
|
+
function yieldToEventLoop3() {
|
|
5466
|
+
return new Promise((resolve) => afterCurrentTurn(resolve));
|
|
5333
5467
|
}
|
|
5334
5468
|
|
|
5335
5469
|
// src/engine/orchestration/replication.ts
|
|
5336
|
-
var MAX_PENDING_REPLICATION_DOCUMENTS = 2e4;
|
|
5337
5470
|
async function dispatchToWorkers(state, action) {
|
|
5338
5471
|
const pool = state.workerPool;
|
|
5339
5472
|
if (!pool) return;
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
reportIneligible(state, action.indexName, ineligibility);
|
|
5344
|
-
return;
|
|
5345
|
-
}
|
|
5346
|
-
pool.addIndexToAll(action.indexName);
|
|
5347
|
-
state.promotedIndexes.add(action.indexName);
|
|
5348
|
-
} else if ("indexName" in action && !state.promotedIndexes.has(action.indexName)) {
|
|
5349
|
-
return;
|
|
5350
|
-
}
|
|
5351
|
-
const allExecutors = pool.getAllExecutors();
|
|
5352
|
-
const results = await Promise.allSettled(allExecutors.map((workerExecutor) => workerExecutor.execute(action)));
|
|
5473
|
+
const executors = "indexName" in action ? pool.executorsHolding(action.indexName) : pool.getAllExecutors();
|
|
5474
|
+
if ("indexName" in action && !state.scaledOutIndexes.has(action.indexName)) return;
|
|
5475
|
+
const results = await Promise.allSettled(executors.map((workerExecutor) => workerExecutor.execute(action)));
|
|
5353
5476
|
for (const result of results) {
|
|
5354
5477
|
if (result.status === "rejected") {
|
|
5355
5478
|
if (action.type === "insert" && alreadyPresentOnWorker(result.reason)) {
|
|
@@ -5417,6 +5540,13 @@ function recordSegmentBroadcast(state, action) {
|
|
|
5417
5540
|
}
|
|
5418
5541
|
return;
|
|
5419
5542
|
}
|
|
5543
|
+
if (action.type === "freezeLiveTail") {
|
|
5544
|
+
partitionLedger(state, action.indexName, action.partitionId).push({
|
|
5545
|
+
segmentId: action.snapshot.segmentId,
|
|
5546
|
+
documentCount: action.snapshot.documentCount
|
|
5547
|
+
});
|
|
5548
|
+
return;
|
|
5549
|
+
}
|
|
5420
5550
|
if (action.type === "swapSegments") {
|
|
5421
5551
|
const entries = partitionLedger(state, action.indexName, action.partitionId);
|
|
5422
5552
|
const dropped = new Set(action.dropSegmentIds);
|
|
@@ -5429,117 +5559,200 @@ function recordSegmentBroadcast(state, action) {
|
|
|
5429
5559
|
function queueFor(state, indexName) {
|
|
5430
5560
|
let queue = state.replicationQueues.get(indexName);
|
|
5431
5561
|
if (queue === void 0) {
|
|
5432
|
-
queue = { tail: Promise.resolve(), pendingActions: 0, pendingDocuments: 0 };
|
|
5562
|
+
queue = { tail: Promise.resolve(), inFlight: [], pendingActions: 0, pendingDocuments: 0, drainWaiters: [] };
|
|
5433
5563
|
state.replicationQueues.set(indexName, queue);
|
|
5434
5564
|
}
|
|
5435
5565
|
return queue;
|
|
5436
5566
|
}
|
|
5437
|
-
function
|
|
5567
|
+
function dispatchGate(queue) {
|
|
5568
|
+
if (queue.inFlight.length < REPLICATION_WINDOW) return Promise.resolve();
|
|
5569
|
+
return queue.inFlight[queue.inFlight.length - REPLICATION_WINDOW];
|
|
5570
|
+
}
|
|
5571
|
+
function enqueueReplication(state, indexName, action) {
|
|
5572
|
+
const weight = documentWeight(action);
|
|
5573
|
+
recordSegmentBroadcast(state, action);
|
|
5438
5574
|
const queue = queueFor(state, indexName);
|
|
5439
5575
|
queue.pendingActions += 1;
|
|
5440
5576
|
queue.pendingDocuments += weight;
|
|
5441
|
-
|
|
5577
|
+
const run = dispatchGate(queue).then(async () => {
|
|
5442
5578
|
try {
|
|
5443
5579
|
await dispatchToWorkers(state, action);
|
|
5444
5580
|
} catch (err) {
|
|
5445
5581
|
console.warn("Worker replication failed:", err);
|
|
5446
5582
|
} finally {
|
|
5583
|
+
const at = queue.inFlight.indexOf(run);
|
|
5584
|
+
if (at !== -1) queue.inFlight.splice(at, 1);
|
|
5447
5585
|
queue.pendingActions -= 1;
|
|
5448
5586
|
queue.pendingDocuments -= weight;
|
|
5587
|
+
if (queue.pendingDocuments < MAX_PENDING_REPLICATION_DOCUMENTS && queue.drainWaiters.length > 0) {
|
|
5588
|
+
for (const resume of queue.drainWaiters.splice(0)) resume();
|
|
5589
|
+
}
|
|
5449
5590
|
if (queue.pendingActions === 0 && state.replicationQueues.get(indexName) === queue) {
|
|
5450
5591
|
state.replicationQueues.delete(indexName);
|
|
5451
5592
|
}
|
|
5452
5593
|
}
|
|
5453
5594
|
});
|
|
5595
|
+
queue.inFlight.push(run);
|
|
5596
|
+
queue.tail = run;
|
|
5454
5597
|
}
|
|
5455
5598
|
async function replicateToWorkers(state, action) {
|
|
5456
|
-
if (
|
|
5457
|
-
state.
|
|
5599
|
+
if (!("indexName" in action)) {
|
|
5600
|
+
if (state.workerPool === null) return;
|
|
5601
|
+
await awaitReplicationIdle(state);
|
|
5602
|
+
await dispatchToWorkers(state, action);
|
|
5458
5603
|
return;
|
|
5459
5604
|
}
|
|
5460
|
-
if (state.workerPool === null) return;
|
|
5461
5605
|
const detached = detachCallerDocuments(action);
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5606
|
+
const loading = state.copyLoadBuffers.get(action.indexName);
|
|
5607
|
+
if (loading !== void 0) {
|
|
5608
|
+
queueForCopies(state, detached);
|
|
5609
|
+
if (loading.length % LOAD_BUFFER_YIELD_INTERVAL === 0) await yieldToEventLoop3();
|
|
5465
5610
|
return;
|
|
5466
5611
|
}
|
|
5467
|
-
const
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5612
|
+
const queue = state.replicationQueues.get(action.indexName);
|
|
5613
|
+
if (queue !== void 0 && queue.pendingDocuments + documentWeight(detached) > MAX_PENDING_REPLICATION_DOCUMENTS) {
|
|
5614
|
+
await new Promise((resume) => queue.drainWaiters.push(resume));
|
|
5615
|
+
}
|
|
5616
|
+
queueForCopies(state, detached);
|
|
5617
|
+
}
|
|
5618
|
+
function queueForCopies(state, action) {
|
|
5619
|
+
if (!("indexName" in action)) return;
|
|
5620
|
+
const loading = state.copyLoadBuffers.get(action.indexName);
|
|
5621
|
+
if (loading !== void 0) {
|
|
5622
|
+
loading.push(action);
|
|
5623
|
+
return;
|
|
5471
5624
|
}
|
|
5472
|
-
|
|
5473
|
-
|
|
5625
|
+
if (state.workerPool === null || !state.scaledOutIndexes.has(action.indexName)) return;
|
|
5626
|
+
enqueueReplication(state, action.indexName, action);
|
|
5627
|
+
}
|
|
5628
|
+
function settled(queue) {
|
|
5629
|
+
return Promise.all([...queue.inFlight, queue.tail]).then(() => void 0);
|
|
5474
5630
|
}
|
|
5475
5631
|
async function awaitReplicationIdle(state, indexName) {
|
|
5476
5632
|
if (indexName !== void 0) {
|
|
5477
5633
|
const queue = state.replicationQueues.get(indexName);
|
|
5478
|
-
if (queue !== void 0) await queue
|
|
5634
|
+
if (queue !== void 0) await settled(queue);
|
|
5479
5635
|
return;
|
|
5480
5636
|
}
|
|
5481
5637
|
while (state.replicationQueues.size > 0) {
|
|
5482
|
-
|
|
5483
|
-
await Promise.all(tails);
|
|
5638
|
+
await Promise.all(Array.from(state.replicationQueues.values(), settled));
|
|
5484
5639
|
}
|
|
5485
5640
|
}
|
|
5486
|
-
async function
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5641
|
+
async function awaitWritesApplied(state, indexName) {
|
|
5642
|
+
let awaitedTransition;
|
|
5643
|
+
let awaitedRepair = null;
|
|
5644
|
+
for (; ; ) {
|
|
5645
|
+
const transition = state.copyTransitions.get(indexName);
|
|
5646
|
+
if (transition !== void 0 && transition !== awaitedTransition) {
|
|
5647
|
+
awaitedTransition = transition;
|
|
5648
|
+
await transition.done;
|
|
5649
|
+
continue;
|
|
5650
|
+
}
|
|
5651
|
+
const repair = state.poolRepair;
|
|
5652
|
+
if (repair !== null && repair !== awaitedRepair) {
|
|
5653
|
+
awaitedRepair = repair;
|
|
5654
|
+
await repair;
|
|
5655
|
+
continue;
|
|
5656
|
+
}
|
|
5657
|
+
await awaitReplicationIdle(state, indexName);
|
|
5658
|
+
return;
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
|
|
5662
|
+
// src/engine/orchestration/live-tail.ts
|
|
5663
|
+
function liveTailCount(manager, partitionId) {
|
|
5664
|
+
if (partitionId >= manager.partitionCount) return 0;
|
|
5665
|
+
const partition = manager.getPartition(partitionId);
|
|
5666
|
+
return isCompositePartition(partition) ? partition.live.count() : partition.count();
|
|
5667
|
+
}
|
|
5668
|
+
function freezeTail(state, indexName, manager, partitionId) {
|
|
5669
|
+
const segment = manager.freezeLiveTail(partitionId, (payload, documents) => {
|
|
5670
|
+
const snapshot = freezeSegmentShared(payload, documents);
|
|
5671
|
+
return snapshot === null ? null : createSharedFrozenSegment(snapshot);
|
|
5672
|
+
});
|
|
5673
|
+
if (segment === null || segment.sharedSnapshot === null) return;
|
|
5674
|
+
queueForCopies(state, {
|
|
5675
|
+
type: "freezeLiveTail",
|
|
5676
|
+
indexName,
|
|
5677
|
+
partitionId,
|
|
5678
|
+
snapshot: segment.sharedSnapshot,
|
|
5679
|
+
requestId: createRequestId()
|
|
5680
|
+
});
|
|
5681
|
+
}
|
|
5682
|
+
function freezeLiveTail(state, indexName, manager, partitionId, floor) {
|
|
5683
|
+
if (!state.scaledOutIndexes.has(indexName)) return;
|
|
5684
|
+
if (liveTailCount(manager, partitionId) < floor) return;
|
|
5685
|
+
try {
|
|
5686
|
+
freezeTail(state, indexName, manager, partitionId);
|
|
5687
|
+
} catch (err) {
|
|
5688
|
+
console.warn(`Freezing the live tail of partition ${partitionId} of "${indexName}" failed:`, err);
|
|
5689
|
+
}
|
|
5690
|
+
}
|
|
5691
|
+
function flushGrownTails(state, indexName) {
|
|
5692
|
+
const manager = state.executor.getManager(indexName);
|
|
5693
|
+
if (manager === void 0) return;
|
|
5694
|
+
for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) {
|
|
5695
|
+
freezeLiveTail(state, indexName, manager, partitionId, LIVE_TAIL_FLUSH_DOCUMENTS);
|
|
5491
5696
|
}
|
|
5492
5697
|
}
|
|
5493
5698
|
|
|
5494
5699
|
// src/engine/orchestration/compaction.ts
|
|
5495
|
-
var COMPACTION_SEGMENT_TRIGGER = 8;
|
|
5496
5700
|
function smallestSegmentIds(entries) {
|
|
5497
5701
|
return [...entries].sort((a, b) => a.documentCount - b.documentCount).slice(0, COMPACTION_SEGMENT_TRIGGER).map((entry) => entry.segmentId);
|
|
5498
5702
|
}
|
|
5703
|
+
function pickSegmentIds(entries, policy) {
|
|
5704
|
+
if (policy === "idle") {
|
|
5705
|
+
return entries.length >= 2 ? entries.map((entry) => entry.segmentId) : null;
|
|
5706
|
+
}
|
|
5707
|
+
return entries.length >= COMPACTION_SEGMENT_TRIGGER ? smallestSegmentIds(entries) : null;
|
|
5708
|
+
}
|
|
5499
5709
|
function mainCompositeOf(manager, partitionId) {
|
|
5500
5710
|
if (partitionId >= manager.partitionCount) return null;
|
|
5501
5711
|
const partition = manager.getPartition(partitionId);
|
|
5502
5712
|
return isCompositePartition(partition) ? partition : null;
|
|
5503
5713
|
}
|
|
5504
|
-
function candidateSegmentIds(state, indexName, manager, partitionId) {
|
|
5714
|
+
function candidateSegmentIds(state, indexName, manager, partitionId, policy) {
|
|
5505
5715
|
const composite = mainCompositeOf(manager, partitionId);
|
|
5506
5716
|
if (composite !== null) {
|
|
5507
5717
|
const sizes = composite.frozenSegmentSizes();
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5718
|
+
const picks = pickSegmentIds(
|
|
5719
|
+
sizes.map((size) => ({ segmentId: size.segmentId, documentCount: size.liveDocumentCount })),
|
|
5720
|
+
policy
|
|
5721
|
+
);
|
|
5722
|
+
if (picks !== null) return picks;
|
|
5513
5723
|
}
|
|
5514
5724
|
const ledger = state.segmentLedger.get(indexName)?.get(partitionId);
|
|
5515
|
-
|
|
5516
|
-
return smallestSegmentIds(ledger);
|
|
5517
|
-
}
|
|
5518
|
-
return null;
|
|
5725
|
+
return ledger === void 0 ? null : pickSegmentIds(ledger, policy);
|
|
5519
5726
|
}
|
|
5520
5727
|
function mainHoldsAll(composite, segmentIds) {
|
|
5521
5728
|
if (composite === null) return false;
|
|
5522
5729
|
const held = new Set(composite.frozenSegmentSizes().map((size) => size.segmentId));
|
|
5523
5730
|
return segmentIds.every((segmentId) => held.has(segmentId));
|
|
5524
5731
|
}
|
|
5525
|
-
async function
|
|
5526
|
-
await awaitReplicationIdle(state, indexName);
|
|
5527
|
-
let snapshot = null;
|
|
5732
|
+
async function compactOnWorker(state, indexName, partitionId, segmentIds) {
|
|
5528
5733
|
const pool = state.workerPool;
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
}
|
|
5540
|
-
|
|
5541
|
-
|
|
5734
|
+
if (pool === null || !state.scaledOutIndexes.has(indexName)) return null;
|
|
5735
|
+
const lease = pool.leaseLeastBusy();
|
|
5736
|
+
if (lease === null) return null;
|
|
5737
|
+
try {
|
|
5738
|
+
return await lease.executor.execute({
|
|
5739
|
+
type: "compactSegments",
|
|
5740
|
+
indexName,
|
|
5741
|
+
partitionId,
|
|
5742
|
+
segmentIds: [...segmentIds],
|
|
5743
|
+
requestId: createRequestId()
|
|
5744
|
+
});
|
|
5745
|
+
} catch (err) {
|
|
5746
|
+
console.warn("Worker segment compaction failed, compacting on the main thread:", err);
|
|
5747
|
+
return null;
|
|
5748
|
+
} finally {
|
|
5749
|
+
lease.release();
|
|
5542
5750
|
}
|
|
5751
|
+
}
|
|
5752
|
+
async function compactPartitionSegments2(state, indexName, manager, partitionId, segmentIds, policy) {
|
|
5753
|
+
await awaitReplicationIdle(state, indexName);
|
|
5754
|
+
if (policy === "idle" && !state.scaledOutIndexes.has(indexName)) return false;
|
|
5755
|
+
let snapshot = await compactOnWorker(state, indexName, partitionId, segmentIds);
|
|
5543
5756
|
const composite = mainCompositeOf(manager, partitionId);
|
|
5544
5757
|
const holdsAll = mainHoldsAll(composite, segmentIds);
|
|
5545
5758
|
let replacement = null;
|
|
@@ -5566,30 +5779,52 @@ async function compactPartitionSegments2(state, indexName, manager, partitionId,
|
|
|
5566
5779
|
}
|
|
5567
5780
|
return true;
|
|
5568
5781
|
}
|
|
5569
|
-
async function compactIndexSegments(state, indexName) {
|
|
5782
|
+
async function compactIndexSegments(state, indexName, policy) {
|
|
5570
5783
|
const manager = state.executor.getManager(indexName);
|
|
5571
5784
|
if (!manager) return;
|
|
5572
5785
|
const partitionIds = /* @__PURE__ */ new Set();
|
|
5573
5786
|
for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) partitionIds.add(partitionId);
|
|
5574
5787
|
for (const partitionId of state.segmentLedger.get(indexName)?.keys() ?? []) partitionIds.add(partitionId);
|
|
5575
5788
|
for (const partitionId of partitionIds) {
|
|
5576
|
-
|
|
5789
|
+
if (policy === "idle") {
|
|
5790
|
+
await awaitReplicationIdle(state, indexName);
|
|
5791
|
+
freezeLiveTail(state, indexName, manager, partitionId, LIVE_TAIL_FREEZE_FLOOR);
|
|
5792
|
+
}
|
|
5793
|
+
let picks = candidateSegmentIds(state, indexName, manager, partitionId, policy);
|
|
5577
5794
|
while (picks !== null) {
|
|
5578
|
-
const compacted = await compactPartitionSegments2(state, indexName, manager, partitionId, picks);
|
|
5795
|
+
const compacted = await compactPartitionSegments2(state, indexName, manager, partitionId, picks, policy);
|
|
5579
5796
|
if (!compacted) break;
|
|
5580
|
-
picks = candidateSegmentIds(state, indexName, manager, partitionId);
|
|
5797
|
+
picks = candidateSegmentIds(state, indexName, manager, partitionId, policy);
|
|
5581
5798
|
}
|
|
5582
5799
|
}
|
|
5583
5800
|
}
|
|
5584
|
-
function maybeCompactSegments(state, indexName) {
|
|
5585
|
-
if (state.compactionsInFlight.has(indexName))
|
|
5586
|
-
|
|
5801
|
+
function maybeCompactSegments(state, indexName, policy = "ingest") {
|
|
5802
|
+
if (state.compactionsInFlight.has(indexName)) {
|
|
5803
|
+
if (policy === "idle") scheduleIdleMerge(state, indexName);
|
|
5804
|
+
return;
|
|
5805
|
+
}
|
|
5806
|
+
const run = compactIndexSegments(state, indexName, policy).catch((err) => {
|
|
5587
5807
|
console.warn("Segment compaction failed:", err);
|
|
5588
5808
|
}).finally(() => {
|
|
5589
5809
|
state.compactionsInFlight.delete(indexName);
|
|
5590
5810
|
});
|
|
5591
5811
|
state.compactionsInFlight.set(indexName, run);
|
|
5592
5812
|
}
|
|
5813
|
+
function scheduleIdleMerge(state, indexName) {
|
|
5814
|
+
cancelIdleMerge(state, indexName);
|
|
5815
|
+
const timer = setTimeout(() => {
|
|
5816
|
+
state.idleMergeTimers.delete(indexName);
|
|
5817
|
+
maybeCompactSegments(state, indexName, "idle");
|
|
5818
|
+
}, IDLE_MERGE_DELAY_MS);
|
|
5819
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
5820
|
+
state.idleMergeTimers.set(indexName, timer);
|
|
5821
|
+
}
|
|
5822
|
+
function cancelIdleMerge(state, indexName) {
|
|
5823
|
+
const timer = state.idleMergeTimers.get(indexName);
|
|
5824
|
+
if (timer === void 0) return;
|
|
5825
|
+
clearTimeout(timer);
|
|
5826
|
+
state.idleMergeTimers.delete(indexName);
|
|
5827
|
+
}
|
|
5593
5828
|
async function awaitCompactions(state) {
|
|
5594
5829
|
while (state.compactionsInFlight.size > 0) {
|
|
5595
5830
|
await Promise.all([...state.compactionsInFlight.values()]);
|
|
@@ -5597,10 +5832,6 @@ async function awaitCompactions(state) {
|
|
|
5597
5832
|
}
|
|
5598
5833
|
|
|
5599
5834
|
// src/workers/worker-executor.ts
|
|
5600
|
-
var DEFAULT_BACKPRESSURE_LIMIT = 100;
|
|
5601
|
-
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
5602
|
-
var SHUTDOWN_TIMEOUT = 5e3;
|
|
5603
|
-
var CONSECUTIVE_TIMEOUTS_BEFORE_DEATH = 3;
|
|
5604
5835
|
function errorFromEventLike(event) {
|
|
5605
5836
|
if (event instanceof Error) {
|
|
5606
5837
|
return event;
|
|
@@ -5618,7 +5849,7 @@ function errorFromEventLike(event) {
|
|
|
5618
5849
|
}
|
|
5619
5850
|
function createWorkerExecutor(worker, config) {
|
|
5620
5851
|
const backpressureLimit = config?.backpressureLimit ?? DEFAULT_BACKPRESSURE_LIMIT;
|
|
5621
|
-
const requestTimeout = config?.requestTimeout ??
|
|
5852
|
+
const requestTimeout = config?.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
5622
5853
|
const pending = /* @__PURE__ */ new Map();
|
|
5623
5854
|
let deathError = null;
|
|
5624
5855
|
let shutdownRequested = false;
|
|
@@ -5667,7 +5898,7 @@ function createWorkerExecutor(worker, config) {
|
|
|
5667
5898
|
});
|
|
5668
5899
|
worker.addEventListener("error", (event) => handleDeath(errorFromEventLike(event)));
|
|
5669
5900
|
}
|
|
5670
|
-
function execute(action) {
|
|
5901
|
+
function execute(action, transfer) {
|
|
5671
5902
|
if (deathError !== null) {
|
|
5672
5903
|
return Promise.reject(deathError);
|
|
5673
5904
|
}
|
|
@@ -5692,7 +5923,8 @@ function createWorkerExecutor(worker, config) {
|
|
|
5692
5923
|
reject,
|
|
5693
5924
|
timeoutId
|
|
5694
5925
|
});
|
|
5695
|
-
worker.postMessage(taggedAction);
|
|
5926
|
+
if (transfer === void 0) worker.postMessage(taggedAction);
|
|
5927
|
+
else worker.postMessage(taggedAction, transfer);
|
|
5696
5928
|
});
|
|
5697
5929
|
}
|
|
5698
5930
|
async function shutdown() {
|
|
@@ -5706,7 +5938,7 @@ function createWorkerExecutor(worker, config) {
|
|
|
5706
5938
|
const timeoutId = setTimeout(() => {
|
|
5707
5939
|
pending.delete(requestId);
|
|
5708
5940
|
reject(new NarsilError(ErrorCodes.WORKER_TIMEOUT, "Shutdown timed out"));
|
|
5709
|
-
},
|
|
5941
|
+
}, SHUTDOWN_TIMEOUT_MS);
|
|
5710
5942
|
pending.set(requestId, {
|
|
5711
5943
|
resolve: () => resolve(),
|
|
5712
5944
|
reject,
|
|
@@ -5741,8 +5973,11 @@ async function createWorkerFactory(entryPoint) {
|
|
|
5741
5973
|
const resolvedEntry = resolveEntryPoint();
|
|
5742
5974
|
if (runtime.supportsWorkerThreads) {
|
|
5743
5975
|
const workerThreadsModule = await import('worker_threads');
|
|
5744
|
-
return function nodeFactory(
|
|
5745
|
-
const instance = new workerThreadsModule.Worker(new URL(resolvedEntry)
|
|
5976
|
+
return function nodeFactory(workerId, onDeath) {
|
|
5977
|
+
const instance = new workerThreadsModule.Worker(new URL(resolvedEntry), {
|
|
5978
|
+
resourceLimits: WORKER_RESOURCE_LIMITS,
|
|
5979
|
+
workerData: { workerId }
|
|
5980
|
+
});
|
|
5746
5981
|
return createWorkerExecutor(instance, { onDeath });
|
|
5747
5982
|
};
|
|
5748
5983
|
}
|
|
@@ -5755,136 +5990,441 @@ async function createWorkerFactory(entryPoint) {
|
|
|
5755
5990
|
throw new NarsilError(ErrorCodes.WORKER_CRASHED, `No worker support available on runtime "${runtime.runtime}"`);
|
|
5756
5991
|
}
|
|
5757
5992
|
|
|
5758
|
-
// src/engine/orchestration/
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
)
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
)
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
partitionId,
|
|
5792
|
-
data: serialized,
|
|
5793
|
-
requestId: `promote-sync-${name}-${partitionId}`
|
|
5794
|
-
})
|
|
5795
|
-
)
|
|
5796
|
-
);
|
|
5993
|
+
// src/engine/orchestration/vector-copies.ts
|
|
5994
|
+
function holdsIndex(state, indexName) {
|
|
5995
|
+
return state.scaledOutIndexes.has(indexName) || state.copyLoadBuffers.has(indexName);
|
|
5996
|
+
}
|
|
5997
|
+
function sharedCopyHostOf(state) {
|
|
5998
|
+
return {
|
|
5999
|
+
get scratchSlotCount() {
|
|
6000
|
+
return state.workerPool?.workerCount ?? state.keywordWorkerCount;
|
|
6001
|
+
},
|
|
6002
|
+
holdsIndex: (indexName) => holdsIndex(state, indexName),
|
|
6003
|
+
resolvePartition: (indexName, docId) => state.executor.getManager(indexName)?.partitionIdOf(docId),
|
|
6004
|
+
async loadShared(indexName, fieldName, handle, copy) {
|
|
6005
|
+
if (!holdsIndex(state, indexName)) return false;
|
|
6006
|
+
await replicateToWorkers(state, {
|
|
6007
|
+
type: "loadVectorCopy",
|
|
6008
|
+
indexName,
|
|
6009
|
+
fieldName,
|
|
6010
|
+
handle,
|
|
6011
|
+
copy,
|
|
6012
|
+
requestId: createRequestId()
|
|
6013
|
+
});
|
|
6014
|
+
await awaitWritesApplied(state, indexName);
|
|
6015
|
+
return state.scaledOutIndexes.has(indexName);
|
|
6016
|
+
},
|
|
6017
|
+
async drop(indexName, fieldName, handle) {
|
|
6018
|
+
if (!holdsIndex(state, indexName)) return;
|
|
6019
|
+
await replicateToWorkers(state, {
|
|
6020
|
+
type: "dropVectorCopy",
|
|
6021
|
+
indexName,
|
|
6022
|
+
fieldName,
|
|
6023
|
+
handle,
|
|
6024
|
+
requestId: createRequestId()
|
|
6025
|
+
});
|
|
5797
6026
|
}
|
|
5798
|
-
}
|
|
6027
|
+
};
|
|
5799
6028
|
}
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
6029
|
+
function refreshVectorCopies(state, indexName) {
|
|
6030
|
+
if (state.vectorCopyPolicy?.host === void 0) return;
|
|
6031
|
+
const manager = state.executor.getManager(indexName);
|
|
6032
|
+
if (manager === void 0) return;
|
|
6033
|
+
for (const vectorIndex of manager.getVectorIndexes().values()) vectorIndex.refreshWorkerCopies();
|
|
6034
|
+
}
|
|
6035
|
+
|
|
6036
|
+
// src/engine/orchestration/request-threads.ts
|
|
6037
|
+
async function announceRequestThreads(state, pool) {
|
|
6038
|
+
const listener = state.requestThreads;
|
|
6039
|
+
if (listener === null) return;
|
|
6040
|
+
for (const { workerId, executor } of pool.executorEntries()) {
|
|
6041
|
+
await listener.onWorkerReady(workerId, executor);
|
|
6042
|
+
}
|
|
6043
|
+
}
|
|
6044
|
+
function scheduleRequestThreadPoolRestart(state) {
|
|
6045
|
+
if (state.requestThreads === null) return;
|
|
6046
|
+
const timer = setTimeout(
|
|
6047
|
+
() => {
|
|
6048
|
+
if (state.requestThreads === null || state.workerPool !== null) return;
|
|
6049
|
+
void ensurePool(state).then(() => scaleOutReadyIndexes(state)).catch((err) => {
|
|
6050
|
+
console.warn("Restarting the request threads failed:", err);
|
|
6051
|
+
scheduleRequestThreadPoolRestart(state);
|
|
6052
|
+
});
|
|
6053
|
+
},
|
|
6054
|
+
Math.max(0, state.poolRetryAt - Date.now())
|
|
6055
|
+
);
|
|
6056
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
6057
|
+
}
|
|
6058
|
+
async function serveRequestsOnWorkers(state, listener) {
|
|
6059
|
+
state.requestThreads = listener;
|
|
6060
|
+
state.keywordWorkerCount = resolveRequestThreadCount(state.config?.workers?.count);
|
|
6061
|
+
if (state.vectorCopyPolicy !== void 0) {
|
|
6062
|
+
state.vectorCopyPolicy.host = sharedCopyHostOf(state);
|
|
6063
|
+
state.vectorCopyPolicy.enabled = state.workersEnabled;
|
|
6064
|
+
}
|
|
6065
|
+
if (state.poolStart !== null) await state.poolStart.catch(() => void 0);
|
|
6066
|
+
const running = state.workerPool;
|
|
6067
|
+
if (running !== null && running.workerCount !== state.keywordWorkerCount) {
|
|
6068
|
+
retirePool(state, running);
|
|
6069
|
+
state.poolRetryAt = 0;
|
|
6070
|
+
}
|
|
6071
|
+
const pool = await ensurePool(state);
|
|
6072
|
+
for (const indexName of state.indexRegistry.keys()) {
|
|
6073
|
+
refreshVectorCopies(state, indexName);
|
|
6074
|
+
if (indexReadyForCopies(state, indexName)) {
|
|
6075
|
+
await scaleOutIndex(state, indexName, copyThresholdReason(state, indexName));
|
|
5806
6076
|
}
|
|
5807
|
-
const factory = await createWorkerFactory();
|
|
5808
|
-
const pool = createWorkerPool({
|
|
5809
|
-
count: state.config?.workers?.count,
|
|
5810
|
-
workerFactory: factory,
|
|
5811
|
-
onWorkerCrash(workerId, indexNames, error) {
|
|
5812
|
-
state.callbacks?.onWorkerCrash?.(workerId, indexNames, error);
|
|
5813
|
-
}
|
|
5814
|
-
});
|
|
5815
|
-
for (const name of promotable) {
|
|
5816
|
-
pool.addIndexToAll(name);
|
|
5817
|
-
}
|
|
5818
|
-
await bootstrapWorkers(state, pool, promotable);
|
|
5819
|
-
state.workerPool = pool;
|
|
5820
|
-
for (const name of promotable) {
|
|
5821
|
-
state.promotedIndexes.add(name);
|
|
5822
|
-
state.awaitingBufferedWrites.add(name);
|
|
5823
|
-
}
|
|
5824
|
-
state.promoter.markPromoted();
|
|
5825
|
-
await drainPromotionBuffer(state);
|
|
5826
|
-
state.awaitingBufferedWrites.clear();
|
|
5827
|
-
state.callbacks?.onPromotion?.(pool.workerCount, reason);
|
|
5828
|
-
} catch (err) {
|
|
5829
|
-
state.promotionBuffer.length = 0;
|
|
5830
|
-
throw err;
|
|
5831
|
-
} finally {
|
|
5832
|
-
state.awaitingBufferedWrites.clear();
|
|
5833
|
-
state.promotionInProgress = false;
|
|
5834
6077
|
}
|
|
6078
|
+
return pool.workerCount;
|
|
5835
6079
|
}
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
6080
|
+
function requestThreadCountOf(state) {
|
|
6081
|
+
return resolveRequestThreadCount(state.config?.workers?.count);
|
|
6082
|
+
}
|
|
6083
|
+
function stopRequestThreads(state) {
|
|
6084
|
+
const listener = state.requestThreads;
|
|
6085
|
+
if (listener === null) return;
|
|
6086
|
+
state.requestThreads = null;
|
|
6087
|
+
const pool = state.workerPool;
|
|
6088
|
+
if (pool === null) return;
|
|
6089
|
+
for (const { workerId } of pool.executorEntries()) listener.onWorkerGone(workerId);
|
|
6090
|
+
}
|
|
6091
|
+
|
|
6092
|
+
// src/engine/orchestration/repair.ts
|
|
6093
|
+
var COPY_RESTART_REASON = "A request arrived after every worker crashed and the restart delay passed";
|
|
6094
|
+
function nextRestartDelay(state) {
|
|
6095
|
+
const delay2 = state.poolRetryDelayMs;
|
|
6096
|
+
state.poolRetryDelayMs = Math.min(delay2 * 2, POOL_RESTART_DELAY_MAX_MS);
|
|
6097
|
+
return delay2;
|
|
6098
|
+
}
|
|
6099
|
+
function deferPoolRestart(state) {
|
|
6100
|
+
state.poolRetryAt = Date.now() + nextRestartDelay(state);
|
|
6101
|
+
}
|
|
6102
|
+
function retirePool(state, pool) {
|
|
6103
|
+
if (state.workerPool !== pool) return;
|
|
6104
|
+
state.workerPool = null;
|
|
6105
|
+
cancelRepair(state);
|
|
6106
|
+
deferPoolRestart(state);
|
|
6107
|
+
for (const indexName of state.scaledOutIndexes) state.droppedCopies.set(indexName, COPY_RESTART_REASON);
|
|
6108
|
+
state.scaledOutIndexes.clear();
|
|
6109
|
+
state.segmentLedger.clear();
|
|
6110
|
+
for (const { workerId } of pool.executorEntries()) state.requestThreads?.onWorkerGone(workerId);
|
|
6111
|
+
void pool.shutdown().catch(() => void 0);
|
|
6112
|
+
scheduleRequestThreadPoolRestart(state);
|
|
6113
|
+
}
|
|
6114
|
+
function handleWorkerCrash(state, pool, workerId, indexNames, error) {
|
|
6115
|
+
state.callbacks?.onWorkerCrash?.(workerId, indexNames, error);
|
|
6116
|
+
state.requestThreads?.onWorkerGone(workerId);
|
|
6117
|
+
if (pool.getAllExecutors().length === 0) {
|
|
6118
|
+
retirePool(state, pool);
|
|
5840
6119
|
return;
|
|
5841
6120
|
}
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
if (
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
state.
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
6121
|
+
scheduleRepair(state, pool);
|
|
6122
|
+
}
|
|
6123
|
+
function scheduleRepair(state, pool) {
|
|
6124
|
+
if (state.workerPool !== pool || state.repairTimer !== null || state.poolRepair !== null) return;
|
|
6125
|
+
const timer = setTimeout(() => {
|
|
6126
|
+
state.repairTimer = null;
|
|
6127
|
+
void repairPool(state, pool);
|
|
6128
|
+
}, state.poolRetryDelayMs);
|
|
6129
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
6130
|
+
state.repairTimer = timer;
|
|
6131
|
+
}
|
|
6132
|
+
function cancelRepair(state) {
|
|
6133
|
+
if (state.repairTimer === null) return;
|
|
6134
|
+
clearTimeout(state.repairTimer);
|
|
6135
|
+
state.repairTimer = null;
|
|
6136
|
+
}
|
|
6137
|
+
async function loadOntoReplacement(state, replacement, indexName) {
|
|
6138
|
+
const entry = state.indexRegistry.get(indexName);
|
|
6139
|
+
const manager = state.executor.getManager(indexName);
|
|
6140
|
+
if (entry === void 0 || manager === void 0 || !state.scaledOutIndexes.has(indexName)) return;
|
|
6141
|
+
const buffered = [];
|
|
6142
|
+
state.copyLoadBuffers.set(indexName, buffered);
|
|
6143
|
+
try {
|
|
6144
|
+
await loadIndexOntoWorkers(
|
|
6145
|
+
indexName,
|
|
6146
|
+
[replacement.executor],
|
|
6147
|
+
entry.config,
|
|
6148
|
+
manager,
|
|
6149
|
+
void 0,
|
|
6150
|
+
state.callbacks?.isAnalysisStale?.(indexName)
|
|
6151
|
+
);
|
|
6152
|
+
replacement.hold(indexName);
|
|
6153
|
+
} finally {
|
|
6154
|
+
state.copyLoadBuffers.delete(indexName);
|
|
6155
|
+
for (const action of buffered) enqueueReplication(state, indexName, action);
|
|
6156
|
+
}
|
|
6157
|
+
refreshVectorCopies(state, indexName);
|
|
6158
|
+
}
|
|
6159
|
+
async function replaceWorker(state, pool, workerId) {
|
|
6160
|
+
const replacement = pool.spawnReplacement(workerId);
|
|
6161
|
+
if (replacement === null) return;
|
|
6162
|
+
try {
|
|
6163
|
+
if (state.bootstrapModule !== void 0) {
|
|
6164
|
+
await replacement.executor.execute({
|
|
6165
|
+
type: "bootstrap",
|
|
6166
|
+
moduleUrl: state.bootstrapModule,
|
|
6167
|
+
requestId: createRequestId()
|
|
6168
|
+
});
|
|
6169
|
+
}
|
|
6170
|
+
for (const indexName of [...state.scaledOutIndexes]) {
|
|
6171
|
+
await loadOntoReplacement(state, replacement, indexName);
|
|
6172
|
+
}
|
|
6173
|
+
replacement.admit();
|
|
6174
|
+
await state.requestThreads?.onWorkerReady(workerId, replacement.executor);
|
|
6175
|
+
} catch (err) {
|
|
6176
|
+
replacement.abandon();
|
|
6177
|
+
throw err;
|
|
6178
|
+
}
|
|
6179
|
+
}
|
|
6180
|
+
async function replaceDeadWorkers(state, pool) {
|
|
6181
|
+
for (const workerId of pool.deadWorkerIds()) {
|
|
6182
|
+
if (state.workerPool !== pool) return;
|
|
6183
|
+
await replaceWorker(state, pool, workerId);
|
|
6184
|
+
}
|
|
6185
|
+
state.poolRetryDelayMs = POOL_RESTART_DELAY_MS;
|
|
6186
|
+
}
|
|
6187
|
+
function repairPool(state, pool) {
|
|
6188
|
+
if (state.workerPool !== pool) return Promise.resolve();
|
|
6189
|
+
if (state.poolRepair !== null) return state.poolRepair;
|
|
6190
|
+
const run = replaceDeadWorkers(state, pool).catch((err) => {
|
|
6191
|
+
nextRestartDelay(state);
|
|
6192
|
+
console.warn("Replacing a crashed worker failed:", err);
|
|
6193
|
+
}).finally(() => {
|
|
6194
|
+
state.poolRepair = null;
|
|
6195
|
+
if (pool.deadWorkerIds().length > 0) scheduleRepair(state, pool);
|
|
6196
|
+
});
|
|
6197
|
+
state.poolRepair = run;
|
|
6198
|
+
return run;
|
|
6199
|
+
}
|
|
6200
|
+
|
|
6201
|
+
// src/engine/orchestration/scale-out.ts
|
|
6202
|
+
var COPY_RELOAD_REASON = "A request arrived after an idle spell dropped the worker copies";
|
|
6203
|
+
function copiesAllowed(state) {
|
|
6204
|
+
if (!state.workersEnabled || state.scaleOutBlocked || state.shuttingDown) return false;
|
|
6205
|
+
return state.workerPool !== null || Date.now() >= state.poolRetryAt;
|
|
6206
|
+
}
|
|
6207
|
+
async function startPool(state) {
|
|
6208
|
+
eligibleIndexNames(state);
|
|
6209
|
+
const factory = await createWorkerFactory();
|
|
6210
|
+
let started = null;
|
|
6211
|
+
const pool = createWorkerPool({
|
|
6212
|
+
count: state.keywordWorkerCount,
|
|
6213
|
+
workerFactory: factory,
|
|
6214
|
+
onWorkerCrash(workerId, indexNames, error) {
|
|
6215
|
+
if (started !== null) handleWorkerCrash(state, started, workerId, indexNames, error);
|
|
6216
|
+
}
|
|
6217
|
+
});
|
|
6218
|
+
started = pool;
|
|
6219
|
+
try {
|
|
6220
|
+
pool.spawnAll();
|
|
6221
|
+
if (state.bootstrapModule !== void 0) {
|
|
6222
|
+
const moduleUrl = state.bootstrapModule;
|
|
6223
|
+
await Promise.all(
|
|
6224
|
+
pool.getAllExecutors().map(
|
|
6225
|
+
(workerExecutor) => workerExecutor.execute({ type: "bootstrap", moduleUrl, requestId: "copies-bootstrap" })
|
|
6226
|
+
)
|
|
6227
|
+
);
|
|
6228
|
+
}
|
|
6229
|
+
} catch (err) {
|
|
6230
|
+
await pool.shutdown().catch(() => void 0);
|
|
6231
|
+
throw err;
|
|
6232
|
+
}
|
|
6233
|
+
return pool;
|
|
6234
|
+
}
|
|
6235
|
+
async function ensurePool(state) {
|
|
6236
|
+
if (state.workerPool !== null) return state.workerPool;
|
|
6237
|
+
if (state.poolStart === null) {
|
|
6238
|
+
state.poolStart = startPool(state).then(
|
|
6239
|
+
async (pool) => {
|
|
6240
|
+
state.workerPool = pool;
|
|
6241
|
+
state.poolStart = null;
|
|
6242
|
+
try {
|
|
6243
|
+
await announceRequestThreads(state, pool);
|
|
6244
|
+
} catch (err) {
|
|
6245
|
+
retirePool(state, pool);
|
|
6246
|
+
throw err;
|
|
6247
|
+
}
|
|
6248
|
+
return pool;
|
|
6249
|
+
},
|
|
6250
|
+
(err) => {
|
|
6251
|
+
state.poolStart = null;
|
|
6252
|
+
deferPoolRestart(state);
|
|
6253
|
+
throw err;
|
|
6254
|
+
}
|
|
6255
|
+
);
|
|
6256
|
+
}
|
|
6257
|
+
return state.poolStart;
|
|
6258
|
+
}
|
|
6259
|
+
function documentCountOf(state, indexName) {
|
|
6260
|
+
return state.executor.getManager(indexName)?.countDocuments() ?? 0;
|
|
6261
|
+
}
|
|
6262
|
+
function copyThresholdReason(state, indexName) {
|
|
6263
|
+
return `Index "${indexName}" holds ${documentCountOf(state, indexName)} documents, reaching the copy threshold of ${state.copyThreshold}`;
|
|
6264
|
+
}
|
|
6265
|
+
function indexReadyForCopies(state, indexName, incomingCount = 0) {
|
|
6266
|
+
if (!copiesAllowed(state)) return false;
|
|
6267
|
+
if (state.scaledOutIndexes.has(indexName) || state.desyncedIndexes.has(indexName)) return false;
|
|
6268
|
+
if (state.copyTransitions.has(indexName)) return false;
|
|
6269
|
+
if (state.callbacks?.shouldDeferCopies?.()) return false;
|
|
6270
|
+
const entry = state.indexRegistry.get(indexName);
|
|
6271
|
+
if (entry === void 0 || state.executor.getManager(indexName) === void 0) return false;
|
|
6272
|
+
if (documentCountOf(state, indexName) + incomingCount < state.copyThreshold) return false;
|
|
6273
|
+
const ineligibility = workerIneligibility(indexName, entry.config, state.bootstrapModule);
|
|
6274
|
+
if (ineligibility !== null) {
|
|
6275
|
+
reportIneligible(state, indexName, ineligibility);
|
|
6276
|
+
return false;
|
|
6277
|
+
}
|
|
6278
|
+
return true;
|
|
6279
|
+
}
|
|
6280
|
+
async function loadCopies(state, indexName, reason) {
|
|
6281
|
+
const entry = state.indexRegistry.get(indexName);
|
|
6282
|
+
const manager = state.executor.getManager(indexName);
|
|
6283
|
+
if (entry === void 0 || manager === void 0) return;
|
|
6284
|
+
const ineligibility = workerIneligibility(indexName, entry.config, state.bootstrapModule);
|
|
6285
|
+
if (ineligibility !== null) {
|
|
6286
|
+
reportIneligible(state, indexName, ineligibility);
|
|
6287
|
+
return;
|
|
6288
|
+
}
|
|
6289
|
+
const pool = await ensurePool(state);
|
|
6290
|
+
if (state.executor.getManager(indexName) === void 0) return;
|
|
6291
|
+
const dropReason = state.droppedCopies.get(indexName);
|
|
6292
|
+
const reload = state.droppedCopies.delete(indexName);
|
|
6293
|
+
const buffered = [];
|
|
6294
|
+
state.copyLoadBuffers.set(indexName, buffered);
|
|
6295
|
+
try {
|
|
6296
|
+
await transferIndexToPool(indexName, pool, entry.config, manager, state.callbacks?.isAnalysisStale?.(indexName));
|
|
6297
|
+
state.scaledOutIndexes.add(indexName);
|
|
6298
|
+
state.lastAccessAt.set(indexName, Date.now());
|
|
6299
|
+
state.poolRetryDelayMs = POOL_RESTART_DELAY_MS;
|
|
6300
|
+
for (const action of buffered) enqueueReplication(state, indexName, action);
|
|
6301
|
+
} catch (err) {
|
|
6302
|
+
if (dropReason !== void 0) state.droppedCopies.set(indexName, dropReason);
|
|
6303
|
+
throw err;
|
|
6304
|
+
} finally {
|
|
6305
|
+
state.copyLoadBuffers.delete(indexName);
|
|
6306
|
+
}
|
|
6307
|
+
if (reload) state.copyReloadCounts.set(indexName, (state.copyReloadCounts.get(indexName) ?? 0) + 1);
|
|
6308
|
+
refreshVectorCopies(state, indexName);
|
|
6309
|
+
scheduleIdleMerge(state, indexName);
|
|
6310
|
+
state.callbacks?.onCopiesLoaded?.(pool.workerCount, reason);
|
|
6311
|
+
}
|
|
6312
|
+
async function loadAfter(previous, state, indexName, reason) {
|
|
6313
|
+
if (previous !== void 0) await previous.done;
|
|
6314
|
+
if (state.poolRepair !== null) await state.poolRepair;
|
|
6315
|
+
if (!copiesAllowed(state)) return;
|
|
6316
|
+
if (state.scaledOutIndexes.has(indexName) || state.desyncedIndexes.has(indexName)) return;
|
|
6317
|
+
await loadCopies(state, indexName, reason);
|
|
6318
|
+
}
|
|
6319
|
+
function scaleOutIndex(state, indexName, reason) {
|
|
6320
|
+
const previous = state.copyTransitions.get(indexName);
|
|
6321
|
+
if (previous !== void 0 && previous.kind !== "drop") return previous.done;
|
|
6322
|
+
const kind = state.droppedCopies.has(indexName) ? "reload" : "load";
|
|
6323
|
+
const done = loadAfter(previous, state, indexName, reason).catch((err) => {
|
|
6324
|
+
const error = toError3(err);
|
|
6325
|
+
state.scaleOutBlocked = isDeterministicFailure(error);
|
|
6326
|
+
state.callbacks?.onCopyLoadFailure?.(reason, error, !state.scaleOutBlocked);
|
|
6327
|
+
}).finally(() => {
|
|
6328
|
+
if (state.copyTransitions.get(indexName)?.done === done) state.copyTransitions.delete(indexName);
|
|
6329
|
+
});
|
|
6330
|
+
state.copyTransitions.set(indexName, { kind, done });
|
|
6331
|
+
return done;
|
|
6332
|
+
}
|
|
6333
|
+
async function scaleOutReadyIndexes(state) {
|
|
6334
|
+
if (!copiesAllowed(state)) return;
|
|
6335
|
+
for (const indexName of state.indexRegistry.keys()) {
|
|
6336
|
+
if (!indexReadyForCopies(state, indexName)) continue;
|
|
6337
|
+
const reason = copyThresholdReason(state, indexName);
|
|
6338
|
+
setTimeout(() => {
|
|
6339
|
+
void scaleOutIndex(state, indexName, reason);
|
|
6340
|
+
}, 0);
|
|
6341
|
+
}
|
|
6342
|
+
}
|
|
6343
|
+
async function scaleOutBeforeBatch(state, indexName, incomingCount) {
|
|
6344
|
+
if (incomingCount <= 0) return;
|
|
6345
|
+
const pending = state.copyTransitions.get(indexName);
|
|
6346
|
+
if (pending !== void 0) {
|
|
6347
|
+
if (pending.kind === "load") await pending.done;
|
|
6348
|
+
return;
|
|
6349
|
+
}
|
|
6350
|
+
if (!indexReadyForCopies(state, indexName, incomingCount)) return;
|
|
6351
|
+
await scaleOutIndex(state, indexName, copyThresholdReason(state, indexName));
|
|
6352
|
+
}
|
|
6353
|
+
|
|
6354
|
+
// src/engine/orchestration/idle.ts
|
|
6355
|
+
function isIndexBusy(state, indexName) {
|
|
6356
|
+
return state.poolRepair !== null || state.copyTransitions.has(indexName) || state.replicationQueues.has(indexName) || state.compactionsInFlight.has(indexName);
|
|
6357
|
+
}
|
|
6358
|
+
function noteAccess(state, indexName) {
|
|
6359
|
+
state.lastAccessAt.set(indexName, Date.now());
|
|
6360
|
+
if (!copiesAllowed(state)) return;
|
|
6361
|
+
const reloadReason = state.droppedCopies.get(indexName);
|
|
6362
|
+
if (reloadReason !== void 0) {
|
|
6363
|
+
const transition = state.copyTransitions.get(indexName);
|
|
6364
|
+
if (transition !== void 0 && transition.kind !== "drop") return;
|
|
6365
|
+
void scaleOutIndex(state, indexName, reloadReason);
|
|
6366
|
+
return;
|
|
6367
|
+
}
|
|
6368
|
+
if (indexReadyForCopies(state, indexName)) {
|
|
6369
|
+
void scaleOutIndex(state, indexName, copyThresholdReason(state, indexName));
|
|
6370
|
+
}
|
|
6371
|
+
}
|
|
6372
|
+
async function releaseCopies(state, indexName) {
|
|
6373
|
+
const pool = state.workerPool;
|
|
6374
|
+
if (pool === null) return;
|
|
6375
|
+
await awaitReplicationIdle(state, indexName);
|
|
6376
|
+
const cutoff = Date.now() - state.copyIdleTimeoutMs;
|
|
6377
|
+
if ((state.lastAccessAt.get(indexName) ?? Date.now()) > cutoff) return;
|
|
6378
|
+
if (!state.scaledOutIndexes.has(indexName) || state.compactionsInFlight.has(indexName)) return;
|
|
6379
|
+
state.scaledOutIndexes.delete(indexName);
|
|
6380
|
+
state.droppedCopies.set(indexName, COPY_RELOAD_REASON);
|
|
6381
|
+
cancelIdleMerge(state, indexName);
|
|
6382
|
+
const outcomes = await Promise.allSettled(
|
|
6383
|
+
pool.executorsHolding(indexName).map((worker) => worker.execute({ type: "dropIndex", indexName, requestId: `idle-drop-${indexName}` }))
|
|
6384
|
+
);
|
|
6385
|
+
for (const outcome of outcomes) {
|
|
6386
|
+
if (outcome.status === "rejected") console.warn("Dropping an idle worker copy failed:", outcome.reason);
|
|
6387
|
+
}
|
|
6388
|
+
pool.removeIndex(indexName);
|
|
6389
|
+
state.segmentLedger.delete(indexName);
|
|
6390
|
+
}
|
|
6391
|
+
async function dropIdleCopies(state, indexName) {
|
|
6392
|
+
const previous = state.copyTransitions.get(indexName);
|
|
6393
|
+
if (previous !== void 0) return;
|
|
6394
|
+
const done = releaseCopies(state, indexName).catch((err) => {
|
|
6395
|
+
console.warn("Dropping idle worker copies failed:", err);
|
|
6396
|
+
}).finally(() => {
|
|
6397
|
+
if (state.copyTransitions.get(indexName)?.done === done) state.copyTransitions.delete(indexName);
|
|
6398
|
+
});
|
|
6399
|
+
state.copyTransitions.set(indexName, { kind: "drop", done });
|
|
6400
|
+
await done;
|
|
6401
|
+
}
|
|
6402
|
+
async function sweepIdleCopies(state) {
|
|
6403
|
+
const cutoff = Date.now() - state.copyIdleTimeoutMs;
|
|
6404
|
+
for (const indexName of [...state.scaledOutIndexes]) {
|
|
6405
|
+
if ((state.lastAccessAt.get(indexName) ?? Date.now()) > cutoff) continue;
|
|
6406
|
+
if (isIndexBusy(state, indexName)) continue;
|
|
6407
|
+
await dropIdleCopies(state, indexName);
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
function startIdleSweep(state) {
|
|
6411
|
+
if (!state.workersEnabled || state.idleSweep !== null) return;
|
|
6412
|
+
const interval = Math.min(MAX_SWEEP_INTERVAL_MS, Math.max(MIN_SWEEP_INTERVAL_MS, state.copyIdleTimeoutMs / 2));
|
|
6413
|
+
const timer = setInterval(() => void sweepIdleCopies(state).catch(() => void 0), interval);
|
|
6414
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
6415
|
+
state.idleSweep = timer;
|
|
6416
|
+
}
|
|
6417
|
+
function stopIdleSweep(state) {
|
|
6418
|
+
if (state.idleSweep === null) return;
|
|
6419
|
+
clearInterval(state.idleSweep);
|
|
6420
|
+
state.idleSweep = null;
|
|
5881
6421
|
}
|
|
5882
6422
|
|
|
5883
6423
|
// src/engine/orchestration/search.ts
|
|
5884
|
-
function
|
|
5885
|
-
const assignments = Array.from({ length:
|
|
5886
|
-
for (let
|
|
5887
|
-
assignments[
|
|
6424
|
+
function partitionsPerLease(scope, leaseCount) {
|
|
6425
|
+
const assignments = Array.from({ length: leaseCount }, () => []);
|
|
6426
|
+
for (let at = 0; at < scope.length; at++) {
|
|
6427
|
+
assignments[at % leaseCount].push(scope[at]);
|
|
5888
6428
|
}
|
|
5889
6429
|
return assignments;
|
|
5890
6430
|
}
|
|
@@ -5902,67 +6442,74 @@ function mergeWorkerResults(results) {
|
|
|
5902
6442
|
facets: workerFacets.length > 0 ? mergeFacets(workerFacets) : void 0
|
|
5903
6443
|
};
|
|
5904
6444
|
}
|
|
6445
|
+
function scoresPerPartition(state, indexName, params, globalStats) {
|
|
6446
|
+
const mode = params.scoring ?? state.indexRegistry.get(indexName)?.config.defaultScoring ?? "local";
|
|
6447
|
+
if (mode === "local") return true;
|
|
6448
|
+
return mode === "broadcast" && globalStats !== void 0;
|
|
6449
|
+
}
|
|
6450
|
+
function queryAction(indexName, params, globalStats, partitionIds) {
|
|
6451
|
+
return {
|
|
6452
|
+
type: "query",
|
|
6453
|
+
indexName,
|
|
6454
|
+
params,
|
|
6455
|
+
requestId: createRequestId(),
|
|
6456
|
+
...partitionIds !== void 0 ? { partitionIds } : {},
|
|
6457
|
+
...globalStats !== void 0 ? { globalStats } : {}
|
|
6458
|
+
};
|
|
6459
|
+
}
|
|
6460
|
+
async function runSplit(leases, scope, indexName, params, globalStats) {
|
|
6461
|
+
const assignments = partitionsPerLease(scope, leases.length);
|
|
6462
|
+
const results = await Promise.all(
|
|
6463
|
+
assignments.map(
|
|
6464
|
+
(partitionIds, at) => leases[at].executor.execute(queryAction(indexName, params, globalStats, partitionIds)).finally(() => leases[at].release())
|
|
6465
|
+
)
|
|
6466
|
+
);
|
|
6467
|
+
return mergeWorkerResults(results);
|
|
6468
|
+
}
|
|
6469
|
+
function takeMainCopyTurn(state) {
|
|
6470
|
+
if (state.mainCopyQueries === "none" || state.mainCopyTurnTaken) return false;
|
|
6471
|
+
state.mainCopyTurnTaken = true;
|
|
6472
|
+
afterCurrentTurn(() => {
|
|
6473
|
+
state.mainCopyTurnTaken = false;
|
|
6474
|
+
});
|
|
6475
|
+
return true;
|
|
6476
|
+
}
|
|
6477
|
+
function answersFasterOnMainCopy(state, pool, manager) {
|
|
6478
|
+
if (pool.queriesInFlight() > 0) return false;
|
|
6479
|
+
if (manager.countDocuments() > MAIN_COPY_LONE_QUERY_DOCUMENTS) return false;
|
|
6480
|
+
return takeMainCopyTurn(state);
|
|
6481
|
+
}
|
|
5905
6482
|
async function searchViaWorker(state, indexName, params, globalStats, partitionIds) {
|
|
5906
6483
|
const pool = state.workerPool;
|
|
5907
6484
|
if (!pool) return null;
|
|
5908
|
-
if (!state.
|
|
5909
|
-
if (state.awaitingBufferedWrites.has(indexName)) return null;
|
|
5910
|
-
const pendingReplication = state.replicationQueues.get(indexName);
|
|
5911
|
-
if (pendingReplication !== void 0 && pendingReplication.pendingActions > 0) return null;
|
|
6485
|
+
if (!state.scaledOutIndexes.has(indexName) || state.copyLoadBuffers.has(indexName)) return null;
|
|
5912
6486
|
const manager = state.executor.getManager(indexName);
|
|
5913
6487
|
if (!manager) return null;
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
const
|
|
5917
|
-
|
|
5918
|
-
try {
|
|
5919
|
-
return await allExecutors[0].execute({
|
|
5920
|
-
type: "query",
|
|
5921
|
-
indexName,
|
|
5922
|
-
params,
|
|
5923
|
-
requestId: createRequestId(),
|
|
5924
|
-
...partitionIds !== void 0 ? { partitionIds } : {},
|
|
5925
|
-
...stats
|
|
5926
|
-
});
|
|
5927
|
-
} catch (err) {
|
|
5928
|
-
console.warn("Worker search failed, falling back to local:", err);
|
|
5929
|
-
return null;
|
|
5930
|
-
}
|
|
5931
|
-
}
|
|
5932
|
-
const assignments = partitionsPerWorker(manager.partitionCount, allExecutors.length);
|
|
5933
|
-
const scopedAssignments = partitionIds === void 0 ? assignments : assignments.map((assigned) => assigned.filter((partitionId) => partitionIds.includes(partitionId)));
|
|
5934
|
-
const activeAssignments = [];
|
|
5935
|
-
for (let index = 0; index < allExecutors.length; index++) {
|
|
5936
|
-
if (scopedAssignments[index].length > 0) {
|
|
5937
|
-
activeAssignments.push({ executor: allExecutors[index], partitionIds: scopedAssignments[index] });
|
|
5938
|
-
}
|
|
5939
|
-
}
|
|
5940
|
-
if (activeAssignments.length === 0) {
|
|
5941
|
-
return { scored: [], totalMatched: 0 };
|
|
5942
|
-
}
|
|
6488
|
+
if (answersFasterOnMainCopy(state, pool, manager)) return null;
|
|
6489
|
+
const scope = partitionIds ?? Array.from({ length: manager.partitionCount }, (_, partitionId) => partitionId);
|
|
6490
|
+
const idle = pool.leaseIdle(scope.length);
|
|
6491
|
+
const leases = [];
|
|
5943
6492
|
try {
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
})
|
|
5954
|
-
)
|
|
5955
|
-
);
|
|
5956
|
-
return mergeWorkerResults(results);
|
|
6493
|
+
if (idle.length >= 2 && scoresPerPartition(state, indexName, params, globalStats)) {
|
|
6494
|
+
leases.push(...idle);
|
|
6495
|
+
return await runSplit(leases, scope, indexName, params, globalStats);
|
|
6496
|
+
}
|
|
6497
|
+
for (const lease2 of idle.slice(1)) lease2.release();
|
|
6498
|
+
const lease = idle[0] ?? (takeMainCopyTurn(state) ? null : pool.leaseLeastBusy());
|
|
6499
|
+
if (lease === null) return null;
|
|
6500
|
+
leases.push(lease);
|
|
6501
|
+
return await lease.executor.execute(queryAction(indexName, params, globalStats, partitionIds));
|
|
5957
6502
|
} catch (err) {
|
|
5958
|
-
console.warn("
|
|
6503
|
+
console.warn("Worker search failed, falling back to local:", err);
|
|
5959
6504
|
return null;
|
|
6505
|
+
} finally {
|
|
6506
|
+
for (const lease of leases) lease.release();
|
|
5960
6507
|
}
|
|
5961
6508
|
}
|
|
5962
6509
|
|
|
5963
6510
|
// src/engine/orchestration/segments.ts
|
|
5964
6511
|
function segmentBuildConcurrency(state, indexName) {
|
|
5965
|
-
if (!state.
|
|
6512
|
+
if (!state.scaledOutIndexes.has(indexName)) return 0;
|
|
5966
6513
|
return state.workerPool?.workerCount ?? 0;
|
|
5967
6514
|
}
|
|
5968
6515
|
async function buildSegments(state, requests) {
|
|
@@ -5979,44 +6526,59 @@ async function buildSegments(state, requests) {
|
|
|
5979
6526
|
}
|
|
5980
6527
|
|
|
5981
6528
|
// src/engine/orchestration/index.ts
|
|
5982
|
-
function
|
|
6529
|
+
function workersEnabledByDefault() {
|
|
6530
|
+
return detectRuntime().runtime !== "browser";
|
|
6531
|
+
}
|
|
6532
|
+
function copyIdleTimeoutBeforeClose(closeAfterMs) {
|
|
6533
|
+
return closeAfterMs === void 0 ? DEFAULT_COPY_IDLE_TIMEOUT_MS : Math.min(DEFAULT_COPY_IDLE_TIMEOUT_MS, closeAfterMs);
|
|
6534
|
+
}
|
|
6535
|
+
function createWorkerOrchestrator(config, executor, indexRegistry, callbacks, vectorCopyPolicy) {
|
|
5983
6536
|
const state = {
|
|
5984
6537
|
config,
|
|
5985
6538
|
executor,
|
|
5986
|
-
promoter,
|
|
5987
6539
|
indexRegistry,
|
|
5988
6540
|
callbacks,
|
|
5989
|
-
|
|
6541
|
+
vectorCopyPolicy,
|
|
6542
|
+
workersEnabled: config?.workers?.enabled ?? workersEnabledByDefault(),
|
|
6543
|
+
keywordWorkerCount: splitWorkerBudget(resolveWorkerCount(config?.workers?.count)).keyword,
|
|
6544
|
+
requestThreads: null,
|
|
6545
|
+
copyThreshold: config?.workers?.promotionThreshold ?? DEFAULT_COPY_THRESHOLD,
|
|
6546
|
+
copyIdleTimeoutMs: config?.workers?.idleTimeoutMs ?? copyIdleTimeoutBeforeClose(config?.lifecycle?.idleTimeoutMs),
|
|
5990
6547
|
bootstrapModule: config?.workers?.bootstrapModule,
|
|
5991
|
-
promotionBuffer: [],
|
|
5992
|
-
awaitingBufferedWrites: /* @__PURE__ */ new Set(),
|
|
5993
6548
|
reportedIneligible: /* @__PURE__ */ new Set(),
|
|
5994
|
-
|
|
6549
|
+
scaledOutIndexes: /* @__PURE__ */ new Set(),
|
|
6550
|
+
desyncedIndexes: /* @__PURE__ */ new Set(),
|
|
6551
|
+
copyLoadBuffers: /* @__PURE__ */ new Map(),
|
|
6552
|
+
copyTransitions: /* @__PURE__ */ new Map(),
|
|
6553
|
+
droppedCopies: /* @__PURE__ */ new Map(),
|
|
6554
|
+
lastAccessAt: /* @__PURE__ */ new Map(),
|
|
6555
|
+
copyReloadCounts: /* @__PURE__ */ new Map(),
|
|
5995
6556
|
replicationQueues: /* @__PURE__ */ new Map(),
|
|
5996
6557
|
segmentLedger: /* @__PURE__ */ new Map(),
|
|
5997
6558
|
compactionsInFlight: /* @__PURE__ */ new Map(),
|
|
6559
|
+
idleMergeTimers: /* @__PURE__ */ new Map(),
|
|
5998
6560
|
workerPool: null,
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6561
|
+
poolStart: null,
|
|
6562
|
+
poolRetryAt: 0,
|
|
6563
|
+
poolRetryDelayMs: POOL_RESTART_DELAY_MS,
|
|
6564
|
+
poolRepair: null,
|
|
6565
|
+
mainCopyTurnTaken: false,
|
|
6566
|
+
mainCopyQueries: config?.workers?.mainCopyQueries,
|
|
6567
|
+
repairTimer: null,
|
|
6568
|
+
scaleOutBlocked: false,
|
|
6569
|
+
shuttingDown: false,
|
|
6570
|
+
idleSweep: null
|
|
6002
6571
|
};
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
if (!
|
|
6012
|
-
|
|
6013
|
-
const manager = executor.getManager(indexName);
|
|
6014
|
-
if (!manager) return;
|
|
6015
|
-
state.promotedIndexes.delete(indexName);
|
|
6016
|
-
await awaitReplicationIdle(state, indexName);
|
|
6017
|
-
state.segmentLedger.delete(indexName);
|
|
6018
|
-
await transferIndexToPool(indexName, pool, entry.config, manager);
|
|
6019
|
-
state.promotedIndexes.add(indexName);
|
|
6572
|
+
startIdleSweep(state);
|
|
6573
|
+
async function resyncIndex(indexName, wasScaledOut) {
|
|
6574
|
+
state.desyncedIndexes.delete(indexName);
|
|
6575
|
+
if (!wasScaledOut) return;
|
|
6576
|
+
await scaleOutIndex(state, indexName, copyThresholdReason(state, indexName));
|
|
6577
|
+
}
|
|
6578
|
+
async function openIndex(indexName) {
|
|
6579
|
+
state.droppedCopies.delete(indexName);
|
|
6580
|
+
if (!indexReadyForCopies(state, indexName)) return;
|
|
6581
|
+
await scaleOutIndex(state, indexName, copyThresholdReason(state, indexName));
|
|
6020
6582
|
}
|
|
6021
6583
|
async function getWorkerMemoryStats() {
|
|
6022
6584
|
if (!state.workerPool) return [];
|
|
@@ -6025,91 +6587,140 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
|
|
|
6025
6587
|
workerId: report.workerId,
|
|
6026
6588
|
heapUsed: report.heapUsed,
|
|
6027
6589
|
heapTotal: report.heapTotal,
|
|
6590
|
+
heapLimit: report.heapLimit,
|
|
6028
6591
|
external: report.external
|
|
6029
6592
|
}));
|
|
6030
6593
|
}
|
|
6594
|
+
function workerCopies() {
|
|
6595
|
+
const reports = [];
|
|
6596
|
+
for (const indexName of indexRegistry.keys()) {
|
|
6597
|
+
reports.push({
|
|
6598
|
+
indexName,
|
|
6599
|
+
scaledOut: state.scaledOutIndexes.has(indexName),
|
|
6600
|
+
reloadCount: state.copyReloadCounts.get(indexName) ?? 0
|
|
6601
|
+
});
|
|
6602
|
+
}
|
|
6603
|
+
return reports;
|
|
6604
|
+
}
|
|
6031
6605
|
async function shutdown() {
|
|
6606
|
+
state.shuttingDown = true;
|
|
6607
|
+
stopIdleSweep(state);
|
|
6608
|
+
cancelRepair(state);
|
|
6609
|
+
stopRequestThreads(state);
|
|
6610
|
+
for (const indexName of [...state.idleMergeTimers.keys()]) cancelIdleMerge(state, indexName);
|
|
6611
|
+
await Promise.allSettled([...state.copyTransitions.values()].map((transition) => transition.done));
|
|
6612
|
+
if (state.poolRepair !== null) await state.poolRepair;
|
|
6613
|
+
if (state.poolStart !== null) await state.poolStart.catch(() => void 0);
|
|
6032
6614
|
if (state.workerPool) {
|
|
6033
6615
|
await awaitCompactions(state);
|
|
6034
6616
|
await awaitReplicationIdle(state);
|
|
6035
6617
|
await state.workerPool.shutdown();
|
|
6036
6618
|
state.workerPool = null;
|
|
6037
|
-
state.
|
|
6619
|
+
state.scaledOutIndexes.clear();
|
|
6038
6620
|
state.replicationQueues.clear();
|
|
6039
6621
|
state.segmentLedger.clear();
|
|
6040
6622
|
}
|
|
6041
6623
|
}
|
|
6042
|
-
async function
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
await
|
|
6624
|
+
async function restoreAfterFailedClose(indexName, failure) {
|
|
6625
|
+
const pool = state.workerPool;
|
|
6626
|
+
const entry = indexRegistry.get(indexName);
|
|
6627
|
+
const manager = executor.getManager(indexName);
|
|
6628
|
+
if (pool !== null && entry !== void 0 && manager !== void 0) {
|
|
6629
|
+
try {
|
|
6630
|
+
await transferIndexToPool(indexName, pool, entry.config, manager, callbacks?.isAnalysisStale?.(indexName));
|
|
6631
|
+
state.scaledOutIndexes.add(indexName);
|
|
6632
|
+
refreshVectorCopies(state, indexName);
|
|
6633
|
+
} catch (repairError) {
|
|
6634
|
+
throw new NarsilError(
|
|
6635
|
+
ErrorCodes.WORKER_CRASHED,
|
|
6636
|
+
`Worker copies for index "${indexName}" could not be restored after close failed`,
|
|
6637
|
+
{
|
|
6638
|
+
indexName,
|
|
6639
|
+
closeError: failure.reason instanceof Error ? failure.reason.message : String(failure.reason),
|
|
6640
|
+
repairError: repairError instanceof Error ? repairError.message : String(repairError)
|
|
6641
|
+
}
|
|
6642
|
+
);
|
|
6049
6643
|
}
|
|
6050
6644
|
}
|
|
6645
|
+
if (failure.reason instanceof NarsilError) throw failure.reason;
|
|
6646
|
+
throw new NarsilError(ErrorCodes.WORKER_CRASHED, `A worker could not close index "${indexName}"`, {
|
|
6647
|
+
indexName,
|
|
6648
|
+
cause: failure.reason instanceof Error ? failure.reason.message : String(failure.reason)
|
|
6649
|
+
});
|
|
6650
|
+
}
|
|
6651
|
+
function withdrawCopies(indexName) {
|
|
6652
|
+
const pool = state.workerPool;
|
|
6653
|
+
if (pool === null) return;
|
|
6654
|
+
const holders = pool.executorsHolding(indexName);
|
|
6655
|
+
pool.removeIndex(indexName);
|
|
6656
|
+
state.segmentLedger.delete(indexName);
|
|
6657
|
+
void Promise.allSettled(
|
|
6658
|
+
holders.map((worker) => worker.execute({ type: "dropIndex", indexName, requestId: `desync-drop-${indexName}` }))
|
|
6659
|
+
);
|
|
6051
6660
|
}
|
|
6052
6661
|
async function closeIndex(indexName) {
|
|
6053
|
-
|
|
6662
|
+
if (state.poolRepair !== null) await state.poolRepair;
|
|
6663
|
+
const transition = state.copyTransitions.get(indexName);
|
|
6664
|
+
if (transition !== void 0) await transition.done;
|
|
6054
6665
|
await awaitReplicationIdle(state, indexName);
|
|
6055
6666
|
await awaitCompactions(state);
|
|
6667
|
+
cancelIdleMerge(state, indexName);
|
|
6056
6668
|
const pool = state.workerPool;
|
|
6057
|
-
if (pool !== null && state.
|
|
6669
|
+
if (pool !== null && state.scaledOutIndexes.has(indexName)) {
|
|
6670
|
+
state.scaledOutIndexes.delete(indexName);
|
|
6058
6671
|
const outcomes = await Promise.allSettled(
|
|
6059
|
-
pool.
|
|
6672
|
+
pool.executorsHolding(indexName).map((worker) => worker.execute({ type: "dropIndex", indexName, requestId: `close-${indexName}` }))
|
|
6060
6673
|
);
|
|
6061
6674
|
const failure = outcomes.find((outcome) => outcome.status === "rejected");
|
|
6062
|
-
if (failure?.status === "rejected")
|
|
6063
|
-
const entry = indexRegistry.get(indexName);
|
|
6064
|
-
const manager = executor.getManager(indexName);
|
|
6065
|
-
if (entry !== void 0 && manager !== void 0) {
|
|
6066
|
-
try {
|
|
6067
|
-
await transferIndexToPool(indexName, pool, entry.config, manager);
|
|
6068
|
-
} catch (repairError) {
|
|
6069
|
-
throw new NarsilError(
|
|
6070
|
-
ErrorCodes.WORKER_CRASHED,
|
|
6071
|
-
`Worker copies for index "${indexName}" could not be restored after close failed`,
|
|
6072
|
-
{
|
|
6073
|
-
indexName,
|
|
6074
|
-
closeError: failure.reason instanceof Error ? failure.reason.message : String(failure.reason),
|
|
6075
|
-
repairError: repairError instanceof Error ? repairError.message : String(repairError)
|
|
6076
|
-
}
|
|
6077
|
-
);
|
|
6078
|
-
}
|
|
6079
|
-
}
|
|
6080
|
-
if (failure.reason instanceof NarsilError) throw failure.reason;
|
|
6081
|
-
throw new NarsilError(ErrorCodes.WORKER_CRASHED, `A worker could not close index "${indexName}"`, {
|
|
6082
|
-
indexName,
|
|
6083
|
-
cause: failure.reason instanceof Error ? failure.reason.message : String(failure.reason)
|
|
6084
|
-
});
|
|
6085
|
-
}
|
|
6675
|
+
if (failure?.status === "rejected") await restoreAfterFailedClose(indexName, failure);
|
|
6086
6676
|
pool.removeIndex(indexName);
|
|
6087
6677
|
}
|
|
6088
|
-
state.
|
|
6678
|
+
state.droppedCopies.delete(indexName);
|
|
6679
|
+
state.desyncedIndexes.delete(indexName);
|
|
6680
|
+
state.lastAccessAt.delete(indexName);
|
|
6089
6681
|
state.replicationQueues.delete(indexName);
|
|
6090
6682
|
state.segmentLedger.delete(indexName);
|
|
6091
6683
|
}
|
|
6092
6684
|
async function replicate(action) {
|
|
6093
6685
|
await replicateToWorkers(state, action);
|
|
6094
|
-
if (
|
|
6095
|
-
|
|
6686
|
+
if (!("indexName" in action)) return;
|
|
6687
|
+
if (action.type === "attachSegments") maybeCompactSegments(state, action.indexName);
|
|
6688
|
+
if (action.type === "insert" || action.type === "update") flushGrownTails(state, action.indexName);
|
|
6689
|
+
if (state.scaledOutIndexes.has(action.indexName) || state.copyLoadBuffers.has(action.indexName)) {
|
|
6690
|
+
scheduleIdleMerge(state, action.indexName);
|
|
6096
6691
|
}
|
|
6097
6692
|
}
|
|
6098
6693
|
return {
|
|
6099
|
-
|
|
6100
|
-
|
|
6694
|
+
scaleOutReadyIndexes: () => scaleOutReadyIndexes(state),
|
|
6695
|
+
scaleOutBeforeBatch: (indexName, incomingCount) => scaleOutBeforeBatch(state, indexName, incomingCount),
|
|
6101
6696
|
replicateToWorkers: replicate,
|
|
6102
6697
|
awaitReplication: (indexName) => awaitReplicationIdle(state, indexName),
|
|
6698
|
+
awaitWrites: (indexName) => awaitWritesApplied(state, indexName),
|
|
6103
6699
|
awaitCompactions: () => awaitCompactions(state),
|
|
6104
|
-
openIndex
|
|
6700
|
+
openIndex,
|
|
6105
6701
|
closeIndex,
|
|
6106
|
-
isIndexBusy: (indexName) => state
|
|
6702
|
+
isIndexBusy: (indexName) => isIndexBusy(state, indexName),
|
|
6107
6703
|
buildSegments: (requests) => buildSegments(state, requests),
|
|
6108
6704
|
segmentBuildConcurrency: (indexName) => segmentBuildConcurrency(state, indexName),
|
|
6109
6705
|
searchViaWorker: (indexName, params, globalStats, partitionIds) => searchViaWorker(state, indexName, params, globalStats, partitionIds),
|
|
6110
|
-
|
|
6111
|
-
|
|
6706
|
+
hasWorkerPool: () => state.workerPool !== null,
|
|
6707
|
+
mainCopyQueries: () => state.mainCopyQueries ?? "lone",
|
|
6708
|
+
shareMainThread: () => {
|
|
6709
|
+
if (state.mainCopyQueries === void 0) state.mainCopyQueries = "none";
|
|
6710
|
+
},
|
|
6711
|
+
serveRequestsOnWorkers: (listener) => serveRequestsOnWorkers(state, listener),
|
|
6712
|
+
requestThreadCount: () => requestThreadCountOf(state),
|
|
6713
|
+
copyIdleTimeoutMs: () => state.copyIdleTimeoutMs,
|
|
6714
|
+
stopRequestThreads: () => stopRequestThreads(state),
|
|
6715
|
+
desyncIndex: (indexName) => {
|
|
6716
|
+
state.desyncedIndexes.add(indexName);
|
|
6717
|
+
const wasScaledOut = state.scaledOutIndexes.delete(indexName);
|
|
6718
|
+
if (wasScaledOut) withdrawCopies(indexName);
|
|
6719
|
+
return wasScaledOut;
|
|
6720
|
+
},
|
|
6112
6721
|
resyncIndex,
|
|
6722
|
+
noteAccess: (indexName) => noteAccess(state, indexName),
|
|
6723
|
+
workerCopies,
|
|
6113
6724
|
getWorkerMemoryStats,
|
|
6114
6725
|
shutdown
|
|
6115
6726
|
};
|
|
@@ -6120,47 +6731,15 @@ function getVectorFieldPaths(schema) {
|
|
|
6120
6731
|
return new Set(extractVectorFieldsFromSchema(schema).keys());
|
|
6121
6732
|
}
|
|
6122
6733
|
|
|
6123
|
-
// src/engine/watermark.ts
|
|
6124
|
-
function createWatermarkNotifier(deps) {
|
|
6125
|
-
const latchedCapacity = /* @__PURE__ */ new Map();
|
|
6126
|
-
function check(indexName) {
|
|
6127
|
-
const partitionConfig = deps.getPartitionConfig(indexName);
|
|
6128
|
-
const maxDocs = partitionConfig?.maxDocsPerPartition;
|
|
6129
|
-
const watermark = partitionConfig?.watermark;
|
|
6130
|
-
if (maxDocs === void 0 || watermark === void 0) {
|
|
6131
|
-
latchedCapacity.delete(indexName);
|
|
6132
|
-
return;
|
|
6133
|
-
}
|
|
6134
|
-
const manager = deps.getManager(indexName);
|
|
6135
|
-
if (!manager) {
|
|
6136
|
-
latchedCapacity.delete(indexName);
|
|
6137
|
-
return;
|
|
6138
|
-
}
|
|
6139
|
-
const partitionCount = manager.partitionCount;
|
|
6140
|
-
const capacity = maxDocs * partitionCount;
|
|
6141
|
-
const documentCount = manager.countDocuments();
|
|
6142
|
-
const threshold = watermark * capacity;
|
|
6143
|
-
if (documentCount < threshold) {
|
|
6144
|
-
latchedCapacity.delete(indexName);
|
|
6145
|
-
return;
|
|
6146
|
-
}
|
|
6147
|
-
if (latchedCapacity.get(indexName) === capacity) return;
|
|
6148
|
-
latchedCapacity.set(indexName, capacity);
|
|
6149
|
-
deps.emit({ indexName, documentCount, capacity, partitionCount });
|
|
6150
|
-
}
|
|
6151
|
-
function forget(indexName) {
|
|
6152
|
-
latchedCapacity.delete(indexName);
|
|
6153
|
-
}
|
|
6154
|
-
return { check, forget };
|
|
6155
|
-
}
|
|
6156
|
-
|
|
6157
6734
|
// src/engine/core.ts
|
|
6158
6735
|
function createEngineCore(config, hooks) {
|
|
6159
|
-
|
|
6160
|
-
const
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6736
|
+
validateWorkerConfig(config?.workers, config?.lifecycle);
|
|
6737
|
+
const vectorWorkerCount = splitWorkerBudget(resolveWorkerCount(config?.workers?.count)).vector;
|
|
6738
|
+
const vectorCopyPolicy = {
|
|
6739
|
+
enabled: (config?.workers?.enabled ?? workersEnabledByDefault()) && vectorWorkerCount > 0,
|
|
6740
|
+
count: vectorWorkerCount
|
|
6741
|
+
};
|
|
6742
|
+
const executor = createDirectExecutor({ vectorWorkerCopies: vectorCopyPolicy });
|
|
6164
6743
|
const pluginRegistry = createPluginRegistry();
|
|
6165
6744
|
if (config?.plugins) {
|
|
6166
6745
|
for (const plugin of config.plugins) pluginRegistry.register(plugin);
|
|
@@ -6178,36 +6757,38 @@ function createEngineCore(config, hooks) {
|
|
|
6178
6757
|
const shutdownState = { isShutdown: false };
|
|
6179
6758
|
const abortController = new AbortController();
|
|
6180
6759
|
const rebalancingIndexes = /* @__PURE__ */ new Set();
|
|
6181
|
-
const orchestrator = createWorkerOrchestrator(
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6760
|
+
const orchestrator = createWorkerOrchestrator(
|
|
6761
|
+
config,
|
|
6762
|
+
executor,
|
|
6763
|
+
indexRegistry,
|
|
6764
|
+
{
|
|
6765
|
+
shouldDeferCopies() {
|
|
6766
|
+
return rebalancingIndexes.size > 0;
|
|
6767
|
+
},
|
|
6768
|
+
isAnalysisStale(indexName) {
|
|
6769
|
+
return analysisRebuild.isStale(indexName);
|
|
6770
|
+
},
|
|
6771
|
+
onCopiesLoaded(workerCount, reason) {
|
|
6772
|
+
emitEngineEvent(eventHandlers, "workerPromote", { workerCount, reason });
|
|
6773
|
+
void Promise.resolve(pluginRegistry.runHook("onWorkerPromote", { workerCount, reason })).catch(
|
|
6774
|
+
(err) => {
|
|
6775
|
+
console.warn("onWorkerPromote plugin hook failed:", err instanceof Error ? err.message : String(err));
|
|
6776
|
+
}
|
|
6777
|
+
);
|
|
6778
|
+
},
|
|
6779
|
+
onCopyLoadFailure(reason, error, retryable) {
|
|
6780
|
+
if (emitEngineEvent(eventHandlers, "workerPromoteFailure", { reason, error, retryable }) === 0) {
|
|
6781
|
+
console.warn(`Loading worker copies failed (${reason}):`, error);
|
|
6782
|
+
}
|
|
6783
|
+
},
|
|
6784
|
+
onWorkerCrash(workerId, indexNames, error) {
|
|
6785
|
+
if (emitEngineEvent(eventHandlers, "workerCrash", { workerId, indexNames, error }) === 0) {
|
|
6786
|
+
console.warn(`Worker ${workerId} crashed:`, error);
|
|
6787
|
+
}
|
|
6199
6788
|
}
|
|
6200
|
-
for (const handler of handlers) handler({ reason, error, retryable });
|
|
6201
6789
|
},
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
if (!handlers || handlers.size === 0) {
|
|
6205
|
-
console.warn(`Worker ${workerId} crashed:`, error);
|
|
6206
|
-
return;
|
|
6207
|
-
}
|
|
6208
|
-
for (const handler of handlers) handler({ workerId, indexNames, error });
|
|
6209
|
-
}
|
|
6210
|
-
});
|
|
6790
|
+
vectorCopyPolicy
|
|
6791
|
+
);
|
|
6211
6792
|
const rebalancer = createRebalancer();
|
|
6212
6793
|
const rebalanceRouter = createPartitionRouter();
|
|
6213
6794
|
const waqMap = /* @__PURE__ */ new Map();
|
|
@@ -6316,10 +6897,7 @@ function createEngineCore(config, hooks) {
|
|
|
6316
6897
|
indexRegistry,
|
|
6317
6898
|
createIndexFromMetadata,
|
|
6318
6899
|
emitFatalError(error) {
|
|
6319
|
-
|
|
6320
|
-
if (handlers) {
|
|
6321
|
-
for (const handler of handlers) handler({ error });
|
|
6322
|
-
}
|
|
6900
|
+
emitEngineEvent(eventHandlers, "durabilityError", { error });
|
|
6323
6901
|
},
|
|
6324
6902
|
publishCheckpointedPartitions: (indexName, partitions) => invalidation?.publishPartitions(indexName, partitions) ?? Promise.resolve(),
|
|
6325
6903
|
recordCheckpoint(indexName, documentCount, partitionCount) {
|
|
@@ -6342,29 +6920,18 @@ function createEngineCore(config, hooks) {
|
|
|
6342
6920
|
},
|
|
6343
6921
|
reloadIndex: (indexName) => durability?.manager.reloadIndex?.(indexName) ?? Promise.resolve(),
|
|
6344
6922
|
onError(error) {
|
|
6345
|
-
|
|
6346
|
-
if (!handlers || handlers.size === 0) {
|
|
6923
|
+
if (emitEngineEvent(eventHandlers, "invalidationError", { error }) === 0) {
|
|
6347
6924
|
console.warn("Invalidation error:", error);
|
|
6348
|
-
return;
|
|
6349
|
-
}
|
|
6350
|
-
for (const handler of handlers) handler({ error });
|
|
6351
|
-
}
|
|
6352
|
-
});
|
|
6353
|
-
const watermarkNotifier = createWatermarkNotifier({
|
|
6354
|
-
getManager: (indexName) => executor.getManager(indexName),
|
|
6355
|
-
getPartitionConfig: (indexName) => indexRegistry.get(indexName)?.config.partitions,
|
|
6356
|
-
emit(payload) {
|
|
6357
|
-
const handlers = eventHandlers.get("partitionWatermark");
|
|
6358
|
-
if (!handlers) return;
|
|
6359
|
-
for (const handler of handlers) {
|
|
6360
|
-
try {
|
|
6361
|
-
handler(payload);
|
|
6362
|
-
} catch (err) {
|
|
6363
|
-
console.warn("partitionWatermark handler error:", err instanceof Error ? err.message : String(err));
|
|
6364
|
-
}
|
|
6365
6925
|
}
|
|
6366
6926
|
}
|
|
6367
6927
|
});
|
|
6928
|
+
const notifierWiring = {
|
|
6929
|
+
eventHandlers,
|
|
6930
|
+
indexRegistry,
|
|
6931
|
+
getManager: (indexName) => executor.getManager(indexName)
|
|
6932
|
+
};
|
|
6933
|
+
const watermarkNotifier = wireWatermarkNotifier(notifierWiring);
|
|
6934
|
+
const heapPressureNotifier = wireHeapPressureNotifier(notifierWiring);
|
|
6368
6935
|
const analysisRebuild = wireAnalysisRebuild({
|
|
6369
6936
|
config: config?.analysis,
|
|
6370
6937
|
eventHandlers,
|
|
@@ -6383,7 +6950,11 @@ function createEngineCore(config, hooks) {
|
|
|
6383
6950
|
rebalancingIndexes,
|
|
6384
6951
|
requireManager,
|
|
6385
6952
|
onClose: hooks?.onIndexClose,
|
|
6386
|
-
onOpen:
|
|
6953
|
+
onOpen: (indexName) => {
|
|
6954
|
+
heapPressureNotifier.check(indexName);
|
|
6955
|
+
return hooks?.onIndexOpen?.(indexName);
|
|
6956
|
+
},
|
|
6957
|
+
onAccess: (indexName) => orchestrator.noteAccess(indexName)
|
|
6387
6958
|
});
|
|
6388
6959
|
const mutationCtx = {
|
|
6389
6960
|
executor,
|
|
@@ -6396,11 +6967,13 @@ function createEngineCore(config, hooks) {
|
|
|
6396
6967
|
requireIndex,
|
|
6397
6968
|
requireManager,
|
|
6398
6969
|
bufferIfRebalancing,
|
|
6970
|
+
awaitRebalanceReplay: (indexName) => waqMap.get(indexName)?.whenReplayed() ?? Promise.resolve(),
|
|
6399
6971
|
isRebalancing: (indexName) => rebalancingIndexes.has(indexName),
|
|
6400
6972
|
pendingRebalanceWrites: (indexName) => waqMap.get(indexName)?.size ?? 0,
|
|
6401
6973
|
rebalanceTargetPartitionCount: (indexName) => rebalanceTargets.get(indexName),
|
|
6402
6974
|
bufferedDocState: (indexName, docId) => waqMap.get(indexName)?.bufferedDocState(docId),
|
|
6403
|
-
checkWatermark: watermarkNotifier.check
|
|
6975
|
+
checkWatermark: watermarkNotifier.check,
|
|
6976
|
+
checkHeapPressure: heapPressureNotifier.check
|
|
6404
6977
|
};
|
|
6405
6978
|
const rebalanceCtx = {
|
|
6406
6979
|
rebalancer,
|
|
@@ -6417,7 +6990,6 @@ function createEngineCore(config, hooks) {
|
|
|
6417
6990
|
};
|
|
6418
6991
|
return {
|
|
6419
6992
|
executor,
|
|
6420
|
-
promoter,
|
|
6421
6993
|
pluginRegistry,
|
|
6422
6994
|
durability,
|
|
6423
6995
|
invalidation,
|
|
@@ -6437,6 +7009,7 @@ function createEngineCore(config, hooks) {
|
|
|
6437
7009
|
requireManager,
|
|
6438
7010
|
bufferIfRebalancing,
|
|
6439
7011
|
watermarkNotifier,
|
|
7012
|
+
heapPressureNotifier,
|
|
6440
7013
|
analysisRebuild,
|
|
6441
7014
|
indexState,
|
|
6442
7015
|
mutationCtx,
|
|
@@ -6444,108 +7017,18 @@ function createEngineCore(config, hooks) {
|
|
|
6444
7017
|
};
|
|
6445
7018
|
}
|
|
6446
7019
|
|
|
6447
|
-
// src/engine/
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
function now() {
|
|
6454
|
-
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
6455
|
-
return performance.now();
|
|
6456
|
-
}
|
|
6457
|
-
return Date.now();
|
|
6458
|
-
}
|
|
6459
|
-
function validateIndexName(name) {
|
|
6460
|
-
if (!name || name.length === 0) {
|
|
6461
|
-
throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, "Index name must not be empty", { indexName: name });
|
|
6462
|
-
}
|
|
6463
|
-
if (name.length > MAX_INDEX_NAME_LENGTH) {
|
|
6464
|
-
throw new NarsilError(
|
|
6465
|
-
ErrorCodes.INDEX_NOT_FOUND,
|
|
6466
|
-
`Index name must not exceed ${MAX_INDEX_NAME_LENGTH} characters`,
|
|
6467
|
-
{
|
|
6468
|
-
indexName: name,
|
|
6469
|
-
length: name.length
|
|
6470
|
-
}
|
|
6471
|
-
);
|
|
6472
|
-
}
|
|
6473
|
-
if (!INDEX_NAME_PATTERN.test(name)) {
|
|
6474
|
-
throw new NarsilError(
|
|
6475
|
-
ErrorCodes.INDEX_NOT_FOUND,
|
|
6476
|
-
`Index name "${name}" contains invalid characters; use alphanumeric, dots, hyphens, and underscores only`,
|
|
6477
|
-
{ indexName: name }
|
|
6478
|
-
);
|
|
6479
|
-
}
|
|
6480
|
-
if (name.includes("..")) {
|
|
6481
|
-
throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index name "${name}" must not contain ".."`, { indexName: name });
|
|
6482
|
-
}
|
|
6483
|
-
}
|
|
6484
|
-
function validateDocId(docId) {
|
|
6485
|
-
if (!docId || docId.length === 0) {
|
|
6486
|
-
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not be empty", { docId });
|
|
6487
|
-
}
|
|
6488
|
-
if (docId.length > MAX_DOC_ID_LENGTH) {
|
|
6489
|
-
throw new NarsilError(
|
|
6490
|
-
ErrorCodes.DOC_VALIDATION_FAILED,
|
|
6491
|
-
`Document ID must not exceed ${MAX_DOC_ID_LENGTH} characters`,
|
|
6492
|
-
{
|
|
6493
|
-
docId,
|
|
6494
|
-
length: docId.length
|
|
6495
|
-
}
|
|
6496
|
-
);
|
|
6497
|
-
}
|
|
6498
|
-
if (docId.includes("\0")) {
|
|
6499
|
-
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not contain null bytes", { docId });
|
|
7020
|
+
// src/engine/index-lifecycle.ts
|
|
7021
|
+
async function createEngineIndex(core, config, name, indexConfig, indexUuid) {
|
|
7022
|
+
core.guardShutdown();
|
|
7023
|
+
validateIndexName(name);
|
|
7024
|
+
if (core.indexRegistry.has(name)) {
|
|
7025
|
+
throw new NarsilError(ErrorCodes.INDEX_ALREADY_EXISTS, `Index "${name}" already exists`, { indexName: name });
|
|
6500
7026
|
}
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
if (
|
|
6505
|
-
|
|
6506
|
-
maxDocsPerPartition
|
|
6507
|
-
});
|
|
6508
|
-
}
|
|
6509
|
-
if (maxPartitions !== void 0 && (!Number.isInteger(maxPartitions) || maxPartitions < 1)) {
|
|
6510
|
-
throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.maxPartitions must be a positive integer", {
|
|
6511
|
-
maxPartitions
|
|
6512
|
-
});
|
|
6513
|
-
}
|
|
6514
|
-
if (watermark !== void 0 && (typeof watermark !== "number" || !(watermark > 0) || watermark > 1)) {
|
|
6515
|
-
throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.watermark must be above 0 and at most 1", {
|
|
6516
|
-
watermark
|
|
6517
|
-
});
|
|
6518
|
-
}
|
|
6519
|
-
}
|
|
6520
|
-
function validateBM25Params(bm25) {
|
|
6521
|
-
if (bm25 === void 0) return;
|
|
6522
|
-
const { k1, b } = bm25;
|
|
6523
|
-
if (k1 !== void 0 && (!Number.isFinite(k1) || k1 < 0)) {
|
|
6524
|
-
throw new NarsilError(ErrorCodes.CONFIG_INVALID, "bm25.k1 must be a finite number at or above 0", { k1 });
|
|
6525
|
-
}
|
|
6526
|
-
if (b !== void 0 && (!Number.isFinite(b) || b < 0 || b > 1)) {
|
|
6527
|
-
throw new NarsilError(ErrorCodes.CONFIG_INVALID, "bm25.b must be between 0 and 1", { b });
|
|
6528
|
-
}
|
|
6529
|
-
}
|
|
6530
|
-
function clampLimit(limit) {
|
|
6531
|
-
return clampRowCount(limit, DEFAULT_LIMIT);
|
|
6532
|
-
}
|
|
6533
|
-
function clampOffset(offset) {
|
|
6534
|
-
return clampRowCount(offset, DEFAULT_OFFSET);
|
|
6535
|
-
}
|
|
6536
|
-
|
|
6537
|
-
// src/engine/index-lifecycle.ts
|
|
6538
|
-
async function createEngineIndex(core, config, name, indexConfig, indexUuid) {
|
|
6539
|
-
core.guardShutdown();
|
|
6540
|
-
validateIndexName(name);
|
|
6541
|
-
if (core.indexRegistry.has(name)) {
|
|
6542
|
-
throw new NarsilError(ErrorCodes.INDEX_ALREADY_EXISTS, `Index "${name}" already exists`, { indexName: name });
|
|
6543
|
-
}
|
|
6544
|
-
validateSchema(indexConfig.schema);
|
|
6545
|
-
validateVectorPromotion(indexConfig.vectorPromotion);
|
|
6546
|
-
validateBM25Params(indexConfig.bm25);
|
|
6547
|
-
if (indexConfig.partitions) {
|
|
6548
|
-
validatePartitionConfig(indexConfig.partitions);
|
|
7027
|
+
validateSchema(indexConfig.schema);
|
|
7028
|
+
validateVectorPromotion(indexConfig.vectorPromotion);
|
|
7029
|
+
validateBM25Params(indexConfig.bm25);
|
|
7030
|
+
if (indexConfig.partitions) {
|
|
7031
|
+
validatePartitionConfig(indexConfig.partitions);
|
|
6549
7032
|
}
|
|
6550
7033
|
if (core.durability) {
|
|
6551
7034
|
if (indexConfig.tokenizer !== void 0 && typeof indexConfig.tokenizer !== "string") {
|
|
@@ -7037,6 +7520,12 @@ async function embedChunkDocuments(entry, documents, chunkStart, chunkEnd, signa
|
|
|
7037
7520
|
}
|
|
7038
7521
|
}
|
|
7039
7522
|
|
|
7523
|
+
// src/engine/mutations/write-visibility.ts
|
|
7524
|
+
async function awaitWriteVisibility(ctx, indexName) {
|
|
7525
|
+
await ctx.awaitRebalanceReplay(indexName);
|
|
7526
|
+
await ctx.orchestrator.awaitWrites(indexName);
|
|
7527
|
+
}
|
|
7528
|
+
|
|
7040
7529
|
// src/engine/mutations/insert.ts
|
|
7041
7530
|
async function insertDocument(ctx, indexName, document, docId, options) {
|
|
7042
7531
|
ctx.guardShutdown();
|
|
@@ -7122,6 +7611,8 @@ async function insertDocument(ctx, indexName, document, docId, options) {
|
|
|
7122
7611
|
}
|
|
7123
7612
|
if (buffered) {
|
|
7124
7613
|
ctx.checkWatermark(indexName);
|
|
7614
|
+
ctx.checkHeapPressure(indexName);
|
|
7615
|
+
if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
|
|
7125
7616
|
return resolvedDocId;
|
|
7126
7617
|
}
|
|
7127
7618
|
try {
|
|
@@ -7144,7 +7635,9 @@ async function insertDocument(ctx, indexName, document, docId, options) {
|
|
|
7144
7635
|
}
|
|
7145
7636
|
}
|
|
7146
7637
|
ctx.checkWatermark(indexName);
|
|
7147
|
-
|
|
7638
|
+
ctx.checkHeapPressure(indexName);
|
|
7639
|
+
await ctx.orchestrator.scaleOutReadyIndexes();
|
|
7640
|
+
if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
|
|
7148
7641
|
return resolvedDocId;
|
|
7149
7642
|
}
|
|
7150
7643
|
|
|
@@ -7298,7 +7791,6 @@ async function applyAdmittedDocuments(ctx, indexName, admitted, options, failed)
|
|
|
7298
7791
|
}
|
|
7299
7792
|
|
|
7300
7793
|
// src/engine/mutations/segment-replication.ts
|
|
7301
|
-
var MIN_DOCUMENTS_FOR_SEGMENTS = 64;
|
|
7302
7794
|
function shardCount(documentCount, workers) {
|
|
7303
7795
|
if (workers <= 1) return 1;
|
|
7304
7796
|
return Math.max(1, Math.min(workers, Math.ceil(documentCount / MIN_DOCUMENTS_FOR_SEGMENTS)));
|
|
@@ -7558,7 +8050,9 @@ async function insertBatchViaSegments(ctx, indexName, documents, options) {
|
|
|
7558
8050
|
vecIndexes.get(fieldPath)?.scheduleBuild();
|
|
7559
8051
|
}
|
|
7560
8052
|
ctx.checkWatermark(indexName);
|
|
7561
|
-
|
|
8053
|
+
ctx.checkHeapPressure(indexName);
|
|
8054
|
+
await ctx.orchestrator.scaleOutReadyIndexes();
|
|
8055
|
+
if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
|
|
7562
8056
|
return { succeeded: outcome.succeeded, failed };
|
|
7563
8057
|
}
|
|
7564
8058
|
|
|
@@ -7566,7 +8060,7 @@ async function insertBatchViaSegments(ctx, indexName, documents, options) {
|
|
|
7566
8060
|
async function insertDocumentBatch(ctx, indexName, documents, options) {
|
|
7567
8061
|
ctx.guardShutdown();
|
|
7568
8062
|
const entry = ctx.requireIndex(indexName);
|
|
7569
|
-
await ctx.orchestrator.
|
|
8063
|
+
await ctx.orchestrator.scaleOutBeforeBatch(indexName, documents.length);
|
|
7570
8064
|
if (documents.length >= MIN_DOCUMENTS_FOR_SEGMENTS && !ctx.isRebalancing(indexName) && ctx.orchestrator.segmentBuildConcurrency(indexName) > 0) {
|
|
7571
8065
|
return insertBatchViaSegments(ctx, indexName, documents, options);
|
|
7572
8066
|
}
|
|
@@ -7720,12 +8214,14 @@ async function insertDocumentBatch(ctx, indexName, documents, options) {
|
|
|
7720
8214
|
}
|
|
7721
8215
|
}
|
|
7722
8216
|
ctx.checkWatermark(indexName);
|
|
7723
|
-
|
|
8217
|
+
ctx.checkHeapPressure(indexName);
|
|
8218
|
+
await ctx.orchestrator.scaleOutReadyIndexes();
|
|
8219
|
+
if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
|
|
7724
8220
|
return { succeeded, failed };
|
|
7725
8221
|
}
|
|
7726
8222
|
|
|
7727
8223
|
// src/engine/mutations/remove.ts
|
|
7728
|
-
async function removeDocument(ctx, indexName, docId) {
|
|
8224
|
+
async function removeDocument(ctx, indexName, docId, options) {
|
|
7729
8225
|
ctx.guardShutdown();
|
|
7730
8226
|
ctx.requireIndex(indexName);
|
|
7731
8227
|
validateDocId(docId);
|
|
@@ -7764,6 +8260,7 @@ async function removeDocument(ctx, indexName, docId) {
|
|
|
7764
8260
|
await applyRemove();
|
|
7765
8261
|
}
|
|
7766
8262
|
if (buffered) {
|
|
8263
|
+
if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
|
|
7767
8264
|
return;
|
|
7768
8265
|
}
|
|
7769
8266
|
try {
|
|
@@ -7777,8 +8274,9 @@ async function removeDocument(ctx, indexName, docId) {
|
|
|
7777
8274
|
docId,
|
|
7778
8275
|
requestId: `replicate-remove-${docId}`
|
|
7779
8276
|
});
|
|
8277
|
+
if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
|
|
7780
8278
|
}
|
|
7781
|
-
async function removeDocumentBatch(ctx, indexName, docIds) {
|
|
8279
|
+
async function removeDocumentBatch(ctx, indexName, docIds, options) {
|
|
7782
8280
|
ctx.guardShutdown();
|
|
7783
8281
|
ctx.requireIndex(indexName);
|
|
7784
8282
|
const succeeded = [];
|
|
@@ -7806,6 +8304,7 @@ async function removeDocumentBatch(ctx, indexName, docIds) {
|
|
|
7806
8304
|
} finally {
|
|
7807
8305
|
manager.endBatchRemove();
|
|
7808
8306
|
}
|
|
8307
|
+
if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
|
|
7809
8308
|
return { succeeded, failed };
|
|
7810
8309
|
}
|
|
7811
8310
|
|
|
@@ -7823,7 +8322,7 @@ function prepareUpdatePartitionDoc(document, extractedVectors) {
|
|
|
7823
8322
|
}
|
|
7824
8323
|
return { partitionDoc };
|
|
7825
8324
|
}
|
|
7826
|
-
async function updateDocument(ctx, indexName, docId, document) {
|
|
8325
|
+
async function updateDocument(ctx, indexName, docId, document, options) {
|
|
7827
8326
|
ctx.guardShutdown();
|
|
7828
8327
|
const entry = ctx.requireIndex(indexName);
|
|
7829
8328
|
validateDocId(docId);
|
|
@@ -7925,6 +8424,8 @@ async function updateDocument(ctx, indexName, docId, document) {
|
|
|
7925
8424
|
await applyUpdate();
|
|
7926
8425
|
}
|
|
7927
8426
|
if (buffered) {
|
|
8427
|
+
ctx.checkHeapPressure(indexName);
|
|
8428
|
+
if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
|
|
7928
8429
|
return;
|
|
7929
8430
|
}
|
|
7930
8431
|
try {
|
|
@@ -7951,8 +8452,10 @@ async function updateDocument(ctx, indexName, docId, document) {
|
|
|
7951
8452
|
vecIndex.scheduleBuild();
|
|
7952
8453
|
}
|
|
7953
8454
|
}
|
|
8455
|
+
ctx.checkHeapPressure(indexName);
|
|
8456
|
+
if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
|
|
7954
8457
|
}
|
|
7955
|
-
async function updateDocumentBatch(ctx, indexName, updates) {
|
|
8458
|
+
async function updateDocumentBatch(ctx, indexName, updates, options) {
|
|
7956
8459
|
ctx.guardShutdown();
|
|
7957
8460
|
const entry = ctx.requireIndex(indexName);
|
|
7958
8461
|
const succeeded = [];
|
|
@@ -7991,14 +8494,19 @@ async function updateDocumentBatch(ctx, indexName, updates) {
|
|
|
7991
8494
|
vecIndex.scheduleBuild();
|
|
7992
8495
|
}
|
|
7993
8496
|
}
|
|
8497
|
+
if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
|
|
7994
8498
|
return { succeeded, failed };
|
|
7995
8499
|
}
|
|
7996
8500
|
|
|
7997
8501
|
// src/partitioning/write-ahead-queue.ts
|
|
7998
|
-
function createWriteAheadQueue(maxSize =
|
|
8502
|
+
function createWriteAheadQueue(maxSize = DEFAULT_WRITE_AHEAD_QUEUE_MAX_SIZE) {
|
|
7999
8503
|
const entries = [];
|
|
8000
8504
|
const docStates = /* @__PURE__ */ new Map();
|
|
8001
8505
|
let nextSequence = 1;
|
|
8506
|
+
let resolveReplayed = () => void 0;
|
|
8507
|
+
const replayed = new Promise((resolve) => {
|
|
8508
|
+
resolveReplayed = resolve;
|
|
8509
|
+
});
|
|
8002
8510
|
return {
|
|
8003
8511
|
push(entry) {
|
|
8004
8512
|
if (entries.length >= maxSize) {
|
|
@@ -8025,6 +8533,12 @@ function createWriteAheadQueue(maxSize = 1e4) {
|
|
|
8025
8533
|
bufferedDocState(docId) {
|
|
8026
8534
|
return docStates.get(docId);
|
|
8027
8535
|
},
|
|
8536
|
+
whenReplayed() {
|
|
8537
|
+
return replayed;
|
|
8538
|
+
},
|
|
8539
|
+
markReplayed() {
|
|
8540
|
+
resolveReplayed();
|
|
8541
|
+
},
|
|
8028
8542
|
get size() {
|
|
8029
8543
|
return entries.length;
|
|
8030
8544
|
},
|
|
@@ -8198,12 +8712,11 @@ async function executeRebalance(manager, indexName, targetPartitionCount, ctx) {
|
|
|
8198
8712
|
const oldCount = manager.partitionCount;
|
|
8199
8713
|
await ctx.rebalancer.rebalance(manager, targetPartitionCount, ctx.router, (progress) => {
|
|
8200
8714
|
if (progress.phase === "complete") {
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
}
|
|
8715
|
+
emitEngineEvent(ctx.eventHandlers, "partitionRebalance", {
|
|
8716
|
+
indexName,
|
|
8717
|
+
oldCount,
|
|
8718
|
+
newCount: targetPartitionCount
|
|
8719
|
+
});
|
|
8207
8720
|
}
|
|
8208
8721
|
});
|
|
8209
8722
|
try {
|
|
@@ -8243,6 +8756,7 @@ async function executeRebalance(manager, indexName, targetPartitionCount, ctx) {
|
|
|
8243
8756
|
ctx.rebalancingIndexes.delete(indexName);
|
|
8244
8757
|
ctx.rebalanceTargets.delete(indexName);
|
|
8245
8758
|
ctx.waqMap.delete(indexName);
|
|
8759
|
+
waq.markReplayed();
|
|
8246
8760
|
}
|
|
8247
8761
|
ctx.checkWatermark(indexName);
|
|
8248
8762
|
}
|
|
@@ -8670,6 +9184,7 @@ function readProcessMemory() {
|
|
|
8670
9184
|
return {
|
|
8671
9185
|
heapUsed: usage.heapUsed,
|
|
8672
9186
|
heapTotal: usage.heapTotal,
|
|
9187
|
+
heapLimit: readHeapStatistics()?.limitBytes ?? null,
|
|
8673
9188
|
external: usage.external,
|
|
8674
9189
|
rss: usage.rss
|
|
8675
9190
|
};
|
|
@@ -8678,1209 +9193,6 @@ function readProcessMemory() {
|
|
|
8678
9193
|
}
|
|
8679
9194
|
}
|
|
8680
9195
|
|
|
8681
|
-
// src/search/cursor-binding.ts
|
|
8682
|
-
var ABSENT_TAG = 0;
|
|
8683
|
-
var NULL_TAG = 1;
|
|
8684
|
-
var FALSE_TAG = 2;
|
|
8685
|
-
var TRUE_TAG = 3;
|
|
8686
|
-
var NUMBER_TAG = 4;
|
|
8687
|
-
var STRING_TAG = 5;
|
|
8688
|
-
var LIST_TAG = 6;
|
|
8689
|
-
var MAP_TAG = 7;
|
|
8690
|
-
var encoder = new TextEncoder();
|
|
8691
|
-
function ensureCapacity(stream, extra) {
|
|
8692
|
-
if (stream.length + extra <= stream.bytes.length) return;
|
|
8693
|
-
const grown = new Uint8Array(Math.max(stream.bytes.length * 2, stream.length + extra));
|
|
8694
|
-
grown.set(stream.bytes.subarray(0, stream.length));
|
|
8695
|
-
stream.bytes = grown;
|
|
8696
|
-
}
|
|
8697
|
-
function writeByte(stream, value) {
|
|
8698
|
-
ensureCapacity(stream, 1);
|
|
8699
|
-
stream.bytes[stream.length] = value;
|
|
8700
|
-
stream.length += 1;
|
|
8701
|
-
}
|
|
8702
|
-
function writeUint32(stream, value) {
|
|
8703
|
-
ensureCapacity(stream, 4);
|
|
8704
|
-
stream.bytes[stream.length] = value >>> 24 & 255;
|
|
8705
|
-
stream.bytes[stream.length + 1] = value >>> 16 & 255;
|
|
8706
|
-
stream.bytes[stream.length + 2] = value >>> 8 & 255;
|
|
8707
|
-
stream.bytes[stream.length + 3] = value & 255;
|
|
8708
|
-
stream.length += 4;
|
|
8709
|
-
}
|
|
8710
|
-
var numberBytes = new DataView(new ArrayBuffer(8));
|
|
8711
|
-
function writeNumber(stream, value) {
|
|
8712
|
-
numberBytes.setFloat64(0, value, false);
|
|
8713
|
-
ensureCapacity(stream, 8);
|
|
8714
|
-
for (let i = 0; i < 8; i++) {
|
|
8715
|
-
stream.bytes[stream.length + i] = numberBytes.getUint8(i);
|
|
8716
|
-
}
|
|
8717
|
-
stream.length += 8;
|
|
8718
|
-
}
|
|
8719
|
-
function writeString(stream, value) {
|
|
8720
|
-
const encoded = encoder.encode(value);
|
|
8721
|
-
writeUint32(stream, encoded.length);
|
|
8722
|
-
ensureCapacity(stream, encoded.length);
|
|
8723
|
-
stream.bytes.set(encoded, stream.length);
|
|
8724
|
-
stream.length += encoded.length;
|
|
8725
|
-
}
|
|
8726
|
-
function writeValue(stream, value) {
|
|
8727
|
-
if (value === void 0) {
|
|
8728
|
-
writeByte(stream, ABSENT_TAG);
|
|
8729
|
-
return;
|
|
8730
|
-
}
|
|
8731
|
-
if (value === null) {
|
|
8732
|
-
writeByte(stream, NULL_TAG);
|
|
8733
|
-
return;
|
|
8734
|
-
}
|
|
8735
|
-
if (typeof value === "boolean") {
|
|
8736
|
-
writeByte(stream, value ? TRUE_TAG : FALSE_TAG);
|
|
8737
|
-
return;
|
|
8738
|
-
}
|
|
8739
|
-
if (typeof value === "number") {
|
|
8740
|
-
writeByte(stream, NUMBER_TAG);
|
|
8741
|
-
writeNumber(stream, value);
|
|
8742
|
-
return;
|
|
8743
|
-
}
|
|
8744
|
-
if (typeof value === "string") {
|
|
8745
|
-
writeByte(stream, STRING_TAG);
|
|
8746
|
-
writeString(stream, value);
|
|
8747
|
-
return;
|
|
8748
|
-
}
|
|
8749
|
-
if (Array.isArray(value)) {
|
|
8750
|
-
writeByte(stream, LIST_TAG);
|
|
8751
|
-
writeUint32(stream, value.length);
|
|
8752
|
-
for (const member of value) {
|
|
8753
|
-
writeValue(stream, member ?? null);
|
|
8754
|
-
}
|
|
8755
|
-
return;
|
|
8756
|
-
}
|
|
8757
|
-
const record = value;
|
|
8758
|
-
const keys = Object.keys(record).sort(compareCodePoints);
|
|
8759
|
-
writeByte(stream, MAP_TAG);
|
|
8760
|
-
writeUint32(stream, keys.length);
|
|
8761
|
-
for (const key of keys) {
|
|
8762
|
-
writeByte(stream, STRING_TAG);
|
|
8763
|
-
writeString(stream, key);
|
|
8764
|
-
writeValue(stream, record[key] ?? null);
|
|
8765
|
-
}
|
|
8766
|
-
}
|
|
8767
|
-
function boundVectorOf(vector) {
|
|
8768
|
-
if (vector === void 0) return void 0;
|
|
8769
|
-
const bound = { ...vector };
|
|
8770
|
-
if (vector.value !== void 0) {
|
|
8771
|
-
bound.value = Array.from(vector.value, Math.fround);
|
|
8772
|
-
}
|
|
8773
|
-
if (vector.text !== void 0) {
|
|
8774
|
-
delete bound.value;
|
|
8775
|
-
}
|
|
8776
|
-
return bound;
|
|
8777
|
-
}
|
|
8778
|
-
var FILTERS_SLOT = 2;
|
|
8779
|
-
var BOUND_SLOT_COUNT = 15;
|
|
8780
|
-
function bindingOf(values) {
|
|
8781
|
-
const stream = { bytes: new Uint8Array(256), length: 0 };
|
|
8782
|
-
for (const value of values) {
|
|
8783
|
-
writeValue(stream, value);
|
|
8784
|
-
}
|
|
8785
|
-
return fnv1aBytes(stream.bytes.subarray(0, stream.length)).toString(16).padStart(8, "0");
|
|
8786
|
-
}
|
|
8787
|
-
function queryBindingOf(params) {
|
|
8788
|
-
return bindingOf([
|
|
8789
|
-
params.term,
|
|
8790
|
-
params.fields,
|
|
8791
|
-
params.filters,
|
|
8792
|
-
params.boost,
|
|
8793
|
-
params.minScore,
|
|
8794
|
-
params.termMatch,
|
|
8795
|
-
params.tolerance,
|
|
8796
|
-
params.prefixLength,
|
|
8797
|
-
params.prefix,
|
|
8798
|
-
params.exact,
|
|
8799
|
-
params.pinned,
|
|
8800
|
-
params.mode,
|
|
8801
|
-
params.hybrid,
|
|
8802
|
-
boundVectorOf(params.vector),
|
|
8803
|
-
params.scoring ?? "local"
|
|
8804
|
-
]);
|
|
8805
|
-
}
|
|
8806
|
-
function listBindingOf(filters) {
|
|
8807
|
-
const values = new Array(BOUND_SLOT_COUNT).fill(void 0);
|
|
8808
|
-
values[FILTERS_SLOT] = filters;
|
|
8809
|
-
return bindingOf(values);
|
|
8810
|
-
}
|
|
8811
|
-
|
|
8812
|
-
// src/engine/list-documents/index.ts
|
|
8813
|
-
function clampListLimit(limit) {
|
|
8814
|
-
const clamped = Math.max(1, clampLimit(limit));
|
|
8815
|
-
requireWithinResultWindow(clamped, 0);
|
|
8816
|
-
return clamped;
|
|
8817
|
-
}
|
|
8818
|
-
function firstIdAfter(sorted, anchor) {
|
|
8819
|
-
let low = 0;
|
|
8820
|
-
let high = sorted.length;
|
|
8821
|
-
while (low < high) {
|
|
8822
|
-
const mid = low + high >>> 1;
|
|
8823
|
-
if (compareCodePoints(sorted[mid], anchor) <= 0) low = mid + 1;
|
|
8824
|
-
else high = mid;
|
|
8825
|
-
}
|
|
8826
|
-
return low;
|
|
8827
|
-
}
|
|
8828
|
-
function collectFilterMatches(partitions, filters, schema) {
|
|
8829
|
-
const perPartition = partitions.map((partition) => partition.filterMatches(filters, schema));
|
|
8830
|
-
let total = 0;
|
|
8831
|
-
for (const matches of perPartition) total += matches.count;
|
|
8832
|
-
return { matchesFor: (partitionIndex) => perPartition[partitionIndex] ?? null, total };
|
|
8833
|
-
}
|
|
8834
|
-
function collectCandidates(partition, anchor, matches, wanted, out) {
|
|
8835
|
-
const sorted = partition.sortedDocIds();
|
|
8836
|
-
let index = anchor === null ? 0 : firstIdAfter(sorted, anchor);
|
|
8837
|
-
let taken = 0;
|
|
8838
|
-
while (index < sorted.length && taken < wanted) {
|
|
8839
|
-
const docId = sorted[index];
|
|
8840
|
-
index++;
|
|
8841
|
-
if (matches !== null && !matches.hasExternal(docId)) continue;
|
|
8842
|
-
out.push(docId);
|
|
8843
|
-
taken++;
|
|
8844
|
-
}
|
|
8845
|
-
}
|
|
8846
|
-
function pageInIdOrder(partitions, cursor, filtered, limit, binding) {
|
|
8847
|
-
const anchor = cursor === null ? null : cursor.anchor;
|
|
8848
|
-
const candidates = [];
|
|
8849
|
-
for (let index = 0; index < partitions.length; index++) {
|
|
8850
|
-
collectCandidates(
|
|
8851
|
-
partitions[index],
|
|
8852
|
-
anchor,
|
|
8853
|
-
filtered === null ? null : filtered.matchesFor(index),
|
|
8854
|
-
limit + 1,
|
|
8855
|
-
candidates
|
|
8856
|
-
);
|
|
8857
|
-
}
|
|
8858
|
-
candidates.sort(compareCodePoints);
|
|
8859
|
-
const hasMore = candidates.length > limit;
|
|
8860
|
-
const ids = hasMore ? candidates.slice(0, limit) : candidates;
|
|
8861
|
-
const nextCursor = hasMore && ids.length > 0 ? { anchor: ids[ids.length - 1], score: null, sortKey: null, sortSignature: null, binding } : null;
|
|
8862
|
-
return { ids, nextCursor };
|
|
8863
|
-
}
|
|
8864
|
-
function pageInSortOrder(partitions, cursor, filtered, limit, sort, signature, schema, binding) {
|
|
8865
|
-
const normalized = normalizeSort(sort);
|
|
8866
|
-
const fields = normalized.map((entry) => entry.field);
|
|
8867
|
-
const directions = normalized.map((entry) => entry.direction);
|
|
8868
|
-
const flatSchema = flattenSchema(schema);
|
|
8869
|
-
const fieldTypes = fields.map((field) => flatSchema[field]);
|
|
8870
|
-
const request = {
|
|
8871
|
-
fields,
|
|
8872
|
-
directions,
|
|
8873
|
-
fieldTypes,
|
|
8874
|
-
limit: limit + 1,
|
|
8875
|
-
anchorKey: cursor === null ? null : cursor.sortKey,
|
|
8876
|
-
anchorId: cursor === null ? null : cursor.anchor
|
|
8877
|
-
};
|
|
8878
|
-
const merged = [];
|
|
8879
|
-
for (let index = 0; index < partitions.length; index++) {
|
|
8880
|
-
const matches = filtered === null ? null : filtered.matchesFor(index);
|
|
8881
|
-
for (const entry of partitions[index].sortedPage({ ...request, matches })) merged.push(entry);
|
|
8882
|
-
}
|
|
8883
|
-
merged.sort((a, b) => compareComparableKeys(a.key, b.key, directions) || compareCodePoints(a.id, b.id));
|
|
8884
|
-
const hasMore = merged.length > limit;
|
|
8885
|
-
const page = hasMore ? merged.slice(0, limit) : merged;
|
|
8886
|
-
const last = page[page.length - 1];
|
|
8887
|
-
const nextCursor = hasMore && last !== void 0 ? { anchor: last.id, score: null, sortKey: last.key, sortSignature: signature, binding } : null;
|
|
8888
|
-
return { ids: page.map((entry) => entry.id), nextCursor };
|
|
8889
|
-
}
|
|
8890
|
-
function countAcrossPartitions(manager, partitionIds, partitions) {
|
|
8891
|
-
if (partitionIds === void 0) {
|
|
8892
|
-
return manager.countDocuments();
|
|
8893
|
-
}
|
|
8894
|
-
let total = 0;
|
|
8895
|
-
for (const partition of partitions) {
|
|
8896
|
-
total += partition.count();
|
|
8897
|
-
}
|
|
8898
|
-
return total;
|
|
8899
|
-
}
|
|
8900
|
-
function executeListDocuments(params, context) {
|
|
8901
|
-
const { manager, schema } = context;
|
|
8902
|
-
const startTime = now();
|
|
8903
|
-
const limit = clampListLimit(params.limit);
|
|
8904
|
-
const signature = sortSignatureOf(params.sort);
|
|
8905
|
-
requireSortableFields(params.sort, schema);
|
|
8906
|
-
const binding = listBindingOf(params.filters);
|
|
8907
|
-
let cursor = null;
|
|
8908
|
-
if (params.cursor !== void 0) {
|
|
8909
|
-
cursor = decodePageCursor(params.cursor);
|
|
8910
|
-
requireMatchingCursor(cursor, params.cursor, signature, false, binding);
|
|
8911
|
-
}
|
|
8912
|
-
const partitions = context.partitionIds === void 0 ? manager.getAllPartitions() : context.partitionIds.map((partitionId) => manager.partitionAt(partitionId)).filter((partition) => partition !== void 0);
|
|
8913
|
-
const filtered = params.filters === void 0 ? null : collectFilterMatches(partitions, params.filters, schema);
|
|
8914
|
-
const total = filtered === null ? countAcrossPartitions(manager, context.partitionIds, partitions) : filtered.total;
|
|
8915
|
-
const page = params.sort === void 0 || signature === null ? pageInIdOrder(partitions, cursor, filtered, limit, binding) : pageInSortOrder(partitions, cursor, filtered, limit, params.sort, signature, schema, binding);
|
|
8916
|
-
const projection = resolveProjection(params.document);
|
|
8917
|
-
const documents = [];
|
|
8918
|
-
for (const docId of page.ids) {
|
|
8919
|
-
if (projection.kind === "none") {
|
|
8920
|
-
documents.push({ id: docId, document: {} });
|
|
8921
|
-
continue;
|
|
8922
|
-
}
|
|
8923
|
-
const stored = manager.get(docId, projection);
|
|
8924
|
-
if (stored === void 0) continue;
|
|
8925
|
-
documents.push({ id: docId, document: stored });
|
|
8926
|
-
}
|
|
8927
|
-
if (page.nextCursor === null) {
|
|
8928
|
-
for (const partition of partitions) partition.releaseSortedDocIds();
|
|
8929
|
-
}
|
|
8930
|
-
return {
|
|
8931
|
-
documents,
|
|
8932
|
-
cursor: page.nextCursor === null ? null : encodePageCursor(page.nextCursor),
|
|
8933
|
-
total,
|
|
8934
|
-
elapsed: now() - startTime
|
|
8935
|
-
};
|
|
8936
|
-
}
|
|
8937
|
-
|
|
8938
|
-
// src/search/grouping.ts
|
|
8939
|
-
function getNestedValue2(obj, path) {
|
|
8940
|
-
const segments = path.split(".");
|
|
8941
|
-
let current = obj;
|
|
8942
|
-
for (const segment of segments) {
|
|
8943
|
-
if (current === null || current === void 0 || typeof current !== "object") {
|
|
8944
|
-
return void 0;
|
|
8945
|
-
}
|
|
8946
|
-
if (!Object.hasOwn(current, segment)) {
|
|
8947
|
-
return void 0;
|
|
8948
|
-
}
|
|
8949
|
-
current = current[segment];
|
|
8950
|
-
}
|
|
8951
|
-
return current;
|
|
8952
|
-
}
|
|
8953
|
-
function applyGrouping(hits, group, getDocument) {
|
|
8954
|
-
if (!group.fields || group.fields.length === 0) {
|
|
8955
|
-
return [{ values: {}, hits }];
|
|
8956
|
-
}
|
|
8957
|
-
const groupMap = /* @__PURE__ */ new Map();
|
|
8958
|
-
for (const hit of hits) {
|
|
8959
|
-
const doc = getDocument(hit.id);
|
|
8960
|
-
const fieldValues = [];
|
|
8961
|
-
const valuesRecord = {};
|
|
8962
|
-
for (const field of group.fields) {
|
|
8963
|
-
const value = doc ? getNestedValue2(doc, field) : void 0;
|
|
8964
|
-
fieldValues.push(value);
|
|
8965
|
-
valuesRecord[field] = value;
|
|
8966
|
-
}
|
|
8967
|
-
const compositeKey = JSON.stringify(fieldValues);
|
|
8968
|
-
const existing = groupMap.get(compositeKey);
|
|
8969
|
-
if (existing) {
|
|
8970
|
-
existing.hits.push(hit);
|
|
8971
|
-
} else {
|
|
8972
|
-
groupMap.set(compositeKey, { values: valuesRecord, hits: [hit] });
|
|
8973
|
-
}
|
|
8974
|
-
}
|
|
8975
|
-
const groups = [];
|
|
8976
|
-
for (const entry of groupMap.values()) {
|
|
8977
|
-
let groupHits = entry.hits;
|
|
8978
|
-
if (group.maxPerGroup !== void 0 && group.maxPerGroup > 0) {
|
|
8979
|
-
groupHits = groupHits.slice(0, group.maxPerGroup);
|
|
8980
|
-
}
|
|
8981
|
-
const result = { values: entry.values, hits: groupHits };
|
|
8982
|
-
if (group.reduce) {
|
|
8983
|
-
const folded = foldGroupReducer(
|
|
8984
|
-
group.reduce,
|
|
8985
|
-
groupHits.map((hit) => ({ document: getDocument(hit.id), score: hit.score ?? 0 }))
|
|
8986
|
-
);
|
|
8987
|
-
if (folded.reducerError !== void 0) {
|
|
8988
|
-
result.reducerError = folded.reducerError;
|
|
8989
|
-
} else {
|
|
8990
|
-
result.reduced = folded.reduced;
|
|
8991
|
-
}
|
|
8992
|
-
}
|
|
8993
|
-
groups.push(result);
|
|
8994
|
-
}
|
|
8995
|
-
groups.sort((a, b) => {
|
|
8996
|
-
const aScore = a.hits.length > 0 ? a.hits[0].score ?? 0 : 0;
|
|
8997
|
-
const bScore = b.hits.length > 0 ? b.hits[0].score ?? 0 : 0;
|
|
8998
|
-
if (aScore !== bScore) return bScore - aScore;
|
|
8999
|
-
const aId = a.hits.length > 0 ? a.hits[0].id : "";
|
|
9000
|
-
const bId = b.hits.length > 0 ? b.hits[0].id : "";
|
|
9001
|
-
return compareCodePoints(aId, bId);
|
|
9002
|
-
});
|
|
9003
|
-
if (group.limit !== void 0 && Number.isFinite(group.limit) && group.limit >= 0) {
|
|
9004
|
-
const cap = Math.floor(group.limit);
|
|
9005
|
-
if (groups.length > cap) {
|
|
9006
|
-
groups.length = cap;
|
|
9007
|
-
}
|
|
9008
|
-
}
|
|
9009
|
-
return groups;
|
|
9010
|
-
}
|
|
9011
|
-
function foldGroupReducer(reduce, rows) {
|
|
9012
|
-
try {
|
|
9013
|
-
let accumulator = reduce.initialValue();
|
|
9014
|
-
for (const row of rows) {
|
|
9015
|
-
if (row.document !== void 0) {
|
|
9016
|
-
accumulator = reduce.reducer(accumulator, row.document, row.score);
|
|
9017
|
-
}
|
|
9018
|
-
}
|
|
9019
|
-
return { reduced: accumulator };
|
|
9020
|
-
} catch (err) {
|
|
9021
|
-
return { reducerError: err instanceof Error ? err.message : String(err) };
|
|
9022
|
-
}
|
|
9023
|
-
}
|
|
9024
|
-
|
|
9025
|
-
// src/search/pinning.ts
|
|
9026
|
-
function dedupePinnedEntries(pinned) {
|
|
9027
|
-
const deduped = [];
|
|
9028
|
-
const seen = /* @__PURE__ */ new Set();
|
|
9029
|
-
for (const entry of pinned) {
|
|
9030
|
-
if (seen.has(entry.docId)) continue;
|
|
9031
|
-
seen.add(entry.docId);
|
|
9032
|
-
deduped.push(entry);
|
|
9033
|
-
}
|
|
9034
|
-
return deduped;
|
|
9035
|
-
}
|
|
9036
|
-
function applyPinning(hits, pinned, resolveDoc) {
|
|
9037
|
-
const result = hits.slice();
|
|
9038
|
-
const deduped = dedupePinnedEntries(pinned);
|
|
9039
|
-
const pinnedDocIds = new Set(deduped.map((entry) => entry.docId));
|
|
9040
|
-
for (let i = result.length - 1; i >= 0; i--) {
|
|
9041
|
-
if (pinnedDocIds.has(result[i].id)) {
|
|
9042
|
-
result.splice(i, 1);
|
|
9043
|
-
}
|
|
9044
|
-
}
|
|
9045
|
-
const sorted = deduped.sort((a, b) => a.position - b.position);
|
|
9046
|
-
for (const entry of sorted) {
|
|
9047
|
-
const doc = resolveDoc(entry.docId);
|
|
9048
|
-
if (!doc) continue;
|
|
9049
|
-
const pos = Math.max(0, Math.min(entry.position, result.length));
|
|
9050
|
-
result.splice(pos, 0, doc);
|
|
9051
|
-
}
|
|
9052
|
-
return result;
|
|
9053
|
-
}
|
|
9054
|
-
|
|
9055
|
-
// src/highlighting/highlighter.ts
|
|
9056
|
-
function buildTokenCharOffsets(text, originalTokens) {
|
|
9057
|
-
const offsets = [];
|
|
9058
|
-
let searchFrom = 0;
|
|
9059
|
-
for (const token of originalTokens) {
|
|
9060
|
-
const idx = text.indexOf(token, searchFrom);
|
|
9061
|
-
if (idx === -1) continue;
|
|
9062
|
-
offsets.push({ start: idx, end: idx + token.length });
|
|
9063
|
-
searchFrom = idx + token.length;
|
|
9064
|
-
}
|
|
9065
|
-
return offsets;
|
|
9066
|
-
}
|
|
9067
|
-
function mergeOverlapping(ranges) {
|
|
9068
|
-
if (ranges.length === 0) return [];
|
|
9069
|
-
const sorted = [...ranges].sort((a, b) => a.start - b.start);
|
|
9070
|
-
const merged = [sorted[0]];
|
|
9071
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
9072
|
-
const prev = merged[merged.length - 1];
|
|
9073
|
-
const curr = sorted[i];
|
|
9074
|
-
if (curr.start <= prev.end) {
|
|
9075
|
-
prev.end = Math.max(prev.end, curr.end);
|
|
9076
|
-
} else {
|
|
9077
|
-
merged.push(curr);
|
|
9078
|
-
}
|
|
9079
|
-
}
|
|
9080
|
-
return merged;
|
|
9081
|
-
}
|
|
9082
|
-
function findDensestWindow(positions, text, windowSize) {
|
|
9083
|
-
if (positions.length === 0) {
|
|
9084
|
-
return { start: 0, end: Math.min(windowSize, text.length) };
|
|
9085
|
-
}
|
|
9086
|
-
let bestStart = 0;
|
|
9087
|
-
let bestCount = 0;
|
|
9088
|
-
for (const pos of positions) {
|
|
9089
|
-
const windowStart = Math.max(0, pos.start - Math.floor(windowSize / 2));
|
|
9090
|
-
const windowEnd = Math.min(text.length, windowStart + windowSize);
|
|
9091
|
-
const adjustedStart = Math.max(0, windowEnd - windowSize);
|
|
9092
|
-
let count = 0;
|
|
9093
|
-
for (const p of positions) {
|
|
9094
|
-
if (p.start >= adjustedStart && p.end <= windowEnd) {
|
|
9095
|
-
count++;
|
|
9096
|
-
}
|
|
9097
|
-
}
|
|
9098
|
-
if (count > bestCount) {
|
|
9099
|
-
bestCount = count;
|
|
9100
|
-
bestStart = adjustedStart;
|
|
9101
|
-
}
|
|
9102
|
-
}
|
|
9103
|
-
return { start: bestStart, end: Math.min(text.length, bestStart + windowSize) };
|
|
9104
|
-
}
|
|
9105
|
-
function applySnippetTruncation(taggedText, originalText, positions, maxLen, preTag, postTag) {
|
|
9106
|
-
if (maxLen <= 0 || originalText.length <= maxLen) {
|
|
9107
|
-
return { snippet: taggedText, adjustedPositions: positions };
|
|
9108
|
-
}
|
|
9109
|
-
const window = findDensestWindow(positions, originalText, maxLen);
|
|
9110
|
-
const snippetRaw = originalText.substring(window.start, window.end);
|
|
9111
|
-
const adjustedPositions = [];
|
|
9112
|
-
for (const pos of positions) {
|
|
9113
|
-
if (pos.start >= window.start && pos.end <= window.end) {
|
|
9114
|
-
adjustedPositions.push({
|
|
9115
|
-
start: pos.start - window.start,
|
|
9116
|
-
end: pos.end - window.start
|
|
9117
|
-
});
|
|
9118
|
-
}
|
|
9119
|
-
}
|
|
9120
|
-
let snippet = snippetRaw;
|
|
9121
|
-
const sortedDesc = [...adjustedPositions].sort((a, b) => b.start - a.start);
|
|
9122
|
-
for (const pos of sortedDesc) {
|
|
9123
|
-
snippet = snippet.substring(0, pos.start) + preTag + snippet.substring(pos.start, pos.end) + postTag + snippet.substring(pos.end);
|
|
9124
|
-
}
|
|
9125
|
-
const needsLeadingEllipsis = window.start > 0;
|
|
9126
|
-
const needsTrailingEllipsis = window.end < originalText.length;
|
|
9127
|
-
if (needsLeadingEllipsis) snippet = `...${snippet}`;
|
|
9128
|
-
if (needsTrailingEllipsis) snippet = `${snippet}...`;
|
|
9129
|
-
return { snippet, adjustedPositions };
|
|
9130
|
-
}
|
|
9131
|
-
function highlightField(text, queryTokens, language, options) {
|
|
9132
|
-
const preTag = options?.preTag ?? "<mark>";
|
|
9133
|
-
const postTag = options?.postTag ?? "</mark>";
|
|
9134
|
-
const maxSnippetLength = options?.maxSnippetLength ?? 200;
|
|
9135
|
-
if (text.length === 0) {
|
|
9136
|
-
return { snippet: text, positions: [] };
|
|
9137
|
-
}
|
|
9138
|
-
const fieldResult = tokenize(text, language, { stem: false, removeStopWords: false });
|
|
9139
|
-
if (fieldResult.tokens.length === 0) {
|
|
9140
|
-
return { snippet: text, positions: [] };
|
|
9141
|
-
}
|
|
9142
|
-
const lowercaseText = text.normalize("NFC").toLowerCase();
|
|
9143
|
-
const charOffsets = buildTokenCharOffsets(lowercaseText, fieldResult.originalTokens);
|
|
9144
|
-
const stemmedQueryTokens = queryTokens.map((qt) => {
|
|
9145
|
-
if (language.stemmer) {
|
|
9146
|
-
return language.stemmer(qt.token.toLowerCase());
|
|
9147
|
-
}
|
|
9148
|
-
return qt.token.toLowerCase();
|
|
9149
|
-
});
|
|
9150
|
-
const matchedRanges = [];
|
|
9151
|
-
const prefixToken = options?.prefixToken;
|
|
9152
|
-
for (let i = 0; i < fieldResult.tokens.length; i++) {
|
|
9153
|
-
if (i >= charOffsets.length) break;
|
|
9154
|
-
const fieldToken = fieldResult.tokens[i].token;
|
|
9155
|
-
const stemmedField = language.stemmer ? language.stemmer(fieldToken) : fieldToken;
|
|
9156
|
-
if (prefixToken !== void 0 && fieldToken.startsWith(prefixToken)) {
|
|
9157
|
-
matchedRanges.push({ start: charOffsets[i].start, end: charOffsets[i].end });
|
|
9158
|
-
continue;
|
|
9159
|
-
}
|
|
9160
|
-
for (const stemmedQuery of stemmedQueryTokens) {
|
|
9161
|
-
if (stemmedField === stemmedQuery) {
|
|
9162
|
-
matchedRanges.push({ start: charOffsets[i].start, end: charOffsets[i].end });
|
|
9163
|
-
break;
|
|
9164
|
-
}
|
|
9165
|
-
}
|
|
9166
|
-
}
|
|
9167
|
-
const mergedPositions = mergeOverlapping(matchedRanges);
|
|
9168
|
-
if (mergedPositions.length === 0) {
|
|
9169
|
-
if (maxSnippetLength > 0 && text.length > maxSnippetLength) {
|
|
9170
|
-
const truncated = text.substring(0, maxSnippetLength);
|
|
9171
|
-
return { snippet: `${truncated}...`, positions: [] };
|
|
9172
|
-
}
|
|
9173
|
-
return { snippet: text, positions: [] };
|
|
9174
|
-
}
|
|
9175
|
-
if (maxSnippetLength > 0 && text.length > maxSnippetLength) {
|
|
9176
|
-
const { snippet, adjustedPositions } = applySnippetTruncation(
|
|
9177
|
-
text,
|
|
9178
|
-
text,
|
|
9179
|
-
mergedPositions,
|
|
9180
|
-
maxSnippetLength,
|
|
9181
|
-
preTag,
|
|
9182
|
-
postTag
|
|
9183
|
-
);
|
|
9184
|
-
return { snippet, positions: adjustedPositions };
|
|
9185
|
-
}
|
|
9186
|
-
let result = text;
|
|
9187
|
-
const sortedDesc = [...mergedPositions].sort((a, b) => b.start - a.start);
|
|
9188
|
-
for (const pos of sortedDesc) {
|
|
9189
|
-
result = result.substring(0, pos.start) + preTag + result.substring(pos.start, pos.end) + postTag + result.substring(pos.end);
|
|
9190
|
-
}
|
|
9191
|
-
return { snippet: result, positions: mergedPositions };
|
|
9192
|
-
}
|
|
9193
|
-
|
|
9194
|
-
// src/engine/query/highlight.ts
|
|
9195
|
-
function applyHighlights(hits, params, language, analysis) {
|
|
9196
|
-
if (!params.highlight) return;
|
|
9197
|
-
const queryTokenResult = tokenize(params.term ?? "", language, {
|
|
9198
|
-
stem: true,
|
|
9199
|
-
removeStopWords: true,
|
|
9200
|
-
customTokenizer: analysis.customTokenizer,
|
|
9201
|
-
stopWordOverride: analysis.stopWords
|
|
9202
|
-
});
|
|
9203
|
-
let prefixToken;
|
|
9204
|
-
if (params.prefix === true && params.exact !== true && queryTokenResult.tokens.length > 0) {
|
|
9205
|
-
const lastPosition = queryTokenResult.tokens[queryTokenResult.tokens.length - 1].position;
|
|
9206
|
-
const unstemmed = tokenize(params.term ?? "", language, {
|
|
9207
|
-
stem: false,
|
|
9208
|
-
removeStopWords: true,
|
|
9209
|
-
customTokenizer: analysis.customTokenizer,
|
|
9210
|
-
stopWordOverride: analysis.stopWords
|
|
9211
|
-
});
|
|
9212
|
-
for (const t of unstemmed.tokens) {
|
|
9213
|
-
if (t.position === lastPosition) {
|
|
9214
|
-
prefixToken = t.token;
|
|
9215
|
-
break;
|
|
9216
|
-
}
|
|
9217
|
-
}
|
|
9218
|
-
}
|
|
9219
|
-
for (const hit of hits) {
|
|
9220
|
-
const highlights = {};
|
|
9221
|
-
for (const field of params.highlight.fields) {
|
|
9222
|
-
const doc = hit.document;
|
|
9223
|
-
const fieldValue = doc[field];
|
|
9224
|
-
if (typeof fieldValue === "string") {
|
|
9225
|
-
highlights[field] = highlightField(fieldValue, queryTokenResult.tokens, language, {
|
|
9226
|
-
preTag: params.highlight.preTag,
|
|
9227
|
-
postTag: params.highlight.postTag,
|
|
9228
|
-
maxSnippetLength: params.highlight.maxSnippetLength,
|
|
9229
|
-
prefixToken
|
|
9230
|
-
});
|
|
9231
|
-
}
|
|
9232
|
-
}
|
|
9233
|
-
hit.highlights = highlights;
|
|
9234
|
-
}
|
|
9235
|
-
}
|
|
9236
|
-
|
|
9237
|
-
// src/engine/query/shared.ts
|
|
9238
|
-
function partitionsFor(manager, partitionIds) {
|
|
9239
|
-
return partitionsIn(manager, partitionIds);
|
|
9240
|
-
}
|
|
9241
|
-
function coverageFor(manager, partitionIds) {
|
|
9242
|
-
if (partitionIds === void 0) {
|
|
9243
|
-
return {
|
|
9244
|
-
totalPartitions: manager.partitionCount,
|
|
9245
|
-
queriedPartitions: manager.partitionCount,
|
|
9246
|
-
timedOutPartitions: 0,
|
|
9247
|
-
failedPartitions: 0
|
|
9248
|
-
};
|
|
9249
|
-
}
|
|
9250
|
-
const queriedPartitions = partitionsFor(manager, partitionIds).length;
|
|
9251
|
-
return {
|
|
9252
|
-
totalPartitions: partitionIds.length,
|
|
9253
|
-
queriedPartitions,
|
|
9254
|
-
timedOutPartitions: 0,
|
|
9255
|
-
failedPartitions: partitionIds.length - queriedPartitions
|
|
9256
|
-
};
|
|
9257
|
-
}
|
|
9258
|
-
function partitionsForVectorSearch(manager, vecIndex, partitionIds) {
|
|
9259
|
-
if (partitionIds === void 0) {
|
|
9260
|
-
return void 0;
|
|
9261
|
-
}
|
|
9262
|
-
if (!vecIndex.partitionsKnown()) {
|
|
9263
|
-
vecIndex.assignPartitions((docId) => manager.partitionIdOf(docId));
|
|
9264
|
-
}
|
|
9265
|
-
return new Set(partitionIds);
|
|
9266
|
-
}
|
|
9267
|
-
function scoringConfigFor(params, context) {
|
|
9268
|
-
const scoringMode = params.scoring ?? context.config.defaultScoring ?? "local";
|
|
9269
|
-
if (scoringMode !== "broadcast") {
|
|
9270
|
-
return { scoringMode, partitionIds: context.partitionIds };
|
|
9271
|
-
}
|
|
9272
|
-
return { scoringMode, globalStats: context.broadcastStats?.(context.indexName), partitionIds: context.partitionIds };
|
|
9273
|
-
}
|
|
9274
|
-
function broadcastStatsForWorker(params, context, scoring) {
|
|
9275
|
-
if (scoring.scoringMode !== "broadcast" || scoring.globalStats === void 0) {
|
|
9276
|
-
return void 0;
|
|
9277
|
-
}
|
|
9278
|
-
const term = params.term;
|
|
9279
|
-
if (term === void 0 || term.trim().length === 0) {
|
|
9280
|
-
return void 0;
|
|
9281
|
-
}
|
|
9282
|
-
return pruneStatsToQueryTerms(scoring.globalStats, term, context.language, context.manager.analysis);
|
|
9283
|
-
}
|
|
9284
|
-
function searchOptionsFor(manager) {
|
|
9285
|
-
return {
|
|
9286
|
-
bm25Params: manager.config.bm25,
|
|
9287
|
-
stopWords: manager.analysis.stopWords,
|
|
9288
|
-
customTokenizer: manager.analysis.customTokenizer
|
|
9289
|
-
};
|
|
9290
|
-
}
|
|
9291
|
-
function collectFilterDocIds(manager, params, schema, partitionIds) {
|
|
9292
|
-
const filterDocIds = /* @__PURE__ */ new Set();
|
|
9293
|
-
if (!params.filters) return filterDocIds;
|
|
9294
|
-
for (const partition of partitionsFor(manager, partitionIds)) {
|
|
9295
|
-
const partitionFiltered = partition.applyFilters(params.filters, schema);
|
|
9296
|
-
for (const docId of partitionFiltered) {
|
|
9297
|
-
filterDocIds.add(docId);
|
|
9298
|
-
}
|
|
9299
|
-
}
|
|
9300
|
-
return filterDocIds;
|
|
9301
|
-
}
|
|
9302
|
-
function vectorResultsToScored(results) {
|
|
9303
|
-
return results.map((r) => ({
|
|
9304
|
-
docId: r.docId,
|
|
9305
|
-
score: r.score,
|
|
9306
|
-
termFrequencies: {},
|
|
9307
|
-
fieldLengths: {},
|
|
9308
|
-
idf: {}
|
|
9309
|
-
}));
|
|
9310
|
-
}
|
|
9311
|
-
function resolveVectorIndex(manager, fieldName) {
|
|
9312
|
-
return manager.getVectorIndexes().get(fieldName);
|
|
9313
|
-
}
|
|
9314
|
-
function clampAlpha(alpha) {
|
|
9315
|
-
if (alpha === void 0) return 0.5;
|
|
9316
|
-
if (!Number.isFinite(alpha)) return 0.5;
|
|
9317
|
-
if (alpha < 0) return 0;
|
|
9318
|
-
if (alpha > 1) return 1;
|
|
9319
|
-
return alpha;
|
|
9320
|
-
}
|
|
9321
|
-
|
|
9322
|
-
// src/engine/query/sorted.ts
|
|
9323
|
-
function sortsWithoutScores(params) {
|
|
9324
|
-
return params.includeScores !== true && params.includeScoreComponents !== true && params.minScore === void 0 && params.group === void 0 && params.pinned === void 0 && (params.termMatch === void 0 || params.termMatch === "any");
|
|
9325
|
-
}
|
|
9326
|
-
function executeSortedQueryPage(params, context, limit, offset, sortSignature) {
|
|
9327
|
-
const { manager, language, config } = context;
|
|
9328
|
-
const schema = config.schema;
|
|
9329
|
-
let anchorKey = null;
|
|
9330
|
-
let anchorId = null;
|
|
9331
|
-
if (params.searchAfter !== void 0) {
|
|
9332
|
-
const decoded = decodePageCursor(params.searchAfter);
|
|
9333
|
-
requireMatchingCursor(decoded, params.searchAfter, sortSignature, true, context.cursorBinding);
|
|
9334
|
-
anchorKey = decoded.sortKey;
|
|
9335
|
-
anchorId = decoded.anchor;
|
|
9336
|
-
}
|
|
9337
|
-
const normalized = normalizeSort(params.sort);
|
|
9338
|
-
const fields = normalized.map((entry) => entry.field);
|
|
9339
|
-
const directions = normalized.map((entry) => entry.direction);
|
|
9340
|
-
const flatSchema = flattenSchema(schema);
|
|
9341
|
-
const fieldTypes = fields.map((field) => flatSchema[field]);
|
|
9342
|
-
const options = searchOptionsFor(manager);
|
|
9343
|
-
const partitions = partitionsFor(manager, context.partitionIds);
|
|
9344
|
-
const partitionLimit = offset + limit + 1;
|
|
9345
|
-
const merged = [];
|
|
9346
|
-
const partitionFacets = [];
|
|
9347
|
-
let count = 0;
|
|
9348
|
-
for (const partition of partitions) {
|
|
9349
|
-
const matches = fulltextMatches(partition, params, language, schema, options);
|
|
9350
|
-
if (params.facets !== void 0) {
|
|
9351
|
-
partitionFacets.push(
|
|
9352
|
-
partition.computeFacets(
|
|
9353
|
-
matches === null ? /* @__PURE__ */ new Set() : { ordinalBitset: matches.ordinalBitset() },
|
|
9354
|
-
params.facets,
|
|
9355
|
-
schema
|
|
9356
|
-
)
|
|
9357
|
-
);
|
|
9358
|
-
}
|
|
9359
|
-
if (matches === null || matches.count === 0) continue;
|
|
9360
|
-
count += matches.count;
|
|
9361
|
-
const page2 = partition.sortedPage({
|
|
9362
|
-
fields,
|
|
9363
|
-
directions,
|
|
9364
|
-
fieldTypes,
|
|
9365
|
-
limit: partitionLimit,
|
|
9366
|
-
anchorKey,
|
|
9367
|
-
anchorId,
|
|
9368
|
-
matches
|
|
9369
|
-
});
|
|
9370
|
-
for (const entry of page2) merged.push(entry);
|
|
9371
|
-
}
|
|
9372
|
-
merged.sort((a, b) => compareComparableKeys(a.key, b.key, directions) || compareCodePoints(a.id, b.id));
|
|
9373
|
-
const hasMore = merged.length > offset + limit;
|
|
9374
|
-
const page = merged.slice(offset, offset + limit);
|
|
9375
|
-
const last = page[page.length - 1];
|
|
9376
|
-
let cursor;
|
|
9377
|
-
if (hasMore && last !== void 0) {
|
|
9378
|
-
cursor = encodePageCursor({
|
|
9379
|
-
anchor: last.id,
|
|
9380
|
-
score: null,
|
|
9381
|
-
sortKey: last.key,
|
|
9382
|
-
sortSignature,
|
|
9383
|
-
binding: context.cursorBinding
|
|
9384
|
-
});
|
|
9385
|
-
}
|
|
9386
|
-
return {
|
|
9387
|
-
hits: page.map((entry) => ({ id: entry.id, document: void 0 })),
|
|
9388
|
-
count,
|
|
9389
|
-
cursor,
|
|
9390
|
-
facets: partitionFacets.length > 0 ? mergeFacets(partitionFacets) : void 0
|
|
9391
|
-
};
|
|
9392
|
-
}
|
|
9393
|
-
|
|
9394
|
-
// src/search/fusion.ts
|
|
9395
|
-
function reciprocalRankFusion(lists, options) {
|
|
9396
|
-
const k = options.k > 0 ? options.k : 60;
|
|
9397
|
-
const scores = /* @__PURE__ */ new Map();
|
|
9398
|
-
const docData = /* @__PURE__ */ new Map();
|
|
9399
|
-
for (const list of lists) {
|
|
9400
|
-
for (let rank = 0; rank < list.length; rank++) {
|
|
9401
|
-
const doc = list[rank];
|
|
9402
|
-
const rrfContribution = 1 / (k + rank + 1);
|
|
9403
|
-
scores.set(doc.docId, (scores.get(doc.docId) ?? 0) + rrfContribution);
|
|
9404
|
-
if (!docData.has(doc.docId)) {
|
|
9405
|
-
docData.set(doc.docId, doc);
|
|
9406
|
-
}
|
|
9407
|
-
}
|
|
9408
|
-
}
|
|
9409
|
-
const result = [];
|
|
9410
|
-
for (const [docId, score] of scores) {
|
|
9411
|
-
const original = docData.get(docId);
|
|
9412
|
-
if (!original) continue;
|
|
9413
|
-
result.push({
|
|
9414
|
-
docId,
|
|
9415
|
-
score,
|
|
9416
|
-
termFrequencies: original.termFrequencies,
|
|
9417
|
-
fieldLengths: original.fieldLengths,
|
|
9418
|
-
idf: original.idf
|
|
9419
|
-
});
|
|
9420
|
-
}
|
|
9421
|
-
result.sort((a, b) => b.score - a.score || compareCodePoints(a.docId, b.docId));
|
|
9422
|
-
return result;
|
|
9423
|
-
}
|
|
9424
|
-
function linearCombination(textResults, vectorResults, options) {
|
|
9425
|
-
const { alpha } = options;
|
|
9426
|
-
const normalizedText = minMaxNormalize(textResults);
|
|
9427
|
-
const normalizedVector = minMaxNormalize(vectorResults);
|
|
9428
|
-
const textScoreMap = /* @__PURE__ */ new Map();
|
|
9429
|
-
for (const doc of normalizedText) {
|
|
9430
|
-
textScoreMap.set(doc.docId, doc.score);
|
|
9431
|
-
}
|
|
9432
|
-
const textDocMap = /* @__PURE__ */ new Map();
|
|
9433
|
-
for (const doc of textResults) {
|
|
9434
|
-
textDocMap.set(doc.docId, doc);
|
|
9435
|
-
}
|
|
9436
|
-
const vectorScoreMap = /* @__PURE__ */ new Map();
|
|
9437
|
-
for (const doc of normalizedVector) {
|
|
9438
|
-
vectorScoreMap.set(doc.docId, doc.score);
|
|
9439
|
-
}
|
|
9440
|
-
const allDocIds = /* @__PURE__ */ new Set();
|
|
9441
|
-
for (const doc of textResults) allDocIds.add(doc.docId);
|
|
9442
|
-
for (const doc of vectorResults) allDocIds.add(doc.docId);
|
|
9443
|
-
const result = [];
|
|
9444
|
-
for (const docId of allDocIds) {
|
|
9445
|
-
const tScore = textScoreMap.get(docId) ?? 0;
|
|
9446
|
-
const vScore = vectorScoreMap.get(docId) ?? 0;
|
|
9447
|
-
const combined = alpha * vScore + (1 - alpha) * tScore;
|
|
9448
|
-
const original = textDocMap.get(docId);
|
|
9449
|
-
result.push({
|
|
9450
|
-
docId,
|
|
9451
|
-
score: combined,
|
|
9452
|
-
termFrequencies: original?.termFrequencies ?? {},
|
|
9453
|
-
fieldLengths: original?.fieldLengths ?? {},
|
|
9454
|
-
idf: original?.idf ?? {}
|
|
9455
|
-
});
|
|
9456
|
-
}
|
|
9457
|
-
result.sort((a, b) => b.score - a.score || compareCodePoints(a.docId, b.docId));
|
|
9458
|
-
return result;
|
|
9459
|
-
}
|
|
9460
|
-
function minMaxNormalize(docs) {
|
|
9461
|
-
if (docs.length === 0) return [];
|
|
9462
|
-
let min = docs[0].score;
|
|
9463
|
-
let max = docs[0].score;
|
|
9464
|
-
for (let i = 1; i < docs.length; i++) {
|
|
9465
|
-
if (docs[i].score < min) min = docs[i].score;
|
|
9466
|
-
if (docs[i].score > max) max = docs[i].score;
|
|
9467
|
-
}
|
|
9468
|
-
const range = max - min;
|
|
9469
|
-
if (range === 0) {
|
|
9470
|
-
return docs.map((d) => ({ docId: d.docId, score: 1 }));
|
|
9471
|
-
}
|
|
9472
|
-
return docs.map((d) => ({ docId: d.docId, score: (d.score - min) / range }));
|
|
9473
|
-
}
|
|
9474
|
-
|
|
9475
|
-
// src/engine/query/vector.ts
|
|
9476
|
-
async function executeVectorSearch(params, manager, config, limit, offset, partitionIds) {
|
|
9477
|
-
const vectorConfig = params.vector;
|
|
9478
|
-
if (!vectorConfig || !vectorConfig.value) {
|
|
9479
|
-
return { scored: [], totalMatched: 0 };
|
|
9480
|
-
}
|
|
9481
|
-
const vecIndex = resolveVectorIndex(manager, vectorConfig.field);
|
|
9482
|
-
if (!vecIndex) {
|
|
9483
|
-
return { scored: [], totalMatched: 0 };
|
|
9484
|
-
}
|
|
9485
|
-
let filterDocIds;
|
|
9486
|
-
let filterPartitions;
|
|
9487
|
-
if (params.filters) {
|
|
9488
|
-
filterDocIds = collectFilterDocIds(manager, params, config.schema, partitionIds);
|
|
9489
|
-
if (filterDocIds.size === 0) {
|
|
9490
|
-
return { scored: [], totalMatched: 0 };
|
|
9491
|
-
}
|
|
9492
|
-
} else {
|
|
9493
|
-
filterPartitions = partitionsForVectorSearch(manager, vecIndex, partitionIds);
|
|
9494
|
-
}
|
|
9495
|
-
const queryVec = new Float32Array(vectorConfig.value);
|
|
9496
|
-
const k = limit + offset + 1;
|
|
9497
|
-
const results = await vecIndex.searchParallel(queryVec, k, {
|
|
9498
|
-
metric: vectorConfig.metric ?? "cosine",
|
|
9499
|
-
minSimilarity: vectorConfig.similarity ?? -Infinity,
|
|
9500
|
-
...filterDocIds !== void 0 ? { filterDocIds } : {},
|
|
9501
|
-
...filterPartitions !== void 0 ? { filterPartitions } : {},
|
|
9502
|
-
efSearch: vectorConfig.efSearch
|
|
9503
|
-
});
|
|
9504
|
-
const scored = vectorResultsToScored(results);
|
|
9505
|
-
return { scored, totalMatched: scored.length };
|
|
9506
|
-
}
|
|
9507
|
-
async function executeHybridSearch(params, context, limit, offset) {
|
|
9508
|
-
const { manager, language, config, workerSearch, indexName } = context;
|
|
9509
|
-
const { vector: _vector, mode: _mode, hybrid: _hybrid, ...textOnlyParams } = params;
|
|
9510
|
-
let filterDocIds;
|
|
9511
|
-
if (params.filters) {
|
|
9512
|
-
filterDocIds = collectFilterDocIds(manager, params, config.schema, context.partitionIds);
|
|
9513
|
-
if (filterDocIds.size === 0) {
|
|
9514
|
-
return { scored: [], totalMatched: 0 };
|
|
9515
|
-
}
|
|
9516
|
-
}
|
|
9517
|
-
let textFanOutResult;
|
|
9518
|
-
const scoring = scoringConfigFor(params, context);
|
|
9519
|
-
const textWorkerResult = workerSearch ? await workerSearch(
|
|
9520
|
-
indexName,
|
|
9521
|
-
textOnlyParams,
|
|
9522
|
-
broadcastStatsForWorker(textOnlyParams, context, scoring),
|
|
9523
|
-
context.partitionIds
|
|
9524
|
-
) : null;
|
|
9525
|
-
if (textWorkerResult) {
|
|
9526
|
-
textFanOutResult = textWorkerResult;
|
|
9527
|
-
} else {
|
|
9528
|
-
textFanOutResult = await fanOutQuery(
|
|
9529
|
-
manager,
|
|
9530
|
-
textOnlyParams,
|
|
9531
|
-
language,
|
|
9532
|
-
config.schema,
|
|
9533
|
-
scoring,
|
|
9534
|
-
searchOptionsFor(manager)
|
|
9535
|
-
);
|
|
9536
|
-
}
|
|
9537
|
-
const vectorConfig = params.vector;
|
|
9538
|
-
if (!vectorConfig || !vectorConfig.value) {
|
|
9539
|
-
return {
|
|
9540
|
-
scored: textFanOutResult.scored,
|
|
9541
|
-
totalMatched: textFanOutResult.totalMatched,
|
|
9542
|
-
facets: textFanOutResult.facets
|
|
9543
|
-
};
|
|
9544
|
-
}
|
|
9545
|
-
let vectorScored = [];
|
|
9546
|
-
const vecIndex = resolveVectorIndex(manager, vectorConfig.field);
|
|
9547
|
-
if (vecIndex) {
|
|
9548
|
-
const queryVec = new Float32Array(vectorConfig.value);
|
|
9549
|
-
const vectorK = limit + offset + 1;
|
|
9550
|
-
const filterPartitions = filterDocIds === void 0 ? partitionsForVectorSearch(manager, vecIndex, context.partitionIds) : void 0;
|
|
9551
|
-
const vectorResults = await vecIndex.searchParallel(queryVec, vectorK, {
|
|
9552
|
-
metric: vectorConfig.metric ?? "cosine",
|
|
9553
|
-
minSimilarity: vectorConfig.similarity ?? -Infinity,
|
|
9554
|
-
...filterDocIds !== void 0 ? { filterDocIds } : {},
|
|
9555
|
-
...filterPartitions !== void 0 ? { filterPartitions } : {},
|
|
9556
|
-
efSearch: vectorConfig.efSearch
|
|
9557
|
-
});
|
|
9558
|
-
vectorScored = vectorResultsToScored(vectorResults);
|
|
9559
|
-
}
|
|
9560
|
-
const hybridConfig = params.hybrid ?? {};
|
|
9561
|
-
const strategy = hybridConfig.strategy ?? "rrf";
|
|
9562
|
-
let fusedScored;
|
|
9563
|
-
if (strategy === "rrf") {
|
|
9564
|
-
const rrfK = hybridConfig.k !== void 0 && hybridConfig.k > 0 ? hybridConfig.k : 60;
|
|
9565
|
-
fusedScored = reciprocalRankFusion([textFanOutResult.scored, vectorScored], { k: rrfK });
|
|
9566
|
-
} else {
|
|
9567
|
-
const alpha = clampAlpha(hybridConfig.alpha);
|
|
9568
|
-
fusedScored = linearCombination(textFanOutResult.scored, vectorScored, { alpha });
|
|
9569
|
-
}
|
|
9570
|
-
if (params.minScore !== void 0 && params.minScore > 0) {
|
|
9571
|
-
const threshold = params.minScore;
|
|
9572
|
-
fusedScored = fusedScored.filter((doc) => doc.score >= threshold);
|
|
9573
|
-
}
|
|
9574
|
-
return {
|
|
9575
|
-
scored: fusedScored,
|
|
9576
|
-
totalMatched: fusedScored.length,
|
|
9577
|
-
facets: textFanOutResult.facets
|
|
9578
|
-
};
|
|
9579
|
-
}
|
|
9580
|
-
|
|
9581
|
-
// src/engine/query/index.ts
|
|
9582
|
-
async function executeQuery(params, context) {
|
|
9583
|
-
const { manager, language, config, workerSearch, indexName } = context;
|
|
9584
|
-
const startTime = now();
|
|
9585
|
-
const limit = clampLimit(params.limit);
|
|
9586
|
-
const offset = clampOffset(params.offset);
|
|
9587
|
-
requireWithinResultWindow(limit, offset);
|
|
9588
|
-
const sortFields = normalizeSort(params.sort);
|
|
9589
|
-
const sortSignature = sortSignatureOf(params.sort);
|
|
9590
|
-
const hasTerm = params.term !== void 0 && params.term.trim().length > 0;
|
|
9591
|
-
const hasVector = params.vector !== void 0 && params.vector.value !== void 0;
|
|
9592
|
-
const isHybridMode = params.mode === "hybrid" || hasTerm && hasVector;
|
|
9593
|
-
const isVectorOnly = (params.mode === "vector" || hasVector && !hasTerm) && !isHybridMode;
|
|
9594
|
-
if (sortSignature !== null && (isHybridMode || params.hybrid !== void 0)) {
|
|
9595
|
-
throw new NarsilError(
|
|
9596
|
-
ErrorCodes.SEARCH_INVALID_MODE,
|
|
9597
|
-
"A hybrid query cannot carry a sort, because fusion defines the order of hybrid results",
|
|
9598
|
-
{ sort: sortFields.map((entry) => entry.field) }
|
|
9599
|
-
);
|
|
9600
|
-
}
|
|
9601
|
-
requireSortableFields(params.sort, config.schema);
|
|
9602
|
-
let paginated;
|
|
9603
|
-
let nextCursor;
|
|
9604
|
-
let count;
|
|
9605
|
-
let facets;
|
|
9606
|
-
let groups;
|
|
9607
|
-
if (sortSignature !== null && hasTerm && !isVectorOnly && !isHybridMode && sortsWithoutScores(params)) {
|
|
9608
|
-
const page = executeSortedQueryPage(params, context, limit, offset, sortSignature);
|
|
9609
|
-
paginated = page.hits;
|
|
9610
|
-
nextCursor = page.cursor;
|
|
9611
|
-
count = page.count;
|
|
9612
|
-
facets = page.facets;
|
|
9613
|
-
} else {
|
|
9614
|
-
const requestedVectorField = params.vector?.field;
|
|
9615
|
-
const hasGlobalVectorIndex = requestedVectorField !== void 0 && manager.getVectorIndexes().has(requestedVectorField);
|
|
9616
|
-
let fanOutResult;
|
|
9617
|
-
if (isVectorOnly && hasGlobalVectorIndex) {
|
|
9618
|
-
fanOutResult = await executeVectorSearch(params, manager, config, limit, offset, context.partitionIds);
|
|
9619
|
-
} else if (isHybridMode && hasGlobalVectorIndex) {
|
|
9620
|
-
fanOutResult = await executeHybridSearch(params, context, limit, offset);
|
|
9621
|
-
} else {
|
|
9622
|
-
const scoring = scoringConfigFor(params, context);
|
|
9623
|
-
const workerResult = workerSearch ? await workerSearch(indexName, params, broadcastStatsForWorker(params, context, scoring), context.partitionIds) : null;
|
|
9624
|
-
if (workerResult) {
|
|
9625
|
-
fanOutResult = workerResult;
|
|
9626
|
-
} else {
|
|
9627
|
-
fanOutResult = await fanOutQuery(manager, params, language, config.schema, scoring, searchOptionsFor(manager));
|
|
9628
|
-
}
|
|
9629
|
-
}
|
|
9630
|
-
const needsFullHits = params.sort !== void 0 || params.group !== void 0 || params.pinned !== void 0 || params.searchAfter !== void 0;
|
|
9631
|
-
let hits;
|
|
9632
|
-
if (needsFullHits) {
|
|
9633
|
-
hits = fanOutResult.scored.map((scored) => ({
|
|
9634
|
-
id: scored.docId,
|
|
9635
|
-
score: scored.score,
|
|
9636
|
-
document: void 0,
|
|
9637
|
-
scoreComponents: params.includeScoreComponents ? { termFrequencies: scored.termFrequencies, fieldLengths: scored.fieldLengths, idf: scored.idf } : void 0
|
|
9638
|
-
}));
|
|
9639
|
-
} else {
|
|
9640
|
-
const end = Math.min(offset + limit + 1, fanOutResult.scored.length);
|
|
9641
|
-
hits = new Array(end);
|
|
9642
|
-
for (let i = 0; i < end; i++) {
|
|
9643
|
-
const scored = fanOutResult.scored[i];
|
|
9644
|
-
hits[i] = {
|
|
9645
|
-
id: scored.docId,
|
|
9646
|
-
score: scored.score,
|
|
9647
|
-
document: void 0,
|
|
9648
|
-
scoreComponents: params.includeScoreComponents ? { termFrequencies: scored.termFrequencies, fieldLengths: scored.fieldLengths, idf: scored.idf } : void 0
|
|
9649
|
-
};
|
|
9650
|
-
}
|
|
9651
|
-
}
|
|
9652
|
-
const sortFieldNames = sortFields.map((entry) => entry.field);
|
|
9653
|
-
const sortFlatSchema = sortFieldNames.length === 0 ? {} : flattenSchema(config.schema);
|
|
9654
|
-
const sortFieldTypes = sortFieldNames.map((field) => sortFlatSchema[field]);
|
|
9655
|
-
const sortKeyCache = /* @__PURE__ */ new Map();
|
|
9656
|
-
const sortKeyOf = (docId) => {
|
|
9657
|
-
let key = sortKeyCache.get(docId);
|
|
9658
|
-
if (key === void 0) {
|
|
9659
|
-
key = manager.sortValues(docId, sortFieldNames, sortFieldTypes);
|
|
9660
|
-
sortKeyCache.set(docId, key);
|
|
9661
|
-
}
|
|
9662
|
-
return key;
|
|
9663
|
-
};
|
|
9664
|
-
if (params.sort) {
|
|
9665
|
-
hits = applySorting(hits, params.sort, sortKeyOf);
|
|
9666
|
-
}
|
|
9667
|
-
if (params.group) {
|
|
9668
|
-
groups = applyGrouping(hits, params.group, (docId) => manager.getRef(docId));
|
|
9669
|
-
}
|
|
9670
|
-
if (params.pinned && params.searchAfter === void 0 && context.partitionIds === void 0) {
|
|
9671
|
-
hits = applyPinning(hits, params.pinned, (docId) => {
|
|
9672
|
-
const doc = manager.getRef(docId);
|
|
9673
|
-
if (!doc) return void 0;
|
|
9674
|
-
return { id: docId, score: 0, document: doc };
|
|
9675
|
-
});
|
|
9676
|
-
}
|
|
9677
|
-
let sortContext;
|
|
9678
|
-
if (sortSignature !== null) {
|
|
9679
|
-
sortContext = {
|
|
9680
|
-
signature: sortSignature,
|
|
9681
|
-
directions: sortFields.map((entry) => entry.direction),
|
|
9682
|
-
sortKeyOf
|
|
9683
|
-
};
|
|
9684
|
-
}
|
|
9685
|
-
const pinnedIds = params.pinned !== void 0 && params.searchAfter === void 0 ? new Set(params.pinned.map((entry) => entry.docId)) : void 0;
|
|
9686
|
-
const paged = applyPagination(
|
|
9687
|
-
hits,
|
|
9688
|
-
limit,
|
|
9689
|
-
offset,
|
|
9690
|
-
context.cursorBinding,
|
|
9691
|
-
params.searchAfter,
|
|
9692
|
-
sortContext,
|
|
9693
|
-
pinnedIds
|
|
9694
|
-
);
|
|
9695
|
-
paginated = paged.paginated;
|
|
9696
|
-
nextCursor = paged.nextCursor;
|
|
9697
|
-
count = fanOutResult.totalMatched;
|
|
9698
|
-
facets = fanOutResult.facets;
|
|
9699
|
-
if (sortSignature !== null && params.includeScores !== true) {
|
|
9700
|
-
for (const hit of hits) hit.score = void 0;
|
|
9701
|
-
}
|
|
9702
|
-
}
|
|
9703
|
-
const projection = resolveProjection(params.document);
|
|
9704
|
-
if (projection.kind === "none") {
|
|
9705
|
-
for (const hit of paginated) {
|
|
9706
|
-
hit.document = {};
|
|
9707
|
-
}
|
|
9708
|
-
} else {
|
|
9709
|
-
for (const hit of paginated) {
|
|
9710
|
-
hit.document = manager.get(hit.id, projection) ?? {};
|
|
9711
|
-
}
|
|
9712
|
-
}
|
|
9713
|
-
if (groups) {
|
|
9714
|
-
for (const group of groups) {
|
|
9715
|
-
for (const hit of group.hits) {
|
|
9716
|
-
if (projection.kind === "none") {
|
|
9717
|
-
hit.document = {};
|
|
9718
|
-
continue;
|
|
9719
|
-
}
|
|
9720
|
-
hit.document = manager.get(hit.id, projection) ?? {};
|
|
9721
|
-
}
|
|
9722
|
-
}
|
|
9723
|
-
}
|
|
9724
|
-
if (params.highlight) {
|
|
9725
|
-
applyHighlights(paginated, params, language, manager.analysis);
|
|
9726
|
-
}
|
|
9727
|
-
const elapsed = now() - startTime;
|
|
9728
|
-
return {
|
|
9729
|
-
hits: paginated,
|
|
9730
|
-
count,
|
|
9731
|
-
elapsed,
|
|
9732
|
-
cursor: nextCursor,
|
|
9733
|
-
facets,
|
|
9734
|
-
groups,
|
|
9735
|
-
coverage: coverageFor(manager, context.partitionIds)
|
|
9736
|
-
};
|
|
9737
|
-
}
|
|
9738
|
-
async function executePreflight(params, context) {
|
|
9739
|
-
const { manager, language, config, workerSearch, indexName } = context;
|
|
9740
|
-
const startTime = now();
|
|
9741
|
-
const hasTerm = params.term !== void 0 && params.term.trim().length > 0;
|
|
9742
|
-
const hasVector = params.vector !== void 0 && params.vector.value !== void 0;
|
|
9743
|
-
const isHybridMode = params.mode === "hybrid" || hasTerm && hasVector;
|
|
9744
|
-
const isVectorOnly = (params.mode === "vector" || hasVector && !hasTerm) && !isHybridMode;
|
|
9745
|
-
const requestedVectorField = params.vector?.field;
|
|
9746
|
-
const hasGlobalVectorIndex = requestedVectorField !== void 0 && manager.getVectorIndexes().has(requestedVectorField);
|
|
9747
|
-
let totalMatched;
|
|
9748
|
-
const preflightLimit = 1e3;
|
|
9749
|
-
const preflightOffset = 0;
|
|
9750
|
-
if (isVectorOnly && hasGlobalVectorIndex) {
|
|
9751
|
-
const result = await executeVectorSearch(
|
|
9752
|
-
params,
|
|
9753
|
-
manager,
|
|
9754
|
-
config,
|
|
9755
|
-
preflightLimit,
|
|
9756
|
-
preflightOffset,
|
|
9757
|
-
context.partitionIds
|
|
9758
|
-
);
|
|
9759
|
-
totalMatched = result.totalMatched;
|
|
9760
|
-
} else if (isHybridMode && hasGlobalVectorIndex) {
|
|
9761
|
-
const result = await executeHybridSearch(params, context, preflightLimit, preflightOffset);
|
|
9762
|
-
totalMatched = result.totalMatched;
|
|
9763
|
-
} else if (countsWithoutScores(params)) {
|
|
9764
|
-
totalMatched = fanOutMatchCount(manager, params, language, config.schema, {
|
|
9765
|
-
searchOptions: searchOptionsFor(manager),
|
|
9766
|
-
partitionIds: context.partitionIds
|
|
9767
|
-
});
|
|
9768
|
-
} else {
|
|
9769
|
-
const scoring = scoringConfigFor(params, context);
|
|
9770
|
-
const workerResult = workerSearch ? await workerSearch(indexName, params, broadcastStatsForWorker(params, context, scoring), context.partitionIds) : null;
|
|
9771
|
-
if (workerResult) {
|
|
9772
|
-
totalMatched = workerResult.totalMatched;
|
|
9773
|
-
} else {
|
|
9774
|
-
const fanOutResult = await fanOutQuery(
|
|
9775
|
-
manager,
|
|
9776
|
-
params,
|
|
9777
|
-
language,
|
|
9778
|
-
config.schema,
|
|
9779
|
-
scoring,
|
|
9780
|
-
searchOptionsFor(manager)
|
|
9781
|
-
);
|
|
9782
|
-
totalMatched = fanOutResult.totalMatched;
|
|
9783
|
-
}
|
|
9784
|
-
}
|
|
9785
|
-
const elapsed = now() - startTime;
|
|
9786
|
-
return { count: totalMatched, elapsed };
|
|
9787
|
-
}
|
|
9788
|
-
|
|
9789
|
-
// src/engine/resolve-vector-text.ts
|
|
9790
|
-
async function resolveVectorText(params, embeddingAdapter, signal, embeddingAdapterName) {
|
|
9791
|
-
if (!params.vector) return params;
|
|
9792
|
-
if (params.vector.text === void 0 && params.vector.value === void 0) return params;
|
|
9793
|
-
if (params.vector.value !== void 0 && params.vector.text === void 0) return params;
|
|
9794
|
-
if (params.vector.text !== void 0 && params.vector.value !== void 0) {
|
|
9795
|
-
throw new NarsilError(ErrorCodes.EMBEDDING_CONFIG_INVALID, "Vector query cannot have both 'text' and 'value'");
|
|
9796
|
-
}
|
|
9797
|
-
if (!embeddingAdapter) {
|
|
9798
|
-
throw new NarsilError(
|
|
9799
|
-
ErrorCodes.EMBEDDING_CONFIG_INVALID,
|
|
9800
|
-
embeddingAdapterName ? `Vector query with 'text' needs embedding adapter "${embeddingAdapterName}", which is not registered on this engine. Register it with registerEmbeddingAdapter or supply a raw vector.` : "Vector query with 'text' requires an embedding adapter on the index or instance",
|
|
9801
|
-
embeddingAdapterName ? { adapter: embeddingAdapterName } : void 0
|
|
9802
|
-
);
|
|
9803
|
-
}
|
|
9804
|
-
if (typeof params.vector.text !== "string") {
|
|
9805
|
-
throw new NarsilError(
|
|
9806
|
-
ErrorCodes.DOC_VALIDATION_FAILED,
|
|
9807
|
-
`Vector query 'text' must be a string, got ${typeof params.vector.text}`
|
|
9808
|
-
);
|
|
9809
|
-
}
|
|
9810
|
-
if (params.vector.text.length === 0) {
|
|
9811
|
-
throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Vector query 'text' must not be empty");
|
|
9812
|
-
}
|
|
9813
|
-
const raw = await embeddingAdapter.embed(params.vector.text, "query", signal);
|
|
9814
|
-
if (!(raw instanceof Float32Array)) {
|
|
9815
|
-
throw new NarsilError(
|
|
9816
|
-
ErrorCodes.EMBEDDING_FAILED,
|
|
9817
|
-
`Adapter returned ${typeof raw} for query embedding, expected Float32Array`
|
|
9818
|
-
);
|
|
9819
|
-
}
|
|
9820
|
-
if (raw.length !== embeddingAdapter.dimensions) {
|
|
9821
|
-
throw new NarsilError(
|
|
9822
|
-
ErrorCodes.EMBEDDING_DIMENSION_MISMATCH,
|
|
9823
|
-
`Adapter returned ${raw.length}-dimensional vector for query, expected ${embeddingAdapter.dimensions}`,
|
|
9824
|
-
{ expected: embeddingAdapter.dimensions, actual: raw.length }
|
|
9825
|
-
);
|
|
9826
|
-
}
|
|
9827
|
-
const resolved = { ...params.vector, value: Array.from(raw) };
|
|
9828
|
-
delete resolved.text;
|
|
9829
|
-
return { ...params, vector: resolved };
|
|
9830
|
-
}
|
|
9831
|
-
|
|
9832
|
-
// src/engine/suggest.ts
|
|
9833
|
-
var DEFAULT_SUGGEST_LIMIT = 10;
|
|
9834
|
-
var MAX_SUGGEST_LIMIT = 100;
|
|
9835
|
-
var SUGGEST_OVERSAMPLE_FACTOR = 1.5;
|
|
9836
|
-
var SUGGEST_OVERSAMPLE_PADDING = 10;
|
|
9837
|
-
var MAX_SUGGEST_SCATTER_LIMIT = Math.ceil(MAX_SUGGEST_LIMIT * SUGGEST_OVERSAMPLE_FACTOR) + SUGGEST_OVERSAMPLE_PADDING;
|
|
9838
|
-
function executeSuggest(manager, language, params, partitionIds) {
|
|
9839
|
-
const t0 = performance.now();
|
|
9840
|
-
const ceiling = partitionIds === void 0 ? MAX_SUGGEST_LIMIT : MAX_SUGGEST_SCATTER_LIMIT;
|
|
9841
|
-
const limit = Math.max(1, Math.min(clampRowCount(params.limit, DEFAULT_SUGGEST_LIMIT), ceiling));
|
|
9842
|
-
const rawPrefix = params.prefix.trim();
|
|
9843
|
-
if (rawPrefix.length === 0) {
|
|
9844
|
-
return { terms: [], elapsed: performance.now() - t0 };
|
|
9845
|
-
}
|
|
9846
|
-
const unstemmed = tokenize(rawPrefix, language, { stem: false, removeStopWords: false });
|
|
9847
|
-
const lastToken = unstemmed.tokens.length > 0 ? unstemmed.tokens[unstemmed.tokens.length - 1].token : rawPrefix.toLowerCase();
|
|
9848
|
-
if (lastToken.length === 0) {
|
|
9849
|
-
return { terms: [], elapsed: performance.now() - t0 };
|
|
9850
|
-
}
|
|
9851
|
-
const stemmed = language.stemmer ? language.stemmer(lastToken) : lastToken;
|
|
9852
|
-
const merged = /* @__PURE__ */ new Map();
|
|
9853
|
-
const perPartitionLimit = limit * 2;
|
|
9854
|
-
const partitions = partitionIds === void 0 ? manager.getAllPartitions() : partitionIds.map((partitionId) => manager.partitionAt(partitionId)).filter((partition) => partition !== void 0);
|
|
9855
|
-
for (const partition of partitions) {
|
|
9856
|
-
for (const suggestion of partition.suggestTerms(lastToken, stemmed, perPartitionLimit)) {
|
|
9857
|
-
let entry = merged.get(suggestion.token);
|
|
9858
|
-
if (!entry) {
|
|
9859
|
-
entry = { documentFrequency: 0, surfaceOccurrences: /* @__PURE__ */ new Map() };
|
|
9860
|
-
merged.set(suggestion.token, entry);
|
|
9861
|
-
}
|
|
9862
|
-
entry.documentFrequency += suggestion.documentFrequency;
|
|
9863
|
-
for (const s of suggestion.surfaces) {
|
|
9864
|
-
entry.surfaceOccurrences.set(s.surface, (entry.surfaceOccurrences.get(s.surface) ?? 0) + s.occurrences);
|
|
9865
|
-
}
|
|
9866
|
-
}
|
|
9867
|
-
}
|
|
9868
|
-
const terms = Array.from(merged.values()).map((entry) => ({ term: pickDisplaySurface(entry.surfaceOccurrences), documentFrequency: entry.documentFrequency })).sort((a, b) => b.documentFrequency - a.documentFrequency || compareCodePoints(a.term, b.term));
|
|
9869
|
-
if (terms.length > limit) terms.length = limit;
|
|
9870
|
-
return { terms, elapsed: performance.now() - t0 };
|
|
9871
|
-
}
|
|
9872
|
-
function pickDisplaySurface(surfaceOccurrences) {
|
|
9873
|
-
let best = "";
|
|
9874
|
-
let bestCount = -1;
|
|
9875
|
-
for (const [surface, occurrences] of surfaceOccurrences) {
|
|
9876
|
-
if (occurrences > bestCount || occurrences === bestCount && compareCodePoints(surface, best) < 0) {
|
|
9877
|
-
best = surface;
|
|
9878
|
-
bestCount = occurrences;
|
|
9879
|
-
}
|
|
9880
|
-
}
|
|
9881
|
-
return best;
|
|
9882
|
-
}
|
|
9883
|
-
|
|
9884
9196
|
// src/narsil/reads.ts
|
|
9885
9197
|
function scopedParams(params, options) {
|
|
9886
9198
|
if (options?.globalStats === void 0) {
|
|
@@ -9912,7 +9224,7 @@ async function runEngineQuery(core, indexName, params, options) {
|
|
|
9912
9224
|
entry.embeddingAdapterName
|
|
9913
9225
|
);
|
|
9914
9226
|
await core.pluginRegistry.runHook("beforeSearch", { indexName, params: resolvedParams });
|
|
9915
|
-
const workerSearch = core.orchestrator.
|
|
9227
|
+
const workerSearch = core.orchestrator.hasWorkerPool() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
|
|
9916
9228
|
const result = await executeQuery(resolvedParams, {
|
|
9917
9229
|
manager,
|
|
9918
9230
|
language: entry.language,
|
|
@@ -9952,7 +9264,7 @@ async function runEnginePreflight(core, indexName, params, options) {
|
|
|
9952
9264
|
core.abortController.signal,
|
|
9953
9265
|
entry.embeddingAdapterName
|
|
9954
9266
|
);
|
|
9955
|
-
const workerSearch = core.orchestrator.
|
|
9267
|
+
const workerSearch = core.orchestrator.hasWorkerPool() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
|
|
9956
9268
|
const result = await executePreflight(resolvedParams, {
|
|
9957
9269
|
manager,
|
|
9958
9270
|
language: entry.language,
|
|
@@ -10115,17 +9427,22 @@ function createNarsilFromCore(core, config) {
|
|
|
10115
9427
|
insertBatch(indexName, documents, options) {
|
|
10116
9428
|
return withOpenIndex(indexName, () => insertDocumentBatch(mutationCtx, indexName, documents, options));
|
|
10117
9429
|
},
|
|
10118
|
-
remove(indexName, docId) {
|
|
10119
|
-
return withOpenIndex(indexName, () => removeDocument(mutationCtx, indexName, docId));
|
|
9430
|
+
remove(indexName, docId, options) {
|
|
9431
|
+
return withOpenIndex(indexName, () => removeDocument(mutationCtx, indexName, docId, options));
|
|
9432
|
+
},
|
|
9433
|
+
removeBatch(indexName, docIds, options) {
|
|
9434
|
+
return withOpenIndex(indexName, () => removeDocumentBatch(mutationCtx, indexName, docIds, options));
|
|
10120
9435
|
},
|
|
10121
|
-
|
|
10122
|
-
return withOpenIndex(indexName, () =>
|
|
9436
|
+
update(indexName, docId, document, options) {
|
|
9437
|
+
return withOpenIndex(indexName, () => updateDocument(mutationCtx, indexName, docId, document, options));
|
|
10123
9438
|
},
|
|
10124
|
-
|
|
10125
|
-
return withOpenIndex(indexName, () =>
|
|
9439
|
+
updateBatch(indexName, updates, options) {
|
|
9440
|
+
return withOpenIndex(indexName, () => updateDocumentBatch(mutationCtx, indexName, updates, options));
|
|
10126
9441
|
},
|
|
10127
|
-
|
|
10128
|
-
|
|
9442
|
+
waitForWrites(indexName) {
|
|
9443
|
+
guardShutdown();
|
|
9444
|
+
requireIndex(indexName);
|
|
9445
|
+
return orchestrator.awaitWrites(indexName);
|
|
10129
9446
|
},
|
|
10130
9447
|
async get(indexName, docId) {
|
|
10131
9448
|
return withOpenIndex(indexName, () => executor.execute({ type: "get", indexName, docId, requestId: docId }));
|
|
@@ -10189,6 +9506,7 @@ function createNarsilFromCore(core, config) {
|
|
|
10189
9506
|
clearAnalysisStale: core.analysisRebuild.clearStale,
|
|
10190
9507
|
indexState: core.indexState
|
|
10191
9508
|
});
|
|
9509
|
+
core.heapPressureNotifier.check(indexName);
|
|
10192
9510
|
},
|
|
10193
9511
|
async checkpoint(indexName) {
|
|
10194
9512
|
guardShutdown();
|
|
@@ -10268,6 +9586,7 @@ function createNarsilFromCore(core, config) {
|
|
|
10268
9586
|
openIndexCount: lifecycleCounts.open,
|
|
10269
9587
|
closedIndexCount: lifecycleCounts.closed,
|
|
10270
9588
|
reopenCount: lifecycleCounts.reopens,
|
|
9589
|
+
workerCopies: orchestrator.workerCopies(),
|
|
10271
9590
|
workers
|
|
10272
9591
|
};
|
|
10273
9592
|
},
|
|
@@ -10313,6 +9632,7 @@ function createNarsilFromCore(core, config) {
|
|
|
10313
9632
|
await shutdownEngine(core);
|
|
10314
9633
|
}
|
|
10315
9634
|
};
|
|
9635
|
+
bindEngineCore(narsil, core);
|
|
10316
9636
|
return narsil;
|
|
10317
9637
|
}
|
|
10318
9638
|
|
|
@@ -10321,6 +9641,7 @@ async function createNarsil(config) {
|
|
|
10321
9641
|
const core = createEngineCore(config);
|
|
10322
9642
|
if (core.durability) {
|
|
10323
9643
|
await core.durability.manager.recover(config?.lifecycle !== void 0);
|
|
9644
|
+
checkHeapAfterRecovery(core);
|
|
10324
9645
|
}
|
|
10325
9646
|
if (core.invalidation) {
|
|
10326
9647
|
await core.invalidation.start();
|
|
@@ -10332,4 +9653,4 @@ async function createNarsil(config) {
|
|
|
10332
9653
|
// src/index.ts
|
|
10333
9654
|
var VERSION = "0.1.0";
|
|
10334
9655
|
|
|
10335
|
-
export {
|
|
9656
|
+
export { VERSION, applyDeleteEntry, applyIndexEntry, checkHeapAfterRecovery, computeEntryChecksum, createAckMessage, createDurableDirectory, createEngineCore, createEngineIndex, createEntryBatchMessage, createEntryMessage, createForwardBatchMessage, createForwardBatchResultMessage, createForwardMessage, createInsyncAddMessage, createInsyncConfirmMessage, createInsyncRemoveMessage, createNarsil, createNarsilFromCore, runEngineListDocuments, runEnginePreflight, runEngineQuery, runEngineQueryStats, runEngineSuggest, validateAckPayload, validateEntryBatchPayload, validateEntryPayload, validateForwardBatchPayload, validateForwardBatchResultPayload, validateInsyncConfirmPayload, validateReplicationEntry, writeSegmentedCheckpoint };
|