@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,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index lifecycle integration tests for pgvector.
|
|
3
|
+
*
|
|
4
|
+
* Tests the full index lifecycle: create, bind, upsert, query,
|
|
5
|
+
* patch, delete documents, delete index.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
9
|
+
import { Pool } from "pg";
|
|
10
|
+
|
|
11
|
+
import { PGSearchIndex } from "../../search";
|
|
12
|
+
|
|
13
|
+
const TEST_DB_URL = process.env.KERNL_PG_TEST_URL;
|
|
14
|
+
const SCHEMA = "kernl_lifecycle_integration_test";
|
|
15
|
+
|
|
16
|
+
describe.sequential("pgvector index lifecycle integration tests", () => {
|
|
17
|
+
if (!TEST_DB_URL) {
|
|
18
|
+
it.skip("requires KERNL_PG_TEST_URL environment variable", () => {});
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let pool: Pool;
|
|
23
|
+
let pgvec: PGSearchIndex;
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
pool = new Pool({ connectionString: TEST_DB_URL });
|
|
27
|
+
|
|
28
|
+
await pool.query(`CREATE EXTENSION IF NOT EXISTS vector`);
|
|
29
|
+
await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
|
|
30
|
+
await pool.query(`CREATE SCHEMA "${SCHEMA}"`);
|
|
31
|
+
|
|
32
|
+
pgvec = new PGSearchIndex({ pool });
|
|
33
|
+
}, 30000);
|
|
34
|
+
|
|
35
|
+
afterAll(async () => {
|
|
36
|
+
await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
|
|
37
|
+
await pool.end();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ============================================================
|
|
41
|
+
// FULL LIFECYCLE TEST
|
|
42
|
+
// ============================================================
|
|
43
|
+
|
|
44
|
+
describe("complete index lifecycle", () => {
|
|
45
|
+
const indexId = "lifecycle_test";
|
|
46
|
+
|
|
47
|
+
it("creates index with schema", async () => {
|
|
48
|
+
await pgvec.createIndex({
|
|
49
|
+
id: indexId,
|
|
50
|
+
schema: {
|
|
51
|
+
id: { type: "string", pk: true },
|
|
52
|
+
title: { type: "string" },
|
|
53
|
+
views: { type: "int" },
|
|
54
|
+
published: { type: "boolean" },
|
|
55
|
+
embedding: { type: "vector", dimensions: 4, similarity: "cosine" },
|
|
56
|
+
},
|
|
57
|
+
providerOptions: { schema: SCHEMA },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Verify table was created
|
|
61
|
+
const tableCheck = await pool.query(
|
|
62
|
+
`SELECT 1 FROM information_schema.tables
|
|
63
|
+
WHERE table_schema = $1 AND table_name = $2`,
|
|
64
|
+
[SCHEMA, indexId],
|
|
65
|
+
);
|
|
66
|
+
expect(tableCheck.rows).toHaveLength(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("index is immediately usable after creation", async () => {
|
|
70
|
+
const handle = pgvec.index(indexId);
|
|
71
|
+
expect(handle.id).toBe(indexId);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("upserts documents", async () => {
|
|
75
|
+
const handle = pgvec.index(indexId);
|
|
76
|
+
|
|
77
|
+
const result = await handle.upsert([
|
|
78
|
+
{
|
|
79
|
+
id: "doc-1",
|
|
80
|
+
title: "First Document",
|
|
81
|
+
views: 100,
|
|
82
|
+
published: true,
|
|
83
|
+
embedding: [1, 0, 0, 0],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "doc-2",
|
|
87
|
+
title: "Second Document",
|
|
88
|
+
views: 200,
|
|
89
|
+
published: false,
|
|
90
|
+
embedding: [0, 1, 0, 0],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "doc-3",
|
|
94
|
+
title: "Third Document",
|
|
95
|
+
views: 50,
|
|
96
|
+
published: true,
|
|
97
|
+
embedding: [0, 0, 1, 0],
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
expect(result.count).toBe(3);
|
|
102
|
+
expect(result.inserted).toBe(3);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("queries documents with vector search", async () => {
|
|
106
|
+
const handle = pgvec.index(indexId);
|
|
107
|
+
|
|
108
|
+
const hits = await handle.query({
|
|
109
|
+
query: [{ embedding: [1, 0, 0, 0] }],
|
|
110
|
+
topK: 10,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(hits).toHaveLength(3);
|
|
114
|
+
expect(hits[0].id).toBe("doc-1");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("queries documents with filter", async () => {
|
|
118
|
+
const handle = pgvec.index(indexId);
|
|
119
|
+
|
|
120
|
+
const hits = await handle.query({
|
|
121
|
+
query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
|
|
122
|
+
filter: { published: true },
|
|
123
|
+
topK: 10,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(hits).toHaveLength(2);
|
|
127
|
+
for (const hit of hits) {
|
|
128
|
+
expect(hit.document?.published).toBe(true);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("patches documents", async () => {
|
|
133
|
+
const handle = pgvec.index(indexId);
|
|
134
|
+
|
|
135
|
+
await handle.patch({
|
|
136
|
+
id: "doc-1",
|
|
137
|
+
views: 500,
|
|
138
|
+
title: "Updated First Document",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const hits = await handle.query({
|
|
142
|
+
filter: { id: "doc-1" },
|
|
143
|
+
topK: 1,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(hits[0].document?.views).toBe(500);
|
|
147
|
+
expect(hits[0].document?.title).toBe("Updated First Document");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("deletes documents", async () => {
|
|
151
|
+
const handle = pgvec.index(indexId);
|
|
152
|
+
|
|
153
|
+
await handle.delete("doc-3");
|
|
154
|
+
|
|
155
|
+
const hits = await handle.query({
|
|
156
|
+
query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
|
|
157
|
+
topK: 10,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(hits).toHaveLength(2);
|
|
161
|
+
expect(hits.find((h) => h.id === "doc-3")).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("describes index", async () => {
|
|
165
|
+
const stats = await pgvec.describeIndex(indexId);
|
|
166
|
+
|
|
167
|
+
expect(stats.id).toBe(indexId);
|
|
168
|
+
expect(stats.count).toBe(2);
|
|
169
|
+
expect(stats.dimensions).toBe(4);
|
|
170
|
+
expect(stats.similarity).toBe("cosine");
|
|
171
|
+
expect(stats.status).toBe("ready");
|
|
172
|
+
expect(stats.sizeb).toBeGreaterThan(0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("lists index", async () => {
|
|
176
|
+
const page = await pgvec.listIndexes({ prefix: "lifecycle" });
|
|
177
|
+
|
|
178
|
+
expect(page.data).toHaveLength(1);
|
|
179
|
+
expect(page.data[0].id).toBe(indexId);
|
|
180
|
+
expect(page.data[0].status).toBe("ready");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("deletes index", async () => {
|
|
184
|
+
await pgvec.deleteIndex(indexId);
|
|
185
|
+
|
|
186
|
+
// Verify table was dropped
|
|
187
|
+
const tableCheck = await pool.query(
|
|
188
|
+
`SELECT 1 FROM information_schema.tables
|
|
189
|
+
WHERE table_schema = $1 AND table_name = $2`,
|
|
190
|
+
[SCHEMA, indexId],
|
|
191
|
+
);
|
|
192
|
+
expect(tableCheck.rows).toHaveLength(0);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("throws on operations after delete", async () => {
|
|
196
|
+
await expect(pgvec.describeIndex(indexId)).rejects.toThrow(
|
|
197
|
+
`Index "${indexId}" not bound`,
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ============================================================
|
|
203
|
+
// BIND EXISTING TABLE
|
|
204
|
+
// ============================================================
|
|
205
|
+
|
|
206
|
+
describe("bindIndex", () => {
|
|
207
|
+
const tableId = "existing_table";
|
|
208
|
+
|
|
209
|
+
beforeAll(async () => {
|
|
210
|
+
// Create table manually
|
|
211
|
+
await pool.query(`
|
|
212
|
+
CREATE TABLE "${SCHEMA}"."${tableId}" (
|
|
213
|
+
doc_id TEXT PRIMARY KEY,
|
|
214
|
+
doc_title TEXT,
|
|
215
|
+
doc_count INTEGER,
|
|
216
|
+
doc_embedding vector(4)
|
|
217
|
+
)
|
|
218
|
+
`);
|
|
219
|
+
|
|
220
|
+
// Insert some data
|
|
221
|
+
await pool.query(`
|
|
222
|
+
INSERT INTO "${SCHEMA}"."${tableId}" (doc_id, doc_title, doc_count, doc_embedding)
|
|
223
|
+
VALUES
|
|
224
|
+
('existing-1', 'Existing Doc 1', 10, '[0.1, 0.2, 0.3, 0.4]'),
|
|
225
|
+
('existing-2', 'Existing Doc 2', 20, '[0.5, 0.6, 0.7, 0.8]')
|
|
226
|
+
`);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("binds to existing table with custom field mapping", async () => {
|
|
230
|
+
await pgvec.bindIndex("custom_binding", {
|
|
231
|
+
schema: SCHEMA,
|
|
232
|
+
table: tableId,
|
|
233
|
+
pkey: "doc_id",
|
|
234
|
+
fields: {
|
|
235
|
+
id: { column: "doc_id", type: "string" },
|
|
236
|
+
title: { column: "doc_title", type: "string" },
|
|
237
|
+
count: { column: "doc_count", type: "int" },
|
|
238
|
+
embedding: {
|
|
239
|
+
column: "doc_embedding",
|
|
240
|
+
type: "vector",
|
|
241
|
+
dimensions: 4,
|
|
242
|
+
similarity: "cosine",
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const handle = pgvec.index("custom_binding");
|
|
248
|
+
expect(handle.id).toBe("custom_binding");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("queries bound table", async () => {
|
|
252
|
+
const handle = pgvec.index("custom_binding");
|
|
253
|
+
|
|
254
|
+
const hits = await handle.query({
|
|
255
|
+
query: [{ embedding: [0.1, 0.2, 0.3, 0.4] }],
|
|
256
|
+
topK: 10,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(hits).toHaveLength(2);
|
|
260
|
+
expect(hits[0].id).toBe("existing-1");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("upserts to bound table", async () => {
|
|
264
|
+
const handle = pgvec.index("custom_binding");
|
|
265
|
+
|
|
266
|
+
await handle.upsert({
|
|
267
|
+
id: "new-doc",
|
|
268
|
+
title: "New Document",
|
|
269
|
+
count: 30,
|
|
270
|
+
embedding: [0.9, 0.9, 0.9, 0.9],
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Verify in raw SQL
|
|
274
|
+
const result = await pool.query(
|
|
275
|
+
`SELECT * FROM "${SCHEMA}"."${tableId}" WHERE doc_id = 'new-doc'`,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
expect(result.rows).toHaveLength(1);
|
|
279
|
+
expect(result.rows[0].doc_title).toBe("New Document");
|
|
280
|
+
expect(result.rows[0].doc_count).toBe(30);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("respects custom primary key", async () => {
|
|
284
|
+
const handle = pgvec.index("custom_binding");
|
|
285
|
+
|
|
286
|
+
// Update existing doc
|
|
287
|
+
await handle.upsert({
|
|
288
|
+
id: "existing-1",
|
|
289
|
+
title: "Updated Existing",
|
|
290
|
+
count: 100,
|
|
291
|
+
embedding: [0.1, 0.2, 0.3, 0.4],
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const result = await pool.query(
|
|
295
|
+
`SELECT * FROM "${SCHEMA}"."${tableId}" WHERE doc_id = 'existing-1'`,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
expect(result.rows[0].doc_title).toBe("Updated Existing");
|
|
299
|
+
expect(result.rows[0].doc_count).toBe(100);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("rebinding updates configuration", async () => {
|
|
303
|
+
// Rebind with different field mapping
|
|
304
|
+
await pgvec.bindIndex("custom_binding", {
|
|
305
|
+
schema: SCHEMA,
|
|
306
|
+
table: tableId,
|
|
307
|
+
pkey: "doc_id",
|
|
308
|
+
fields: {
|
|
309
|
+
id: { column: "doc_id", type: "string" },
|
|
310
|
+
title: { column: "doc_title", type: "string" },
|
|
311
|
+
// count field omitted
|
|
312
|
+
embedding: {
|
|
313
|
+
column: "doc_embedding",
|
|
314
|
+
type: "vector",
|
|
315
|
+
dimensions: 4,
|
|
316
|
+
similarity: "cosine",
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Should still work
|
|
322
|
+
const handle = pgvec.index("custom_binding");
|
|
323
|
+
const hits = await handle.query({
|
|
324
|
+
query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
|
|
325
|
+
topK: 1,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(hits).toHaveLength(1);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// ============================================================
|
|
333
|
+
// MULTIPLE INDEXES
|
|
334
|
+
// ============================================================
|
|
335
|
+
|
|
336
|
+
describe("multiple indexes", () => {
|
|
337
|
+
it("manages multiple indexes independently", async () => {
|
|
338
|
+
// Create two indexes
|
|
339
|
+
await pgvec.createIndex({
|
|
340
|
+
id: "multi_index_a",
|
|
341
|
+
schema: {
|
|
342
|
+
id: { type: "string", pk: true },
|
|
343
|
+
name: { type: "string" },
|
|
344
|
+
embedding: { type: "vector", dimensions: 4 },
|
|
345
|
+
},
|
|
346
|
+
providerOptions: { schema: SCHEMA },
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
await pgvec.createIndex({
|
|
350
|
+
id: "multi_index_b",
|
|
351
|
+
schema: {
|
|
352
|
+
id: { type: "string", pk: true },
|
|
353
|
+
title: { type: "string" },
|
|
354
|
+
embedding: { type: "vector", dimensions: 4 },
|
|
355
|
+
},
|
|
356
|
+
providerOptions: { schema: SCHEMA },
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Insert different data
|
|
360
|
+
const handleA = pgvec.index("multi_index_a");
|
|
361
|
+
const handleB = pgvec.index("multi_index_b");
|
|
362
|
+
|
|
363
|
+
await handleA.upsert({ id: "a-1", name: "Index A Doc", embedding: [1, 0, 0, 0] });
|
|
364
|
+
await handleB.upsert({ id: "b-1", title: "Index B Doc", embedding: [0, 1, 0, 0] });
|
|
365
|
+
|
|
366
|
+
// Query each
|
|
367
|
+
const hitsA = await handleA.query({
|
|
368
|
+
query: [{ embedding: [1, 0, 0, 0] }],
|
|
369
|
+
topK: 10,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const hitsB = await handleB.query({
|
|
373
|
+
query: [{ embedding: [0, 1, 0, 0] }],
|
|
374
|
+
topK: 10,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
expect(hitsA).toHaveLength(1);
|
|
378
|
+
expect(hitsA[0].document?.name).toBe("Index A Doc");
|
|
379
|
+
|
|
380
|
+
expect(hitsB).toHaveLength(1);
|
|
381
|
+
expect(hitsB[0].document?.title).toBe("Index B Doc");
|
|
382
|
+
|
|
383
|
+
// Delete one doesn't affect the other
|
|
384
|
+
await pgvec.deleteIndex("multi_index_a");
|
|
385
|
+
|
|
386
|
+
const stillExists = await handleB.query({
|
|
387
|
+
query: [{ embedding: [0, 1, 0, 0] }],
|
|
388
|
+
topK: 10,
|
|
389
|
+
});
|
|
390
|
+
expect(stillExists).toHaveLength(1);
|
|
391
|
+
|
|
392
|
+
// Cleanup
|
|
393
|
+
await pgvec.deleteIndex("multi_index_b");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("lists multiple indexes with pagination", async () => {
|
|
397
|
+
// Create several indexes
|
|
398
|
+
for (let i = 0; i < 5; i++) {
|
|
399
|
+
await pgvec.createIndex({
|
|
400
|
+
id: `paginated_${i}`,
|
|
401
|
+
schema: {
|
|
402
|
+
id: { type: "string", pk: true },
|
|
403
|
+
embedding: { type: "vector", dimensions: 4 },
|
|
404
|
+
},
|
|
405
|
+
providerOptions: { schema: SCHEMA },
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// List with limit
|
|
410
|
+
const page1 = await pgvec.listIndexes({ prefix: "paginated_", limit: 2 });
|
|
411
|
+
expect(page1.data).toHaveLength(2);
|
|
412
|
+
expect(page1.last).toBe(false);
|
|
413
|
+
|
|
414
|
+
const page2 = await page1.next();
|
|
415
|
+
expect(page2).not.toBeNull();
|
|
416
|
+
expect(page2!.data).toHaveLength(2);
|
|
417
|
+
|
|
418
|
+
const page3 = await page2!.next();
|
|
419
|
+
expect(page3).not.toBeNull();
|
|
420
|
+
expect(page3!.data).toHaveLength(1);
|
|
421
|
+
expect(page3!.last).toBe(true);
|
|
422
|
+
|
|
423
|
+
// Cleanup
|
|
424
|
+
for (let i = 0; i < 5; i++) {
|
|
425
|
+
await pgvec.deleteIndex(`paginated_${i}`);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// ============================================================
|
|
431
|
+
// SCHEMA VARIATIONS
|
|
432
|
+
// ============================================================
|
|
433
|
+
|
|
434
|
+
describe("schema variations", () => {
|
|
435
|
+
it("creates index with all field types", async () => {
|
|
436
|
+
await pgvec.createIndex({
|
|
437
|
+
id: "all_types",
|
|
438
|
+
schema: {
|
|
439
|
+
id: { type: "string", pk: true },
|
|
440
|
+
str_field: { type: "string" },
|
|
441
|
+
int_field: { type: "int" },
|
|
442
|
+
float_field: { type: "float" },
|
|
443
|
+
bool_field: { type: "boolean" },
|
|
444
|
+
date_field: { type: "date" },
|
|
445
|
+
embedding: { type: "vector", dimensions: 4 },
|
|
446
|
+
},
|
|
447
|
+
providerOptions: { schema: SCHEMA },
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Verify column types
|
|
451
|
+
const result = await pool.query<{
|
|
452
|
+
column_name: string;
|
|
453
|
+
data_type: string;
|
|
454
|
+
}>(
|
|
455
|
+
`SELECT column_name, data_type
|
|
456
|
+
FROM information_schema.columns
|
|
457
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
458
|
+
ORDER BY ordinal_position`,
|
|
459
|
+
[SCHEMA, "all_types"],
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
const columns = Object.fromEntries(
|
|
463
|
+
result.rows.map((r) => [r.column_name, r.data_type]),
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
expect(columns.str_field).toBe("text");
|
|
467
|
+
expect(columns.int_field).toBe("integer");
|
|
468
|
+
expect(columns.float_field).toBe("double precision");
|
|
469
|
+
expect(columns.bool_field).toBe("boolean");
|
|
470
|
+
expect(columns.date_field).toBe("timestamp with time zone");
|
|
471
|
+
|
|
472
|
+
await pgvec.deleteIndex("all_types");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("creates index with high-dimensional vectors", async () => {
|
|
476
|
+
await pgvec.createIndex({
|
|
477
|
+
id: "high_dim",
|
|
478
|
+
schema: {
|
|
479
|
+
id: { type: "string", pk: true },
|
|
480
|
+
embedding: { type: "vector", dimensions: 1536, similarity: "cosine" },
|
|
481
|
+
},
|
|
482
|
+
providerOptions: { schema: SCHEMA },
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const handle = pgvec.index("high_dim");
|
|
486
|
+
const vec = new Array(1536).fill(0.1);
|
|
487
|
+
|
|
488
|
+
await handle.upsert({ id: "high-1", embedding: vec });
|
|
489
|
+
|
|
490
|
+
const hits = await handle.query({
|
|
491
|
+
query: [{ embedding: vec }],
|
|
492
|
+
topK: 1,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
expect(hits).toHaveLength(1);
|
|
496
|
+
expect(hits[0].document?.embedding).toHaveLength(1536);
|
|
497
|
+
|
|
498
|
+
await pgvec.deleteIndex("high_dim");
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it("creates index with multiple vector fields", async () => {
|
|
502
|
+
await pgvec.createIndex({
|
|
503
|
+
id: "multi_vec",
|
|
504
|
+
schema: {
|
|
505
|
+
id: { type: "string", pk: true },
|
|
506
|
+
title_embedding: { type: "vector", dimensions: 4, similarity: "cosine" },
|
|
507
|
+
content_embedding: { type: "vector", dimensions: 4, similarity: "euclidean" },
|
|
508
|
+
},
|
|
509
|
+
providerOptions: { schema: SCHEMA },
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const handle = pgvec.index("multi_vec");
|
|
513
|
+
|
|
514
|
+
await handle.upsert({
|
|
515
|
+
id: "mv-1",
|
|
516
|
+
title_embedding: [1, 0, 0, 0],
|
|
517
|
+
content_embedding: [0, 1, 0, 0],
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Query on first vector field
|
|
521
|
+
const hits = await handle.query({
|
|
522
|
+
query: [{ title_embedding: [1, 0, 0, 0] }],
|
|
523
|
+
topK: 1,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
expect(hits).toHaveLength(1);
|
|
527
|
+
|
|
528
|
+
await pgvec.deleteIndex("multi_vec");
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// ============================================================
|
|
533
|
+
// PERSISTENCE ACROSS INSTANCES
|
|
534
|
+
// ============================================================
|
|
535
|
+
|
|
536
|
+
describe("persistence", () => {
|
|
537
|
+
it("loads existing indexes on new instance", async () => {
|
|
538
|
+
// Create index with first instance
|
|
539
|
+
await pgvec.createIndex({
|
|
540
|
+
id: "persist_test",
|
|
541
|
+
schema: {
|
|
542
|
+
id: { type: "string", pk: true },
|
|
543
|
+
name: { type: "string" },
|
|
544
|
+
embedding: { type: "vector", dimensions: 4 },
|
|
545
|
+
},
|
|
546
|
+
providerOptions: { schema: SCHEMA },
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
const handle1 = pgvec.index("persist_test");
|
|
550
|
+
await handle1.upsert({ id: "p-1", name: "Persisted", embedding: [1, 0, 0, 0] });
|
|
551
|
+
|
|
552
|
+
// Create new instance
|
|
553
|
+
const pgvec2 = new PGSearchIndex({ pool });
|
|
554
|
+
|
|
555
|
+
// Should be able to access the index
|
|
556
|
+
const stats = await pgvec2.describeIndex("persist_test");
|
|
557
|
+
expect(stats.count).toBe(1);
|
|
558
|
+
|
|
559
|
+
const handle2 = pgvec2.index("persist_test");
|
|
560
|
+
const hits = await handle2.query({
|
|
561
|
+
query: [{ embedding: [1, 0, 0, 0] }],
|
|
562
|
+
topK: 1,
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
expect(hits[0].document?.name).toBe("Persisted");
|
|
566
|
+
|
|
567
|
+
await pgvec.deleteIndex("persist_test");
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
});
|