eigen-db 4.3.0 → 5.0.0

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.
@@ -94,6 +94,16 @@ describe("VectorDB", () => {
94
94
  expect(db.size).toBe(0);
95
95
  });
96
96
 
97
+ it("exposes dimensions property", async () => {
98
+ const db = await VectorDB.open({
99
+ dimensions: 4,
100
+ storage,
101
+ wasmBinary,
102
+ });
103
+
104
+ expect(db.dimensions).toBe(4);
105
+ });
106
+
97
107
  // --- set and get ---
98
108
  it("stores and retrieves a vector by key", async () => {
99
109
  const db = await VectorDB.open({
@@ -238,7 +248,7 @@ describe("VectorDB", () => {
238
248
  expect(results[2].similarity).toBeCloseTo(0.0, 2);
239
249
  });
240
250
 
241
- it("query respects topK option", async () => {
251
+ it("query respects limit option", async () => {
242
252
  const db = await VectorDB.open({
243
253
  dimensions: 4,
244
254
  storage,
@@ -249,7 +259,7 @@ describe("VectorDB", () => {
249
259
  db.set("b", [0, 1, 0, 0]);
250
260
  db.set("c", [0, 0, 1, 0]);
251
261
 
252
- const results = db.query([1, 0, 0, 0], { topK: 2 });
262
+ const results = db.query([1, 0, 0, 0], { limit: 2 });
253
263
  expect(results.length).toBe(2);
254
264
  });
255
265
 
@@ -361,7 +371,7 @@ describe("VectorDB", () => {
361
371
  expect(results[1].key).toBe("xy-axis");
362
372
  });
363
373
 
364
- it("query topK defaults to Infinity (returns all results)", async () => {
374
+ it("query limit defaults to Infinity (returns all results)", async () => {
365
375
  const db = await VectorDB.open({
366
376
  dimensions: 4,
367
377
  storage,
@@ -374,11 +384,195 @@ describe("VectorDB", () => {
374
384
  db.set(`v${i}`, vec);
375
385
  }
376
386
 
377
- // Without topK, all 10 results should be returned
387
+ // Without limit, all 10 results should be returned
378
388
  const results = db.query([1, 0, 0, 0]);
379
389
  expect(results.length).toBe(10);
380
390
  });
381
391
 
392
+ it("query with order ascend returns least similar first", async () => {
393
+ const db = await VectorDB.open({
394
+ dimensions: 4,
395
+ storage,
396
+ wasmBinary,
397
+ });
398
+
399
+ db.set("x-axis", [1, 0, 0, 0]);
400
+ db.set("y-axis", [0, 1, 0, 0]);
401
+ db.set("xy-axis", [1, 1, 0, 0]);
402
+
403
+ const results = db.query([1, 0, 0, 0], { order: "ascend" });
404
+ expect(results.length).toBe(3);
405
+ // y-axis (similarity ≈ 0) should be first in ascending order
406
+ expect(results[0].key).toBe("y-axis");
407
+ expect(results[0].similarity).toBeCloseTo(0.0, 2);
408
+ // x-axis (similarity ≈ 1) should be last
409
+ expect(results[2].key).toBe("x-axis");
410
+ expect(results[2].similarity).toBeCloseTo(1.0, 2);
411
+ });
412
+
413
+ it("query with order ascend and limit returns bottomK", async () => {
414
+ const db = await VectorDB.open({
415
+ dimensions: 4,
416
+ storage,
417
+ wasmBinary,
418
+ });
419
+
420
+ db.set("x-axis", [1, 0, 0, 0]);
421
+ db.set("y-axis", [0, 1, 0, 0]);
422
+ db.set("xy-axis", [1, 1, 0, 0]);
423
+
424
+ const results = db.query([1, 0, 0, 0], { order: "ascend", limit: 1 });
425
+ expect(results.length).toBe(1);
426
+ expect(results[0].key).toBe("y-axis");
427
+ });
428
+
429
+ it("query respects maxSimilarity option", async () => {
430
+ const db = await VectorDB.open({
431
+ dimensions: 4,
432
+ storage,
433
+ wasmBinary,
434
+ });
435
+
436
+ db.set("x-axis", [1, 0, 0, 0]);
437
+ db.set("y-axis", [0, 1, 0, 0]);
438
+ db.set("xy-axis", [1, 1, 0, 0]);
439
+
440
+ // Only return results with similarity ≤ 0.8 from the x-axis query
441
+ const results = db.query([1, 0, 0, 0], { maxSimilarity: 0.8 });
442
+ // x-axis: similarity ≈ 1 (excluded), xy-axis: similarity ≈ 0.71, y-axis: similarity ≈ 0
443
+ expect(results.length).toBe(2);
444
+ expect(results[0].key).toBe("xy-axis");
445
+ expect(results[1].key).toBe("y-axis");
446
+ });
447
+
448
+ it("query with both minSimilarity and maxSimilarity", async () => {
449
+ const db = await VectorDB.open({
450
+ dimensions: 4,
451
+ storage,
452
+ wasmBinary,
453
+ });
454
+
455
+ db.set("x-axis", [1, 0, 0, 0]);
456
+ db.set("y-axis", [0, 1, 0, 0]);
457
+ db.set("xy-axis", [1, 1, 0, 0]);
458
+
459
+ // Only return results with 0.5 ≤ similarity ≤ 0.8
460
+ const results = db.query([1, 0, 0, 0], { minSimilarity: 0.5, maxSimilarity: 0.8 });
461
+ // xy-axis: similarity ≈ 0.71 (included)
462
+ // x-axis ≈ 1.0 (excluded), y-axis ≈ 0.0 (excluded)
463
+ expect(results.length).toBe(1);
464
+ expect(results[0].key).toBe("xy-axis");
465
+ });
466
+
467
+ it("query maxSimilarity works with iterable mode", async () => {
468
+ const db = await VectorDB.open({
469
+ dimensions: 4,
470
+ storage,
471
+ wasmBinary,
472
+ });
473
+
474
+ db.set("x-axis", [1, 0, 0, 0]);
475
+ db.set("y-axis", [0, 1, 0, 0]);
476
+ db.set("xy-axis", [1, 1, 0, 0]);
477
+
478
+ const results = [...db.query([1, 0, 0, 0], { maxSimilarity: 0.8, iterable: true })];
479
+ expect(results.length).toBe(2);
480
+ expect(results[0].key).toBe("xy-axis");
481
+ expect(results[1].key).toBe("y-axis");
482
+ });
483
+
484
+ it("query order ascend with iterable mode", async () => {
485
+ const db = await VectorDB.open({
486
+ dimensions: 4,
487
+ storage,
488
+ wasmBinary,
489
+ });
490
+
491
+ db.set("x-axis", [1, 0, 0, 0]);
492
+ db.set("y-axis", [0, 1, 0, 0]);
493
+ db.set("xy-axis", [1, 1, 0, 0]);
494
+
495
+ const results = [...db.query([1, 0, 0, 0], { order: "ascend", iterable: true })];
496
+ expect(results.length).toBe(3);
497
+ expect(results[0].key).toBe("y-axis");
498
+ expect(results[2].key).toBe("x-axis");
499
+ });
500
+
501
+ it("query supports full similarity range [-1, 1] with opposite vectors", async () => {
502
+ const db = await VectorDB.open({
503
+ dimensions: 4,
504
+ storage,
505
+ wasmBinary,
506
+ });
507
+
508
+ db.set("same", [1, 0, 0, 0]); // similarity ≈ 1
509
+ db.set("ortho", [0, 1, 0, 0]); // similarity ≈ 0
510
+ db.set("opposite", [-1, 0, 0, 0]); // similarity ≈ -1
511
+
512
+ const results = db.query([1, 0, 0, 0]);
513
+ expect(results.length).toBe(3);
514
+ expect(results[0].key).toBe("same");
515
+ expect(results[0].similarity).toBeCloseTo(1.0, 2);
516
+ expect(results[1].key).toBe("ortho");
517
+ expect(results[1].similarity).toBeCloseTo(0.0, 2);
518
+ expect(results[2].key).toBe("opposite");
519
+ expect(results[2].similarity).toBeCloseTo(-1.0, 2);
520
+ });
521
+
522
+ it("query minSimilarity works with negative values", async () => {
523
+ const db = await VectorDB.open({
524
+ dimensions: 4,
525
+ storage,
526
+ wasmBinary,
527
+ });
528
+
529
+ db.set("same", [1, 0, 0, 0]); // similarity ≈ 1
530
+ db.set("ortho", [0, 1, 0, 0]); // similarity ≈ 0
531
+ db.set("opposite", [-1, 0, 0, 0]); // similarity ≈ -1
532
+
533
+ // minSimilarity = -0.5 should include same and ortho, exclude opposite
534
+ const results = db.query([1, 0, 0, 0], { minSimilarity: -0.5 });
535
+ expect(results.length).toBe(2);
536
+ expect(results[0].key).toBe("same");
537
+ expect(results[1].key).toBe("ortho");
538
+ });
539
+
540
+ it("query maxSimilarity works with negative values", async () => {
541
+ const db = await VectorDB.open({
542
+ dimensions: 4,
543
+ storage,
544
+ wasmBinary,
545
+ });
546
+
547
+ db.set("same", [1, 0, 0, 0]); // similarity ≈ 1
548
+ db.set("ortho", [0, 1, 0, 0]); // similarity ≈ 0
549
+ db.set("opposite", [-1, 0, 0, 0]); // similarity ≈ -1
550
+
551
+ // maxSimilarity = -0.5 should include only opposite
552
+ const results = db.query([1, 0, 0, 0], { maxSimilarity: -0.5 });
553
+ expect(results.length).toBe(1);
554
+ expect(results[0].key).toBe("opposite");
555
+ });
556
+
557
+ it("query ascending order with negative similarities", async () => {
558
+ const db = await VectorDB.open({
559
+ dimensions: 4,
560
+ storage,
561
+ wasmBinary,
562
+ });
563
+
564
+ db.set("same", [1, 0, 0, 0]);
565
+ db.set("ortho", [0, 1, 0, 0]);
566
+ db.set("opposite", [-1, 0, 0, 0]);
567
+
568
+ const results = db.query([1, 0, 0, 0], { order: "ascend" });
569
+ expect(results.length).toBe(3);
570
+ expect(results[0].key).toBe("opposite");
571
+ expect(results[0].similarity).toBeCloseTo(-1.0, 2);
572
+ expect(results[2].key).toBe("same");
573
+ expect(results[2].similarity).toBeCloseTo(1.0, 2);
574
+ });
575
+
382
576
  // --- flush and persistence ---
383
577
  it("flush persists data and reopen loads it", async () => {
384
578
  const db1 = await VectorDB.open({
@@ -838,6 +1032,284 @@ describe("VectorDB", () => {
838
1032
  expect(db2.get("gamma")![2]).toBeCloseTo(1);
839
1033
  });
840
1034
 
1035
+ // --- has ---
1036
+ it("has returns true for existing key", async () => {
1037
+ const db = await VectorDB.open({
1038
+ dimensions: 4,
1039
+ normalize: false,
1040
+ storage,
1041
+ wasmBinary,
1042
+ });
1043
+
1044
+ db.set("a", [1, 0, 0, 0]);
1045
+ expect(db.has("a")).toBe(true);
1046
+ });
1047
+
1048
+ it("has returns false for non-existent key", async () => {
1049
+ const db = await VectorDB.open({
1050
+ dimensions: 4,
1051
+ storage,
1052
+ wasmBinary,
1053
+ });
1054
+
1055
+ expect(db.has("nonexistent")).toBe(false);
1056
+ });
1057
+
1058
+ it("has throws on closed database", async () => {
1059
+ const db = await VectorDB.open({
1060
+ dimensions: 4,
1061
+ storage,
1062
+ wasmBinary,
1063
+ });
1064
+
1065
+ await db.close();
1066
+ expect(() => db.has("a")).toThrow("closed");
1067
+ });
1068
+
1069
+ // --- delete ---
1070
+ it("delete removes an existing entry and returns true", async () => {
1071
+ const db = await VectorDB.open({
1072
+ dimensions: 4,
1073
+ normalize: false,
1074
+ storage,
1075
+ wasmBinary,
1076
+ });
1077
+
1078
+ db.set("a", [1, 0, 0, 0]);
1079
+ db.set("b", [0, 1, 0, 0]);
1080
+ expect(db.size).toBe(2);
1081
+
1082
+ const result = db.delete("a");
1083
+ expect(result).toBe(true);
1084
+ expect(db.size).toBe(1);
1085
+ expect(db.get("a")).toBeUndefined();
1086
+ expect(db.has("a")).toBe(false);
1087
+ expect(db.get("b")).toBeDefined();
1088
+ });
1089
+
1090
+ it("delete returns false for non-existent key", async () => {
1091
+ const db = await VectorDB.open({
1092
+ dimensions: 4,
1093
+ storage,
1094
+ wasmBinary,
1095
+ });
1096
+
1097
+ expect(db.delete("nonexistent")).toBe(false);
1098
+ });
1099
+
1100
+ it("delete last entry leaves empty database", async () => {
1101
+ const db = await VectorDB.open({
1102
+ dimensions: 4,
1103
+ normalize: false,
1104
+ storage,
1105
+ wasmBinary,
1106
+ });
1107
+
1108
+ db.set("only", [1, 2, 3, 4]);
1109
+ db.delete("only");
1110
+ expect(db.size).toBe(0);
1111
+ expect(db.get("only")).toBeUndefined();
1112
+ });
1113
+
1114
+ it("delete preserves remaining entries and query works", async () => {
1115
+ const db = await VectorDB.open({
1116
+ dimensions: 4,
1117
+ storage,
1118
+ wasmBinary,
1119
+ });
1120
+
1121
+ db.set("x-axis", [1, 0, 0, 0]);
1122
+ db.set("y-axis", [0, 1, 0, 0]);
1123
+ db.set("z-axis", [0, 0, 1, 0]);
1124
+
1125
+ db.delete("y-axis");
1126
+ expect(db.size).toBe(2);
1127
+
1128
+ const results = db.query([1, 0, 0, 0]);
1129
+ expect(results.length).toBe(2);
1130
+ expect(results[0].key).toBe("x-axis");
1131
+ expect(results.find((r) => r.key === "y-axis")).toBeUndefined();
1132
+ });
1133
+
1134
+ it("delete then set reuses the database correctly", async () => {
1135
+ const db = await VectorDB.open({
1136
+ dimensions: 4,
1137
+ normalize: false,
1138
+ storage,
1139
+ wasmBinary,
1140
+ });
1141
+
1142
+ db.set("a", [1, 0, 0, 0]);
1143
+ db.set("b", [0, 1, 0, 0]);
1144
+ db.delete("a");
1145
+
1146
+ db.set("c", [0, 0, 1, 0]);
1147
+ expect(db.size).toBe(2);
1148
+ expect(db.get("a")).toBeUndefined();
1149
+ expect(db.get("b")![1]).toBeCloseTo(1);
1150
+ expect(db.get("c")![2]).toBeCloseTo(1);
1151
+ });
1152
+
1153
+ it("delete persists correctly after flush", async () => {
1154
+ const db1 = await VectorDB.open({
1155
+ dimensions: 4,
1156
+ normalize: false,
1157
+ storage,
1158
+ wasmBinary,
1159
+ });
1160
+
1161
+ db1.set("a", [1, 0, 0, 0]);
1162
+ db1.set("b", [0, 1, 0, 0]);
1163
+ db1.delete("a");
1164
+ await db1.flush();
1165
+
1166
+ const db2 = await VectorDB.open({
1167
+ dimensions: 4,
1168
+ normalize: false,
1169
+ storage,
1170
+ wasmBinary,
1171
+ });
1172
+
1173
+ expect(db2.size).toBe(1);
1174
+ expect(db2.get("a")).toBeUndefined();
1175
+ expect(db2.get("b")![1]).toBeCloseTo(1);
1176
+ });
1177
+
1178
+ it("delete throws on closed database", async () => {
1179
+ const db = await VectorDB.open({
1180
+ dimensions: 4,
1181
+ storage,
1182
+ wasmBinary,
1183
+ });
1184
+
1185
+ await db.close();
1186
+ expect(() => db.delete("a")).toThrow("closed");
1187
+ });
1188
+
1189
+ // --- keys ---
1190
+ it("keys returns an iterable of all keys", async () => {
1191
+ const db = await VectorDB.open({
1192
+ dimensions: 4,
1193
+ normalize: false,
1194
+ storage,
1195
+ wasmBinary,
1196
+ });
1197
+
1198
+ db.set("a", [1, 0, 0, 0]);
1199
+ db.set("b", [0, 1, 0, 0]);
1200
+ db.set("c", [0, 0, 1, 0]);
1201
+
1202
+ const keys = [...db.keys()];
1203
+ expect(keys).toHaveLength(3);
1204
+ expect(keys).toContain("a");
1205
+ expect(keys).toContain("b");
1206
+ expect(keys).toContain("c");
1207
+ });
1208
+
1209
+ it("keys returns empty iterable for empty database", async () => {
1210
+ const db = await VectorDB.open({
1211
+ dimensions: 4,
1212
+ storage,
1213
+ wasmBinary,
1214
+ });
1215
+
1216
+ expect([...db.keys()]).toEqual([]);
1217
+ });
1218
+
1219
+ it("keys throws on closed database", async () => {
1220
+ const db = await VectorDB.open({
1221
+ dimensions: 4,
1222
+ storage,
1223
+ wasmBinary,
1224
+ });
1225
+
1226
+ await db.close();
1227
+ expect(() => db.keys()).toThrow("closed");
1228
+ });
1229
+
1230
+ // --- entries ---
1231
+ it("entries returns an iterable of [key, value] pairs", async () => {
1232
+ const db = await VectorDB.open({
1233
+ dimensions: 4,
1234
+ normalize: false,
1235
+ storage,
1236
+ wasmBinary,
1237
+ });
1238
+
1239
+ db.set("a", [1, 0, 0, 0]);
1240
+ db.set("b", [0, 1, 0, 0]);
1241
+
1242
+ const entries = [...db.entries()];
1243
+ expect(entries).toHaveLength(2);
1244
+
1245
+ const aEntry = entries.find(([key]) => key === "a");
1246
+ expect(aEntry).toBeDefined();
1247
+ expect(aEntry![1][0]).toBeCloseTo(1);
1248
+
1249
+ const bEntry = entries.find(([key]) => key === "b");
1250
+ expect(bEntry).toBeDefined();
1251
+ expect(bEntry![1][1]).toBeCloseTo(1);
1252
+ });
1253
+
1254
+ it("entries returns empty iterable for empty database", async () => {
1255
+ const db = await VectorDB.open({
1256
+ dimensions: 4,
1257
+ storage,
1258
+ wasmBinary,
1259
+ });
1260
+
1261
+ expect([...db.entries()]).toEqual([]);
1262
+ });
1263
+
1264
+ it("entries throws on closed database", async () => {
1265
+ const db = await VectorDB.open({
1266
+ dimensions: 4,
1267
+ storage,
1268
+ wasmBinary,
1269
+ });
1270
+
1271
+ await db.close();
1272
+ expect(() => db.entries()).toThrow("closed");
1273
+ });
1274
+
1275
+ // --- Symbol.iterator ---
1276
+ it("supports spread operator via Symbol.iterator", async () => {
1277
+ const db = await VectorDB.open({
1278
+ dimensions: 4,
1279
+ normalize: false,
1280
+ storage,
1281
+ wasmBinary,
1282
+ });
1283
+
1284
+ db.set("a", [1, 0, 0, 0]);
1285
+ db.set("b", [0, 1, 0, 0]);
1286
+
1287
+ const spread = [...db];
1288
+ expect(spread).toHaveLength(2);
1289
+
1290
+ // Same as entries()
1291
+ const entries = [...db.entries()];
1292
+ expect(spread).toEqual(entries);
1293
+ });
1294
+
1295
+ it("supports for-of iteration", async () => {
1296
+ const db = await VectorDB.open({
1297
+ dimensions: 4,
1298
+ normalize: false,
1299
+ storage,
1300
+ wasmBinary,
1301
+ });
1302
+
1303
+ db.set("a", [1, 0, 0, 0]);
1304
+ db.set("b", [0, 1, 0, 0]);
1305
+
1306
+ const collected: [string, number[]][] = [];
1307
+ for (const entry of db) {
1308
+ collected.push(entry);
1309
+ }
1310
+ expect(collected).toHaveLength(2);
1311
+ });
1312
+
841
1313
  it("import works correctly with single-byte stream chunks", async () => {
842
1314
  const db1 = await VectorDB.open({
843
1315
  dimensions: 4,
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Utility functions for sorting scores and producing query results.
5
5
  * Two modes:
6
- * 1. topKResults — eagerly materializes a ResultItem[] (default query path)
6
+ * 1. queryResults — eagerly materializes a ResultItem[] (default query path)
7
7
  * 2. iterableResults — returns a lazy Iterable<ResultItem> where keys are
8
8
  * resolved only as each item is consumed (for pagination / streaming)
9
9
  *
@@ -19,41 +19,65 @@ export interface ResultItem {
19
19
 
20
20
  export type KeyResolver = (index: number) => string;
21
21
 
22
+ export interface ResultOptions {
23
+ limit: number;
24
+ order: "ascend" | "descend";
25
+ minSimilarity?: number;
26
+ maxSimilarity?: number;
27
+ }
28
+
29
+ /** Check whether a score falls within the [minSimilarity, maxSimilarity] range. */
30
+ function inRange(similarity: number, minSimilarity?: number, maxSimilarity?: number): boolean {
31
+ if (minSimilarity !== undefined && similarity < minSimilarity) return false;
32
+ if (maxSimilarity !== undefined && similarity > maxSimilarity) return false;
33
+ return true;
34
+ }
35
+
22
36
  /**
23
- * Sort by descending similarity and return the top K results as a plain array.
37
+ * Sort scores and return the top results as a plain array.
24
38
  * All keys are resolved eagerly.
25
- * If minSimilarity is provided, results with similarity < minSimilarity are excluded.
39
+ *
40
+ * `order` controls sort direction:
41
+ * - "descend" (default) — highest similarity first
42
+ * - "ascend" — lowest similarity first
43
+ *
44
+ * Results outside [minSimilarity, maxSimilarity] are excluded.
26
45
  */
27
- export function topKResults(
46
+ export function queryResults(
28
47
  scores: Float32Array,
29
48
  resolveKey: KeyResolver,
30
- topK: number,
31
- minSimilarity?: number,
49
+ options: ResultOptions,
32
50
  ): ResultItem[] {
51
+ const { limit, order, minSimilarity, maxSimilarity } = options;
33
52
  const n = scores.length;
34
53
  if (n === 0) return [];
35
54
 
36
55
  const indices = new Uint32Array(n);
37
56
  for (let i = 0; i < n; i++) indices[i] = i;
38
- indices.sort((a, b) => scores[b] - scores[a]);
39
57
 
40
- const k = Math.min(topK, n);
58
+ if (order === "ascend") {
59
+ indices.sort((a, b) => scores[a] - scores[b]);
60
+ } else {
61
+ indices.sort((a, b) => scores[b] - scores[a]);
62
+ }
63
+
64
+ const k = Math.min(limit, n);
41
65
  const results: ResultItem[] = [];
42
- for (let i = 0; i < k; i++) {
66
+ for (let i = 0; i < n && results.length < k; i++) {
43
67
  const idx = indices[i];
44
68
  const similarity = scores[idx];
45
- if (minSimilarity !== undefined && similarity < minSimilarity) break;
69
+ if (!inRange(similarity, minSimilarity, maxSimilarity)) continue;
46
70
  results.push({ key: resolveKey(idx), similarity });
47
71
  }
48
72
  return results;
49
73
  }
50
74
 
51
75
  /**
52
- * Sort by descending similarity and return a lazy iterable over the top K results.
76
+ * Sort scores and return a lazy iterable over the results.
53
77
  * Keys are resolved only when each item is consumed, saving allocations
54
78
  * when the caller iterates partially (e.g., pagination).
55
79
  *
56
- * If minSimilarity is provided, iteration stops when similarity < minSimilarity.
80
+ * Results outside [minSimilarity, maxSimilarity] are skipped.
57
81
  *
58
82
  * The returned iterable is re-iterable — each call to [Symbol.iterator]()
59
83
  * produces a fresh cursor over the same pre-sorted data.
@@ -61,31 +85,38 @@ export function topKResults(
61
85
  export function iterableResults(
62
86
  scores: Float32Array,
63
87
  resolveKey: KeyResolver,
64
- topK: number,
65
- minSimilarity?: number,
88
+ options: ResultOptions,
66
89
  ): Iterable<ResultItem> {
90
+ const { limit, order, minSimilarity, maxSimilarity } = options;
67
91
  const n = scores.length;
68
92
  if (n === 0) return [];
69
93
 
70
94
  const indices = new Uint32Array(n);
71
95
  for (let i = 0; i < n; i++) indices[i] = i;
72
- indices.sort((a, b) => scores[b] - scores[a]);
73
96
 
74
- const k = Math.min(topK, n);
97
+ if (order === "ascend") {
98
+ indices.sort((a, b) => scores[a] - scores[b]);
99
+ } else {
100
+ indices.sort((a, b) => scores[b] - scores[a]);
101
+ }
75
102
 
76
103
  return {
77
104
  [Symbol.iterator](): Iterator<ResultItem> {
78
105
  let i = 0;
106
+ let emitted = 0;
79
107
  return {
80
108
  next(): IteratorResult<ResultItem> {
81
- if (i >= k) return { done: true, value: undefined };
82
- const idx = indices[i++];
83
- const similarity = scores[idx];
84
- if (minSimilarity !== undefined && similarity < minSimilarity) return { done: true, value: undefined };
85
- return {
86
- done: false,
87
- value: { key: resolveKey(idx), similarity },
88
- };
109
+ while (i < n && emitted < limit) {
110
+ const idx = indices[i++];
111
+ const similarity = scores[idx];
112
+ if (!inRange(similarity, minSimilarity, maxSimilarity)) continue;
113
+ emitted++;
114
+ return {
115
+ done: false,
116
+ value: { key: resolveKey(idx), similarity },
117
+ };
118
+ }
119
+ return { done: true, value: undefined };
89
120
  },
90
121
  };
91
122
  },
package/src/lib/types.ts CHANGED
@@ -38,9 +38,13 @@ export interface SetOptions {
38
38
  */
39
39
  export interface QueryOptions {
40
40
  /** Maximum number of results to return. Defaults to Infinity (all results). */
41
- topK?: number;
41
+ limit?: number;
42
+ /** Result ordering. "ascend" sorts by ascending similarity, "descend" sorts by descending similarity. Defaults to "descend". */
43
+ order?: "ascend" | "descend";
42
44
  /** Minimum similarity threshold (inclusive). Results with similarity < minSimilarity are excluded. */
43
45
  minSimilarity?: number;
46
+ /** Maximum similarity threshold (inclusive). Results with similarity > maxSimilarity are excluded. */
47
+ maxSimilarity?: number;
44
48
  /** Override normalization for this call. */
45
49
  normalize?: boolean;
46
50
  /** When true, returns an Iterable<ResultItem> instead of ResultItem[]. */