@delali/narsil 0.3.1-next.71 → 0.3.1-next.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/filesystem.mjs +1 -1
- package/dist/adapters/indexeddb.mjs +1 -1
- package/dist/{capabilities-Dt-7B4Wz.d.ts → capabilities-D6gjjQyA.d.ts} +2 -2
- package/dist/{chunk-P4DSQ6M4.mjs → chunk-2X2ZQOAB.mjs} +69 -28
- package/dist/chunk-3HQPUCCT.mjs +785 -0
- package/dist/{chunk-IOR2K26O.mjs → chunk-7QMTA7AA.mjs} +146 -37
- package/dist/{chunk-FVLQ63B7.mjs → chunk-CVPA5Q7R.mjs} +24 -3
- package/dist/{chunk-2STVUTNA.mjs → chunk-EDMCZLSJ.mjs} +4 -3
- package/dist/{chunk-M76V3OGJ.mjs → chunk-HVJYTKK7.mjs} +940 -647
- package/dist/{chunk-HWQLTLNG.mjs → chunk-KZTI25D3.mjs} +1 -1
- package/dist/{chunk-YJDQ7NLA.mjs → chunk-LCCL6EEH.mjs} +3 -3
- package/dist/{chunk-OI42GWLR.mjs → chunk-LJYTW7GT.mjs} +57 -11
- package/dist/{chunk-653BNMAF.mjs → chunk-M6XF5HDE.mjs} +57 -17
- package/dist/{chunk-YNVIUGF5.mjs → chunk-P2ZZN32C.mjs} +1 -1
- package/dist/{chunk-LMO5TJUS.mjs → chunk-QR4OKRVY.mjs} +637 -196
- package/dist/{chunk-USKFU7J5.mjs → chunk-RV77FR2F.mjs} +1 -0
- package/dist/{chunk-GIVAGD3Q.mjs → chunk-XZ4PAREY.mjs} +1 -1
- package/dist/{chunk-3JGWULDF.mjs → chunk-Y52WZWAU.mjs} +5 -5
- package/dist/chunk-ZKRV5M2P.mjs +75 -0
- package/dist/{client-COek5GXK.d.ts → client-C2TiW29y.d.ts} +3 -3
- package/dist/client.d.ts +8 -8
- package/dist/client.mjs +1 -0
- package/dist/{config-rBpbgTw8.d.ts → config-BLguB-om.d.ts} +2 -2
- package/dist/{config-Dfanyqqt.d.ts → config-C7Ip8Ife.d.ts} +17 -2
- package/dist/direct-executor-SVCHXR74.mjs +14 -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 +58 -35
- package/dist/embeddings/openai.mjs +1 -1
- package/dist/{engine-igk0aMpQ.d.ts → engine-Dm4446j1.d.ts} +2 -2
- package/dist/index.browser.mjs +2183 -1087
- package/dist/index.d.ts +27 -8
- package/dist/index.mjs +14 -12
- package/dist/invalidation/filesystem.mjs +1 -1
- package/dist/{memory-C7UMCMH3.d.ts → memory-CKHTU1CL.d.ts} +66 -25
- package/dist/pattern-Cledccbl.d.ts +18 -0
- package/dist/pattern.d.ts +22 -0
- package/dist/pattern.mjs +7 -0
- package/dist/persistence/durability/checkpoint-worker.d.ts +17 -1
- package/dist/persistence/durability/checkpoint-worker.mjs +14 -12
- package/dist/react.d.ts +7 -7
- package/dist/react.mjs +1 -0
- package/dist/{schema-Bgzr4g7b.d.ts → schema-BBOCdhND.d.ts} +9 -7
- package/dist/server.d.ts +10 -10
- package/dist/server.mjs +13 -12
- package/dist/{tasks-BZt_zes6.d.ts → tasks-BECd0b__.d.ts} +1 -1
- package/dist/{thread-JEVAYETR.mjs → thread-IBSIZZ6W.mjs} +22 -13
- package/dist/{types-gOUqhs2F.d.ts → types-BiUmaiWT.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 +15 -7
- package/dist/direct-executor-GYJEKNV5.mjs +0 -12
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { isWordIndexedBase, flattenSchema, parsedTypeOf, isStringValuedBase, withSortableOption, isGeopointOrVectorType, MAX_SORT_FIELDS, MAX_SORT_FIELD_NAME_LENGTH, MAX_CURSOR_LENGTH, MAX_CURSOR_ANCHOR_CODE_POINTS, RESULT_WINDOW, DEFAULT_FUZZY_PREFIX_LENGTH, DEFAULT_PAGE_SIZE, PROTOTYPE_POLLUTION_KEYS, isWordIndexedFieldType, PREFIX_MAX_EXPANSIONS } from './chunk-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { isWordIndexedBase, flattenSchema, MAX_LATITUDE_DEGREES, MAX_LONGITUDE_DEGREES, MIN_POLYGON_POINTS, parsedTypeOf, isStringValuedBase, withSortableOption, isGeopointOrVectorType, MAX_SORT_FIELDS, MAX_SORT_FIELD_NAME_LENGTH, MAX_CURSOR_LENGTH, MAX_CURSOR_ANCHOR_CODE_POINTS, RESULT_WINDOW, DEFAULT_FUZZY_PREFIX_LENGTH, EARTH_RADIUS_METERS, RADIUS_BOX_MARGIN_RATIO, RADIUS_BOX_MARGIN_RADIANS, FULL_TURN_DEGREES, ANTIMERIDIAN_SPAN_DEGREES, DEFAULT_PAGE_SIZE, PROTOTYPE_POLLUTION_KEYS, DEG_TO_RAD, LOCATION_CODE_BITS_PER_AXIS, isLatitude, isLongitude, isWordIndexedFieldType, PREFIX_MAX_EXPANSIONS, MAX_COVERING_CELLS } from './chunk-M6XF5HDE.mjs';
|
|
2
|
+
import { DEFAULT_PATTERN_WORK_CAP, PATTERN_TEST_OPERATORS, bitsetIsEmpty, MAX_PATTERN_TEXT_CODE_POINTS, MAX_PATTERN_POSITIONS, createBitSet, bitsetSet, PATTERN_WORK_FLUSH_UNITS, MATCHER_STATE_BUDGET_BYTES } from './chunk-ZKRV5M2P.mjs';
|
|
3
|
+
import { compareCodePoints, sortModeOf, defaultSortMode, SORT_VALUE_MAX_CODE_POINTS, toReducedSortValue, readSortField, compareComparableKeys, toComparableSortValue, exceedsCodePoints, holdsUnpairedSurrogate, isSortMode, SORT_MODES, compareSortValues, foldedCodePointCount } from './chunk-LJYTW7GT.mjs';
|
|
4
|
+
import { createNarsilError, ErrorCodes, NarsilError } from './chunk-RV77FR2F.mjs';
|
|
4
5
|
|
|
5
6
|
// src/core/tokenizer/constants.ts
|
|
6
7
|
var TOKEN_CACHE_SIZE_FLOOR = 5e4;
|
|
@@ -458,6 +459,100 @@ function* tokenizeIterator(text, language, options) {
|
|
|
458
459
|
}
|
|
459
460
|
}
|
|
460
461
|
|
|
462
|
+
// src/core/pattern-index/work-meter.ts
|
|
463
|
+
var COUNTER_BYTES = 8;
|
|
464
|
+
function capExceeded(cap) {
|
|
465
|
+
return new NarsilError(
|
|
466
|
+
ErrorCodes.SEARCH_WORK_CAP_EXCEEDED,
|
|
467
|
+
`The engine stopped this search before it checked every candidate, because the work on its pattern tests passed the cap of ${cap} units. Narrow the tests, or raise the patternWorkCap engine setting`,
|
|
468
|
+
{ cap }
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
function localMeter(cap, spent, matcherStateBytes, flushUnits) {
|
|
472
|
+
let total = spent;
|
|
473
|
+
let sharedCount = null;
|
|
474
|
+
const spentSoFar = () => sharedCount === null ? total : Number(Atomics.load(sharedCount, 0));
|
|
475
|
+
const meter = {
|
|
476
|
+
cap,
|
|
477
|
+
matcherStateBytes,
|
|
478
|
+
flushUnits,
|
|
479
|
+
add(units) {
|
|
480
|
+
if (units <= 0) return;
|
|
481
|
+
if (sharedCount === null) total += units;
|
|
482
|
+
else total = Number(Atomics.add(sharedCount, 0, BigInt(units))) + units;
|
|
483
|
+
if (total > cap) throw capExceeded(cap);
|
|
484
|
+
},
|
|
485
|
+
remaining() {
|
|
486
|
+
return cap - spentSoFar();
|
|
487
|
+
},
|
|
488
|
+
fork(workerCount) {
|
|
489
|
+
const copies = Math.max(1, workerCount);
|
|
490
|
+
const seed = spentSoFar();
|
|
491
|
+
let counter = null;
|
|
492
|
+
if (typeof SharedArrayBuffer === "function") {
|
|
493
|
+
counter = new SharedArrayBuffer(COUNTER_BYTES);
|
|
494
|
+
new BigInt64Array(counter)[0] = BigInt(seed);
|
|
495
|
+
}
|
|
496
|
+
const shared = {
|
|
497
|
+
cap,
|
|
498
|
+
spent: seed,
|
|
499
|
+
counter,
|
|
500
|
+
matcherStateBytes: Math.floor(matcherStateBytes / copies),
|
|
501
|
+
flushUnits: Math.max(1, Math.floor(flushUnits / copies))
|
|
502
|
+
};
|
|
503
|
+
return {
|
|
504
|
+
shared,
|
|
505
|
+
settle(reportedUnits) {
|
|
506
|
+
if (counter === null) {
|
|
507
|
+
meter.add(reportedUnits);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const unitsSinceFork = spentSoFar() - seed;
|
|
511
|
+
const view = new BigInt64Array(counter);
|
|
512
|
+
if (unitsSinceFork > 0) Atomics.add(view, 0, BigInt(unitsSinceFork));
|
|
513
|
+
sharedCount = view;
|
|
514
|
+
total = spentSoFar();
|
|
515
|
+
if (total > cap) throw capExceeded(cap);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
return meter;
|
|
521
|
+
}
|
|
522
|
+
function createPatternWorkMeter(cap = DEFAULT_PATTERN_WORK_CAP) {
|
|
523
|
+
return localMeter(cap, 0, MATCHER_STATE_BUDGET_BYTES, PATTERN_WORK_FLUSH_UNITS);
|
|
524
|
+
}
|
|
525
|
+
function sharedMeter(work, counter) {
|
|
526
|
+
const view = new BigInt64Array(counter);
|
|
527
|
+
return {
|
|
528
|
+
matcherStateBytes: work.matcherStateBytes,
|
|
529
|
+
flushUnits: work.flushUnits,
|
|
530
|
+
add(units) {
|
|
531
|
+
if (units <= 0) return;
|
|
532
|
+
const total = Number(Atomics.add(view, 0, BigInt(units))) + units;
|
|
533
|
+
if (total > work.cap) throw capExceeded(work.cap);
|
|
534
|
+
},
|
|
535
|
+
remaining() {
|
|
536
|
+
return work.cap - Number(Atomics.load(view, 0));
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function joinPatternWorkMeter(work) {
|
|
541
|
+
const meter = work.counter === null ? localMeter(work.cap, work.spent, work.matcherStateBytes, work.flushUnits) : sharedMeter(work, work.counter);
|
|
542
|
+
let spent = 0;
|
|
543
|
+
return {
|
|
544
|
+
matcherStateBytes: meter.matcherStateBytes,
|
|
545
|
+
flushUnits: meter.flushUnits,
|
|
546
|
+
add(units) {
|
|
547
|
+
if (units <= 0) return;
|
|
548
|
+
spent += units;
|
|
549
|
+
meter.add(units);
|
|
550
|
+
},
|
|
551
|
+
remaining: () => meter.remaining(),
|
|
552
|
+
spent: () => spent
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
461
556
|
// src/partitioning/distributed-scoring.ts
|
|
462
557
|
function analysedQueryTokens(term, language, analysis) {
|
|
463
558
|
const { tokens } = tokenize(term, language, {
|
|
@@ -673,67 +768,6 @@ function heapPop(heap) {
|
|
|
673
768
|
return top;
|
|
674
769
|
}
|
|
675
770
|
|
|
676
|
-
// src/core/bitset.ts
|
|
677
|
-
function createBitSet(capacity) {
|
|
678
|
-
return new Uint32Array(capacity + 31 >>> 5 || 1);
|
|
679
|
-
}
|
|
680
|
-
function bitsetSet(bs, index) {
|
|
681
|
-
const word = index >>> 5;
|
|
682
|
-
if (word < bs.length) {
|
|
683
|
-
bs[word] |= 1 << (index & 31);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
function bitsetHas(bs, index) {
|
|
687
|
-
const word = index >>> 5;
|
|
688
|
-
if (word >= bs.length) return false;
|
|
689
|
-
return (bs[word] & 1 << (index & 31)) !== 0;
|
|
690
|
-
}
|
|
691
|
-
function bitsetAnd(a, b) {
|
|
692
|
-
const len = Math.min(a.length, b.length);
|
|
693
|
-
const result = new Uint32Array(len);
|
|
694
|
-
for (let i = 0; i < len; i++) {
|
|
695
|
-
result[i] = a[i] & b[i];
|
|
696
|
-
}
|
|
697
|
-
return result;
|
|
698
|
-
}
|
|
699
|
-
function bitsetOr(a, b) {
|
|
700
|
-
const longer = a.length >= b.length ? a : b;
|
|
701
|
-
const shorter = a.length >= b.length ? b : a;
|
|
702
|
-
const result = new Uint32Array(longer.length);
|
|
703
|
-
for (let i = 0; i < shorter.length; i++) {
|
|
704
|
-
result[i] = longer[i] | shorter[i];
|
|
705
|
-
}
|
|
706
|
-
for (let i = shorter.length; i < longer.length; i++) {
|
|
707
|
-
result[i] = longer[i];
|
|
708
|
-
}
|
|
709
|
-
return result;
|
|
710
|
-
}
|
|
711
|
-
function bitsetNot(bs, capacity) {
|
|
712
|
-
const result = new Uint32Array(bs.length);
|
|
713
|
-
const fullWords = capacity >>> 5;
|
|
714
|
-
for (let i = 0; i < fullWords; i++) {
|
|
715
|
-
result[i] = ~bs[i];
|
|
716
|
-
}
|
|
717
|
-
const remainder = capacity & 31;
|
|
718
|
-
if (remainder > 0 && fullWords < bs.length) {
|
|
719
|
-
result[fullWords] = ~bs[fullWords] & (1 << remainder) - 1;
|
|
720
|
-
}
|
|
721
|
-
return result;
|
|
722
|
-
}
|
|
723
|
-
function bitsetIsEmpty(bs) {
|
|
724
|
-
for (let i = 0; i < bs.length; i++) {
|
|
725
|
-
if (bs[i] !== 0) return false;
|
|
726
|
-
}
|
|
727
|
-
return true;
|
|
728
|
-
}
|
|
729
|
-
function bitsetFromSet(set, capacity) {
|
|
730
|
-
const bs = createBitSet(capacity);
|
|
731
|
-
for (const id of set) {
|
|
732
|
-
bs[id >>> 5] |= 1 << (id & 31);
|
|
733
|
-
}
|
|
734
|
-
return bs;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
771
|
// src/core/partition/utils.ts
|
|
738
772
|
function getOrCreateFieldNameIndex(table, fieldName) {
|
|
739
773
|
const existing = table.indexMap.get(fieldName);
|
|
@@ -800,13 +834,6 @@ function getFieldValueByInternalId(docStore, internalId, fieldPath) {
|
|
|
800
834
|
if (externalId === void 0) return void 0;
|
|
801
835
|
return getFieldValueForDoc(docStore, externalId, fieldPath);
|
|
802
836
|
}
|
|
803
|
-
function getAllInternalDocIds(docStore) {
|
|
804
|
-
const ids = /* @__PURE__ */ new Set();
|
|
805
|
-
for (const internalId of docStore.allInternalIds()) {
|
|
806
|
-
ids.add(internalId);
|
|
807
|
-
}
|
|
808
|
-
return ids;
|
|
809
|
-
}
|
|
810
837
|
|
|
811
838
|
// src/core/partition/facets.ts
|
|
812
839
|
var UNDECLARED_FIELD_TYPE = "";
|
|
@@ -1084,13 +1111,205 @@ function decodeCursorText(encoded) {
|
|
|
1084
1111
|
return new TextDecoder().decode(bytes);
|
|
1085
1112
|
}
|
|
1086
1113
|
|
|
1087
|
-
// src/
|
|
1088
|
-
var
|
|
1089
|
-
var
|
|
1090
|
-
var
|
|
1091
|
-
var
|
|
1092
|
-
|
|
1093
|
-
|
|
1114
|
+
// src/filters/constants.ts
|
|
1115
|
+
var MAX_FILTER_DEPTH = 30;
|
|
1116
|
+
var MAX_FILTER_TESTS = 1024;
|
|
1117
|
+
var MAX_FILTER_ARRAY_SIZE = 65536;
|
|
1118
|
+
var GEO_UNIT_METRES = { km: 1e3, mi: 1609.344, m: 1 };
|
|
1119
|
+
|
|
1120
|
+
// src/filters/operands/checks.ts
|
|
1121
|
+
function operandKindOf(type) {
|
|
1122
|
+
if (type === void 0) return void 0;
|
|
1123
|
+
if (type === "number" || type === "number[]") return "number";
|
|
1124
|
+
if (type === "boolean" || type === "boolean[]") return "boolean";
|
|
1125
|
+
if (type === "geopoint") return "geopoint";
|
|
1126
|
+
if (type.startsWith("vector[")) return "vector";
|
|
1127
|
+
return "text";
|
|
1128
|
+
}
|
|
1129
|
+
function isFiniteNumber(value) {
|
|
1130
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1131
|
+
}
|
|
1132
|
+
function kindOfValue(value) {
|
|
1133
|
+
if (isFiniteNumber(value)) return "number";
|
|
1134
|
+
if (typeof value === "boolean") return "boolean";
|
|
1135
|
+
if (typeof value === "string") return "text";
|
|
1136
|
+
return void 0;
|
|
1137
|
+
}
|
|
1138
|
+
function isObjectRecord(value) {
|
|
1139
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1140
|
+
}
|
|
1141
|
+
function invalid(message, fieldPath, operator) {
|
|
1142
|
+
throw new NarsilError(ErrorCodes.SEARCH_INVALID_FILTER, message, { fieldPath, operator });
|
|
1143
|
+
}
|
|
1144
|
+
function requireFieldKind(fieldPath, operator, type, accepted) {
|
|
1145
|
+
const kind = operandKindOf(type);
|
|
1146
|
+
if (kind === void 0 || accepted.includes(kind)) return;
|
|
1147
|
+
invalid(
|
|
1148
|
+
`The engine applies "${operator}" to ${accepted.join(" or ")} fields alone, and the schema declares field "${fieldPath}" as "${type}"`,
|
|
1149
|
+
fieldPath,
|
|
1150
|
+
operator
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
function requireTextRangeField(fieldPath, operator, type) {
|
|
1154
|
+
const parsed = parsedTypeOf(type);
|
|
1155
|
+
if (parsed?.sortable && (parsed.base === "string" || parsed.base === "verbatim")) return;
|
|
1156
|
+
const declared = type === void 0 ? `the schema has no field "${fieldPath}"` : `field "${fieldPath}" has type "${type}"`;
|
|
1157
|
+
invalid(
|
|
1158
|
+
`The engine applies "${operator}" with a text bound to a "string" or "verbatim" field whose type includes "sortable", but ${declared}`,
|
|
1159
|
+
fieldPath,
|
|
1160
|
+
operator
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
function requireListLength(fieldPath, operator, values) {
|
|
1164
|
+
if (values.length <= MAX_FILTER_ARRAY_SIZE) return;
|
|
1165
|
+
invalid(
|
|
1166
|
+
`"${operator}" on field "${fieldPath}" holds ${values.length} values, while a filter list holds at most ${MAX_FILTER_ARRAY_SIZE}`,
|
|
1167
|
+
fieldPath,
|
|
1168
|
+
operator
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/geo/cells.ts
|
|
1173
|
+
var AXIS_CELLS = 2 ** LOCATION_CODE_BITS_PER_AXIS;
|
|
1174
|
+
function axisCell(value, lowest, span) {
|
|
1175
|
+
const cell = Math.floor((value - lowest) / span * AXIS_CELLS);
|
|
1176
|
+
if (!(cell > 0)) return 0;
|
|
1177
|
+
return cell >= AXIS_CELLS ? AXIS_CELLS - 1 : cell;
|
|
1178
|
+
}
|
|
1179
|
+
function latitudeCell(lat) {
|
|
1180
|
+
return axisCell(lat, -MAX_LATITUDE_DEGREES, 2 * MAX_LATITUDE_DEGREES);
|
|
1181
|
+
}
|
|
1182
|
+
function longitudeCell(lon) {
|
|
1183
|
+
return axisCell(lon, -MAX_LONGITUDE_DEGREES, FULL_TURN_DEGREES);
|
|
1184
|
+
}
|
|
1185
|
+
function interleave(row, column, bits) {
|
|
1186
|
+
let code = 0;
|
|
1187
|
+
for (let bit = bits - 1; bit >= 0; bit--) code = code * 4 + (row >>> bit & 1) * 2 + (column >>> bit & 1);
|
|
1188
|
+
return code;
|
|
1189
|
+
}
|
|
1190
|
+
function locationCode(lat, lon) {
|
|
1191
|
+
return interleave(latitudeCell(lat), longitudeCell(lon), LOCATION_CODE_BITS_PER_AXIS);
|
|
1192
|
+
}
|
|
1193
|
+
function appendBoxRanges(box, out) {
|
|
1194
|
+
const south = latitudeCell(box.south);
|
|
1195
|
+
const north = latitudeCell(box.north);
|
|
1196
|
+
const west = longitudeCell(box.west);
|
|
1197
|
+
const east = longitudeCell(box.east);
|
|
1198
|
+
let level = LOCATION_CODE_BITS_PER_AXIS;
|
|
1199
|
+
while (level > 0) {
|
|
1200
|
+
const shift2 = LOCATION_CODE_BITS_PER_AXIS - level;
|
|
1201
|
+
const rows = (north >>> shift2) - (south >>> shift2) + 1;
|
|
1202
|
+
const columns = (east >>> shift2) - (west >>> shift2) + 1;
|
|
1203
|
+
if (rows * columns <= MAX_COVERING_CELLS) break;
|
|
1204
|
+
level--;
|
|
1205
|
+
}
|
|
1206
|
+
const shift = LOCATION_CODE_BITS_PER_AXIS - level;
|
|
1207
|
+
const codesPerCell = 4 ** shift;
|
|
1208
|
+
for (let row = south >>> shift; row <= north >>> shift; row++) {
|
|
1209
|
+
for (let column = west >>> shift; column <= east >>> shift; column++) {
|
|
1210
|
+
const from = interleave(row, column, level) * codesPerCell;
|
|
1211
|
+
out.push({ from, to: from + codesPerCell });
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
function coveringRanges(boxes) {
|
|
1216
|
+
const ranges = [];
|
|
1217
|
+
for (const box of boxes) appendBoxRanges(box, ranges);
|
|
1218
|
+
ranges.sort((left, right) => left.from - right.from);
|
|
1219
|
+
const merged = [];
|
|
1220
|
+
for (const range of ranges) {
|
|
1221
|
+
const last = merged[merged.length - 1];
|
|
1222
|
+
if (last !== void 0 && range.from <= last.to) {
|
|
1223
|
+
if (range.to > last.to) last.to = range.to;
|
|
1224
|
+
} else {
|
|
1225
|
+
merged.push({ from: range.from, to: range.to });
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return merged;
|
|
1229
|
+
}
|
|
1230
|
+
function longitudeBoxes(south, north, west, east) {
|
|
1231
|
+
if (east - west >= FULL_TURN_DEGREES) {
|
|
1232
|
+
return [{ south, north, west: -MAX_LONGITUDE_DEGREES, east: MAX_LONGITUDE_DEGREES }];
|
|
1233
|
+
}
|
|
1234
|
+
if (west < -MAX_LONGITUDE_DEGREES) {
|
|
1235
|
+
return [
|
|
1236
|
+
{ south, north, west: west + FULL_TURN_DEGREES, east: MAX_LONGITUDE_DEGREES },
|
|
1237
|
+
{ south, north, west: -MAX_LONGITUDE_DEGREES, east }
|
|
1238
|
+
];
|
|
1239
|
+
}
|
|
1240
|
+
if (east > MAX_LONGITUDE_DEGREES) {
|
|
1241
|
+
return [
|
|
1242
|
+
{ south, north, west, east: MAX_LONGITUDE_DEGREES },
|
|
1243
|
+
{ south, north, west: -MAX_LONGITUDE_DEGREES, east: east - FULL_TURN_DEGREES }
|
|
1244
|
+
];
|
|
1245
|
+
}
|
|
1246
|
+
return [{ south, north, west, east }];
|
|
1247
|
+
}
|
|
1248
|
+
function radiusBoxes(lat, lon, distanceMeters) {
|
|
1249
|
+
const angle = distanceMeters / EARTH_RADIUS_METERS * RADIUS_BOX_MARGIN_RATIO + RADIUS_BOX_MARGIN_RADIANS;
|
|
1250
|
+
const angleDegrees = angle / DEG_TO_RAD;
|
|
1251
|
+
const south = lat - angleDegrees;
|
|
1252
|
+
const north = lat + angleDegrees;
|
|
1253
|
+
if (angle >= Math.PI || south <= -MAX_LATITUDE_DEGREES || north >= MAX_LATITUDE_DEGREES) {
|
|
1254
|
+
return [
|
|
1255
|
+
{
|
|
1256
|
+
south: Math.max(south, -MAX_LATITUDE_DEGREES),
|
|
1257
|
+
north: Math.min(north, MAX_LATITUDE_DEGREES),
|
|
1258
|
+
west: -MAX_LONGITUDE_DEGREES,
|
|
1259
|
+
east: MAX_LONGITUDE_DEGREES
|
|
1260
|
+
}
|
|
1261
|
+
];
|
|
1262
|
+
}
|
|
1263
|
+
const spread = Math.asin(Math.sin(angle) / Math.cos(lat * DEG_TO_RAD)) / DEG_TO_RAD;
|
|
1264
|
+
return longitudeBoxes(south, north, lon - spread, lon + spread);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// src/geo/edge-tree.ts
|
|
1268
|
+
function buildLongitudeEdgeTree(lons) {
|
|
1269
|
+
const n = lons.length;
|
|
1270
|
+
const edges = [];
|
|
1271
|
+
for (let i = 0; i < n; i++) {
|
|
1272
|
+
if (lons[i] !== lons[i === 0 ? n - 1 : i - 1]) edges.push(i);
|
|
1273
|
+
}
|
|
1274
|
+
const lowOf = (i) => Math.min(lons[i], lons[i === 0 ? n - 1 : i - 1]);
|
|
1275
|
+
edges.sort((left, right) => lowOf(left) - lowOf(right));
|
|
1276
|
+
const count = edges.length;
|
|
1277
|
+
const tree = {
|
|
1278
|
+
from: new Int32Array(count),
|
|
1279
|
+
to: new Int32Array(count),
|
|
1280
|
+
low: new Float64Array(count),
|
|
1281
|
+
high: new Float64Array(count),
|
|
1282
|
+
subtreeHigh: new Float64Array(count)
|
|
1283
|
+
};
|
|
1284
|
+
edges.forEach((i, at) => {
|
|
1285
|
+
const j = i === 0 ? n - 1 : i - 1;
|
|
1286
|
+
tree.from[at] = i;
|
|
1287
|
+
tree.to[at] = j;
|
|
1288
|
+
tree.low[at] = Math.min(lons[i], lons[j]);
|
|
1289
|
+
tree.high[at] = Math.max(lons[i], lons[j]);
|
|
1290
|
+
});
|
|
1291
|
+
const fillSubtreeHigh = (start, end) => {
|
|
1292
|
+
if (start >= end) return Number.NEGATIVE_INFINITY;
|
|
1293
|
+
const mid = start + end >>> 1;
|
|
1294
|
+
const highest = Math.max(tree.high[mid], fillSubtreeHigh(start, mid), fillSubtreeHigh(mid + 1, end));
|
|
1295
|
+
tree.subtreeHigh[mid] = highest;
|
|
1296
|
+
return highest;
|
|
1297
|
+
};
|
|
1298
|
+
fillSubtreeHigh(0, count);
|
|
1299
|
+
return tree;
|
|
1300
|
+
}
|
|
1301
|
+
function forEachEdgeSpanning(tree, lon, visit) {
|
|
1302
|
+
const walk = (start, end) => {
|
|
1303
|
+
if (start >= end) return;
|
|
1304
|
+
const mid = start + end >>> 1;
|
|
1305
|
+
if (tree.subtreeHigh[mid] <= lon) return;
|
|
1306
|
+
walk(start, mid);
|
|
1307
|
+
if (tree.low[mid] > lon) return;
|
|
1308
|
+
if (lon < tree.high[mid]) visit(tree.from[mid], tree.to[mid]);
|
|
1309
|
+
walk(mid + 1, end);
|
|
1310
|
+
};
|
|
1311
|
+
walk(0, tree.from.length);
|
|
1312
|
+
}
|
|
1094
1313
|
|
|
1095
1314
|
// src/geo/polygon.ts
|
|
1096
1315
|
function requirePolygonRing(points) {
|
|
@@ -1103,11 +1322,18 @@ function requirePolygonRing(points) {
|
|
|
1103
1322
|
}
|
|
1104
1323
|
points.forEach((point, position) => {
|
|
1105
1324
|
const { lat, lon } = point ?? {};
|
|
1106
|
-
if (typeof lat
|
|
1325
|
+
if (typeof lat !== "number" || !Number.isFinite(lat) || typeof lon !== "number" || !Number.isFinite(lon)) {
|
|
1326
|
+
throw new NarsilError(
|
|
1327
|
+
ErrorCodes.SEARCH_INVALID_FILTER,
|
|
1328
|
+
`A polygon point carries a finite "lat" and "lon", and the point at position ${position} does not`,
|
|
1329
|
+
{ position }
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
if (isLatitude(lat) && isLongitude(lon)) return;
|
|
1107
1333
|
throw new NarsilError(
|
|
1108
1334
|
ErrorCodes.SEARCH_INVALID_FILTER,
|
|
1109
|
-
`A polygon point
|
|
1110
|
-
{ position }
|
|
1335
|
+
`A polygon point must hold a "lat" from -${MAX_LATITUDE_DEGREES} to ${MAX_LATITUDE_DEGREES} and a "lon" from -${MAX_LONGITUDE_DEGREES} to ${MAX_LONGITUDE_DEGREES}, while the point at position ${position} holds ${lat}, ${lon}`,
|
|
1336
|
+
{ position, lat, lon }
|
|
1111
1337
|
);
|
|
1112
1338
|
});
|
|
1113
1339
|
}
|
|
@@ -1140,67 +1366,40 @@ function polygonContainment(polygon) {
|
|
|
1140
1366
|
lats[i] = polygon[i].lat;
|
|
1141
1367
|
lons[i] = unwrap ? eastward(polygon[i].lon) : polygon[i].lon;
|
|
1142
1368
|
}
|
|
1369
|
+
const edges = buildLongitudeEdgeTree(lons);
|
|
1143
1370
|
return (lat, rawLon) => {
|
|
1144
1371
|
const lon = unwrap ? eastward(rawLon) : rawLon;
|
|
1145
1372
|
let inside = false;
|
|
1146
|
-
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
const intersectLat = lats[i] + (lats[j] - lats[i]) / (lonJ - lonI) * (lon - lonI);
|
|
1151
|
-
if (lat < intersectLat) inside = !inside;
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1373
|
+
forEachEdgeSpanning(edges, lon, (i, j) => {
|
|
1374
|
+
const intersectLat = lats[i] + (lats[j] - lats[i]) / (lons[j] - lons[i]) * (lon - lons[i]);
|
|
1375
|
+
if (lat < intersectLat) inside = !inside;
|
|
1376
|
+
});
|
|
1154
1377
|
return inside;
|
|
1155
1378
|
};
|
|
1156
1379
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
if (
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
function isFiniteNumber(value) {
|
|
1175
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
1176
|
-
}
|
|
1177
|
-
function kindOfValue(value) {
|
|
1178
|
-
if (isFiniteNumber(value)) return "number";
|
|
1179
|
-
if (typeof value === "boolean") return "boolean";
|
|
1180
|
-
if (typeof value === "string") return "text";
|
|
1181
|
-
return void 0;
|
|
1182
|
-
}
|
|
1183
|
-
function invalid(message, fieldPath, operator) {
|
|
1184
|
-
throw new NarsilError(ErrorCodes.SEARCH_INVALID_FILTER, message, { fieldPath, operator });
|
|
1185
|
-
}
|
|
1186
|
-
function requireFieldKind(fieldPath, operator, type, accepted) {
|
|
1187
|
-
const kind = operandKindOf(type);
|
|
1188
|
-
if (kind === void 0 || accepted.includes(kind)) return;
|
|
1189
|
-
invalid(
|
|
1190
|
-
`The engine applies "${operator}" to ${accepted.join(" or ")} fields alone, and the schema declares field "${fieldPath}" as "${type}"`,
|
|
1191
|
-
fieldPath,
|
|
1192
|
-
operator
|
|
1193
|
-
);
|
|
1380
|
+
function polygonBoxes(polygon) {
|
|
1381
|
+
if (polygon.length < MIN_POLYGON_POINTS) return [];
|
|
1382
|
+
const unwrap = crossesAntimeridian(polygon);
|
|
1383
|
+
let south = Number.POSITIVE_INFINITY;
|
|
1384
|
+
let north = Number.NEGATIVE_INFINITY;
|
|
1385
|
+
let west = Number.POSITIVE_INFINITY;
|
|
1386
|
+
let east = Number.NEGATIVE_INFINITY;
|
|
1387
|
+
for (const point of polygon) {
|
|
1388
|
+
const lon = unwrap ? eastward(point.lon) : point.lon;
|
|
1389
|
+
south = Math.min(south, point.lat);
|
|
1390
|
+
north = Math.max(north, point.lat);
|
|
1391
|
+
west = Math.min(west, lon);
|
|
1392
|
+
east = Math.max(east, lon);
|
|
1393
|
+
}
|
|
1394
|
+
if (west >= MAX_LONGITUDE_DEGREES)
|
|
1395
|
+
return longitudeBoxes(south, north, west - FULL_TURN_DEGREES, east - FULL_TURN_DEGREES);
|
|
1396
|
+
return longitudeBoxes(south, north, west, east);
|
|
1194
1397
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
invalid(
|
|
1200
|
-
`The engine applies "${operator}" with a text bound to a "string" or "verbatim" field whose type includes "sortable", but ${declared}`,
|
|
1201
|
-
fieldPath,
|
|
1202
|
-
operator
|
|
1203
|
-
);
|
|
1398
|
+
|
|
1399
|
+
// src/filters/operands/geo.ts
|
|
1400
|
+
function requireFlag(fieldPath, operator, flag, value) {
|
|
1401
|
+
if (value === void 0 || typeof value === "boolean") return;
|
|
1402
|
+
invalid(`"${operator}.${flag}" on field "${fieldPath}" must be true or false`, fieldPath, operator);
|
|
1204
1403
|
}
|
|
1205
1404
|
function requireRadius(fieldPath, radius) {
|
|
1206
1405
|
const circle = radius;
|
|
@@ -1212,6 +1411,15 @@ function requireRadius(fieldPath, radius) {
|
|
|
1212
1411
|
invalid(`"radius.${key}" on field "${fieldPath}" must be a finite number`, fieldPath, "radius");
|
|
1213
1412
|
}
|
|
1214
1413
|
}
|
|
1414
|
+
if (typeof circle.unit !== "string" || !Object.hasOwn(GEO_UNIT_METRES, circle.unit)) {
|
|
1415
|
+
invalid(
|
|
1416
|
+
`"radius.unit" on field "${fieldPath}" must be one of ${Object.keys(GEO_UNIT_METRES).join(", ")}, and this filter sets ${String(circle.unit)}`,
|
|
1417
|
+
fieldPath,
|
|
1418
|
+
"radius"
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
requireFlag(fieldPath, "radius", "inside", circle.inside);
|
|
1422
|
+
requireFlag(fieldPath, "radius", "highPrecision", circle.highPrecision);
|
|
1215
1423
|
if (circle.distance < 0) {
|
|
1216
1424
|
invalid(
|
|
1217
1425
|
`"radius.distance" on field "${fieldPath}" must be zero or more, and this filter sets ${String(circle.distance)}`,
|
|
@@ -1219,13 +1427,197 @@ function requireRadius(fieldPath, radius) {
|
|
|
1219
1427
|
"radius"
|
|
1220
1428
|
);
|
|
1221
1429
|
}
|
|
1430
|
+
if (!isLatitude(circle.lat)) {
|
|
1431
|
+
invalid(
|
|
1432
|
+
`"radius.lat" on field "${fieldPath}" must be from -${MAX_LATITUDE_DEGREES} to ${MAX_LATITUDE_DEGREES}, and this filter sets ${String(circle.lat)}`,
|
|
1433
|
+
fieldPath,
|
|
1434
|
+
"radius"
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
if (!isLongitude(circle.lon)) {
|
|
1438
|
+
invalid(
|
|
1439
|
+
`"radius.lon" on field "${fieldPath}" must be from -${MAX_LONGITUDE_DEGREES} to ${MAX_LONGITUDE_DEGREES}, and this filter sets ${String(circle.lon)}`,
|
|
1440
|
+
fieldPath,
|
|
1441
|
+
"radius"
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
function requireGeoOperands(fieldPath, filter) {
|
|
1446
|
+
if (filter.radius !== void 0) requireRadius(fieldPath, filter.radius);
|
|
1447
|
+
if (filter.polygon === void 0) return;
|
|
1448
|
+
if (!isObjectRecord(filter.polygon)) {
|
|
1449
|
+
invalid(`"polygon" on field "${fieldPath}" must be an object`, fieldPath, "polygon");
|
|
1450
|
+
}
|
|
1451
|
+
requireFlag(fieldPath, "polygon", "inside", filter.polygon.inside);
|
|
1452
|
+
const points = filter.polygon.points;
|
|
1453
|
+
if (Array.isArray(points)) requireListLength(fieldPath, "polygon", points);
|
|
1454
|
+
requirePolygonRing(points);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// src/filters/operands/lists.ts
|
|
1458
|
+
var WHOLE_VALUE_OPERATORS = ["eq", "ne", "in", "nin", "startsWith", "endsWith", "matchesAny", "containsAll"];
|
|
1459
|
+
var ELEMENT_OPERATORS = ["matchesAny", "containsAll"];
|
|
1460
|
+
var PRESENCE_OPERATORS = ["exists", "notExists", "isEmpty", "isNotEmpty"];
|
|
1461
|
+
var SIZE_OPERATORS = ["eq", "ne", "gt", "gte", "lt", "lte", "between"];
|
|
1462
|
+
var SIZE_OPERATOR_SET = new Set(SIZE_OPERATORS);
|
|
1463
|
+
var ELEMENT_KIND_BY_BASE = {
|
|
1464
|
+
"number[]": "number",
|
|
1465
|
+
"boolean[]": "boolean",
|
|
1466
|
+
"enum[]": "text",
|
|
1467
|
+
"string[]": "text",
|
|
1468
|
+
"verbatim[]": "text"
|
|
1469
|
+
};
|
|
1470
|
+
function requireWholeValueIndex(fieldPath, filter, type) {
|
|
1471
|
+
const parsed = parsedTypeOf(type);
|
|
1472
|
+
if (parsed === void 0 || parsed.pattern || parsed.base !== "string" && parsed.base !== "string[]") return;
|
|
1473
|
+
for (const operator of WHOLE_VALUE_OPERATORS) {
|
|
1474
|
+
if (filter[operator] === void 0) continue;
|
|
1475
|
+
invalid(
|
|
1476
|
+
`"${operator}" compares whole values of field "${fieldPath}", which the engine finds only through a pattern index. A "${parsed.base}" field keeps a pattern index where its type includes "pattern", so declare the field as "${parsed.base}:pattern" in a new index to filter it this way`,
|
|
1477
|
+
fieldPath,
|
|
1478
|
+
operator
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
function requirePresenceOperands(fieldPath, filter) {
|
|
1483
|
+
for (const operator of PRESENCE_OPERATORS) {
|
|
1484
|
+
const operand = filter[operator];
|
|
1485
|
+
if (operand !== void 0 && typeof operand !== "boolean") {
|
|
1486
|
+
invalid(`"${operator}" on field "${fieldPath}" must be true or false`, fieldPath, operator);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
function listElementKind(fieldPath, operator, type) {
|
|
1491
|
+
const base = parsedTypeOf(type)?.base;
|
|
1492
|
+
const kind = base === void 0 ? void 0 : ELEMENT_KIND_BY_BASE[base];
|
|
1493
|
+
if (kind !== void 0) return kind;
|
|
1494
|
+
return invalid(
|
|
1495
|
+
`"${operator}" tests the elements of a list field, while the schema declares field "${fieldPath}" as "${type}"`,
|
|
1496
|
+
fieldPath,
|
|
1497
|
+
operator
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
function requireElementOperands(fieldPath, filter, type) {
|
|
1501
|
+
for (const operator of ELEMENT_OPERATORS) {
|
|
1502
|
+
const values = filter[operator];
|
|
1503
|
+
if (values === void 0) continue;
|
|
1504
|
+
if (!Array.isArray(values)) invalid(`"${operator}" on field "${fieldPath}" must be a list`, fieldPath, operator);
|
|
1505
|
+
requireListLength(fieldPath, operator, values);
|
|
1506
|
+
const kind = listElementKind(fieldPath, operator, type);
|
|
1507
|
+
if (values.some((value) => kindOfValue(value) !== kind)) {
|
|
1508
|
+
invalid(
|
|
1509
|
+
`Every value of "${operator}" on field "${fieldPath}" must be a ${kind === "text" ? "string" : kind === "number" ? "finite number" : "boolean"}, because the field holds a list of them`,
|
|
1510
|
+
fieldPath,
|
|
1511
|
+
operator
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
function requireSizeOperands(fieldPath, filter, type) {
|
|
1517
|
+
const size = filter.size;
|
|
1518
|
+
if (size === void 0) return;
|
|
1519
|
+
if (!isObjectRecord(size)) invalid(`"size" on field "${fieldPath}" must be a comparison object`, fieldPath, "size");
|
|
1520
|
+
listElementKind(fieldPath, "size", type);
|
|
1521
|
+
for (const [operator, operand] of Object.entries(size)) {
|
|
1522
|
+
if (!SIZE_OPERATOR_SET.has(operator)) {
|
|
1523
|
+
invalid(
|
|
1524
|
+
`"size" on field "${fieldPath}" takes ${SIZE_OPERATORS.join(", ")}, while "${operator}" is none of them`,
|
|
1525
|
+
fieldPath,
|
|
1526
|
+
"size"
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
const bounds = operator === "between" ? operand : [operand];
|
|
1530
|
+
if (!Array.isArray(bounds) || bounds.length !== (operator === "between" ? 2 : 1) || !bounds.every(isFiniteNumber)) {
|
|
1531
|
+
invalid(
|
|
1532
|
+
`"size.${operator}" on field "${fieldPath}" must be ${operator === "between" ? "two finite numbers, the lower bound first" : "a finite number"}`,
|
|
1533
|
+
fieldPath,
|
|
1534
|
+
"size"
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1222
1538
|
}
|
|
1539
|
+
|
|
1540
|
+
// src/filters/operands/pattern.ts
|
|
1541
|
+
function requirePatternField(fieldPath, operator, type) {
|
|
1542
|
+
if (parsedTypeOf(type)?.pattern === true) return;
|
|
1543
|
+
const declared = type === void 0 ? `The schema declares no field "${fieldPath}"` : `Field "${fieldPath}" has type "${type}"`;
|
|
1544
|
+
invalid(
|
|
1545
|
+
`"${operator}" applies only to a pattern field, because only a pattern field has a pattern index. A pattern field is a "verbatim" field, or a text field whose type includes "pattern". ${declared}`,
|
|
1546
|
+
fieldPath,
|
|
1547
|
+
operator
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
function requirePatternText(fieldPath, operator, text, caseFold) {
|
|
1551
|
+
if (exceedsCodePoints(text, MAX_PATTERN_TEXT_CODE_POINTS)) {
|
|
1552
|
+
invalid(
|
|
1553
|
+
`The text of "${operator}" on pattern field "${fieldPath}" holds more than ${MAX_PATTERN_TEXT_CODE_POINTS} code points`,
|
|
1554
|
+
fieldPath,
|
|
1555
|
+
operator
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
if (caseFold && foldedCodePointCount(text) > MAX_PATTERN_POSITIONS) {
|
|
1559
|
+
invalid(
|
|
1560
|
+
`The text of "${operator}" on pattern field "${fieldPath}" has a case fold of more than ${MAX_PATTERN_POSITIONS} code points, while a pattern holds at most ${MAX_PATTERN_POSITIONS} positions`,
|
|
1561
|
+
fieldPath,
|
|
1562
|
+
operator
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
var ELEMENT_OPERATORS2 = ["matchesAny", "containsAll"];
|
|
1567
|
+
function requirePatternOperands(fieldPath, filter, type) {
|
|
1568
|
+
if (filter.contains !== void 0) {
|
|
1569
|
+
if (typeof filter.contains !== "string") {
|
|
1570
|
+
invalid(`"contains" on field "${fieldPath}" must be a string`, fieldPath, "contains");
|
|
1571
|
+
}
|
|
1572
|
+
requirePatternField(fieldPath, "contains", type);
|
|
1573
|
+
}
|
|
1574
|
+
if (filter.caseFold !== void 0) {
|
|
1575
|
+
if (typeof filter.caseFold !== "boolean") {
|
|
1576
|
+
invalid(`"caseFold" on field "${fieldPath}" must be true or false`, fieldPath, "caseFold");
|
|
1577
|
+
}
|
|
1578
|
+
requirePatternField(fieldPath, "caseFold", type);
|
|
1579
|
+
}
|
|
1580
|
+
if (parsedTypeOf(type)?.pattern !== true) return;
|
|
1581
|
+
const caseFold = filter.caseFold === true;
|
|
1582
|
+
for (const operator of PATTERN_TEST_OPERATORS) {
|
|
1583
|
+
const operand = filter[operator];
|
|
1584
|
+
if (typeof operand === "string") requirePatternText(fieldPath, operator, operand, caseFold);
|
|
1585
|
+
if (!Array.isArray(operand)) continue;
|
|
1586
|
+
for (const text of operand) {
|
|
1587
|
+
if (typeof text === "string") requirePatternText(fieldPath, operator, text, caseFold);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
for (const operator of ELEMENT_OPERATORS2) {
|
|
1591
|
+
const operand = filter[operator];
|
|
1592
|
+
if (!Array.isArray(operand)) continue;
|
|
1593
|
+
for (const text of operand) {
|
|
1594
|
+
if (typeof text === "string") requirePatternText(fieldPath, operator, text, false);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
function requireWellFormedText(fieldPath, filter) {
|
|
1599
|
+
for (const [operator, operand] of Object.entries(filter)) {
|
|
1600
|
+
const texts = Array.isArray(operand) ? operand : [operand];
|
|
1601
|
+
for (const text of texts) {
|
|
1602
|
+
if (typeof text !== "string" || !holdsUnpairedSurrogate(text)) continue;
|
|
1603
|
+
invalid(
|
|
1604
|
+
`The text of "${operator}" on field "${fieldPath}" holds an unpaired surrogate, the half of a character such as an emoji whose other half is missing, so no stored value can hold it`,
|
|
1605
|
+
fieldPath,
|
|
1606
|
+
operator
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// src/filters/operands/index.ts
|
|
1613
|
+
var RANGE_OPERATORS = ["gt", "lt", "gte", "lte"];
|
|
1614
|
+
var TEXT_OPERATORS = ["startsWith", "endsWith"];
|
|
1615
|
+
var EQUALITY_OPERATORS = ["eq", "ne"];
|
|
1616
|
+
var LIST_OPERATORS = ["in", "nin"];
|
|
1617
|
+
var GEO_OPERATORS = ["radius", "polygon"];
|
|
1223
1618
|
function invalidShape(message) {
|
|
1224
1619
|
throw new NarsilError(ErrorCodes.SEARCH_INVALID_FILTER, message);
|
|
1225
1620
|
}
|
|
1226
|
-
function isObjectRecord(value) {
|
|
1227
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1228
|
-
}
|
|
1229
1621
|
function undeclaredFieldOnStrictIndex(field, option) {
|
|
1230
1622
|
return new NarsilError(
|
|
1231
1623
|
ErrorCodes.SEARCH_INVALID_FIELD,
|
|
@@ -1233,34 +1625,14 @@ function undeclaredFieldOnStrictIndex(field, option) {
|
|
|
1233
1625
|
{ field, option }
|
|
1234
1626
|
);
|
|
1235
1627
|
}
|
|
1236
|
-
function
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
for (const [fieldPath, filter] of Object.entries(fields)) {
|
|
1243
|
-
if (rules.declaredOnly && !Object.hasOwn(rules.fieldTypes, fieldPath)) {
|
|
1244
|
-
throw undeclaredFieldOnStrictIndex(fieldPath, "filter");
|
|
1245
|
-
}
|
|
1246
|
-
if (isObjectRecord(filter)) requireValidOperands(fieldPath, filter, rules.fieldTypes[fieldPath]);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
for (const [clause, list] of [
|
|
1250
|
-
["and", and],
|
|
1251
|
-
["or", or]
|
|
1252
|
-
]) {
|
|
1253
|
-
if (list === void 0) continue;
|
|
1254
|
-
if (!Array.isArray(list)) invalidShape(`"${clause}" in a filter expression must be a list of filter expressions`);
|
|
1255
|
-
for (const nested of list) requireValidExpression(nested, rules, depth + 1);
|
|
1256
|
-
}
|
|
1257
|
-
if (not !== void 0) requireValidExpression(not, rules, depth + 1);
|
|
1258
|
-
}
|
|
1259
|
-
function requireValidFilter(expression, fieldTypes, declaredOnly = false) {
|
|
1260
|
-
if (expression === void 0 || expression === null) return;
|
|
1261
|
-
requireValidExpression(expression, { fieldTypes, declaredOnly }, 0);
|
|
1628
|
+
function undeclaredFilterField(field) {
|
|
1629
|
+
return new NarsilError(
|
|
1630
|
+
ErrorCodes.SEARCH_INVALID_FIELD,
|
|
1631
|
+
`The schema declares no field "${field}", while the engine answers a filter only from the indexes that it keeps for declared fields. Declare the field in the schema of a new index to filter by it`,
|
|
1632
|
+
{ field, option: "filter" }
|
|
1633
|
+
);
|
|
1262
1634
|
}
|
|
1263
|
-
function
|
|
1635
|
+
function requireRangeOperands(fieldPath, filter, type) {
|
|
1264
1636
|
for (const operator of RANGE_OPERATORS) {
|
|
1265
1637
|
const bound = filter[operator];
|
|
1266
1638
|
if (bound === void 0) continue;
|
|
@@ -1273,22 +1645,23 @@ function requireValidOperands(fieldPath, filter, type) {
|
|
|
1273
1645
|
}
|
|
1274
1646
|
requireFieldKind(fieldPath, operator, type, ["number"]);
|
|
1275
1647
|
}
|
|
1276
|
-
if (filter.between
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
if (!isPair || !isFiniteNumber(bounds[0]) || !isFiniteNumber(bounds[1])) {
|
|
1283
|
-
invalid(
|
|
1284
|
-
`"between" on field "${fieldPath}" must be two finite numbers or two strings, the lower bound first`,
|
|
1285
|
-
fieldPath,
|
|
1286
|
-
"between"
|
|
1287
|
-
);
|
|
1288
|
-
}
|
|
1289
|
-
requireFieldKind(fieldPath, "between", type, ["number"]);
|
|
1290
|
-
}
|
|
1648
|
+
if (filter.between === void 0) return;
|
|
1649
|
+
const bounds = filter.between;
|
|
1650
|
+
const isPair = Array.isArray(bounds) && bounds.length === 2;
|
|
1651
|
+
if (isPair && typeof bounds[0] === "string" && typeof bounds[1] === "string") {
|
|
1652
|
+
requireTextRangeField(fieldPath, "between", type);
|
|
1653
|
+
return;
|
|
1291
1654
|
}
|
|
1655
|
+
if (!isPair || !isFiniteNumber(bounds[0]) || !isFiniteNumber(bounds[1])) {
|
|
1656
|
+
invalid(
|
|
1657
|
+
`"between" on field "${fieldPath}" must be two finite numbers or two strings, the lower bound first`,
|
|
1658
|
+
fieldPath,
|
|
1659
|
+
"between"
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
requireFieldKind(fieldPath, "between", type, ["number"]);
|
|
1663
|
+
}
|
|
1664
|
+
function requireValueOperands(fieldPath, filter, type) {
|
|
1292
1665
|
for (const operator of TEXT_OPERATORS) {
|
|
1293
1666
|
if (filter[operator] === void 0) continue;
|
|
1294
1667
|
if (typeof filter[operator] !== "string") {
|
|
@@ -1314,22 +1687,81 @@ function requireValidOperands(fieldPath, filter, type) {
|
|
|
1314
1687
|
if (!Array.isArray(values) || values.some((value) => typeof value !== "string")) {
|
|
1315
1688
|
invalid(`"${operator}" on field "${fieldPath}" must be a list of strings`, fieldPath, operator);
|
|
1316
1689
|
}
|
|
1690
|
+
requireListLength(fieldPath, operator, values);
|
|
1317
1691
|
requireFieldKind(fieldPath, operator, type, ["text"]);
|
|
1318
1692
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
if (size === null || typeof size !== "object") {
|
|
1322
|
-
invalid(`"size" on field "${fieldPath}" must be a comparison object`, fieldPath, "size");
|
|
1323
|
-
}
|
|
1324
|
-
requireValidOperands(`${fieldPath}.size`, size, "number");
|
|
1693
|
+
for (const operator of GEO_OPERATORS) {
|
|
1694
|
+
if (filter[operator] !== void 0) requireFieldKind(fieldPath, operator, type, ["geopoint"]);
|
|
1325
1695
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1696
|
+
}
|
|
1697
|
+
function requireValidOperands(fieldPath, filter, type) {
|
|
1698
|
+
requireWellFormedText(fieldPath, filter);
|
|
1699
|
+
requireRangeOperands(fieldPath, filter, type);
|
|
1700
|
+
requireValueOperands(fieldPath, filter, type);
|
|
1701
|
+
requireWholeValueIndex(fieldPath, filter, type);
|
|
1702
|
+
requirePresenceOperands(fieldPath, filter);
|
|
1703
|
+
requireElementOperands(fieldPath, filter, type);
|
|
1704
|
+
requireSizeOperands(fieldPath, filter, type);
|
|
1705
|
+
requirePatternOperands(fieldPath, filter, type);
|
|
1706
|
+
requireGeoOperands(fieldPath, filter);
|
|
1707
|
+
}
|
|
1708
|
+
function filterTestCount(expression) {
|
|
1709
|
+
if (!isObjectRecord(expression)) return 0;
|
|
1710
|
+
const fieldFilters = isObjectRecord(expression.fields) ? Object.values(expression.fields) : [];
|
|
1711
|
+
let tests = fieldFilters.length === 0 && !holdsNestedExpressions(expression) ? 1 : 0;
|
|
1712
|
+
for (const filter of fieldFilters) tests += isObjectRecord(filter) ? testsIn(filter) : 1;
|
|
1713
|
+
for (const list of [expression.and, expression.or]) {
|
|
1714
|
+
if (Array.isArray(list)) for (const nested of list) tests += filterTestCount(nested);
|
|
1715
|
+
}
|
|
1716
|
+
return tests + filterTestCount(expression.not);
|
|
1717
|
+
}
|
|
1718
|
+
function testsIn(filter) {
|
|
1719
|
+
let tests = 0;
|
|
1720
|
+
for (const [operator, operand] of Object.entries(filter)) {
|
|
1721
|
+
if (operator === "caseFold") continue;
|
|
1722
|
+
tests += operator === "size" && isObjectRecord(operand) ? Math.max(1, Object.keys(operand).length) : 1;
|
|
1723
|
+
}
|
|
1724
|
+
return Math.max(1, tests);
|
|
1725
|
+
}
|
|
1726
|
+
function countTests(walk, tests) {
|
|
1727
|
+
walk.tests += tests;
|
|
1728
|
+
if (walk.tests > MAX_FILTER_TESTS) {
|
|
1729
|
+
invalidShape(`A filter holds at most ${MAX_FILTER_TESTS} tests across all of its fields and nested expressions`);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
function holdsNestedExpressions(expression) {
|
|
1733
|
+
const { and, or, not } = expression;
|
|
1734
|
+
return Array.isArray(and) && and.length > 0 || Array.isArray(or) && or.length > 0 || not !== void 0;
|
|
1735
|
+
}
|
|
1736
|
+
function requireValidExpression(expression, walk, depth) {
|
|
1737
|
+
if (depth > MAX_FILTER_DEPTH) invalidShape(`A filter expression nests at most ${MAX_FILTER_DEPTH} levels deep`);
|
|
1738
|
+
if (!isObjectRecord(expression)) invalidShape("Each filter expression must be an object");
|
|
1739
|
+
const { fields, and, or, not } = expression;
|
|
1740
|
+
if (fields !== void 0 && !isObjectRecord(fields)) {
|
|
1741
|
+
invalidShape('"fields" in a filter expression must be an object keyed by field path');
|
|
1742
|
+
}
|
|
1743
|
+
const fieldFilters = Object.entries(fields ?? {});
|
|
1744
|
+
if (fieldFilters.length === 0 && !holdsNestedExpressions(expression)) countTests(walk, 1);
|
|
1745
|
+
for (const [fieldPath, filter] of fieldFilters) {
|
|
1746
|
+
const type = walk.fieldTypes[fieldPath];
|
|
1747
|
+
if (!Object.hasOwn(walk.fieldTypes, fieldPath) || type === void 0) throw undeclaredFilterField(fieldPath);
|
|
1748
|
+
if (!isObjectRecord(filter)) invalidShape(`The filter on field "${fieldPath}" must be an object of tests`);
|
|
1749
|
+
countTests(walk, testsIn(filter));
|
|
1750
|
+
requireValidOperands(fieldPath, filter, type);
|
|
1751
|
+
}
|
|
1752
|
+
for (const [clause, list] of [
|
|
1753
|
+
["and", and],
|
|
1754
|
+
["or", or]
|
|
1755
|
+
]) {
|
|
1756
|
+
if (list === void 0) continue;
|
|
1757
|
+
if (!Array.isArray(list)) invalidShape(`"${clause}" in a filter expression must be a list of filter expressions`);
|
|
1758
|
+
for (const nested of list) requireValidExpression(nested, walk, depth + 1);
|
|
1332
1759
|
}
|
|
1760
|
+
if (not !== void 0) requireValidExpression(not, walk, depth + 1);
|
|
1761
|
+
}
|
|
1762
|
+
function requireValidFilter(expression, fieldTypes) {
|
|
1763
|
+
if (expression === void 0 || expression === null) return;
|
|
1764
|
+
requireValidExpression(expression, { fieldTypes, tests: 0 }, 0);
|
|
1333
1765
|
}
|
|
1334
1766
|
|
|
1335
1767
|
// src/search/sorting.ts
|
|
@@ -1679,7 +2111,11 @@ function prepareFulltextQuery(partition, params, language, schema, options) {
|
|
|
1679
2111
|
}
|
|
1680
2112
|
let filterBitset;
|
|
1681
2113
|
if (params.filters) {
|
|
1682
|
-
filterBitset = partition.applyFiltersBitset(
|
|
2114
|
+
filterBitset = partition.applyFiltersBitset(
|
|
2115
|
+
params.filters,
|
|
2116
|
+
schema,
|
|
2117
|
+
options?.patternWork ?? createPatternWorkMeter()
|
|
2118
|
+
);
|
|
1683
2119
|
if (bitsetIsEmpty(filterBitset)) return null;
|
|
1684
2120
|
}
|
|
1685
2121
|
return { queryTokens, prefixExpansion, filterBitset };
|
|
@@ -1922,7 +2358,8 @@ function resolveEffectiveMode(config) {
|
|
|
1922
2358
|
function buildSearchOptions(base, globalStats) {
|
|
1923
2359
|
return {
|
|
1924
2360
|
...base,
|
|
1925
|
-
globalStats: globalStats ?? base?.globalStats
|
|
2361
|
+
globalStats: globalStats ?? base?.globalStats,
|
|
2362
|
+
patternWork: base?.patternWork ?? createPatternWorkMeter()
|
|
1926
2363
|
};
|
|
1927
2364
|
}
|
|
1928
2365
|
function dispatchWithDispatcher(partitions, params, language, schema, options, dispatcher) {
|
|
@@ -1949,8 +2386,12 @@ function countsWithoutScores(params) {
|
|
|
1949
2386
|
}
|
|
1950
2387
|
function fanOutMatchCount(manager, params, language, schema, options) {
|
|
1951
2388
|
let count = 0;
|
|
2389
|
+
const searchOptions = {
|
|
2390
|
+
...options?.searchOptions,
|
|
2391
|
+
patternWork: options?.searchOptions?.patternWork ?? createPatternWorkMeter()
|
|
2392
|
+
};
|
|
1952
2393
|
for (const partition of partitionsIn(manager, options?.partitionIds)) {
|
|
1953
|
-
const matches = fulltextMatches(partition, params, language, schema,
|
|
2394
|
+
const matches = fulltextMatches(partition, params, language, schema, searchOptions);
|
|
1954
2395
|
if (matches !== null) count += matches.count;
|
|
1955
2396
|
}
|
|
1956
2397
|
return count;
|
|
@@ -2132,4 +2573,4 @@ function setNestedValue(obj, path, value) {
|
|
|
2132
2573
|
current[segments[segments.length - 1]] = value;
|
|
2133
2574
|
}
|
|
2134
2575
|
|
|
2135
|
-
export {
|
|
2576
|
+
export { GEO_UNIT_METRES, MAX_FILTER_ARRAY_SIZE, MAX_FILTER_DEPTH, MAX_FILTER_TESTS, applyPagination, applyProjection, applySorting, boundedLevenshtein, clampRowCount, clearNormalizationCache, cloneProjected, collectQueryTermStats, computeFacets, configureNormalizationCache, countsWithoutScores, coveringRanges, createPatternWorkMeter, decodePageCursor, encodePageCursor, everyValueFacetConfig, fanOutMatchCount, fanOutQuery, filterTestCount, fnv1a, fnv1aBytes, fnv1aUint32s, fulltextMatches, fuzzyPrefixOf, fuzzyTermMatches, getFieldValueByInternalId, getFlatSchema, getNestedValue, getNormalizationCacheSize, getOrCreateFieldNameIndex, joinPatternWorkMeter, kWayMerge, locationCode, mergeFacets, mergePartitionStats, normalizeSort, oversampledFacetConfig, oversampledShardSize, partitionsIn, polygonBoxes, polygonContainment, projectionKeepsField, pruneStatsToQueryTerms, radiusBoxes, readSortValues, requireMatchingCursor, requireSortableFields, requireValidFilter, requireValidOperands, requireWithinResultWindow, resetNormalizationCache, resolveProjection, sanitizeGlobalStats, setNestedValue, sortModesOf, sortSignatureEntry, sortSignatureOf, textFieldsChanged, tokenize, tokenizeIterator, tokenizeOptions, undeclaredFieldOnStrictIndex, undeclaredFilterField, visitTokens };
|