@kernl-sdk/pg 0.1.11 → 0.1.12

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 (153) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-check-types.log +36 -0
  3. package/CHANGELOG.md +32 -0
  4. package/README.md +124 -0
  5. package/dist/__tests__/integration.test.js +2 -2
  6. package/dist/__tests__/memory-integration.test.d.ts +2 -0
  7. package/dist/__tests__/memory-integration.test.d.ts.map +1 -0
  8. package/dist/__tests__/memory-integration.test.js +287 -0
  9. package/dist/__tests__/memory.test.d.ts +2 -0
  10. package/dist/__tests__/memory.test.d.ts.map +1 -0
  11. package/dist/__tests__/memory.test.js +357 -0
  12. package/dist/index.d.ts +5 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +5 -3
  15. package/dist/memory/sql.d.ts +30 -0
  16. package/dist/memory/sql.d.ts.map +1 -0
  17. package/dist/memory/sql.js +100 -0
  18. package/dist/memory/store.d.ts +41 -0
  19. package/dist/memory/store.d.ts.map +1 -0
  20. package/dist/memory/store.js +114 -0
  21. package/dist/migrations.d.ts +1 -1
  22. package/dist/migrations.d.ts.map +1 -1
  23. package/dist/migrations.js +9 -3
  24. package/dist/pgvector/__tests__/handle.test.d.ts +2 -0
  25. package/dist/pgvector/__tests__/handle.test.d.ts.map +1 -0
  26. package/dist/pgvector/__tests__/handle.test.js +277 -0
  27. package/dist/pgvector/__tests__/hit.test.d.ts +2 -0
  28. package/dist/pgvector/__tests__/hit.test.d.ts.map +1 -0
  29. package/dist/pgvector/__tests__/hit.test.js +134 -0
  30. package/dist/pgvector/__tests__/integration/document.integration.test.d.ts +7 -0
  31. package/dist/pgvector/__tests__/integration/document.integration.test.d.ts.map +1 -0
  32. package/dist/pgvector/__tests__/integration/document.integration.test.js +587 -0
  33. package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts +8 -0
  34. package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts.map +1 -0
  35. package/dist/pgvector/__tests__/integration/edge.integration.test.js +663 -0
  36. package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts +8 -0
  37. package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts.map +1 -0
  38. package/dist/pgvector/__tests__/integration/filters.integration.test.js +609 -0
  39. package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts +8 -0
  40. package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts.map +1 -0
  41. package/dist/pgvector/__tests__/integration/lifecycle.integration.test.js +449 -0
  42. package/dist/pgvector/__tests__/integration/query.integration.test.d.ts +8 -0
  43. package/dist/pgvector/__tests__/integration/query.integration.test.d.ts.map +1 -0
  44. package/dist/pgvector/__tests__/integration/query.integration.test.js +544 -0
  45. package/dist/pgvector/__tests__/search.test.d.ts +2 -0
  46. package/dist/pgvector/__tests__/search.test.d.ts.map +1 -0
  47. package/dist/pgvector/__tests__/search.test.js +279 -0
  48. package/dist/pgvector/handle.d.ts +60 -0
  49. package/dist/pgvector/handle.d.ts.map +1 -0
  50. package/dist/pgvector/handle.js +213 -0
  51. package/dist/pgvector/hit.d.ts +10 -0
  52. package/dist/pgvector/hit.d.ts.map +1 -0
  53. package/dist/pgvector/hit.js +44 -0
  54. package/dist/pgvector/index.d.ts +7 -0
  55. package/dist/pgvector/index.d.ts.map +1 -0
  56. package/dist/pgvector/index.js +5 -0
  57. package/dist/pgvector/search.d.ts +60 -0
  58. package/dist/pgvector/search.d.ts.map +1 -0
  59. package/dist/pgvector/search.js +227 -0
  60. package/dist/pgvector/sql/__tests__/limit.test.d.ts +2 -0
  61. package/dist/pgvector/sql/__tests__/limit.test.d.ts.map +1 -0
  62. package/dist/pgvector/sql/__tests__/limit.test.js +161 -0
  63. package/dist/pgvector/sql/__tests__/order.test.d.ts +2 -0
  64. package/dist/pgvector/sql/__tests__/order.test.d.ts.map +1 -0
  65. package/dist/pgvector/sql/__tests__/order.test.js +218 -0
  66. package/dist/pgvector/sql/__tests__/query.test.d.ts +2 -0
  67. package/dist/pgvector/sql/__tests__/query.test.d.ts.map +1 -0
  68. package/dist/pgvector/sql/__tests__/query.test.js +392 -0
  69. package/dist/pgvector/sql/__tests__/select.test.d.ts +2 -0
  70. package/dist/pgvector/sql/__tests__/select.test.d.ts.map +1 -0
  71. package/dist/pgvector/sql/__tests__/select.test.js +293 -0
  72. package/dist/pgvector/sql/__tests__/where.test.d.ts +2 -0
  73. package/dist/pgvector/sql/__tests__/where.test.d.ts.map +1 -0
  74. package/dist/pgvector/sql/__tests__/where.test.js +488 -0
  75. package/dist/pgvector/sql/index.d.ts +7 -0
  76. package/dist/pgvector/sql/index.d.ts.map +1 -0
  77. package/dist/pgvector/sql/index.js +6 -0
  78. package/dist/pgvector/sql/limit.d.ts +8 -0
  79. package/dist/pgvector/sql/limit.d.ts.map +1 -0
  80. package/dist/pgvector/sql/limit.js +20 -0
  81. package/dist/pgvector/sql/order.d.ts +9 -0
  82. package/dist/pgvector/sql/order.d.ts.map +1 -0
  83. package/dist/pgvector/sql/order.js +47 -0
  84. package/dist/pgvector/sql/query.d.ts +46 -0
  85. package/dist/pgvector/sql/query.d.ts.map +1 -0
  86. package/dist/pgvector/sql/query.js +54 -0
  87. package/dist/pgvector/sql/schema.d.ts +16 -0
  88. package/dist/pgvector/sql/schema.d.ts.map +1 -0
  89. package/dist/pgvector/sql/schema.js +47 -0
  90. package/dist/pgvector/sql/select.d.ts +11 -0
  91. package/dist/pgvector/sql/select.d.ts.map +1 -0
  92. package/dist/pgvector/sql/select.js +87 -0
  93. package/dist/pgvector/sql/where.d.ts +8 -0
  94. package/dist/pgvector/sql/where.d.ts.map +1 -0
  95. package/dist/pgvector/sql/where.js +137 -0
  96. package/dist/pgvector/types.d.ts +20 -0
  97. package/dist/pgvector/types.d.ts.map +1 -0
  98. package/dist/pgvector/types.js +1 -0
  99. package/dist/pgvector/utils.d.ts +18 -0
  100. package/dist/pgvector/utils.d.ts.map +1 -0
  101. package/dist/pgvector/utils.js +22 -0
  102. package/dist/postgres.d.ts +19 -26
  103. package/dist/postgres.d.ts.map +1 -1
  104. package/dist/postgres.js +15 -27
  105. package/dist/storage.d.ts +48 -0
  106. package/dist/storage.d.ts.map +1 -1
  107. package/dist/storage.js +32 -9
  108. package/dist/thread/sql.d.ts +38 -0
  109. package/dist/thread/sql.d.ts.map +1 -0
  110. package/dist/thread/sql.js +112 -0
  111. package/dist/thread/store.d.ts +2 -2
  112. package/dist/thread/store.d.ts.map +1 -1
  113. package/dist/thread/store.js +32 -102
  114. package/package.json +7 -4
  115. package/src/__tests__/integration.test.ts +15 -17
  116. package/src/__tests__/memory-integration.test.ts +355 -0
  117. package/src/__tests__/memory.test.ts +428 -0
  118. package/src/index.ts +19 -3
  119. package/src/memory/sql.ts +141 -0
  120. package/src/memory/store.ts +166 -0
  121. package/src/migrations.ts +13 -3
  122. package/src/pgvector/README.md +50 -0
  123. package/src/pgvector/__tests__/handle.test.ts +335 -0
  124. package/src/pgvector/__tests__/hit.test.ts +165 -0
  125. package/src/pgvector/__tests__/integration/document.integration.test.ts +717 -0
  126. package/src/pgvector/__tests__/integration/edge.integration.test.ts +835 -0
  127. package/src/pgvector/__tests__/integration/filters.integration.test.ts +721 -0
  128. package/src/pgvector/__tests__/integration/lifecycle.integration.test.ts +570 -0
  129. package/src/pgvector/__tests__/integration/query.integration.test.ts +667 -0
  130. package/src/pgvector/__tests__/search.test.ts +366 -0
  131. package/src/pgvector/handle.ts +285 -0
  132. package/src/pgvector/hit.ts +56 -0
  133. package/src/pgvector/index.ts +7 -0
  134. package/src/pgvector/search.ts +330 -0
  135. package/src/pgvector/sql/__tests__/limit.test.ts +180 -0
  136. package/src/pgvector/sql/__tests__/order.test.ts +248 -0
  137. package/src/pgvector/sql/__tests__/query.test.ts +548 -0
  138. package/src/pgvector/sql/__tests__/select.test.ts +367 -0
  139. package/src/pgvector/sql/__tests__/where.test.ts +554 -0
  140. package/src/pgvector/sql/index.ts +14 -0
  141. package/src/pgvector/sql/limit.ts +29 -0
  142. package/src/pgvector/sql/order.ts +55 -0
  143. package/src/pgvector/sql/query.ts +112 -0
  144. package/src/pgvector/sql/schema.ts +61 -0
  145. package/src/pgvector/sql/select.ts +100 -0
  146. package/src/pgvector/sql/where.ts +152 -0
  147. package/src/pgvector/types.ts +21 -0
  148. package/src/pgvector/utils.ts +24 -0
  149. package/src/postgres.ts +31 -33
  150. package/src/storage.ts +77 -9
  151. package/src/thread/sql.ts +159 -0
  152. package/src/thread/store.ts +40 -127
  153. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,667 @@
1
+ /**
2
+ * Query behavior integration tests for pgvector.
3
+ *
4
+ * Tests vector search, topK behavior, offset pagination, orderBy,
5
+ * and result structure against real PostgreSQL.
6
+ */
7
+
8
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
9
+ import { Pool } from "pg";
10
+
11
+ import { PGSearchIndex } from "../../search";
12
+ import type { IndexHandle } from "@kernl-sdk/retrieval";
13
+
14
+ const TEST_DB_URL = process.env.KERNL_PG_TEST_URL;
15
+ const SCHEMA = "kernl_query_integration_test";
16
+
17
+ /**
18
+ * Test document type.
19
+ */
20
+ interface TestDoc {
21
+ id: string;
22
+ title: string;
23
+ category: string;
24
+ priority: number;
25
+ score: number;
26
+ embedding: number[];
27
+ }
28
+
29
+ /**
30
+ * Deterministic test dataset for query testing.
31
+ *
32
+ * Documents with orthogonal vectors for predictable similarity results.
33
+ */
34
+ const TEST_DOCS: TestDoc[] = [
35
+ {
36
+ id: "vec-1",
37
+ title: "Machine Learning Basics",
38
+ category: "ml",
39
+ priority: 1,
40
+ score: 95.5,
41
+ embedding: [1.0, 0.0, 0.0, 0.0], // Basis vector 1
42
+ },
43
+ {
44
+ id: "vec-2",
45
+ title: "Advanced Neural Networks",
46
+ category: "ml",
47
+ priority: 2,
48
+ score: 88.0,
49
+ embedding: [0.0, 1.0, 0.0, 0.0], // Basis vector 2
50
+ },
51
+ {
52
+ id: "vec-3",
53
+ title: "Database Fundamentals",
54
+ category: "db",
55
+ priority: 3,
56
+ score: 92.0,
57
+ embedding: [0.0, 0.0, 1.0, 0.0], // Basis vector 3
58
+ },
59
+ {
60
+ id: "vec-4",
61
+ title: "Vector Databases",
62
+ category: "db",
63
+ priority: 4,
64
+ score: 75.5,
65
+ embedding: [0.0, 0.0, 0.0, 1.0], // Basis vector 4
66
+ },
67
+ {
68
+ id: "vec-5",
69
+ title: "Search Engine Optimization",
70
+ category: "search",
71
+ priority: 5,
72
+ score: 82.0,
73
+ embedding: [0.5, 0.5, 0.0, 0.0], // Mix of 1 and 2
74
+ },
75
+ {
76
+ id: "vec-6",
77
+ title: "Hybrid Search Systems",
78
+ category: "search",
79
+ priority: 6,
80
+ score: 79.0,
81
+ embedding: [0.0, 0.5, 0.5, 0.0], // Mix of 2 and 3
82
+ },
83
+ ];
84
+
85
+ describe.sequential("pgvector query integration tests", () => {
86
+ if (!TEST_DB_URL) {
87
+ it.skip("requires KERNL_PG_TEST_URL environment variable", () => {});
88
+ return;
89
+ }
90
+
91
+ let pool: Pool;
92
+ let pgvec: PGSearchIndex;
93
+ let handle: IndexHandle<TestDoc>;
94
+ const testIndexId = "query_test_docs";
95
+
96
+ beforeAll(async () => {
97
+ pool = new Pool({ connectionString: TEST_DB_URL });
98
+
99
+ await pool.query(`CREATE EXTENSION IF NOT EXISTS vector`);
100
+ await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
101
+ await pool.query(`CREATE SCHEMA "${SCHEMA}"`);
102
+
103
+ pgvec = new PGSearchIndex({ pool });
104
+
105
+ await pgvec.createIndex({
106
+ id: testIndexId,
107
+ schema: {
108
+ id: { type: "string", pk: true },
109
+ title: { type: "string" },
110
+ category: { type: "string" },
111
+ priority: { type: "int" },
112
+ score: { type: "float" },
113
+ embedding: { type: "vector", dimensions: 4, similarity: "cosine" },
114
+ },
115
+ providerOptions: { schema: SCHEMA },
116
+ });
117
+
118
+ handle = pgvec.index<TestDoc>(testIndexId);
119
+
120
+ // Insert test documents
121
+ await handle.upsert(TEST_DOCS);
122
+ }, 30000);
123
+
124
+ afterAll(async () => {
125
+ await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
126
+ await pool.end();
127
+ });
128
+
129
+ // ============================================================
130
+ // VECTOR SEARCH
131
+ // ============================================================
132
+
133
+ describe("vector search", () => {
134
+ it("returns exact match as top result", async () => {
135
+ // Query with basis vector 1 - should match vec-1 best
136
+ const hits = await handle.query({
137
+ query: [{ embedding: [1.0, 0.0, 0.0, 0.0] }],
138
+ topK: 10,
139
+ });
140
+
141
+ expect(hits.length).toBeGreaterThan(0);
142
+ expect(hits[0].id).toBe("vec-1");
143
+ });
144
+
145
+ it("returns results in similarity order", async () => {
146
+ // Query with basis vector 2
147
+ const hits = await handle.query({
148
+ query: [{ embedding: [0.0, 1.0, 0.0, 0.0] }],
149
+ topK: 10,
150
+ });
151
+
152
+ expect(hits.length).toBeGreaterThan(0);
153
+ expect(hits[0].id).toBe("vec-2");
154
+
155
+ // Scores should be in descending order
156
+ for (let i = 1; i < hits.length; i++) {
157
+ expect(hits[i].score).toBeLessThanOrEqual(hits[i - 1].score);
158
+ }
159
+ });
160
+
161
+ it("mixed vector query finds composite matches", async () => {
162
+ // Query with mix of basis 1 and 2 - should prefer vec-5 which has [0.5, 0.5, 0, 0]
163
+ const hits = await handle.query({
164
+ query: [{ embedding: [0.5, 0.5, 0.0, 0.0] }],
165
+ topK: 10,
166
+ });
167
+
168
+ expect(hits.length).toBeGreaterThan(0);
169
+ expect(hits[0].id).toBe("vec-5");
170
+ });
171
+
172
+ it("returns high similarity score for exact match", async () => {
173
+ const hits = await handle.query({
174
+ query: [{ embedding: [1.0, 0.0, 0.0, 0.0] }],
175
+ topK: 1,
176
+ });
177
+
178
+ // Cosine similarity of identical vectors should be very close to 1
179
+ expect(hits[0].score).toBeGreaterThan(0.99);
180
+ });
181
+
182
+ it("returns lower similarity for orthogonal vectors", async () => {
183
+ const hits = await handle.query({
184
+ query: [{ embedding: [1.0, 0.0, 0.0, 0.0] }],
185
+ topK: 10,
186
+ });
187
+
188
+ // Find vec-4 which is orthogonal to query
189
+ const orthogonal = hits.find((h) => h.id === "vec-4");
190
+ expect(orthogonal).toBeDefined();
191
+ // Orthogonal vectors should have similarity close to 0
192
+ expect(orthogonal!.score).toBeLessThan(0.1);
193
+ });
194
+
195
+ it("handles normalized query vector", async () => {
196
+ // Already normalized
197
+ const hits = await handle.query({
198
+ query: [{ embedding: [0.707, 0.707, 0.0, 0.0] }],
199
+ topK: 3,
200
+ });
201
+
202
+ // Should still find vec-5 as best match
203
+ expect(hits[0].id).toBe("vec-5");
204
+ });
205
+
206
+ it("handles unnormalized query vector", async () => {
207
+ // Not normalized (should still work with cosine similarity)
208
+ const hits = await handle.query({
209
+ query: [{ embedding: [2.0, 2.0, 0.0, 0.0] }],
210
+ topK: 3,
211
+ });
212
+
213
+ // Should still find vec-5 as best match
214
+ expect(hits[0].id).toBe("vec-5");
215
+ });
216
+ });
217
+
218
+ // ============================================================
219
+ // TOPK BEHAVIOR
220
+ // ============================================================
221
+
222
+ describe("topK behavior", () => {
223
+ it("topK smaller than doc count returns exactly topK", async () => {
224
+ const hits = await handle.query({
225
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
226
+ topK: 3,
227
+ });
228
+
229
+ expect(hits.length).toBe(3);
230
+ });
231
+
232
+ it("topK larger than doc count returns all docs", async () => {
233
+ const hits = await handle.query({
234
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
235
+ topK: 100,
236
+ });
237
+
238
+ expect(hits.length).toBe(6);
239
+ });
240
+
241
+ it("topK of 1 returns single best match", async () => {
242
+ const hits = await handle.query({
243
+ query: [{ embedding: [1.0, 0.0, 0.0, 0.0] }],
244
+ topK: 1,
245
+ });
246
+
247
+ expect(hits.length).toBe(1);
248
+ expect(hits[0].id).toBe("vec-1");
249
+ });
250
+
251
+ it("topK with filter returns limited filtered results", async () => {
252
+ const hits = await handle.query({
253
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
254
+ topK: 1,
255
+ filter: { category: "db" },
256
+ });
257
+
258
+ expect(hits.length).toBe(1);
259
+ expect(hits[0].document?.category).toBe("db");
260
+ });
261
+
262
+ it("topK of 0 returns empty array", async () => {
263
+ const hits = await handle.query({
264
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
265
+ topK: 0,
266
+ });
267
+
268
+ expect(hits.length).toBe(0);
269
+ });
270
+ });
271
+
272
+ // ============================================================
273
+ // OFFSET PAGINATION
274
+ // ============================================================
275
+
276
+ describe("offset pagination", () => {
277
+ it("offset skips first N results", async () => {
278
+ // Get all results
279
+ const allHits = await handle.query({
280
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
281
+ topK: 10,
282
+ });
283
+
284
+ // Get results with offset
285
+ const offsetHits = await handle.query({
286
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
287
+ topK: 10,
288
+ offset: 2,
289
+ });
290
+
291
+ expect(offsetHits.length).toBe(4); // 6 total - 2 skipped
292
+ expect(offsetHits[0].id).toBe(allHits[2].id);
293
+ });
294
+
295
+ it("offset with topK limits correctly", async () => {
296
+ const hits = await handle.query({
297
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
298
+ topK: 2,
299
+ offset: 2,
300
+ });
301
+
302
+ expect(hits.length).toBe(2);
303
+ });
304
+
305
+ it("offset beyond result count returns empty", async () => {
306
+ const hits = await handle.query({
307
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
308
+ topK: 10,
309
+ offset: 100,
310
+ });
311
+
312
+ expect(hits.length).toBe(0);
313
+ });
314
+
315
+ it("pagination works correctly", async () => {
316
+ // Page 1
317
+ const page1 = await handle.query({
318
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
319
+ topK: 2,
320
+ offset: 0,
321
+ });
322
+
323
+ // Page 2
324
+ const page2 = await handle.query({
325
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
326
+ topK: 2,
327
+ offset: 2,
328
+ });
329
+
330
+ // Page 3
331
+ const page3 = await handle.query({
332
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
333
+ topK: 2,
334
+ offset: 4,
335
+ });
336
+
337
+ expect(page1.length).toBe(2);
338
+ expect(page2.length).toBe(2);
339
+ expect(page3.length).toBe(2);
340
+
341
+ // All IDs should be unique across pages
342
+ const allIds = [...page1, ...page2, ...page3].map((h) => h.id);
343
+ const uniqueIds = new Set(allIds);
344
+ expect(uniqueIds.size).toBe(6);
345
+ });
346
+
347
+ it("offset with filter works correctly", async () => {
348
+ // 2 docs in "ml" category
349
+ const allMl = await handle.query({
350
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
351
+ topK: 10,
352
+ filter: { category: "ml" },
353
+ });
354
+
355
+ const offsetMl = await handle.query({
356
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
357
+ topK: 10,
358
+ offset: 1,
359
+ filter: { category: "ml" },
360
+ });
361
+
362
+ expect(allMl.length).toBe(2);
363
+ expect(offsetMl.length).toBe(1);
364
+ expect(offsetMl[0].id).toBe(allMl[1].id);
365
+ });
366
+ });
367
+
368
+ // ============================================================
369
+ // ORDER BY (NON-VECTOR)
370
+ // ============================================================
371
+
372
+ describe("orderBy", () => {
373
+ it("orders by integer field ascending", async () => {
374
+ const hits = await handle.query({
375
+ orderBy: { field: "priority", direction: "asc" },
376
+ topK: 10,
377
+ });
378
+
379
+ expect(hits.length).toBe(6);
380
+ expect(hits[0].document?.priority).toBe(1);
381
+ expect(hits[5].document?.priority).toBe(6);
382
+
383
+ // Verify order
384
+ for (let i = 1; i < hits.length; i++) {
385
+ expect(hits[i].document?.priority).toBeGreaterThanOrEqual(
386
+ hits[i - 1].document?.priority ?? 0,
387
+ );
388
+ }
389
+ });
390
+
391
+ it("orders by integer field descending", async () => {
392
+ const hits = await handle.query({
393
+ orderBy: { field: "priority", direction: "desc" },
394
+ topK: 10,
395
+ });
396
+
397
+ expect(hits[0].document?.priority).toBe(6);
398
+ expect(hits[5].document?.priority).toBe(1);
399
+ });
400
+
401
+ it("orders by float field", async () => {
402
+ // Note: We can still order by the "score" field even though it's excluded from the result
403
+ const hits = await handle.query({
404
+ orderBy: { field: "priority", direction: "desc" },
405
+ topK: 10,
406
+ });
407
+
408
+ // Verify descending order by priority
409
+ for (let i = 1; i < hits.length; i++) {
410
+ expect(hits[i].document?.priority).toBeLessThanOrEqual(
411
+ hits[i - 1].document?.priority ?? 0,
412
+ );
413
+ }
414
+ });
415
+
416
+ it("orders by string field", async () => {
417
+ const hits = await handle.query({
418
+ orderBy: { field: "title", direction: "asc" },
419
+ topK: 10,
420
+ });
421
+
422
+ // Verify alphabetical order
423
+ for (let i = 1; i < hits.length; i++) {
424
+ const prev = hits[i - 1].document?.title ?? "";
425
+ const curr = hits[i].document?.title ?? "";
426
+ expect(curr.localeCompare(prev)).toBeGreaterThanOrEqual(0);
427
+ }
428
+ });
429
+
430
+ it("orderBy with filter", async () => {
431
+ const hits = await handle.query({
432
+ filter: { category: "ml" },
433
+ orderBy: { field: "priority", direction: "desc" },
434
+ topK: 10,
435
+ });
436
+
437
+ expect(hits.length).toBe(2);
438
+ expect(hits[0].document?.priority).toBe(2);
439
+ expect(hits[1].document?.priority).toBe(1);
440
+ });
441
+
442
+ it("orderBy with topK limits after ordering", async () => {
443
+ const hits = await handle.query({
444
+ orderBy: { field: "priority", direction: "asc" },
445
+ topK: 3,
446
+ });
447
+
448
+ expect(hits.length).toBe(3);
449
+ expect(hits[0].document?.priority).toBe(1);
450
+ expect(hits[1].document?.priority).toBe(2);
451
+ expect(hits[2].document?.priority).toBe(3);
452
+ });
453
+
454
+ it("orderBy with offset", async () => {
455
+ const hits = await handle.query({
456
+ orderBy: { field: "priority", direction: "asc" },
457
+ topK: 2,
458
+ offset: 2,
459
+ });
460
+
461
+ expect(hits.length).toBe(2);
462
+ expect(hits[0].document?.priority).toBe(3);
463
+ expect(hits[1].document?.priority).toBe(4);
464
+ });
465
+ });
466
+
467
+ // ============================================================
468
+ // QUERY WITHOUT VECTOR
469
+ // ============================================================
470
+
471
+ describe("query without vector", () => {
472
+ it("filter-only query returns all matching docs", async () => {
473
+ const hits = await handle.query({
474
+ filter: { category: "db" },
475
+ topK: 10,
476
+ });
477
+
478
+ expect(hits.length).toBe(2);
479
+ for (const hit of hits) {
480
+ expect(hit.document?.category).toBe("db");
481
+ }
482
+ });
483
+
484
+ it("empty query with orderBy returns ordered docs", async () => {
485
+ const hits = await handle.query({
486
+ orderBy: { field: "priority", direction: "asc" },
487
+ topK: 10,
488
+ });
489
+
490
+ expect(hits.length).toBe(6);
491
+ expect(hits[0].document?.priority).toBe(1);
492
+ });
493
+
494
+ it("filter + orderBy combination", async () => {
495
+ const hits = await handle.query({
496
+ filter: { category: "search" },
497
+ orderBy: { field: "priority", direction: "desc" },
498
+ topK: 10,
499
+ });
500
+
501
+ expect(hits.length).toBe(2);
502
+ // Ordered by priority desc: vec-6 (priority 6), vec-5 (priority 5)
503
+ expect(hits[0].document?.priority).toBe(6);
504
+ expect(hits[1].document?.priority).toBe(5);
505
+ });
506
+ });
507
+
508
+ // ============================================================
509
+ // RESULT STRUCTURE
510
+ // ============================================================
511
+
512
+ describe("result structure", () => {
513
+ it("results have required fields", async () => {
514
+ const hits = await handle.query({
515
+ query: [{ embedding: [1.0, 0.0, 0.0, 0.0] }],
516
+ topK: 1,
517
+ });
518
+
519
+ expect(hits.length).toBe(1);
520
+ expect(hits[0]).toHaveProperty("id");
521
+ expect(hits[0]).toHaveProperty("index", testIndexId);
522
+ expect(hits[0]).toHaveProperty("score");
523
+ expect(typeof hits[0].id).toBe("string");
524
+ expect(typeof hits[0].score).toBe("number");
525
+ });
526
+
527
+ it("score is a valid number", async () => {
528
+ const hits = await handle.query({
529
+ query: [{ embedding: [1.0, 0.0, 0.0, 0.0] }],
530
+ topK: 5,
531
+ });
532
+
533
+ for (const hit of hits) {
534
+ expect(typeof hit.score).toBe("number");
535
+ expect(Number.isFinite(hit.score)).toBe(true);
536
+ }
537
+ });
538
+
539
+ it("document fields are included by default", async () => {
540
+ const hits = await handle.query({
541
+ query: [{ embedding: [1.0, 0.0, 0.0, 0.0] }],
542
+ topK: 1,
543
+ });
544
+
545
+ expect(hits[0].document).toBeDefined();
546
+ expect(hits[0].document).toHaveProperty("title");
547
+ expect(hits[0].document).toHaveProperty("category");
548
+ expect(hits[0].document).toHaveProperty("priority");
549
+ // Note: document "score" field is excluded to avoid conflict with hit.score
550
+ expect(hits[0].document).not.toHaveProperty("score");
551
+ expect(hits[0].document).toHaveProperty("embedding");
552
+ // But the hit should have a score
553
+ expect(hits[0].score).toBeDefined();
554
+ });
555
+
556
+ it("index field matches query index", async () => {
557
+ const hits = await handle.query({
558
+ query: [{ embedding: [1.0, 0.0, 0.0, 0.0] }],
559
+ topK: 5,
560
+ });
561
+
562
+ for (const hit of hits) {
563
+ expect(hit.index).toBe(testIndexId);
564
+ }
565
+ });
566
+ });
567
+
568
+ // ============================================================
569
+ // EMPTY RESULTS
570
+ // ============================================================
571
+
572
+ describe("empty results", () => {
573
+ it("filter with no matches returns empty array", async () => {
574
+ const hits = await handle.query({
575
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
576
+ filter: { category: "nonexistent" },
577
+ topK: 10,
578
+ });
579
+
580
+ expect(hits).toEqual([]);
581
+ });
582
+
583
+ it("offset beyond result count returns empty array", async () => {
584
+ const hits = await handle.query({
585
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
586
+ topK: 10,
587
+ offset: 1000,
588
+ });
589
+
590
+ expect(hits).toEqual([]);
591
+ });
592
+
593
+ it("topK 0 returns empty array", async () => {
594
+ const hits = await handle.query({
595
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
596
+ topK: 0,
597
+ });
598
+
599
+ expect(hits).toEqual([]);
600
+ });
601
+ });
602
+
603
+ // ============================================================
604
+ // DIFFERENT SIMILARITY METRICS
605
+ // ============================================================
606
+
607
+ describe("similarity metrics", () => {
608
+ it("euclidean distance index works correctly", async () => {
609
+ await pgvec.createIndex({
610
+ id: "euclidean_test",
611
+ schema: {
612
+ id: { type: "string", pk: true },
613
+ embedding: { type: "vector", dimensions: 4, similarity: "euclidean" },
614
+ },
615
+ providerOptions: { schema: SCHEMA },
616
+ });
617
+
618
+ const eucHandle = pgvec.index("euclidean_test");
619
+
620
+ await eucHandle.upsert([
621
+ { id: "e1", embedding: [1, 0, 0, 0] },
622
+ { id: "e2", embedding: [0, 1, 0, 0] },
623
+ { id: "e3", embedding: [0.9, 0.1, 0, 0] },
624
+ ]);
625
+
626
+ const hits = await eucHandle.query({
627
+ query: [{ embedding: [1, 0, 0, 0] }],
628
+ topK: 3,
629
+ });
630
+
631
+ expect(hits[0].id).toBe("e1"); // Exact match
632
+ expect(hits[1].id).toBe("e3"); // Closest
633
+ });
634
+
635
+ it("dot product index works correctly", async () => {
636
+ await pgvec.createIndex({
637
+ id: "dot_test",
638
+ schema: {
639
+ id: { type: "string", pk: true },
640
+ embedding: {
641
+ type: "vector",
642
+ dimensions: 4,
643
+ similarity: "dot_product",
644
+ },
645
+ },
646
+ providerOptions: { schema: SCHEMA },
647
+ });
648
+
649
+ const dotHandle = pgvec.index("dot_test");
650
+
651
+ await dotHandle.upsert([
652
+ { id: "d1", embedding: [1, 0, 0, 0] },
653
+ { id: "d2", embedding: [0, 1, 0, 0] },
654
+ { id: "d3", embedding: [0.5, 0.5, 0, 0] },
655
+ ]);
656
+
657
+ const hits = await dotHandle.query({
658
+ query: [{ embedding: [1, 1, 0, 0] }],
659
+ topK: 3,
660
+ });
661
+
662
+ // Dot product: d1=1, d2=1, d3=1
663
+ // With equal dot products, order may vary, but d3 should be competitive
664
+ expect(hits.map((h) => h.id).sort()).toEqual(["d1", "d2", "d3"]);
665
+ });
666
+ });
667
+ });