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