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