@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.
Files changed (117) hide show
  1. package/bin/copyDb.ts +19 -7
  2. package/components/deploymentRecorder.ts +102 -11
  3. package/components/operations.js +4 -0
  4. package/config/configUtils.ts +66 -11
  5. package/config/harperConfigEnvVars.ts +114 -17
  6. package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
  7. package/dist/bin/copyDb.js +20 -7
  8. package/dist/bin/copyDb.js.map +1 -1
  9. package/dist/components/deploymentRecorder.d.ts +6 -0
  10. package/dist/components/deploymentRecorder.js +94 -12
  11. package/dist/components/deploymentRecorder.js.map +1 -1
  12. package/dist/components/operations.js +4 -0
  13. package/dist/components/operations.js.map +1 -1
  14. package/dist/config/configUtils.d.ts +6 -0
  15. package/dist/config/configUtils.js +65 -9
  16. package/dist/config/configUtils.js.map +1 -1
  17. package/dist/config/harperConfigEnvVars.js +108 -16
  18. package/dist/config/harperConfigEnvVars.js.map +1 -1
  19. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
  20. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
  21. package/dist/resources/DatabaseTransaction.d.ts +2 -0
  22. package/dist/resources/DatabaseTransaction.js +6 -5
  23. package/dist/resources/DatabaseTransaction.js.map +1 -1
  24. package/dist/resources/Table.d.ts +2 -1
  25. package/dist/resources/Table.js +66 -16
  26. package/dist/resources/Table.js.map +1 -1
  27. package/dist/resources/auditStore.js +27 -1
  28. package/dist/resources/auditStore.js.map +1 -1
  29. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +15 -2
  30. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +128 -39
  31. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  32. package/dist/resources/search.d.ts +1 -1
  33. package/dist/resources/search.js +7 -3
  34. package/dist/resources/search.js.map +1 -1
  35. package/dist/server/threads/manageThreads.js +8 -0
  36. package/dist/server/threads/manageThreads.js.map +1 -1
  37. package/dist/sqlEngine/parser/normalizer.js +37 -9
  38. package/dist/sqlEngine/parser/normalizer.js.map +1 -1
  39. package/dist/utility/environment/environmentManager.d.ts +11 -0
  40. package/dist/utility/environment/environmentManager.js +154 -0
  41. package/dist/utility/environment/environmentManager.js.map +1 -1
  42. package/dist/utility/lmdb/environmentUtility.js +2 -1
  43. package/dist/utility/lmdb/environmentUtility.js.map +1 -1
  44. package/npm-shrinkwrap.json +13 -7
  45. package/package.json +2 -2
  46. package/resources/DESIGN.md +1 -0
  47. package/resources/DatabaseTransaction.ts +7 -5
  48. package/resources/Table.ts +77 -16
  49. package/resources/auditStore.ts +32 -1
  50. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +119 -45
  51. package/resources/search.ts +8 -3
  52. package/server/threads/manageThreads.js +8 -0
  53. package/studio/web/assets/{Chat-aApwhRmz.js → Chat-BMUjPklt.js} +5 -5
  54. package/studio/web/assets/{Chat-aApwhRmz.js.map → Chat-BMUjPklt.js.map} +1 -1
  55. package/studio/web/assets/{FloatingChat-sC0H91n9.js → FloatingChat-Cbghuxe5.js} +5 -5
  56. package/studio/web/assets/{FloatingChat-sC0H91n9.js.map → FloatingChat-Cbghuxe5.js.map} +1 -1
  57. package/studio/web/assets/{apiToken-DZ8JhHOI.js → apiToken-CkgTQAqc.js} +2 -2
  58. package/studio/web/assets/{apiToken-DZ8JhHOI.js.map → apiToken-CkgTQAqc.js.map} +1 -1
  59. package/studio/web/assets/{applications-9SEVLIO5.js → applications-WvF4NOTN.js} +13 -13
  60. package/studio/web/assets/applications-WvF4NOTN.js.map +1 -0
  61. package/studio/web/assets/chevron-right-anDJCGlp.js +2 -0
  62. package/studio/web/assets/chevron-right-anDJCGlp.js.map +1 -0
  63. package/studio/web/assets/index-CDeqVx71.css +1 -0
  64. package/studio/web/assets/index-Sevt3MVx.js +819 -0
  65. package/studio/web/assets/index-Sevt3MVx.js.map +1 -0
  66. package/studio/web/assets/index.lazy-Dg5YYPGg.js +14 -0
  67. package/studio/web/assets/{index.lazy-CN1zq4I4.js.map → index.lazy-Dg5YYPGg.js.map} +1 -1
  68. package/studio/web/assets/{jsonMode-Cjj7dtlr.js → jsonMode-CBSEleE7.js} +2 -2
  69. package/studio/web/assets/{jsonMode-Cjj7dtlr.js.map → jsonMode-CBSEleE7.js.map} +1 -1
  70. package/studio/web/assets/languageServices-gxedeeTo.js +2 -0
  71. package/studio/web/assets/languageServices-gxedeeTo.js.map +1 -0
  72. package/studio/web/assets/{notifications-BFAF07xr.js → notifications-Ap84YyQp.js} +2 -2
  73. package/studio/web/assets/{notifications-BFAF07xr.js.map → notifications-Ap84YyQp.js.map} +1 -1
  74. package/studio/web/assets/notifications-vehqyOLM.js +2 -0
  75. package/studio/web/assets/{notifications-CKlYVvVN.js.map → notifications-vehqyOLM.js.map} +1 -1
  76. package/studio/web/assets/{pollUnlessForbidden-HHdQZW1N.js → pollUnlessForbidden-BhzefVRn.js} +2 -2
  77. package/studio/web/assets/pollUnlessForbidden-BhzefVRn.js.map +1 -0
  78. package/studio/web/assets/{profile-A1zhEdFG.js → profile-DzDpRLHY.js} +2 -2
  79. package/studio/web/assets/{profile-A1zhEdFG.js.map → profile-DzDpRLHY.js.map} +1 -1
  80. package/studio/web/assets/queryClient-CgtkoiCY.js +2 -0
  81. package/studio/web/assets/queryClient-CgtkoiCY.js.map +1 -0
  82. package/studio/web/assets/regions-DpJ8s7iN.js +2 -0
  83. package/studio/web/assets/regions-DpJ8s7iN.js.map +1 -0
  84. package/studio/web/assets/register-BDy01-q3.js +3 -0
  85. package/studio/web/assets/register-BDy01-q3.js.map +1 -0
  86. package/studio/web/assets/register-CQHRwNP4.js +3 -0
  87. package/studio/web/assets/register-CQHRwNP4.js.map +1 -0
  88. package/studio/web/assets/{setComponentFile-SEtBt_GV.js → setComponentFile-D0aPcI3W.js} +2 -2
  89. package/studio/web/assets/{setComponentFile-SEtBt_GV.js.map → setComponentFile-D0aPcI3W.js.map} +1 -1
  90. package/studio/web/assets/setup-I15FAF9H.js +3 -0
  91. package/studio/web/assets/{setup-BisINqdH.js.map → setup-I15FAF9H.js.map} +1 -1
  92. package/studio/web/assets/status-BF1eJshJ.js +62 -0
  93. package/studio/web/assets/status-BF1eJshJ.js.map +1 -0
  94. package/studio/web/assets/{swagger-ui-react-CAi_s1PC.js → swagger-ui-react-DwSPXXhl.js} +2 -2
  95. package/studio/web/assets/{swagger-ui-react-CAi_s1PC.js.map → swagger-ui-react-DwSPXXhl.js.map} +1 -1
  96. package/studio/web/assets/{textarea-DhLMZ3PA.js → table-DpOjNQHt.js} +2 -10
  97. package/studio/web/assets/table-DpOjNQHt.js.map +1 -0
  98. package/studio/web/assets/{tsMode-DT74tlkM.js → tsMode-Bu51A-VG.js} +2 -2
  99. package/studio/web/assets/{tsMode-DT74tlkM.js.map → tsMode-Bu51A-VG.js.map} +1 -1
  100. package/studio/web/assets/{useEntityRestURL-CGRGc1n7.js → useEntityRestURL-DrV1zMrN.js} +2 -2
  101. package/studio/web/assets/{useEntityRestURL-CGRGc1n7.js.map → useEntityRestURL-DrV1zMrN.js.map} +1 -1
  102. package/studio/web/index.html +6 -7
  103. package/utility/environment/environmentManager.ts +152 -0
  104. package/utility/lmdb/environmentUtility.ts +2 -1
  105. package/studio/web/assets/applications-9SEVLIO5.js.map +0 -1
  106. package/studio/web/assets/index-C8Mv9kHV.css +0 -1
  107. package/studio/web/assets/index-Dy3uDGXb.js +0 -809
  108. package/studio/web/assets/index-Dy3uDGXb.js.map +0 -1
  109. package/studio/web/assets/index.lazy-CN1zq4I4.js +0 -14
  110. package/studio/web/assets/notifications-CKlYVvVN.js +0 -2
  111. package/studio/web/assets/pollUnlessForbidden-HHdQZW1N.js.map +0 -1
  112. package/studio/web/assets/queryClient-CbA8wM7J.js +0 -2
  113. package/studio/web/assets/queryClient-CbA8wM7J.js.map +0 -1
  114. package/studio/web/assets/setup-BisINqdH.js +0 -3
  115. package/studio/web/assets/status-BuQoCc7l.js +0 -62
  116. package/studio/web/assets/status-BuQoCc7l.js.map +0 -1
  117. 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
- const AUTO_EF_REF = 1000;
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
- // For each level from top to bottom
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
- if (ef !== undefined && ef > 0) effectiveEf = ef;
982
- else if (!this.efSearchConfigured) {
983
- const nodeCount = this.indexStore.getKeysCount
984
- ? this.indexStore.getKeysCount()
985
- : (this.indexStore.getStats?.()?.entryCount ?? 0);
986
- effectiveEf = autoScaleEf(nodeCount);
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: effectiveEf * (filterExpansion && filterExpansion > 0 ? filterExpansion : this.filterExpansion),
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 (recordVector == null) return Infinity; // missing vector sorts last
1044
- // distance fns require a plain Array (they guard on Array.isArray); records normally store a
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
- : this.distance;
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
- const count =
1227
- this.indexStore instanceof RocksDatabase ? this.indexStore.getKeysCount() : this.indexStore.getStats().entryCount;
1228
- return Math.sqrt(count * this.efConstructionSearch);
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 vectorDistances = sortDefinition.vectorDistances;
1243
- if (vectorDistances) {
1244
- const difference = vectorDistances.get(entry);
1245
- if (difference) return difference;
1246
- } else vectorDistances = context.vectorDistances = sortDefinition.vectorDistances = new Map();
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
- let distanceFunction = this.distance;
1249
- if (sortDefinition.type)
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;
@@ -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;