@delali/narsil 0.3.1-next.71 → 0.3.1-next.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/adapters/filesystem.mjs +1 -1
- package/dist/adapters/indexeddb.mjs +1 -1
- package/dist/{capabilities-Dt-7B4Wz.d.ts → capabilities-CYeAWYxQ.d.ts} +19 -4
- package/dist/{chunk-3JGWULDF.mjs → chunk-2WLD2NFW.mjs} +5 -5
- package/dist/{chunk-LMO5TJUS.mjs → chunk-3YKWSO54.mjs} +642 -196
- package/dist/{chunk-HWQLTLNG.mjs → chunk-4MQ5F4IL.mjs} +1 -1
- package/dist/{chunk-FVLQ63B7.mjs → chunk-AFKH4ZK3.mjs} +30 -6
- package/dist/{chunk-653BNMAF.mjs → chunk-AK3BLGCI.mjs} +195 -25
- package/dist/{chunk-IOR2K26O.mjs → chunk-CVFAFZSN.mjs} +8000 -4391
- package/dist/{chunk-USKFU7J5.mjs → chunk-I64LXKO3.mjs} +1 -1
- package/dist/{chunk-OI42GWLR.mjs → chunk-LJYTW7GT.mjs} +57 -11
- package/dist/{chunk-2STVUTNA.mjs → chunk-LPBWCHBE.mjs} +194 -13
- package/dist/{chunk-YNVIUGF5.mjs → chunk-PU43WDY4.mjs} +4 -2
- package/dist/{chunk-M76V3OGJ.mjs → chunk-THMSFOJO.mjs} +1289 -1405
- package/dist/{chunk-YJDQ7NLA.mjs → chunk-WVDFG45M.mjs} +93 -95
- package/dist/chunk-XW25X6AU.mjs +614 -0
- package/dist/chunk-ZKRV5M2P.mjs +75 -0
- package/dist/{chunk-P4DSQ6M4.mjs → chunk-ZULSEZCE.mjs} +68 -28
- package/dist/{client-COek5GXK.d.ts → client-D698E5ge.d.ts} +77 -26
- package/dist/client.d.ts +9 -9
- package/dist/client.mjs +45 -22
- package/dist/{config-rBpbgTw8.d.ts → config-Bv8etwUZ.d.ts} +11 -4
- package/dist/{config-Dfanyqqt.d.ts → config-QXU3HMpq.d.ts} +26 -2
- package/dist/direct-executor-5SRDFZAS.mjs +13 -0
- package/dist/distribution/coordinator/etcd.d.ts +29 -16
- package/dist/distribution/coordinator/etcd.mjs +163 -76
- package/dist/distribution/coordinator/in-memory.d.ts +2 -2
- package/dist/distribution/coordinator/in-memory.mjs +1 -1
- package/dist/distribution/transport/grpc.d.ts +2 -2
- package/dist/distribution/transport/grpc.mjs +22 -2
- package/dist/distribution/transport/in-memory.d.ts +2 -2
- package/dist/distribution/transport/in-memory.mjs +6 -3
- package/dist/distribution/transport/tcp.d.ts +2 -2
- package/dist/distribution/transport/tcp.mjs +40 -16
- package/dist/distribution.d.ts +9 -9
- package/dist/distribution.mjs +682 -388
- package/dist/embeddings/openai.mjs +1 -1
- package/dist/{engine-igk0aMpQ.d.ts → engine-D_OzKZ-6.d.ts} +97 -188
- package/dist/index.browser.mjs +11671 -7427
- package/dist/index.d.ts +27 -8
- package/dist/index.mjs +13 -14
- package/dist/invalidation/filesystem.mjs +1 -1
- package/dist/{memory-C7UMCMH3.d.ts → memory-B4jMmygw.d.ts} +279 -27
- 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 +41 -17
- package/dist/persistence/durability/checkpoint-worker.mjs +24 -16
- package/dist/react.d.ts +7 -7
- package/dist/react.mjs +1 -1
- package/dist/{schema-Bgzr4g7b.d.ts → schema-BBOCdhND.d.ts} +9 -7
- package/dist/server.d.ts +11 -11
- package/dist/server.mjs +213 -34
- package/dist/{tasks-BZt_zes6.d.ts → tasks-DpgCpW1x.d.ts} +3 -3
- package/dist/{thread-JEVAYETR.mjs → thread-3LNVA43V.mjs} +94 -27
- package/dist/{types-BQDlIvfS.d.ts → types-B3b68nfF.d.ts} +1 -1
- package/dist/{types-gOUqhs2F.d.ts → types-BiUmaiWT.d.ts} +1 -1
- package/dist/{types-D5OwhtsQ.d.ts → types-Cb9kiY5P.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/chunk-3L4U6EJQ.mjs +0 -49
- package/dist/chunk-GIVAGD3Q.mjs +0 -25
- package/dist/chunk-IFBKLBGB.mjs +0 -7
- package/dist/direct-executor-GYJEKNV5.mjs +0 -12
- package/dist/serialization/crc32-worker.d.ts +0 -20
- package/dist/serialization/crc32-worker.mjs +0 -62
|
@@ -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-AK3BLGCI.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-I64LXKO3.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);
|
|
@@ -743,6 +777,7 @@ function getOrCreateFieldNameIndex(table, fieldName) {
|
|
|
743
777
|
table.indexMap.set(fieldName, idx);
|
|
744
778
|
return idx;
|
|
745
779
|
}
|
|
780
|
+
var VECTOR_PATTERN = /^vector\[(\d+)]$/;
|
|
746
781
|
function getNestedValue(obj, path) {
|
|
747
782
|
const parts = path.split(".");
|
|
748
783
|
let current = obj;
|
|
@@ -775,6 +810,10 @@ function textFieldsChanged(oldDoc, newDoc, fields) {
|
|
|
775
810
|
}
|
|
776
811
|
return false;
|
|
777
812
|
}
|
|
813
|
+
function parseVectorDimension(fieldType) {
|
|
814
|
+
const match = VECTOR_PATTERN.exec(fieldType);
|
|
815
|
+
return match ? Number.parseInt(match[1], 10) : null;
|
|
816
|
+
}
|
|
778
817
|
function tokenizeOptions(options) {
|
|
779
818
|
return {
|
|
780
819
|
stem: true,
|
|
@@ -800,13 +839,6 @@ function getFieldValueByInternalId(docStore, internalId, fieldPath) {
|
|
|
800
839
|
if (externalId === void 0) return void 0;
|
|
801
840
|
return getFieldValueForDoc(docStore, externalId, fieldPath);
|
|
802
841
|
}
|
|
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
842
|
|
|
811
843
|
// src/core/partition/facets.ts
|
|
812
844
|
var UNDECLARED_FIELD_TYPE = "";
|
|
@@ -1084,13 +1116,205 @@ function decodeCursorText(encoded) {
|
|
|
1084
1116
|
return new TextDecoder().decode(bytes);
|
|
1085
1117
|
}
|
|
1086
1118
|
|
|
1087
|
-
// src/
|
|
1088
|
-
var
|
|
1089
|
-
var
|
|
1090
|
-
var
|
|
1091
|
-
var
|
|
1092
|
-
|
|
1093
|
-
|
|
1119
|
+
// src/filters/constants.ts
|
|
1120
|
+
var MAX_FILTER_DEPTH = 30;
|
|
1121
|
+
var MAX_FILTER_TESTS = 1024;
|
|
1122
|
+
var MAX_FILTER_ARRAY_SIZE = 65536;
|
|
1123
|
+
var GEO_UNIT_METRES = { km: 1e3, mi: 1609.344, m: 1 };
|
|
1124
|
+
|
|
1125
|
+
// src/filters/operands/checks.ts
|
|
1126
|
+
function operandKindOf(type) {
|
|
1127
|
+
if (type === void 0) return void 0;
|
|
1128
|
+
if (type === "number" || type === "number[]") return "number";
|
|
1129
|
+
if (type === "boolean" || type === "boolean[]") return "boolean";
|
|
1130
|
+
if (type === "geopoint") return "geopoint";
|
|
1131
|
+
if (type.startsWith("vector[")) return "vector";
|
|
1132
|
+
return "text";
|
|
1133
|
+
}
|
|
1134
|
+
function isFiniteNumber(value) {
|
|
1135
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1136
|
+
}
|
|
1137
|
+
function kindOfValue(value) {
|
|
1138
|
+
if (isFiniteNumber(value)) return "number";
|
|
1139
|
+
if (typeof value === "boolean") return "boolean";
|
|
1140
|
+
if (typeof value === "string") return "text";
|
|
1141
|
+
return void 0;
|
|
1142
|
+
}
|
|
1143
|
+
function isObjectRecord(value) {
|
|
1144
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1145
|
+
}
|
|
1146
|
+
function invalid(message, fieldPath, operator) {
|
|
1147
|
+
throw new NarsilError(ErrorCodes.SEARCH_INVALID_FILTER, message, { fieldPath, operator });
|
|
1148
|
+
}
|
|
1149
|
+
function requireFieldKind(fieldPath, operator, type, accepted) {
|
|
1150
|
+
const kind = operandKindOf(type);
|
|
1151
|
+
if (kind === void 0 || accepted.includes(kind)) return;
|
|
1152
|
+
invalid(
|
|
1153
|
+
`The engine applies "${operator}" to ${accepted.join(" or ")} fields alone, and the schema declares field "${fieldPath}" as "${type}"`,
|
|
1154
|
+
fieldPath,
|
|
1155
|
+
operator
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
function requireTextRangeField(fieldPath, operator, type) {
|
|
1159
|
+
const parsed = parsedTypeOf(type);
|
|
1160
|
+
if (parsed?.sortable && (parsed.base === "string" || parsed.base === "verbatim")) return;
|
|
1161
|
+
const declared = type === void 0 ? `the schema has no field "${fieldPath}"` : `field "${fieldPath}" has type "${type}"`;
|
|
1162
|
+
invalid(
|
|
1163
|
+
`The engine applies "${operator}" with a text bound to a "string" or "verbatim" field whose type includes "sortable", but ${declared}`,
|
|
1164
|
+
fieldPath,
|
|
1165
|
+
operator
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
function requireListLength(fieldPath, operator, values) {
|
|
1169
|
+
if (values.length <= MAX_FILTER_ARRAY_SIZE) return;
|
|
1170
|
+
invalid(
|
|
1171
|
+
`"${operator}" on field "${fieldPath}" holds ${values.length} values, while a filter list holds at most ${MAX_FILTER_ARRAY_SIZE}`,
|
|
1172
|
+
fieldPath,
|
|
1173
|
+
operator
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// src/geo/cells.ts
|
|
1178
|
+
var AXIS_CELLS = 2 ** LOCATION_CODE_BITS_PER_AXIS;
|
|
1179
|
+
function axisCell(value, lowest, span) {
|
|
1180
|
+
const cell = Math.floor((value - lowest) / span * AXIS_CELLS);
|
|
1181
|
+
if (!(cell > 0)) return 0;
|
|
1182
|
+
return cell >= AXIS_CELLS ? AXIS_CELLS - 1 : cell;
|
|
1183
|
+
}
|
|
1184
|
+
function latitudeCell(lat) {
|
|
1185
|
+
return axisCell(lat, -MAX_LATITUDE_DEGREES, 2 * MAX_LATITUDE_DEGREES);
|
|
1186
|
+
}
|
|
1187
|
+
function longitudeCell(lon) {
|
|
1188
|
+
return axisCell(lon, -MAX_LONGITUDE_DEGREES, FULL_TURN_DEGREES);
|
|
1189
|
+
}
|
|
1190
|
+
function interleave(row, column, bits) {
|
|
1191
|
+
let code = 0;
|
|
1192
|
+
for (let bit = bits - 1; bit >= 0; bit--) code = code * 4 + (row >>> bit & 1) * 2 + (column >>> bit & 1);
|
|
1193
|
+
return code;
|
|
1194
|
+
}
|
|
1195
|
+
function locationCode(lat, lon) {
|
|
1196
|
+
return interleave(latitudeCell(lat), longitudeCell(lon), LOCATION_CODE_BITS_PER_AXIS);
|
|
1197
|
+
}
|
|
1198
|
+
function appendBoxRanges(box, out) {
|
|
1199
|
+
const south = latitudeCell(box.south);
|
|
1200
|
+
const north = latitudeCell(box.north);
|
|
1201
|
+
const west = longitudeCell(box.west);
|
|
1202
|
+
const east = longitudeCell(box.east);
|
|
1203
|
+
let level = LOCATION_CODE_BITS_PER_AXIS;
|
|
1204
|
+
while (level > 0) {
|
|
1205
|
+
const shift2 = LOCATION_CODE_BITS_PER_AXIS - level;
|
|
1206
|
+
const rows = (north >>> shift2) - (south >>> shift2) + 1;
|
|
1207
|
+
const columns = (east >>> shift2) - (west >>> shift2) + 1;
|
|
1208
|
+
if (rows * columns <= MAX_COVERING_CELLS) break;
|
|
1209
|
+
level--;
|
|
1210
|
+
}
|
|
1211
|
+
const shift = LOCATION_CODE_BITS_PER_AXIS - level;
|
|
1212
|
+
const codesPerCell = 4 ** shift;
|
|
1213
|
+
for (let row = south >>> shift; row <= north >>> shift; row++) {
|
|
1214
|
+
for (let column = west >>> shift; column <= east >>> shift; column++) {
|
|
1215
|
+
const from = interleave(row, column, level) * codesPerCell;
|
|
1216
|
+
out.push({ from, to: from + codesPerCell });
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
function coveringRanges(boxes) {
|
|
1221
|
+
const ranges = [];
|
|
1222
|
+
for (const box of boxes) appendBoxRanges(box, ranges);
|
|
1223
|
+
ranges.sort((left, right) => left.from - right.from);
|
|
1224
|
+
const merged = [];
|
|
1225
|
+
for (const range of ranges) {
|
|
1226
|
+
const last = merged[merged.length - 1];
|
|
1227
|
+
if (last !== void 0 && range.from <= last.to) {
|
|
1228
|
+
if (range.to > last.to) last.to = range.to;
|
|
1229
|
+
} else {
|
|
1230
|
+
merged.push({ from: range.from, to: range.to });
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
return merged;
|
|
1234
|
+
}
|
|
1235
|
+
function longitudeBoxes(south, north, west, east) {
|
|
1236
|
+
if (east - west >= FULL_TURN_DEGREES) {
|
|
1237
|
+
return [{ south, north, west: -MAX_LONGITUDE_DEGREES, east: MAX_LONGITUDE_DEGREES }];
|
|
1238
|
+
}
|
|
1239
|
+
if (west < -MAX_LONGITUDE_DEGREES) {
|
|
1240
|
+
return [
|
|
1241
|
+
{ south, north, west: west + FULL_TURN_DEGREES, east: MAX_LONGITUDE_DEGREES },
|
|
1242
|
+
{ south, north, west: -MAX_LONGITUDE_DEGREES, east }
|
|
1243
|
+
];
|
|
1244
|
+
}
|
|
1245
|
+
if (east > MAX_LONGITUDE_DEGREES) {
|
|
1246
|
+
return [
|
|
1247
|
+
{ south, north, west, east: MAX_LONGITUDE_DEGREES },
|
|
1248
|
+
{ south, north, west: -MAX_LONGITUDE_DEGREES, east: east - FULL_TURN_DEGREES }
|
|
1249
|
+
];
|
|
1250
|
+
}
|
|
1251
|
+
return [{ south, north, west, east }];
|
|
1252
|
+
}
|
|
1253
|
+
function radiusBoxes(lat, lon, distanceMeters) {
|
|
1254
|
+
const angle = distanceMeters / EARTH_RADIUS_METERS * RADIUS_BOX_MARGIN_RATIO + RADIUS_BOX_MARGIN_RADIANS;
|
|
1255
|
+
const angleDegrees = angle / DEG_TO_RAD;
|
|
1256
|
+
const south = lat - angleDegrees;
|
|
1257
|
+
const north = lat + angleDegrees;
|
|
1258
|
+
if (angle >= Math.PI || south <= -MAX_LATITUDE_DEGREES || north >= MAX_LATITUDE_DEGREES) {
|
|
1259
|
+
return [
|
|
1260
|
+
{
|
|
1261
|
+
south: Math.max(south, -MAX_LATITUDE_DEGREES),
|
|
1262
|
+
north: Math.min(north, MAX_LATITUDE_DEGREES),
|
|
1263
|
+
west: -MAX_LONGITUDE_DEGREES,
|
|
1264
|
+
east: MAX_LONGITUDE_DEGREES
|
|
1265
|
+
}
|
|
1266
|
+
];
|
|
1267
|
+
}
|
|
1268
|
+
const spread = Math.asin(Math.sin(angle) / Math.cos(lat * DEG_TO_RAD)) / DEG_TO_RAD;
|
|
1269
|
+
return longitudeBoxes(south, north, lon - spread, lon + spread);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// src/geo/edge-tree.ts
|
|
1273
|
+
function buildLongitudeEdgeTree(lons) {
|
|
1274
|
+
const n = lons.length;
|
|
1275
|
+
const edges = [];
|
|
1276
|
+
for (let i = 0; i < n; i++) {
|
|
1277
|
+
if (lons[i] !== lons[i === 0 ? n - 1 : i - 1]) edges.push(i);
|
|
1278
|
+
}
|
|
1279
|
+
const lowOf = (i) => Math.min(lons[i], lons[i === 0 ? n - 1 : i - 1]);
|
|
1280
|
+
edges.sort((left, right) => lowOf(left) - lowOf(right));
|
|
1281
|
+
const count = edges.length;
|
|
1282
|
+
const tree = {
|
|
1283
|
+
from: new Int32Array(count),
|
|
1284
|
+
to: new Int32Array(count),
|
|
1285
|
+
low: new Float64Array(count),
|
|
1286
|
+
high: new Float64Array(count),
|
|
1287
|
+
subtreeHigh: new Float64Array(count)
|
|
1288
|
+
};
|
|
1289
|
+
edges.forEach((i, at) => {
|
|
1290
|
+
const j = i === 0 ? n - 1 : i - 1;
|
|
1291
|
+
tree.from[at] = i;
|
|
1292
|
+
tree.to[at] = j;
|
|
1293
|
+
tree.low[at] = Math.min(lons[i], lons[j]);
|
|
1294
|
+
tree.high[at] = Math.max(lons[i], lons[j]);
|
|
1295
|
+
});
|
|
1296
|
+
const fillSubtreeHigh = (start, end) => {
|
|
1297
|
+
if (start >= end) return Number.NEGATIVE_INFINITY;
|
|
1298
|
+
const mid = start + end >>> 1;
|
|
1299
|
+
const highest = Math.max(tree.high[mid], fillSubtreeHigh(start, mid), fillSubtreeHigh(mid + 1, end));
|
|
1300
|
+
tree.subtreeHigh[mid] = highest;
|
|
1301
|
+
return highest;
|
|
1302
|
+
};
|
|
1303
|
+
fillSubtreeHigh(0, count);
|
|
1304
|
+
return tree;
|
|
1305
|
+
}
|
|
1306
|
+
function forEachEdgeSpanning(tree, lon, visit) {
|
|
1307
|
+
const walk = (start, end) => {
|
|
1308
|
+
if (start >= end) return;
|
|
1309
|
+
const mid = start + end >>> 1;
|
|
1310
|
+
if (tree.subtreeHigh[mid] <= lon) return;
|
|
1311
|
+
walk(start, mid);
|
|
1312
|
+
if (tree.low[mid] > lon) return;
|
|
1313
|
+
if (lon < tree.high[mid]) visit(tree.from[mid], tree.to[mid]);
|
|
1314
|
+
walk(mid + 1, end);
|
|
1315
|
+
};
|
|
1316
|
+
walk(0, tree.from.length);
|
|
1317
|
+
}
|
|
1094
1318
|
|
|
1095
1319
|
// src/geo/polygon.ts
|
|
1096
1320
|
function requirePolygonRing(points) {
|
|
@@ -1103,11 +1327,18 @@ function requirePolygonRing(points) {
|
|
|
1103
1327
|
}
|
|
1104
1328
|
points.forEach((point, position) => {
|
|
1105
1329
|
const { lat, lon } = point ?? {};
|
|
1106
|
-
if (typeof lat
|
|
1330
|
+
if (typeof lat !== "number" || !Number.isFinite(lat) || typeof lon !== "number" || !Number.isFinite(lon)) {
|
|
1331
|
+
throw new NarsilError(
|
|
1332
|
+
ErrorCodes.SEARCH_INVALID_FILTER,
|
|
1333
|
+
`A polygon point carries a finite "lat" and "lon", and the point at position ${position} does not`,
|
|
1334
|
+
{ position }
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
if (isLatitude(lat) && isLongitude(lon)) return;
|
|
1107
1338
|
throw new NarsilError(
|
|
1108
1339
|
ErrorCodes.SEARCH_INVALID_FILTER,
|
|
1109
|
-
`A polygon point
|
|
1110
|
-
{ position }
|
|
1340
|
+
`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}`,
|
|
1341
|
+
{ position, lat, lon }
|
|
1111
1342
|
);
|
|
1112
1343
|
});
|
|
1113
1344
|
}
|
|
@@ -1140,67 +1371,40 @@ function polygonContainment(polygon) {
|
|
|
1140
1371
|
lats[i] = polygon[i].lat;
|
|
1141
1372
|
lons[i] = unwrap ? eastward(polygon[i].lon) : polygon[i].lon;
|
|
1142
1373
|
}
|
|
1374
|
+
const edges = buildLongitudeEdgeTree(lons);
|
|
1143
1375
|
return (lat, rawLon) => {
|
|
1144
1376
|
const lon = unwrap ? eastward(rawLon) : rawLon;
|
|
1145
1377
|
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
|
-
}
|
|
1378
|
+
forEachEdgeSpanning(edges, lon, (i, j) => {
|
|
1379
|
+
const intersectLat = lats[i] + (lats[j] - lats[i]) / (lons[j] - lons[i]) * (lon - lons[i]);
|
|
1380
|
+
if (lat < intersectLat) inside = !inside;
|
|
1381
|
+
});
|
|
1154
1382
|
return inside;
|
|
1155
1383
|
};
|
|
1156
1384
|
}
|
|
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
|
-
);
|
|
1385
|
+
function polygonBoxes(polygon) {
|
|
1386
|
+
if (polygon.length < MIN_POLYGON_POINTS) return [];
|
|
1387
|
+
const unwrap = crossesAntimeridian(polygon);
|
|
1388
|
+
let south = Number.POSITIVE_INFINITY;
|
|
1389
|
+
let north = Number.NEGATIVE_INFINITY;
|
|
1390
|
+
let west = Number.POSITIVE_INFINITY;
|
|
1391
|
+
let east = Number.NEGATIVE_INFINITY;
|
|
1392
|
+
for (const point of polygon) {
|
|
1393
|
+
const lon = unwrap ? eastward(point.lon) : point.lon;
|
|
1394
|
+
south = Math.min(south, point.lat);
|
|
1395
|
+
north = Math.max(north, point.lat);
|
|
1396
|
+
west = Math.min(west, lon);
|
|
1397
|
+
east = Math.max(east, lon);
|
|
1398
|
+
}
|
|
1399
|
+
if (west >= MAX_LONGITUDE_DEGREES)
|
|
1400
|
+
return longitudeBoxes(south, north, west - FULL_TURN_DEGREES, east - FULL_TURN_DEGREES);
|
|
1401
|
+
return longitudeBoxes(south, north, west, east);
|
|
1194
1402
|
}
|
|
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
|
-
);
|
|
1403
|
+
|
|
1404
|
+
// src/filters/operands/geo.ts
|
|
1405
|
+
function requireFlag(fieldPath, operator, flag, value) {
|
|
1406
|
+
if (value === void 0 || typeof value === "boolean") return;
|
|
1407
|
+
invalid(`"${operator}.${flag}" on field "${fieldPath}" must be true or false`, fieldPath, operator);
|
|
1204
1408
|
}
|
|
1205
1409
|
function requireRadius(fieldPath, radius) {
|
|
1206
1410
|
const circle = radius;
|
|
@@ -1212,6 +1416,15 @@ function requireRadius(fieldPath, radius) {
|
|
|
1212
1416
|
invalid(`"radius.${key}" on field "${fieldPath}" must be a finite number`, fieldPath, "radius");
|
|
1213
1417
|
}
|
|
1214
1418
|
}
|
|
1419
|
+
if (typeof circle.unit !== "string" || !Object.hasOwn(GEO_UNIT_METRES, circle.unit)) {
|
|
1420
|
+
invalid(
|
|
1421
|
+
`"radius.unit" on field "${fieldPath}" must be one of ${Object.keys(GEO_UNIT_METRES).join(", ")}, and this filter sets ${String(circle.unit)}`,
|
|
1422
|
+
fieldPath,
|
|
1423
|
+
"radius"
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
requireFlag(fieldPath, "radius", "inside", circle.inside);
|
|
1427
|
+
requireFlag(fieldPath, "radius", "highPrecision", circle.highPrecision);
|
|
1215
1428
|
if (circle.distance < 0) {
|
|
1216
1429
|
invalid(
|
|
1217
1430
|
`"radius.distance" on field "${fieldPath}" must be zero or more, and this filter sets ${String(circle.distance)}`,
|
|
@@ -1219,13 +1432,197 @@ function requireRadius(fieldPath, radius) {
|
|
|
1219
1432
|
"radius"
|
|
1220
1433
|
);
|
|
1221
1434
|
}
|
|
1435
|
+
if (!isLatitude(circle.lat)) {
|
|
1436
|
+
invalid(
|
|
1437
|
+
`"radius.lat" on field "${fieldPath}" must be from -${MAX_LATITUDE_DEGREES} to ${MAX_LATITUDE_DEGREES}, and this filter sets ${String(circle.lat)}`,
|
|
1438
|
+
fieldPath,
|
|
1439
|
+
"radius"
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
if (!isLongitude(circle.lon)) {
|
|
1443
|
+
invalid(
|
|
1444
|
+
`"radius.lon" on field "${fieldPath}" must be from -${MAX_LONGITUDE_DEGREES} to ${MAX_LONGITUDE_DEGREES}, and this filter sets ${String(circle.lon)}`,
|
|
1445
|
+
fieldPath,
|
|
1446
|
+
"radius"
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
function requireGeoOperands(fieldPath, filter) {
|
|
1451
|
+
if (filter.radius !== void 0) requireRadius(fieldPath, filter.radius);
|
|
1452
|
+
if (filter.polygon === void 0) return;
|
|
1453
|
+
if (!isObjectRecord(filter.polygon)) {
|
|
1454
|
+
invalid(`"polygon" on field "${fieldPath}" must be an object`, fieldPath, "polygon");
|
|
1455
|
+
}
|
|
1456
|
+
requireFlag(fieldPath, "polygon", "inside", filter.polygon.inside);
|
|
1457
|
+
const points = filter.polygon.points;
|
|
1458
|
+
if (Array.isArray(points)) requireListLength(fieldPath, "polygon", points);
|
|
1459
|
+
requirePolygonRing(points);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// src/filters/operands/lists.ts
|
|
1463
|
+
var WHOLE_VALUE_OPERATORS = ["eq", "ne", "in", "nin", "startsWith", "endsWith", "matchesAny", "containsAll"];
|
|
1464
|
+
var ELEMENT_OPERATORS = ["matchesAny", "containsAll"];
|
|
1465
|
+
var PRESENCE_OPERATORS = ["exists", "notExists", "isEmpty", "isNotEmpty"];
|
|
1466
|
+
var SIZE_OPERATORS = ["eq", "ne", "gt", "gte", "lt", "lte", "between"];
|
|
1467
|
+
var SIZE_OPERATOR_SET = new Set(SIZE_OPERATORS);
|
|
1468
|
+
var ELEMENT_KIND_BY_BASE = {
|
|
1469
|
+
"number[]": "number",
|
|
1470
|
+
"boolean[]": "boolean",
|
|
1471
|
+
"enum[]": "text",
|
|
1472
|
+
"string[]": "text",
|
|
1473
|
+
"verbatim[]": "text"
|
|
1474
|
+
};
|
|
1475
|
+
function requireWholeValueIndex(fieldPath, filter, type) {
|
|
1476
|
+
const parsed = parsedTypeOf(type);
|
|
1477
|
+
if (parsed === void 0 || parsed.pattern || parsed.base !== "string" && parsed.base !== "string[]") return;
|
|
1478
|
+
for (const operator of WHOLE_VALUE_OPERATORS) {
|
|
1479
|
+
if (filter[operator] === void 0) continue;
|
|
1480
|
+
invalid(
|
|
1481
|
+
`"${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`,
|
|
1482
|
+
fieldPath,
|
|
1483
|
+
operator
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
function requirePresenceOperands(fieldPath, filter) {
|
|
1488
|
+
for (const operator of PRESENCE_OPERATORS) {
|
|
1489
|
+
const operand = filter[operator];
|
|
1490
|
+
if (operand !== void 0 && typeof operand !== "boolean") {
|
|
1491
|
+
invalid(`"${operator}" on field "${fieldPath}" must be true or false`, fieldPath, operator);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
function listElementKind(fieldPath, operator, type) {
|
|
1496
|
+
const base = parsedTypeOf(type)?.base;
|
|
1497
|
+
const kind = base === void 0 ? void 0 : ELEMENT_KIND_BY_BASE[base];
|
|
1498
|
+
if (kind !== void 0) return kind;
|
|
1499
|
+
return invalid(
|
|
1500
|
+
`"${operator}" tests the elements of a list field, while the schema declares field "${fieldPath}" as "${type}"`,
|
|
1501
|
+
fieldPath,
|
|
1502
|
+
operator
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
function requireElementOperands(fieldPath, filter, type) {
|
|
1506
|
+
for (const operator of ELEMENT_OPERATORS) {
|
|
1507
|
+
const values = filter[operator];
|
|
1508
|
+
if (values === void 0) continue;
|
|
1509
|
+
if (!Array.isArray(values)) invalid(`"${operator}" on field "${fieldPath}" must be a list`, fieldPath, operator);
|
|
1510
|
+
requireListLength(fieldPath, operator, values);
|
|
1511
|
+
const kind = listElementKind(fieldPath, operator, type);
|
|
1512
|
+
if (values.some((value) => kindOfValue(value) !== kind)) {
|
|
1513
|
+
invalid(
|
|
1514
|
+
`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`,
|
|
1515
|
+
fieldPath,
|
|
1516
|
+
operator
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
function requireSizeOperands(fieldPath, filter, type) {
|
|
1522
|
+
const size = filter.size;
|
|
1523
|
+
if (size === void 0) return;
|
|
1524
|
+
if (!isObjectRecord(size)) invalid(`"size" on field "${fieldPath}" must be a comparison object`, fieldPath, "size");
|
|
1525
|
+
listElementKind(fieldPath, "size", type);
|
|
1526
|
+
for (const [operator, operand] of Object.entries(size)) {
|
|
1527
|
+
if (!SIZE_OPERATOR_SET.has(operator)) {
|
|
1528
|
+
invalid(
|
|
1529
|
+
`"size" on field "${fieldPath}" takes ${SIZE_OPERATORS.join(", ")}, while "${operator}" is none of them`,
|
|
1530
|
+
fieldPath,
|
|
1531
|
+
"size"
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
const bounds = operator === "between" ? operand : [operand];
|
|
1535
|
+
if (!Array.isArray(bounds) || bounds.length !== (operator === "between" ? 2 : 1) || !bounds.every(isFiniteNumber)) {
|
|
1536
|
+
invalid(
|
|
1537
|
+
`"size.${operator}" on field "${fieldPath}" must be ${operator === "between" ? "two finite numbers, the lower bound first" : "a finite number"}`,
|
|
1538
|
+
fieldPath,
|
|
1539
|
+
"size"
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1222
1543
|
}
|
|
1544
|
+
|
|
1545
|
+
// src/filters/operands/pattern.ts
|
|
1546
|
+
function requirePatternField(fieldPath, operator, type) {
|
|
1547
|
+
if (parsedTypeOf(type)?.pattern === true) return;
|
|
1548
|
+
const declared = type === void 0 ? `The schema declares no field "${fieldPath}"` : `Field "${fieldPath}" has type "${type}"`;
|
|
1549
|
+
invalid(
|
|
1550
|
+
`"${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}`,
|
|
1551
|
+
fieldPath,
|
|
1552
|
+
operator
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
function requirePatternText(fieldPath, operator, text, caseFold) {
|
|
1556
|
+
if (exceedsCodePoints(text, MAX_PATTERN_TEXT_CODE_POINTS)) {
|
|
1557
|
+
invalid(
|
|
1558
|
+
`The text of "${operator}" on pattern field "${fieldPath}" holds more than ${MAX_PATTERN_TEXT_CODE_POINTS} code points`,
|
|
1559
|
+
fieldPath,
|
|
1560
|
+
operator
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
if (caseFold && foldedCodePointCount(text) > MAX_PATTERN_POSITIONS) {
|
|
1564
|
+
invalid(
|
|
1565
|
+
`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`,
|
|
1566
|
+
fieldPath,
|
|
1567
|
+
operator
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
var ELEMENT_OPERATORS2 = ["matchesAny", "containsAll"];
|
|
1572
|
+
function requirePatternOperands(fieldPath, filter, type) {
|
|
1573
|
+
if (filter.contains !== void 0) {
|
|
1574
|
+
if (typeof filter.contains !== "string") {
|
|
1575
|
+
invalid(`"contains" on field "${fieldPath}" must be a string`, fieldPath, "contains");
|
|
1576
|
+
}
|
|
1577
|
+
requirePatternField(fieldPath, "contains", type);
|
|
1578
|
+
}
|
|
1579
|
+
if (filter.caseFold !== void 0) {
|
|
1580
|
+
if (typeof filter.caseFold !== "boolean") {
|
|
1581
|
+
invalid(`"caseFold" on field "${fieldPath}" must be true or false`, fieldPath, "caseFold");
|
|
1582
|
+
}
|
|
1583
|
+
requirePatternField(fieldPath, "caseFold", type);
|
|
1584
|
+
}
|
|
1585
|
+
if (parsedTypeOf(type)?.pattern !== true) return;
|
|
1586
|
+
const caseFold = filter.caseFold === true;
|
|
1587
|
+
for (const operator of PATTERN_TEST_OPERATORS) {
|
|
1588
|
+
const operand = filter[operator];
|
|
1589
|
+
if (typeof operand === "string") requirePatternText(fieldPath, operator, operand, caseFold);
|
|
1590
|
+
if (!Array.isArray(operand)) continue;
|
|
1591
|
+
for (const text of operand) {
|
|
1592
|
+
if (typeof text === "string") requirePatternText(fieldPath, operator, text, caseFold);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
for (const operator of ELEMENT_OPERATORS2) {
|
|
1596
|
+
const operand = filter[operator];
|
|
1597
|
+
if (!Array.isArray(operand)) continue;
|
|
1598
|
+
for (const text of operand) {
|
|
1599
|
+
if (typeof text === "string") requirePatternText(fieldPath, operator, text, false);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
function requireWellFormedText(fieldPath, filter) {
|
|
1604
|
+
for (const [operator, operand] of Object.entries(filter)) {
|
|
1605
|
+
const texts = Array.isArray(operand) ? operand : [operand];
|
|
1606
|
+
for (const text of texts) {
|
|
1607
|
+
if (typeof text !== "string" || !holdsUnpairedSurrogate(text)) continue;
|
|
1608
|
+
invalid(
|
|
1609
|
+
`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`,
|
|
1610
|
+
fieldPath,
|
|
1611
|
+
operator
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// src/filters/operands/index.ts
|
|
1618
|
+
var RANGE_OPERATORS = ["gt", "lt", "gte", "lte"];
|
|
1619
|
+
var TEXT_OPERATORS = ["startsWith", "endsWith"];
|
|
1620
|
+
var EQUALITY_OPERATORS = ["eq", "ne"];
|
|
1621
|
+
var LIST_OPERATORS = ["in", "nin"];
|
|
1622
|
+
var GEO_OPERATORS = ["radius", "polygon"];
|
|
1223
1623
|
function invalidShape(message) {
|
|
1224
1624
|
throw new NarsilError(ErrorCodes.SEARCH_INVALID_FILTER, message);
|
|
1225
1625
|
}
|
|
1226
|
-
function isObjectRecord(value) {
|
|
1227
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1228
|
-
}
|
|
1229
1626
|
function undeclaredFieldOnStrictIndex(field, option) {
|
|
1230
1627
|
return new NarsilError(
|
|
1231
1628
|
ErrorCodes.SEARCH_INVALID_FIELD,
|
|
@@ -1233,34 +1630,14 @@ function undeclaredFieldOnStrictIndex(field, option) {
|
|
|
1233
1630
|
{ field, option }
|
|
1234
1631
|
);
|
|
1235
1632
|
}
|
|
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);
|
|
1633
|
+
function undeclaredFilterField(field) {
|
|
1634
|
+
return new NarsilError(
|
|
1635
|
+
ErrorCodes.SEARCH_INVALID_FIELD,
|
|
1636
|
+
`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`,
|
|
1637
|
+
{ field, option: "filter" }
|
|
1638
|
+
);
|
|
1262
1639
|
}
|
|
1263
|
-
function
|
|
1640
|
+
function requireRangeOperands(fieldPath, filter, type) {
|
|
1264
1641
|
for (const operator of RANGE_OPERATORS) {
|
|
1265
1642
|
const bound = filter[operator];
|
|
1266
1643
|
if (bound === void 0) continue;
|
|
@@ -1273,22 +1650,23 @@ function requireValidOperands(fieldPath, filter, type) {
|
|
|
1273
1650
|
}
|
|
1274
1651
|
requireFieldKind(fieldPath, operator, type, ["number"]);
|
|
1275
1652
|
}
|
|
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
|
-
}
|
|
1653
|
+
if (filter.between === void 0) return;
|
|
1654
|
+
const bounds = filter.between;
|
|
1655
|
+
const isPair = Array.isArray(bounds) && bounds.length === 2;
|
|
1656
|
+
if (isPair && typeof bounds[0] === "string" && typeof bounds[1] === "string") {
|
|
1657
|
+
requireTextRangeField(fieldPath, "between", type);
|
|
1658
|
+
return;
|
|
1291
1659
|
}
|
|
1660
|
+
if (!isPair || !isFiniteNumber(bounds[0]) || !isFiniteNumber(bounds[1])) {
|
|
1661
|
+
invalid(
|
|
1662
|
+
`"between" on field "${fieldPath}" must be two finite numbers or two strings, the lower bound first`,
|
|
1663
|
+
fieldPath,
|
|
1664
|
+
"between"
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
requireFieldKind(fieldPath, "between", type, ["number"]);
|
|
1668
|
+
}
|
|
1669
|
+
function requireValueOperands(fieldPath, filter, type) {
|
|
1292
1670
|
for (const operator of TEXT_OPERATORS) {
|
|
1293
1671
|
if (filter[operator] === void 0) continue;
|
|
1294
1672
|
if (typeof filter[operator] !== "string") {
|
|
@@ -1314,22 +1692,81 @@ function requireValidOperands(fieldPath, filter, type) {
|
|
|
1314
1692
|
if (!Array.isArray(values) || values.some((value) => typeof value !== "string")) {
|
|
1315
1693
|
invalid(`"${operator}" on field "${fieldPath}" must be a list of strings`, fieldPath, operator);
|
|
1316
1694
|
}
|
|
1695
|
+
requireListLength(fieldPath, operator, values);
|
|
1317
1696
|
requireFieldKind(fieldPath, operator, type, ["text"]);
|
|
1318
1697
|
}
|
|
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");
|
|
1698
|
+
for (const operator of GEO_OPERATORS) {
|
|
1699
|
+
if (filter[operator] !== void 0) requireFieldKind(fieldPath, operator, type, ["geopoint"]);
|
|
1325
1700
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1701
|
+
}
|
|
1702
|
+
function requireValidOperands(fieldPath, filter, type) {
|
|
1703
|
+
requireWellFormedText(fieldPath, filter);
|
|
1704
|
+
requireRangeOperands(fieldPath, filter, type);
|
|
1705
|
+
requireValueOperands(fieldPath, filter, type);
|
|
1706
|
+
requireWholeValueIndex(fieldPath, filter, type);
|
|
1707
|
+
requirePresenceOperands(fieldPath, filter);
|
|
1708
|
+
requireElementOperands(fieldPath, filter, type);
|
|
1709
|
+
requireSizeOperands(fieldPath, filter, type);
|
|
1710
|
+
requirePatternOperands(fieldPath, filter, type);
|
|
1711
|
+
requireGeoOperands(fieldPath, filter);
|
|
1712
|
+
}
|
|
1713
|
+
function filterTestCount(expression) {
|
|
1714
|
+
if (!isObjectRecord(expression)) return 0;
|
|
1715
|
+
const fieldFilters = isObjectRecord(expression.fields) ? Object.values(expression.fields) : [];
|
|
1716
|
+
let tests = fieldFilters.length === 0 && !holdsNestedExpressions(expression) ? 1 : 0;
|
|
1717
|
+
for (const filter of fieldFilters) tests += isObjectRecord(filter) ? testsIn(filter) : 1;
|
|
1718
|
+
for (const list of [expression.and, expression.or]) {
|
|
1719
|
+
if (Array.isArray(list)) for (const nested of list) tests += filterTestCount(nested);
|
|
1720
|
+
}
|
|
1721
|
+
return tests + filterTestCount(expression.not);
|
|
1722
|
+
}
|
|
1723
|
+
function testsIn(filter) {
|
|
1724
|
+
let tests = 0;
|
|
1725
|
+
for (const [operator, operand] of Object.entries(filter)) {
|
|
1726
|
+
if (operator === "caseFold") continue;
|
|
1727
|
+
tests += operator === "size" && isObjectRecord(operand) ? Math.max(1, Object.keys(operand).length) : 1;
|
|
1728
|
+
}
|
|
1729
|
+
return Math.max(1, tests);
|
|
1730
|
+
}
|
|
1731
|
+
function countTests(walk, tests) {
|
|
1732
|
+
walk.tests += tests;
|
|
1733
|
+
if (walk.tests > MAX_FILTER_TESTS) {
|
|
1734
|
+
invalidShape(`A filter holds at most ${MAX_FILTER_TESTS} tests across all of its fields and nested expressions`);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
function holdsNestedExpressions(expression) {
|
|
1738
|
+
const { and, or, not } = expression;
|
|
1739
|
+
return Array.isArray(and) && and.length > 0 || Array.isArray(or) && or.length > 0 || not !== void 0;
|
|
1740
|
+
}
|
|
1741
|
+
function requireValidExpression(expression, walk, depth) {
|
|
1742
|
+
if (depth > MAX_FILTER_DEPTH) invalidShape(`A filter expression nests at most ${MAX_FILTER_DEPTH} levels deep`);
|
|
1743
|
+
if (!isObjectRecord(expression)) invalidShape("Each filter expression must be an object");
|
|
1744
|
+
const { fields, and, or, not } = expression;
|
|
1745
|
+
if (fields !== void 0 && !isObjectRecord(fields)) {
|
|
1746
|
+
invalidShape('"fields" in a filter expression must be an object keyed by field path');
|
|
1747
|
+
}
|
|
1748
|
+
const fieldFilters = Object.entries(fields ?? {});
|
|
1749
|
+
if (fieldFilters.length === 0 && !holdsNestedExpressions(expression)) countTests(walk, 1);
|
|
1750
|
+
for (const [fieldPath, filter] of fieldFilters) {
|
|
1751
|
+
const type = walk.fieldTypes[fieldPath];
|
|
1752
|
+
if (!Object.hasOwn(walk.fieldTypes, fieldPath) || type === void 0) throw undeclaredFilterField(fieldPath);
|
|
1753
|
+
if (!isObjectRecord(filter)) invalidShape(`The filter on field "${fieldPath}" must be an object of tests`);
|
|
1754
|
+
countTests(walk, testsIn(filter));
|
|
1755
|
+
requireValidOperands(fieldPath, filter, type);
|
|
1756
|
+
}
|
|
1757
|
+
for (const [clause, list] of [
|
|
1758
|
+
["and", and],
|
|
1759
|
+
["or", or]
|
|
1760
|
+
]) {
|
|
1761
|
+
if (list === void 0) continue;
|
|
1762
|
+
if (!Array.isArray(list)) invalidShape(`"${clause}" in a filter expression must be a list of filter expressions`);
|
|
1763
|
+
for (const nested of list) requireValidExpression(nested, walk, depth + 1);
|
|
1332
1764
|
}
|
|
1765
|
+
if (not !== void 0) requireValidExpression(not, walk, depth + 1);
|
|
1766
|
+
}
|
|
1767
|
+
function requireValidFilter(expression, fieldTypes) {
|
|
1768
|
+
if (expression === void 0 || expression === null) return;
|
|
1769
|
+
requireValidExpression(expression, { fieldTypes, tests: 0 }, 0);
|
|
1333
1770
|
}
|
|
1334
1771
|
|
|
1335
1772
|
// src/search/sorting.ts
|
|
@@ -1679,7 +2116,11 @@ function prepareFulltextQuery(partition, params, language, schema, options) {
|
|
|
1679
2116
|
}
|
|
1680
2117
|
let filterBitset;
|
|
1681
2118
|
if (params.filters) {
|
|
1682
|
-
filterBitset = partition.applyFiltersBitset(
|
|
2119
|
+
filterBitset = partition.applyFiltersBitset(
|
|
2120
|
+
params.filters,
|
|
2121
|
+
schema,
|
|
2122
|
+
options?.patternWork ?? createPatternWorkMeter()
|
|
2123
|
+
);
|
|
1683
2124
|
if (bitsetIsEmpty(filterBitset)) return null;
|
|
1684
2125
|
}
|
|
1685
2126
|
return { queryTokens, prefixExpansion, filterBitset };
|
|
@@ -1922,7 +2363,8 @@ function resolveEffectiveMode(config) {
|
|
|
1922
2363
|
function buildSearchOptions(base, globalStats) {
|
|
1923
2364
|
return {
|
|
1924
2365
|
...base,
|
|
1925
|
-
globalStats: globalStats ?? base?.globalStats
|
|
2366
|
+
globalStats: globalStats ?? base?.globalStats,
|
|
2367
|
+
patternWork: base?.patternWork ?? createPatternWorkMeter()
|
|
1926
2368
|
};
|
|
1927
2369
|
}
|
|
1928
2370
|
function dispatchWithDispatcher(partitions, params, language, schema, options, dispatcher) {
|
|
@@ -1949,8 +2391,12 @@ function countsWithoutScores(params) {
|
|
|
1949
2391
|
}
|
|
1950
2392
|
function fanOutMatchCount(manager, params, language, schema, options) {
|
|
1951
2393
|
let count = 0;
|
|
2394
|
+
const searchOptions = {
|
|
2395
|
+
...options?.searchOptions,
|
|
2396
|
+
patternWork: options?.searchOptions?.patternWork ?? createPatternWorkMeter()
|
|
2397
|
+
};
|
|
1952
2398
|
for (const partition of partitionsIn(manager, options?.partitionIds)) {
|
|
1953
|
-
const matches = fulltextMatches(partition, params, language, schema,
|
|
2399
|
+
const matches = fulltextMatches(partition, params, language, schema, searchOptions);
|
|
1954
2400
|
if (matches !== null) count += matches.count;
|
|
1955
2401
|
}
|
|
1956
2402
|
return count;
|
|
@@ -2132,4 +2578,4 @@ function setNestedValue(obj, path, value) {
|
|
|
2132
2578
|
current[segments[segments.length - 1]] = value;
|
|
2133
2579
|
}
|
|
2134
2580
|
|
|
2135
|
-
export {
|
|
2581
|
+
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, parseVectorDimension, 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 };
|