@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.
- package/.turbo/turbo-build.log +4 -5
- package/.turbo/turbo-check-types.log +36 -0
- package/CHANGELOG.md +41 -0
- package/README.md +124 -0
- package/dist/__tests__/integration.test.js +81 -1
- package/dist/__tests__/memory-integration.test.d.ts +2 -0
- package/dist/__tests__/memory-integration.test.d.ts.map +1 -0
- package/dist/__tests__/memory-integration.test.js +287 -0
- package/dist/__tests__/memory.test.d.ts +2 -0
- package/dist/__tests__/memory.test.d.ts.map +1 -0
- package/dist/__tests__/memory.test.js +357 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/memory/sql.d.ts +30 -0
- package/dist/memory/sql.d.ts.map +1 -0
- package/dist/memory/sql.js +100 -0
- package/dist/memory/store.d.ts +41 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +114 -0
- package/dist/migrations.d.ts +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +9 -3
- package/dist/pgvector/__tests__/handle.test.d.ts +2 -0
- package/dist/pgvector/__tests__/handle.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/handle.test.js +277 -0
- package/dist/pgvector/__tests__/hit.test.d.ts +2 -0
- package/dist/pgvector/__tests__/hit.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/hit.test.js +134 -0
- package/dist/pgvector/__tests__/integration/document.integration.test.d.ts +7 -0
- package/dist/pgvector/__tests__/integration/document.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/document.integration.test.js +587 -0
- package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/edge.integration.test.js +663 -0
- package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/filters.integration.test.js +609 -0
- package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/lifecycle.integration.test.js +449 -0
- package/dist/pgvector/__tests__/integration/query.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/query.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/query.integration.test.js +544 -0
- package/dist/pgvector/__tests__/search.test.d.ts +2 -0
- package/dist/pgvector/__tests__/search.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/search.test.js +279 -0
- package/dist/pgvector/handle.d.ts +60 -0
- package/dist/pgvector/handle.d.ts.map +1 -0
- package/dist/pgvector/handle.js +213 -0
- package/dist/pgvector/hit.d.ts +10 -0
- package/dist/pgvector/hit.d.ts.map +1 -0
- package/dist/pgvector/hit.js +44 -0
- package/dist/pgvector/index.d.ts +7 -0
- package/dist/pgvector/index.d.ts.map +1 -0
- package/dist/pgvector/index.js +5 -0
- package/dist/pgvector/search.d.ts +60 -0
- package/dist/pgvector/search.d.ts.map +1 -0
- package/dist/pgvector/search.js +227 -0
- package/dist/pgvector/sql/__tests__/limit.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/limit.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/limit.test.js +161 -0
- package/dist/pgvector/sql/__tests__/order.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/order.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/order.test.js +218 -0
- package/dist/pgvector/sql/__tests__/query.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/query.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/query.test.js +392 -0
- package/dist/pgvector/sql/__tests__/select.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/select.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/select.test.js +293 -0
- package/dist/pgvector/sql/__tests__/where.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/where.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/where.test.js +488 -0
- package/dist/pgvector/sql/index.d.ts +7 -0
- package/dist/pgvector/sql/index.d.ts.map +1 -0
- package/dist/pgvector/sql/index.js +6 -0
- package/dist/pgvector/sql/limit.d.ts +8 -0
- package/dist/pgvector/sql/limit.d.ts.map +1 -0
- package/dist/pgvector/sql/limit.js +20 -0
- package/dist/pgvector/sql/order.d.ts +9 -0
- package/dist/pgvector/sql/order.d.ts.map +1 -0
- package/dist/pgvector/sql/order.js +47 -0
- package/dist/pgvector/sql/query.d.ts +46 -0
- package/dist/pgvector/sql/query.d.ts.map +1 -0
- package/dist/pgvector/sql/query.js +54 -0
- package/dist/pgvector/sql/schema.d.ts +16 -0
- package/dist/pgvector/sql/schema.d.ts.map +1 -0
- package/dist/pgvector/sql/schema.js +47 -0
- package/dist/pgvector/sql/select.d.ts +11 -0
- package/dist/pgvector/sql/select.d.ts.map +1 -0
- package/dist/pgvector/sql/select.js +87 -0
- package/dist/pgvector/sql/where.d.ts +8 -0
- package/dist/pgvector/sql/where.d.ts.map +1 -0
- package/dist/pgvector/sql/where.js +137 -0
- package/dist/pgvector/types.d.ts +20 -0
- package/dist/pgvector/types.d.ts.map +1 -0
- package/dist/pgvector/types.js +1 -0
- package/dist/pgvector/utils.d.ts +18 -0
- package/dist/pgvector/utils.d.ts.map +1 -0
- package/dist/pgvector/utils.js +22 -0
- package/dist/postgres.d.ts +19 -26
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +15 -27
- package/dist/storage.d.ts +62 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +55 -10
- package/dist/thread/sql.d.ts +38 -0
- package/dist/thread/sql.d.ts.map +1 -0
- package/dist/thread/sql.js +112 -0
- package/dist/thread/store.d.ts +7 -3
- package/dist/thread/store.d.ts.map +1 -1
- package/dist/thread/store.js +46 -105
- package/package.json +8 -5
- package/src/__tests__/integration.test.ts +114 -15
- package/src/__tests__/memory-integration.test.ts +355 -0
- package/src/__tests__/memory.test.ts +428 -0
- package/src/index.ts +19 -3
- package/src/memory/sql.ts +141 -0
- package/src/memory/store.ts +166 -0
- package/src/migrations.ts +13 -3
- package/src/pgvector/README.md +50 -0
- package/src/pgvector/__tests__/handle.test.ts +335 -0
- package/src/pgvector/__tests__/hit.test.ts +165 -0
- package/src/pgvector/__tests__/integration/document.integration.test.ts +717 -0
- package/src/pgvector/__tests__/integration/edge.integration.test.ts +835 -0
- package/src/pgvector/__tests__/integration/filters.integration.test.ts +721 -0
- package/src/pgvector/__tests__/integration/lifecycle.integration.test.ts +570 -0
- package/src/pgvector/__tests__/integration/query.integration.test.ts +667 -0
- package/src/pgvector/__tests__/search.test.ts +366 -0
- package/src/pgvector/handle.ts +285 -0
- package/src/pgvector/hit.ts +56 -0
- package/src/pgvector/index.ts +7 -0
- package/src/pgvector/search.ts +330 -0
- package/src/pgvector/sql/__tests__/limit.test.ts +180 -0
- package/src/pgvector/sql/__tests__/order.test.ts +248 -0
- package/src/pgvector/sql/__tests__/query.test.ts +548 -0
- package/src/pgvector/sql/__tests__/select.test.ts +367 -0
- package/src/pgvector/sql/__tests__/where.test.ts +554 -0
- package/src/pgvector/sql/index.ts +14 -0
- package/src/pgvector/sql/limit.ts +29 -0
- package/src/pgvector/sql/order.ts +55 -0
- package/src/pgvector/sql/query.ts +112 -0
- package/src/pgvector/sql/schema.ts +61 -0
- package/src/pgvector/sql/select.ts +100 -0
- package/src/pgvector/sql/where.ts +152 -0
- package/src/pgvector/types.ts +21 -0
- package/src/pgvector/utils.ts +24 -0
- package/src/postgres.ts +31 -33
- package/src/storage.ts +102 -11
- package/src/thread/sql.ts +159 -0
- package/src/thread/store.ts +58 -127
- 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 @@
|
|
|
1
|
+
{"version":3,"file":"search.test.d.ts","sourceRoot":"","sources":["../../../src/pgvector/__tests__/search.test.ts"],"names":[],"mappings":""}
|