@harperfast/harper 5.2.1 → 5.2.2
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/bin/copyDb.ts +19 -7
- package/components/deploymentRecorder.ts +102 -11
- package/components/operations.js +4 -0
- package/config/configUtils.ts +66 -11
- package/config/harperConfigEnvVars.ts +114 -17
- package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
- package/dist/bin/copyDb.js +20 -7
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +6 -0
- package/dist/components/deploymentRecorder.js +94 -12
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/operations.js +4 -0
- package/dist/components/operations.js.map +1 -1
- package/dist/config/configUtils.d.ts +6 -0
- package/dist/config/configUtils.js +65 -9
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/harperConfigEnvVars.js +108 -16
- package/dist/config/harperConfigEnvVars.js.map +1 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
- package/dist/resources/DatabaseTransaction.d.ts +2 -0
- package/dist/resources/DatabaseTransaction.js +6 -5
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/Table.d.ts +2 -1
- package/dist/resources/Table.js +66 -16
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/auditStore.js +27 -1
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +15 -2
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +128 -39
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/search.d.ts +1 -1
- package/dist/resources/search.js +7 -3
- package/dist/resources/search.js.map +1 -1
- package/dist/server/threads/manageThreads.js +8 -0
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/sqlEngine/parser/normalizer.js +37 -9
- package/dist/sqlEngine/parser/normalizer.js.map +1 -1
- package/dist/utility/environment/environmentManager.d.ts +11 -0
- package/dist/utility/environment/environmentManager.js +154 -0
- package/dist/utility/environment/environmentManager.js.map +1 -1
- package/dist/utility/lmdb/environmentUtility.js +2 -1
- package/dist/utility/lmdb/environmentUtility.js.map +1 -1
- package/npm-shrinkwrap.json +13 -7
- package/package.json +2 -2
- package/resources/DESIGN.md +1 -0
- package/resources/DatabaseTransaction.ts +7 -5
- package/resources/Table.ts +77 -16
- package/resources/auditStore.ts +32 -1
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +119 -45
- package/resources/search.ts +8 -3
- package/server/threads/manageThreads.js +8 -0
- package/studio/web/assets/{Chat-aApwhRmz.js → Chat-BMUjPklt.js} +5 -5
- package/studio/web/assets/{Chat-aApwhRmz.js.map → Chat-BMUjPklt.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-sC0H91n9.js → FloatingChat-Cbghuxe5.js} +5 -5
- package/studio/web/assets/{FloatingChat-sC0H91n9.js.map → FloatingChat-Cbghuxe5.js.map} +1 -1
- package/studio/web/assets/{apiToken-DZ8JhHOI.js → apiToken-CkgTQAqc.js} +2 -2
- package/studio/web/assets/{apiToken-DZ8JhHOI.js.map → apiToken-CkgTQAqc.js.map} +1 -1
- package/studio/web/assets/{applications-9SEVLIO5.js → applications-WvF4NOTN.js} +13 -13
- package/studio/web/assets/applications-WvF4NOTN.js.map +1 -0
- package/studio/web/assets/chevron-right-anDJCGlp.js +2 -0
- package/studio/web/assets/chevron-right-anDJCGlp.js.map +1 -0
- package/studio/web/assets/index-CDeqVx71.css +1 -0
- package/studio/web/assets/index-Sevt3MVx.js +819 -0
- package/studio/web/assets/index-Sevt3MVx.js.map +1 -0
- package/studio/web/assets/index.lazy-Dg5YYPGg.js +14 -0
- package/studio/web/assets/{index.lazy-CN1zq4I4.js.map → index.lazy-Dg5YYPGg.js.map} +1 -1
- package/studio/web/assets/{jsonMode-Cjj7dtlr.js → jsonMode-CBSEleE7.js} +2 -2
- package/studio/web/assets/{jsonMode-Cjj7dtlr.js.map → jsonMode-CBSEleE7.js.map} +1 -1
- package/studio/web/assets/languageServices-gxedeeTo.js +2 -0
- package/studio/web/assets/languageServices-gxedeeTo.js.map +1 -0
- package/studio/web/assets/{notifications-BFAF07xr.js → notifications-Ap84YyQp.js} +2 -2
- package/studio/web/assets/{notifications-BFAF07xr.js.map → notifications-Ap84YyQp.js.map} +1 -1
- package/studio/web/assets/notifications-vehqyOLM.js +2 -0
- package/studio/web/assets/{notifications-CKlYVvVN.js.map → notifications-vehqyOLM.js.map} +1 -1
- package/studio/web/assets/{pollUnlessForbidden-HHdQZW1N.js → pollUnlessForbidden-BhzefVRn.js} +2 -2
- package/studio/web/assets/pollUnlessForbidden-BhzefVRn.js.map +1 -0
- package/studio/web/assets/{profile-A1zhEdFG.js → profile-DzDpRLHY.js} +2 -2
- package/studio/web/assets/{profile-A1zhEdFG.js.map → profile-DzDpRLHY.js.map} +1 -1
- package/studio/web/assets/queryClient-CgtkoiCY.js +2 -0
- package/studio/web/assets/queryClient-CgtkoiCY.js.map +1 -0
- package/studio/web/assets/regions-DpJ8s7iN.js +2 -0
- package/studio/web/assets/regions-DpJ8s7iN.js.map +1 -0
- package/studio/web/assets/register-BDy01-q3.js +3 -0
- package/studio/web/assets/register-BDy01-q3.js.map +1 -0
- package/studio/web/assets/register-CQHRwNP4.js +3 -0
- package/studio/web/assets/register-CQHRwNP4.js.map +1 -0
- package/studio/web/assets/{setComponentFile-SEtBt_GV.js → setComponentFile-D0aPcI3W.js} +2 -2
- package/studio/web/assets/{setComponentFile-SEtBt_GV.js.map → setComponentFile-D0aPcI3W.js.map} +1 -1
- package/studio/web/assets/setup-I15FAF9H.js +3 -0
- package/studio/web/assets/{setup-BisINqdH.js.map → setup-I15FAF9H.js.map} +1 -1
- package/studio/web/assets/status-BF1eJshJ.js +62 -0
- package/studio/web/assets/status-BF1eJshJ.js.map +1 -0
- package/studio/web/assets/{swagger-ui-react-CAi_s1PC.js → swagger-ui-react-DwSPXXhl.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-CAi_s1PC.js.map → swagger-ui-react-DwSPXXhl.js.map} +1 -1
- package/studio/web/assets/{textarea-DhLMZ3PA.js → table-DpOjNQHt.js} +2 -10
- package/studio/web/assets/table-DpOjNQHt.js.map +1 -0
- package/studio/web/assets/{tsMode-DT74tlkM.js → tsMode-Bu51A-VG.js} +2 -2
- package/studio/web/assets/{tsMode-DT74tlkM.js.map → tsMode-Bu51A-VG.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-CGRGc1n7.js → useEntityRestURL-DrV1zMrN.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-CGRGc1n7.js.map → useEntityRestURL-DrV1zMrN.js.map} +1 -1
- package/studio/web/index.html +6 -7
- package/utility/environment/environmentManager.ts +152 -0
- package/utility/lmdb/environmentUtility.ts +2 -1
- package/studio/web/assets/applications-9SEVLIO5.js.map +0 -1
- package/studio/web/assets/index-C8Mv9kHV.css +0 -1
- package/studio/web/assets/index-Dy3uDGXb.js +0 -809
- package/studio/web/assets/index-Dy3uDGXb.js.map +0 -1
- package/studio/web/assets/index.lazy-CN1zq4I4.js +0 -14
- package/studio/web/assets/notifications-CKlYVvVN.js +0 -2
- package/studio/web/assets/pollUnlessForbidden-HHdQZW1N.js.map +0 -1
- package/studio/web/assets/queryClient-CbA8wM7J.js +0 -2
- package/studio/web/assets/queryClient-CbA8wM7J.js.map +0 -1
- package/studio/web/assets/setup-BisINqdH.js +0 -3
- package/studio/web/assets/status-BuQoCc7l.js +0 -62
- package/studio/web/assets/status-BuQoCc7l.js.map +0 -1
- package/studio/web/assets/textarea-DhLMZ3PA.js.map +0 -1
|
@@ -3,7 +3,6 @@ import { FLOAT32_OPTIONS } from 'msgpackr';
|
|
|
3
3
|
import { loggerWithTag } from '../../utility/logging/logger.ts';
|
|
4
4
|
import { ClientError } from '../../utility/errors/hdbError.ts';
|
|
5
5
|
import type { Id } from '../../resources/ResourceInterface.ts';
|
|
6
|
-
import { RocksDatabase } from '@harperfast/rocksdb-js';
|
|
7
6
|
import { SKIP } from '@harperfast/extended-iterable';
|
|
8
7
|
|
|
9
8
|
const logger = loggerWithTag('HNSW');
|
|
@@ -50,13 +49,35 @@ function dequantizeInt8(q: Int8Array, scale: number): number[] {
|
|
|
50
49
|
// cap deliberately favors latency — apps wanting higher recall set efConstructionSearch or a per-query
|
|
51
50
|
// ef. Tune as graph build quality / larger-N data improves.
|
|
52
51
|
const AUTO_EF_BASE = 100;
|
|
53
|
-
|
|
52
|
+
// The index store holds a graph node plus a primary-key mapping per record, so a key count is twice
|
|
53
|
+
// the node count. Sizes here are in nodes; this converts back for the one consumer still calibrated
|
|
54
|
+
// against keys.
|
|
55
|
+
const INDEX_KEYS_PER_NODE = 2;
|
|
56
|
+
// Graph nodes at which ef equals AUTO_EF_BASE. Originally expressed per index-store key against a
|
|
57
|
+
// reference of 1000, so 500 nodes is the same point. The node count is a high-water mark rather than
|
|
58
|
+
// a live count, so the resolved ef can differ from that formula by one at a rounding boundary.
|
|
59
|
+
const AUTO_EF_REF = 500;
|
|
54
60
|
const AUTO_EF_MAX = 512;
|
|
55
61
|
function autoScaleEf(nodeCount: number): number {
|
|
56
62
|
const scaled = Math.round(AUTO_EF_BASE * Math.sqrt(Math.max(1, nodeCount / AUTO_EF_REF)));
|
|
57
63
|
return Math.min(AUTO_EF_MAX, Math.max(AUTO_EF_BASE, scaled));
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
// Candidate-list size used when searching a layer above 0. Those layers only supply the entry point
|
|
67
|
+
// for the next layer down, so a greedy walk is enough; a larger value costs work proportional to the
|
|
68
|
+
// layer's population without improving the entry point it hands off.
|
|
69
|
+
const ROUTING_EF = 1;
|
|
70
|
+
// Ceiling on the ef a query's own `offset + limit` can ask for. `limit` is unprivileged and set on
|
|
71
|
+
// every request, so this bounds what a caller can make one thread do synchronously: layer 0 holds
|
|
72
|
+
// `ef` candidates in a sorted array with an O(len) insert. Kept within a small multiple of
|
|
73
|
+
// AUTO_EF_MAX so the worst case stays the same order as the index's own auto-scaled ceiling; a
|
|
74
|
+
// caller who genuinely wants more sets an explicit `ef` and owns the cost.
|
|
75
|
+
const LIMIT_EF_MAX = 4 * AUTO_EF_MAX;
|
|
76
|
+
// How long a resolved graph size is reused before it is looked up again (see approximateNodeCount).
|
|
77
|
+
// ef moves with the square root of the count and is capped, so a slightly stale size is immaterial;
|
|
78
|
+
// this only has to be short enough that a table growing from empty picks up a larger ef promptly.
|
|
79
|
+
const NODE_COUNT_TTL = 10_000;
|
|
80
|
+
|
|
60
81
|
class MinHeap {
|
|
61
82
|
private data: Candidate[] = [];
|
|
62
83
|
get size() {
|
|
@@ -181,6 +202,8 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
181
202
|
// frozen node the object store hands back. WeakMap so entries are collected when the store evicts
|
|
182
203
|
// the frozen node — without it, every cache hit on a frozen node would re-slice and re-clone.
|
|
183
204
|
private convertedNodes = new WeakMap<object, any>();
|
|
205
|
+
private nodeCount = 0;
|
|
206
|
+
private nodeCountAt = 0;
|
|
184
207
|
constructor(indexStore: any, options: any) {
|
|
185
208
|
this.indexStore = indexStore;
|
|
186
209
|
if (indexStore) {
|
|
@@ -334,17 +357,12 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
334
357
|
this.indexStore.put(ENTRY_POINT, nodeId, options);
|
|
335
358
|
}
|
|
336
359
|
|
|
337
|
-
//
|
|
360
|
+
// Pure descent — only neighbors[0] is used — so it runs greedily for the same reason
|
|
361
|
+
// search() does. The connection-building pass below keeps efConstruction: it selects the
|
|
362
|
+
// edges that get stored.
|
|
338
363
|
while (currentLevel > level) {
|
|
339
364
|
// Search for closest neighbors at current level
|
|
340
|
-
const neighbors = this.searchLayer(
|
|
341
|
-
vector,
|
|
342
|
-
entryPointId,
|
|
343
|
-
entryPoint,
|
|
344
|
-
this.efConstruction,
|
|
345
|
-
currentLevel,
|
|
346
|
-
options
|
|
347
|
-
);
|
|
365
|
+
const neighbors = this.searchLayer(vector, entryPointId, entryPoint, ROUTING_EF, currentLevel, options);
|
|
348
366
|
|
|
349
367
|
if (neighbors.length > 0) {
|
|
350
368
|
entryPointId = neighbors[0].id; // closest neighbor becomes new entry point
|
|
@@ -692,6 +710,34 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
692
710
|
}
|
|
693
711
|
}
|
|
694
712
|
|
|
713
|
+
/**
|
|
714
|
+
* Number of nodes in the graph, for the ef auto-scale. Must stay O(1): it runs per query, and an
|
|
715
|
+
* exact count is a full key scan. RocksDB's `estimate-num-keys` is not a usable substitute — it
|
|
716
|
+
* counts unreconciled overwrites, which graph construction produces in bulk. See DESIGN.md for the
|
|
717
|
+
* measurements behind both. The memo is the only gate on how often the size is resolved; nothing on
|
|
718
|
+
* the query path may bypass it, or the O(1) lookup becomes per-query work again.
|
|
719
|
+
*/
|
|
720
|
+
private approximateNodeCount(): number {
|
|
721
|
+
const now = Date.now();
|
|
722
|
+
if (this.nodeCountAt > 0 && now - this.nodeCountAt < NODE_COUNT_TTL) return this.nodeCount;
|
|
723
|
+
this.nodeCount = this.resolveNodeCount();
|
|
724
|
+
this.nodeCountAt = now;
|
|
725
|
+
return this.nodeCount;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** O(1) node count — the shared id counter, else a single reverse seek to the largest node id. */
|
|
729
|
+
private resolveNodeCount(): number {
|
|
730
|
+
if (this.idIncrementer) return Number(Atomics.load(this.idIncrementer, 0));
|
|
731
|
+
try {
|
|
732
|
+
for (const key of this.indexStore.getKeys({ reverse: true, limit: 1, start: Infinity, end: 0 })) {
|
|
733
|
+
if (typeof key === 'number') return key + 1;
|
|
734
|
+
}
|
|
735
|
+
} catch (error) {
|
|
736
|
+
logger.debug?.('could not resolve node count from the largest node id', error);
|
|
737
|
+
}
|
|
738
|
+
return 0; // empty (or unreadable) graph — autoScaleEf falls back to its floor
|
|
739
|
+
}
|
|
740
|
+
|
|
695
741
|
private safeGetSync(key: any, options?: any): any {
|
|
696
742
|
try {
|
|
697
743
|
let node = this.indexStore.getSync(key, options);
|
|
@@ -945,7 +991,12 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
945
991
|
// returns true are admitted to the result list at layer 0; routing is unaffected. Composed by
|
|
946
992
|
// search.ts from companion AND conditions and caller-supplied vector/row filters. Must be
|
|
947
993
|
// synchronous and side-effect free. JS-API only (never from a REST query string).
|
|
948
|
-
filter?: (primaryKey: Id) => boolean
|
|
994
|
+
filter?: (primaryKey: Id) => boolean,
|
|
995
|
+
// offset + limit for a bounded query. A layer-0 search returns at most `ef` candidates, so a
|
|
996
|
+
// query asking for more rows than that used to come back short with no error — capped at 512
|
|
997
|
+
// (AUTO_EF_MAX) however large the limit was. Raising ef to cover the request keeps `limit`
|
|
998
|
+
// meaningful; the caller pays for what it asked for.
|
|
999
|
+
minResults?: number
|
|
949
1000
|
) {
|
|
950
1001
|
let limit: number | undefined; // only set for threshold comparators; 0 is a valid threshold (e.g. dotProduct)
|
|
951
1002
|
let limitInclusive = false; // true for `le`, false for `lt`
|
|
@@ -978,18 +1029,33 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
978
1029
|
// Resolve search ef: per-query ef wins; else an explicitly-configured efConstructionSearch;
|
|
979
1030
|
// else auto-scale with the graph size so recall holds as the table grows.
|
|
980
1031
|
let effectiveEf = this.efConstructionSearch;
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1032
|
+
const explicitEf = ef !== undefined && ef > 0;
|
|
1033
|
+
if (explicitEf) effectiveEf = ef;
|
|
1034
|
+
else if (!this.efSearchConfigured) effectiveEf = autoScaleEf(this.approximateNodeCount());
|
|
1035
|
+
// The ef the index chose for itself, before any limit-derived widening. The filter budget below
|
|
1036
|
+
// stays calibrated against this rather than against what a caller's `limit` asked for.
|
|
1037
|
+
const resolvedEf = effectiveEf;
|
|
1038
|
+
// A bounded query must be able to come back full: layer 0 keeps at most `ef` candidates, so a
|
|
1039
|
+
// limit above the resolved ef truncated the result set with no error. Widen the candidate list
|
|
1040
|
+
// to cover the request, up to LIMIT_EF_MAX — `ef` sizes a synchronous traversal that holds every
|
|
1041
|
+
// admitted candidate in a sorted array with an O(len) insert, so an unbounded one lets a plain
|
|
1042
|
+
// `limit` stall the thread. Past the ceiling the result set is still short, as it was before.
|
|
1043
|
+
// A per-query `ef` is left authoritative: it is an explicit cost ceiling, and a caller who sets
|
|
1044
|
+
// one has said what they are willing to spend.
|
|
1045
|
+
// The ceiling is the only bound: clamping to the graph size as well would need a count exact as
|
|
1046
|
+
// of this query — the memo reads low while a table grows, truncating the very limit this
|
|
1047
|
+
// honours — and an ef above the node count is free, the traversal ending at the graph, not ef.
|
|
1048
|
+
if (minResults !== undefined && !explicitEf && minResults > effectiveEf) {
|
|
1049
|
+
effectiveEf = Math.max(effectiveEf, Math.min(minResults, LIMIT_EF_MAX));
|
|
987
1050
|
}
|
|
988
1051
|
// Predicate-aware traversal budget (#1241): matches accrue slower than visits under a selective
|
|
989
1052
|
// filter, so bound layer-0 work at ef * filterExpansion nodes. Only built when a filter is active.
|
|
1053
|
+
// Deliberately `resolvedEf`, not the limit-widened ef: this budget is what stops a selective
|
|
1054
|
+
// filter crawling the whole graph, and multiplying it by a caller's limit would turn a filtered
|
|
1055
|
+
// vector query into a record-loading scan.
|
|
990
1056
|
const filterState: FilterState | undefined = filter
|
|
991
1057
|
? {
|
|
992
|
-
maxVisits:
|
|
1058
|
+
maxVisits: resolvedEf * (filterExpansion && filterExpansion > 0 ? filterExpansion : this.filterExpansion),
|
|
993
1059
|
nodesVisited: 0,
|
|
994
1060
|
filterEvaluations: 0,
|
|
995
1061
|
}
|
|
@@ -1000,13 +1066,17 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
1000
1066
|
let results: Candidate[] = [];
|
|
1001
1067
|
// For each level from top to bottom. The filter applies only at layer 0 (result admission);
|
|
1002
1068
|
// upper layers route unfiltered so non-matching hubs still guide the descent.
|
|
1069
|
+
//
|
|
1070
|
+
// Only layer 0 gets the full candidate list; the layers above it just hand down an entry point,
|
|
1071
|
+
// so searching them at the full ef costs work proportional to the layer's population (~N/M
|
|
1072
|
+
// nodes) rather than to ef. See DESIGN.md.
|
|
1003
1073
|
for (let l = entryPoint.level; l >= 0; l--) {
|
|
1004
1074
|
// Search for closest neighbors at current level
|
|
1005
1075
|
results = this.searchLayer(
|
|
1006
1076
|
target,
|
|
1007
1077
|
entryPointId,
|
|
1008
1078
|
entryPoint,
|
|
1009
|
-
effectiveEf,
|
|
1079
|
+
l === 0 ? effectiveEf : ROUTING_EF,
|
|
1010
1080
|
l,
|
|
1011
1081
|
options,
|
|
1012
1082
|
distanceFunction,
|
|
@@ -1040,10 +1110,8 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
1040
1110
|
* and $distance.
|
|
1041
1111
|
*/
|
|
1042
1112
|
exactDistance(searchCondition: { target: number[]; distance?: string }, recordVector: number[] | Int8Array): number {
|
|
1043
|
-
if (
|
|
1044
|
-
|
|
1045
|
-
// float[] vector, but convert defensively in case a typed array slips through.
|
|
1046
|
-
const vec = Array.isArray(recordVector) ? recordVector : Array.from(recordVector);
|
|
1113
|
+
if (!searchCondition.target) throw new ClientError('A target vector must be provided for an HNSW query');
|
|
1114
|
+
if (!Array.isArray(searchCondition.target)) throw new ClientError('The target vector must be an array');
|
|
1047
1115
|
const fn =
|
|
1048
1116
|
searchCondition.distance === 'euclidean'
|
|
1049
1117
|
? euclideanDistance
|
|
@@ -1051,7 +1119,14 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
1051
1119
|
? dotProductDistance
|
|
1052
1120
|
: searchCondition.distance === 'cosine'
|
|
1053
1121
|
? cosineDistance
|
|
1054
|
-
:
|
|
1122
|
+
: searchCondition.distance
|
|
1123
|
+
? null
|
|
1124
|
+
: this.distance;
|
|
1125
|
+
if (!fn) throw new ClientError('Unknown distance function');
|
|
1126
|
+
if (recordVector == null) return Infinity; // missing vector sorts last
|
|
1127
|
+
// distance fns require a plain Array (they guard on Array.isArray); records normally store a
|
|
1128
|
+
// float[] vector, but convert defensively in case a typed array slips through.
|
|
1129
|
+
const vec = Array.isArray(recordVector) ? recordVector : Array.from(recordVector);
|
|
1055
1130
|
return fn(searchCondition.target, vec);
|
|
1056
1131
|
}
|
|
1057
1132
|
/**
|
|
@@ -1223,9 +1298,11 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
1223
1298
|
* @returns
|
|
1224
1299
|
*/
|
|
1225
1300
|
estimateCountAsSort() {
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1301
|
+
// Same O(1) source search() uses — this runs per query whenever a vector sort is planned
|
|
1302
|
+
// alongside another condition, where an exact getKeysCount() is a second full key scan on the
|
|
1303
|
+
// query path. Scaled back to the index-store key count it used to be given, so the planner's
|
|
1304
|
+
// condition ordering is unchanged by the switch to a node count.
|
|
1305
|
+
return Math.sqrt(this.approximateNodeCount() * INDEX_KEYS_PER_NODE * this.efConstructionSearch);
|
|
1229
1306
|
}
|
|
1230
1307
|
|
|
1231
1308
|
/**
|
|
@@ -1234,27 +1311,24 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
1234
1311
|
* @param vector
|
|
1235
1312
|
* @param context
|
|
1236
1313
|
* @param entry
|
|
1314
|
+
* @param sortDefinition
|
|
1237
1315
|
*/
|
|
1238
|
-
propertyResolver(vector: number[], context: any, entry: any) {
|
|
1239
|
-
const sortDefinition = context?.sort;
|
|
1316
|
+
propertyResolver(vector: number[], context: any, entry: any, sortDefinition?: any) {
|
|
1240
1317
|
if (sortDefinition) {
|
|
1318
|
+
if (!context) return this.exactDistance(sortDefinition, vector);
|
|
1241
1319
|
// set up a cache for these so they can be accessed by $distance and not be recalculated during a sort
|
|
1242
|
-
let
|
|
1243
|
-
if (
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1320
|
+
let vectorDistanceCaches = context.vectorDistanceCaches;
|
|
1321
|
+
if (!vectorDistanceCaches) vectorDistanceCaches = context.vectorDistanceCaches = new WeakMap();
|
|
1322
|
+
let vectorDistances = vectorDistanceCaches.get(sortDefinition);
|
|
1323
|
+
const cacheKey =
|
|
1324
|
+
entry && typeof entry === 'object' ? entry : vector && typeof vector === 'object' ? vector : null;
|
|
1325
|
+
if (vectorDistances && cacheKey) {
|
|
1326
|
+
const difference = vectorDistances.get(cacheKey);
|
|
1327
|
+
if (difference !== undefined) return difference;
|
|
1328
|
+
} else if (!vectorDistances) vectorDistanceCaches.set(sortDefinition, (vectorDistances = new WeakMap()));
|
|
1247
1329
|
|
|
1248
|
-
|
|
1249
|
-
if (
|
|
1250
|
-
distanceFunction =
|
|
1251
|
-
sortDefinition.distance === 'euclidean'
|
|
1252
|
-
? euclideanDistance
|
|
1253
|
-
: sortDefinition.distance === 'dotProduct'
|
|
1254
|
-
? dotProductDistance
|
|
1255
|
-
: cosineDistance;
|
|
1256
|
-
const distance = distanceFunction(sortDefinition.target, vector);
|
|
1257
|
-
vectorDistances.set(entry, distance);
|
|
1330
|
+
const distance = this.exactDistance(sortDefinition, vector);
|
|
1331
|
+
if (cacheKey) vectorDistances.set(cacheKey, distance);
|
|
1258
1332
|
return distance;
|
|
1259
1333
|
}
|
|
1260
1334
|
return vector;
|
package/resources/search.ts
CHANGED
|
@@ -147,7 +147,8 @@ export function executeConditions(
|
|
|
147
147
|
table,
|
|
148
148
|
request.allowFullScan,
|
|
149
149
|
filtered,
|
|
150
|
-
context
|
|
150
|
+
context,
|
|
151
|
+
request.limit !== undefined ? (request.offset || 0) + request.limit : undefined
|
|
151
152
|
);
|
|
152
153
|
}
|
|
153
154
|
function mapConditionsToFilters(conditions, intersection, estimatedIncomingCount) {
|
|
@@ -247,7 +248,11 @@ export function searchByIndex(
|
|
|
247
248
|
Table: any,
|
|
248
249
|
allowFullScan?: boolean,
|
|
249
250
|
filtered?: any,
|
|
250
|
-
context?: any
|
|
251
|
+
context?: any,
|
|
252
|
+
// How many rows the query will ultimately consume (offset + limit), when it is bounded. An
|
|
253
|
+
// approximate index returns a fixed-size candidate list, so without this a query asking for more
|
|
254
|
+
// rows than that list holds silently gets a short result set. Only custom indexes read it.
|
|
255
|
+
minResults?: number
|
|
251
256
|
): AsyncIterable<Id | { key: Id; value: any }> {
|
|
252
257
|
let attribute_name = searchCondition[0] ?? searchCondition.attribute;
|
|
253
258
|
let value = searchCondition[1] ?? searchCondition.value;
|
|
@@ -505,7 +510,7 @@ export function searchByIndex(
|
|
|
505
510
|
// exploring until it has enough MATCHING results, rather than post-filtering an under-filled
|
|
506
511
|
// candidate set. Only indexes that opt in (filteredSearch) receive it; others post-filter as before.
|
|
507
512
|
const recordFilter = index.customIndex.filteredSearch ? searchCondition.recordFilter : undefined;
|
|
508
|
-
const loaded = index.customIndex.search(searchCondition, context, recordFilter).map((entry) => {
|
|
513
|
+
const loaded = index.customIndex.search(searchCondition, context, recordFilter, minResults).map((entry) => {
|
|
509
514
|
// if the custom index returns an entry with metadata, merge it with the loaded entry
|
|
510
515
|
if (typeof entry === 'object' && entry) {
|
|
511
516
|
const { key, ...otherProps } = entry;
|
|
@@ -232,6 +232,14 @@ function registerWorkerDataProvider(name, provider) {
|
|
|
232
232
|
if (workerDataProviders.get(name) === provider) workerDataProviders.delete(name);
|
|
233
233
|
};
|
|
234
234
|
}
|
|
235
|
+
// Propagate this thread's in-process config overrides (env.setProperty — installer bootstrap or
|
|
236
|
+
// the unit-test harness's per-run isolation) to every worker spawned from here, so a worker's
|
|
237
|
+
// effective config is never silently whatever happens to be installed on disk. Worker-side replay
|
|
238
|
+
// is environmentManager.ts's applyInheritedConfigOverrides(), invoked from initSync(). Registered
|
|
239
|
+
// on every thread (not just main) so a nested worker-of-a-worker also inherits the full chain.
|
|
240
|
+
// setProperty() clones each value as it records it, so this provider cannot hit the log-and-skip
|
|
241
|
+
// path below — which for this one would mean spawning the worker on the on-disk config.
|
|
242
|
+
registerWorkerDataProvider('configOverrides', () => envMgr.getConfigOverrides());
|
|
235
243
|
function collectProvidedWorkerData(options) {
|
|
236
244
|
if (workerDataProviders.size === 0) return undefined;
|
|
237
245
|
let provided;
|