@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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-check-types.log +36 -0
- package/CHANGELOG.md +32 -0
- package/README.md +124 -0
- package/dist/__tests__/integration.test.js +2 -2
- 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 +48 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +32 -9
- 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 +2 -2
- package/dist/thread/store.d.ts.map +1 -1
- package/dist/thread/store.js +32 -102
- package/package.json +7 -4
- package/src/__tests__/integration.test.ts +15 -17
- 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 +77 -9
- package/src/thread/sql.ts +159 -0
- package/src/thread/store.ts +40 -127
- package/tsconfig.tsbuildinfo +1 -0
package/dist/migrations.d.ts
CHANGED
package/dist/migrations.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AACrC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AACrC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAOxD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9E;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9C;AAED;;GAEG;AACH,eAAO,MAAM,UAAU,EAAE,SAAS,EAcjC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,uBAAuB,iBAAiB,CAAC"}
|
package/dist/migrations.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Database migrations.
|
|
3
3
|
*/
|
|
4
|
-
import { TABLE_THREADS, TABLE_THREAD_EVENTS } from "@kernl-sdk/storage";
|
|
4
|
+
import { TABLE_THREADS, TABLE_THREAD_EVENTS, TABLE_MEMORIES, } from "@kernl-sdk/storage";
|
|
5
5
|
/**
|
|
6
6
|
* List of all migrations in order.
|
|
7
7
|
*/
|
|
8
|
-
export const
|
|
8
|
+
export const MIGRATIONS = [
|
|
9
9
|
{
|
|
10
|
-
id: "
|
|
10
|
+
id: "001_threads",
|
|
11
11
|
async up(ctx) {
|
|
12
12
|
await ctx.createTable(TABLE_THREADS);
|
|
13
13
|
await ctx.createTable(TABLE_THREAD_EVENTS);
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
|
+
{
|
|
17
|
+
id: "002_memories",
|
|
18
|
+
async up(ctx) {
|
|
19
|
+
await ctx.createTable(TABLE_MEMORIES);
|
|
20
|
+
},
|
|
21
|
+
},
|
|
16
22
|
];
|
|
17
23
|
/**
|
|
18
24
|
* Minimum schema version required by this version of @kernl/pg.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handle.test.d.ts","sourceRoot":"","sources":["../../../src/pgvector/__tests__/handle.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
|
|
2
|
+
import { Pool } from "pg";
|
|
3
|
+
import { PGSearchIndex } from "../search.js";
|
|
4
|
+
const TEST_DB_URL = process.env.KERNL_PG_TEST_URL;
|
|
5
|
+
const SCHEMA = "kernl_search_test";
|
|
6
|
+
describe.sequential("PGIndexHandle", () => {
|
|
7
|
+
if (!TEST_DB_URL) {
|
|
8
|
+
it.skip("requires KERNL_PG_TEST_URL environment variable", () => { });
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
let pool;
|
|
12
|
+
let search;
|
|
13
|
+
let initialized = false;
|
|
14
|
+
const ensureInit = async () => {
|
|
15
|
+
if (initialized)
|
|
16
|
+
return;
|
|
17
|
+
await pool.query(`CREATE EXTENSION IF NOT EXISTS vector`);
|
|
18
|
+
await pool.query(`CREATE SCHEMA IF NOT EXISTS "${SCHEMA}"`);
|
|
19
|
+
await pool.query(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS "${SCHEMA}"."documents" (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
title TEXT,
|
|
23
|
+
content TEXT,
|
|
24
|
+
status TEXT,
|
|
25
|
+
views INTEGER DEFAULT 0,
|
|
26
|
+
embedding vector(3)
|
|
27
|
+
)
|
|
28
|
+
`);
|
|
29
|
+
await pool.query(`
|
|
30
|
+
CREATE INDEX IF NOT EXISTS documents_embedding_idx
|
|
31
|
+
ON "${SCHEMA}"."documents"
|
|
32
|
+
USING hnsw (embedding vector_cosine_ops)
|
|
33
|
+
`);
|
|
34
|
+
initialized = true;
|
|
35
|
+
};
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
pool = new Pool({ connectionString: TEST_DB_URL });
|
|
38
|
+
search = new PGSearchIndex({ pool, ensureInit });
|
|
39
|
+
// Clean slate
|
|
40
|
+
await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
|
|
41
|
+
});
|
|
42
|
+
afterAll(async () => {
|
|
43
|
+
await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
|
|
44
|
+
await pool.end();
|
|
45
|
+
});
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
// Clear table between tests
|
|
48
|
+
if (initialized) {
|
|
49
|
+
await pool.query(`DELETE FROM "${SCHEMA}"."documents"`);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
describe("bindIndex", () => {
|
|
53
|
+
it("registers binding for later use", async () => {
|
|
54
|
+
await search.bindIndex("docs", {
|
|
55
|
+
schema: SCHEMA,
|
|
56
|
+
table: "documents",
|
|
57
|
+
pkey: "id",
|
|
58
|
+
fields: {
|
|
59
|
+
title: { column: "title", type: "string" },
|
|
60
|
+
content: { column: "content", type: "string" },
|
|
61
|
+
embedding: {
|
|
62
|
+
column: "embedding",
|
|
63
|
+
type: "vector",
|
|
64
|
+
dimensions: 3,
|
|
65
|
+
similarity: "cosine",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
// Can get handle without error
|
|
70
|
+
const handle = search.index("docs");
|
|
71
|
+
expect(handle.id).toBe("docs");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe("index().query()", () => {
|
|
75
|
+
beforeAll(async () => {
|
|
76
|
+
await search.bindIndex("docs", {
|
|
77
|
+
schema: SCHEMA,
|
|
78
|
+
table: "documents",
|
|
79
|
+
pkey: "id",
|
|
80
|
+
fields: {
|
|
81
|
+
title: { column: "title", type: "string" },
|
|
82
|
+
content: { column: "content", type: "string" },
|
|
83
|
+
status: { column: "status", type: "string" },
|
|
84
|
+
views: { column: "views", type: "int" },
|
|
85
|
+
embedding: {
|
|
86
|
+
column: "embedding",
|
|
87
|
+
type: "vector",
|
|
88
|
+
dimensions: 3,
|
|
89
|
+
similarity: "cosine",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
async function insertDocs() {
|
|
95
|
+
await ensureInit();
|
|
96
|
+
await pool.query(`
|
|
97
|
+
INSERT INTO "${SCHEMA}"."documents" (id, title, content, status, views, embedding)
|
|
98
|
+
VALUES
|
|
99
|
+
('doc1', 'Hello World', 'First document', 'active', 100, '[0.1, 0.2, 0.3]'),
|
|
100
|
+
('doc2', 'Goodbye World', 'Second document', 'active', 200, '[0.4, 0.5, 0.6]'),
|
|
101
|
+
('doc3', 'Hello Again', 'Third document', 'draft', 50, '[0.15, 0.25, 0.35]'),
|
|
102
|
+
('doc4', 'Final Doc', 'Fourth document', 'archived', 500, '[0.9, 0.8, 0.7]')
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
it("queries with vector search", async () => {
|
|
106
|
+
await insertDocs();
|
|
107
|
+
const handle = search.index("docs");
|
|
108
|
+
const results = await handle.query({
|
|
109
|
+
query: [{ embedding: [0.1, 0.2, 0.3] }],
|
|
110
|
+
topK: 2,
|
|
111
|
+
});
|
|
112
|
+
expect(results).toHaveLength(2);
|
|
113
|
+
expect(results[0].id).toBe("doc1"); // closest match
|
|
114
|
+
expect(results[0].score).toBeGreaterThan(0.9); // high similarity
|
|
115
|
+
expect(results[0].index).toBe("docs");
|
|
116
|
+
});
|
|
117
|
+
it("queries with filter", async () => {
|
|
118
|
+
await insertDocs();
|
|
119
|
+
const handle = search.index("docs");
|
|
120
|
+
const results = await handle.query({
|
|
121
|
+
query: [{ embedding: [0.1, 0.2, 0.3] }],
|
|
122
|
+
filter: { status: "active" },
|
|
123
|
+
topK: 10,
|
|
124
|
+
});
|
|
125
|
+
expect(results).toHaveLength(2);
|
|
126
|
+
results.forEach((r) => {
|
|
127
|
+
expect(r.document?.status).toBe("active");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
it("queries with comparison filter", async () => {
|
|
131
|
+
await insertDocs();
|
|
132
|
+
const handle = search.index("docs");
|
|
133
|
+
const results = await handle.query({
|
|
134
|
+
query: [{ embedding: [0.1, 0.2, 0.3] }],
|
|
135
|
+
filter: { views: { $gte: 100 } },
|
|
136
|
+
topK: 10,
|
|
137
|
+
});
|
|
138
|
+
expect(results.length).toBeGreaterThanOrEqual(2);
|
|
139
|
+
results.forEach((r) => {
|
|
140
|
+
expect(r.document?.views).toBeGreaterThanOrEqual(100);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
it("queries with $or filter", async () => {
|
|
144
|
+
await insertDocs();
|
|
145
|
+
const handle = search.index("docs");
|
|
146
|
+
const results = await handle.query({
|
|
147
|
+
query: [{ embedding: [0.1, 0.2, 0.3] }],
|
|
148
|
+
filter: {
|
|
149
|
+
$or: [{ status: "draft" }, { status: "archived" }],
|
|
150
|
+
},
|
|
151
|
+
topK: 10,
|
|
152
|
+
});
|
|
153
|
+
expect(results).toHaveLength(2);
|
|
154
|
+
results.forEach((r) => {
|
|
155
|
+
expect(["draft", "archived"]).toContain(r.document?.status);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
it("queries with $in filter", async () => {
|
|
159
|
+
await insertDocs();
|
|
160
|
+
const handle = search.index("docs");
|
|
161
|
+
const results = await handle.query({
|
|
162
|
+
query: [{ embedding: [0.1, 0.2, 0.3] }],
|
|
163
|
+
filter: { status: { $in: ["active", "draft"] } },
|
|
164
|
+
topK: 10,
|
|
165
|
+
});
|
|
166
|
+
expect(results).toHaveLength(3);
|
|
167
|
+
});
|
|
168
|
+
it("respects topK limit", async () => {
|
|
169
|
+
await insertDocs();
|
|
170
|
+
const handle = search.index("docs");
|
|
171
|
+
const results = await handle.query({
|
|
172
|
+
query: [{ embedding: [0.5, 0.5, 0.5] }],
|
|
173
|
+
topK: 2,
|
|
174
|
+
});
|
|
175
|
+
expect(results).toHaveLength(2);
|
|
176
|
+
});
|
|
177
|
+
it("respects offset for pagination", async () => {
|
|
178
|
+
await insertDocs();
|
|
179
|
+
const handle = search.index("docs");
|
|
180
|
+
// Get first 2
|
|
181
|
+
const page1 = await handle.query({
|
|
182
|
+
query: [{ embedding: [0.5, 0.5, 0.5] }],
|
|
183
|
+
topK: 2,
|
|
184
|
+
offset: 0,
|
|
185
|
+
});
|
|
186
|
+
// Get next 2
|
|
187
|
+
const page2 = await handle.query({
|
|
188
|
+
query: [{ embedding: [0.5, 0.5, 0.5] }],
|
|
189
|
+
topK: 2,
|
|
190
|
+
offset: 2,
|
|
191
|
+
});
|
|
192
|
+
expect(page1).toHaveLength(2);
|
|
193
|
+
expect(page2).toHaveLength(2);
|
|
194
|
+
expect(page1[0].id).not.toBe(page2[0].id);
|
|
195
|
+
});
|
|
196
|
+
it("queries with orderBy (non-vector)", async () => {
|
|
197
|
+
await insertDocs();
|
|
198
|
+
const handle = search.index("docs");
|
|
199
|
+
const results = await handle.query({
|
|
200
|
+
filter: { status: "active" },
|
|
201
|
+
orderBy: { field: "views", direction: "desc" },
|
|
202
|
+
topK: 10,
|
|
203
|
+
});
|
|
204
|
+
expect(results).toHaveLength(2);
|
|
205
|
+
expect(results[0].document?.views).toBe(200);
|
|
206
|
+
expect(results[1].document?.views).toBe(100);
|
|
207
|
+
});
|
|
208
|
+
it("returns documents with mapped field names", async () => {
|
|
209
|
+
await insertDocs();
|
|
210
|
+
const handle = search.index("docs");
|
|
211
|
+
const results = await handle.query({
|
|
212
|
+
query: [{ embedding: [0.1, 0.2, 0.3] }],
|
|
213
|
+
topK: 1,
|
|
214
|
+
});
|
|
215
|
+
expect(results[0].document).toHaveProperty("title");
|
|
216
|
+
expect(results[0].document).toHaveProperty("content");
|
|
217
|
+
expect(results[0].document).toHaveProperty("status");
|
|
218
|
+
expect(results[0].document).toHaveProperty("views");
|
|
219
|
+
expect(results[0].document).toHaveProperty("embedding");
|
|
220
|
+
});
|
|
221
|
+
it("returns empty array when no matches", async () => {
|
|
222
|
+
await insertDocs();
|
|
223
|
+
const handle = search.index("docs");
|
|
224
|
+
const results = await handle.query({
|
|
225
|
+
query: [{ embedding: [0.1, 0.2, 0.3] }],
|
|
226
|
+
filter: { status: "nonexistent" },
|
|
227
|
+
topK: 10,
|
|
228
|
+
});
|
|
229
|
+
expect(results).toEqual([]);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
describe("index() generic type", () => {
|
|
233
|
+
beforeAll(async () => {
|
|
234
|
+
await search.bindIndex("typed-docs", {
|
|
235
|
+
schema: SCHEMA,
|
|
236
|
+
table: "documents",
|
|
237
|
+
pkey: "id",
|
|
238
|
+
fields: {
|
|
239
|
+
title: { column: "title", type: "string" },
|
|
240
|
+
content: { column: "content", type: "string" },
|
|
241
|
+
status: { column: "status", type: "string" },
|
|
242
|
+
views: { column: "views", type: "int" },
|
|
243
|
+
embedding: {
|
|
244
|
+
column: "embedding",
|
|
245
|
+
type: "vector",
|
|
246
|
+
dimensions: 3,
|
|
247
|
+
similarity: "cosine",
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
it("provides typed document access", async () => {
|
|
253
|
+
await ensureInit();
|
|
254
|
+
await pool.query(`
|
|
255
|
+
INSERT INTO "${SCHEMA}"."documents" (id, title, content, status, views, embedding)
|
|
256
|
+
VALUES ('typed1', 'Typed Doc', 'Typed content', 'active', 42, '[0.1, 0.2, 0.3]')
|
|
257
|
+
`);
|
|
258
|
+
const handle = search.index("typed-docs");
|
|
259
|
+
const results = await handle.query({
|
|
260
|
+
query: [{ embedding: [0.1, 0.2, 0.3] }],
|
|
261
|
+
topK: 1,
|
|
262
|
+
});
|
|
263
|
+
// TypeScript should allow these without errors
|
|
264
|
+
const doc = results[0].document;
|
|
265
|
+
expect(doc?.title).toBe("Typed Doc");
|
|
266
|
+
expect(doc?.views).toBe(42);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
describe("error handling", () => {
|
|
270
|
+
it("throws when table does not exist", async () => {
|
|
271
|
+
const handle = search.index("unbound-index");
|
|
272
|
+
// Convention-based indexing tries to use "public.unbound-index"
|
|
273
|
+
// which doesn't exist, so PostgreSQL throws an error
|
|
274
|
+
await expect(handle.query({ query: [{ embedding: [0.1, 0.2, 0.3] }] })).rejects.toThrow('relation "public.unbound-index" does not exist');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hit.test.d.ts","sourceRoot":"","sources":["../../../src/pgvector/__tests__/hit.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { SEARCH_HIT } from "../hit.js";
|
|
3
|
+
describe("SEARCH_HIT", () => {
|
|
4
|
+
describe("decode", () => {
|
|
5
|
+
it("converts basic row to SearchHit", () => {
|
|
6
|
+
const row = {
|
|
7
|
+
id: "doc-123",
|
|
8
|
+
score: 0.95,
|
|
9
|
+
title: "Hello World",
|
|
10
|
+
content: "Some content",
|
|
11
|
+
};
|
|
12
|
+
const result = SEARCH_HIT.decode(row, "docs");
|
|
13
|
+
expect(result).toEqual({
|
|
14
|
+
id: "doc-123",
|
|
15
|
+
index: "docs",
|
|
16
|
+
score: 0.95,
|
|
17
|
+
document: {
|
|
18
|
+
id: "doc-123",
|
|
19
|
+
title: "Hello World",
|
|
20
|
+
content: "Some content",
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
it("converts id to string", () => {
|
|
25
|
+
const row = { id: 123, score: 0.5 };
|
|
26
|
+
const result = SEARCH_HIT.decode(row, "docs");
|
|
27
|
+
expect(result.id).toBe("123");
|
|
28
|
+
});
|
|
29
|
+
it("defaults score to 0 when not a number", () => {
|
|
30
|
+
const row = { id: "1", score: null };
|
|
31
|
+
const result = SEARCH_HIT.decode(row, "docs");
|
|
32
|
+
expect(result.score).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
it("defaults score to 0 when undefined", () => {
|
|
35
|
+
const row = { id: "1" };
|
|
36
|
+
const result = SEARCH_HIT.decode(row, "docs");
|
|
37
|
+
expect(result.score).toBe(0);
|
|
38
|
+
});
|
|
39
|
+
it("returns document with only id when no other fields", () => {
|
|
40
|
+
const row = { id: "1", score: 0.5 };
|
|
41
|
+
const result = SEARCH_HIT.decode(row, "docs");
|
|
42
|
+
expect(result.document).toEqual({ id: "1" });
|
|
43
|
+
});
|
|
44
|
+
describe("with binding", () => {
|
|
45
|
+
const binding = {
|
|
46
|
+
schema: "public",
|
|
47
|
+
table: "documents",
|
|
48
|
+
pkey: "id",
|
|
49
|
+
fields: {
|
|
50
|
+
title: { column: "doc_title", type: "string" },
|
|
51
|
+
content: { column: "doc_content", type: "string" },
|
|
52
|
+
embedding: {
|
|
53
|
+
column: "vec",
|
|
54
|
+
type: "vector",
|
|
55
|
+
dimensions: 3,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
it("maps columns to logical field names", () => {
|
|
60
|
+
const row = {
|
|
61
|
+
id: "1",
|
|
62
|
+
score: 0.9,
|
|
63
|
+
doc_title: "Hello",
|
|
64
|
+
doc_content: "World",
|
|
65
|
+
vec: [0.1, 0.2, 0.3],
|
|
66
|
+
};
|
|
67
|
+
const result = SEARCH_HIT.decode(row, "docs", binding);
|
|
68
|
+
expect(result.document).toEqual({
|
|
69
|
+
id: "1",
|
|
70
|
+
title: "Hello",
|
|
71
|
+
content: "World",
|
|
72
|
+
embedding: [0.1, 0.2, 0.3],
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
it("only includes fields defined in binding", () => {
|
|
76
|
+
const row = {
|
|
77
|
+
id: "1",
|
|
78
|
+
score: 0.9,
|
|
79
|
+
doc_title: "Hello",
|
|
80
|
+
extra_col: "should be ignored",
|
|
81
|
+
internal_field: "also ignored",
|
|
82
|
+
};
|
|
83
|
+
const result = SEARCH_HIT.decode(row, "docs", binding);
|
|
84
|
+
expect(result.document).toEqual({
|
|
85
|
+
id: "1",
|
|
86
|
+
title: "Hello",
|
|
87
|
+
});
|
|
88
|
+
expect(result.document?.extra_col).toBeUndefined();
|
|
89
|
+
expect(result.document?.internal_field).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
it("handles missing columns gracefully", () => {
|
|
92
|
+
const row = {
|
|
93
|
+
id: "1",
|
|
94
|
+
score: 0.9,
|
|
95
|
+
doc_title: "Hello",
|
|
96
|
+
// doc_content and vec are missing
|
|
97
|
+
};
|
|
98
|
+
const result = SEARCH_HIT.decode(row, "docs", binding);
|
|
99
|
+
expect(result.document).toEqual({
|
|
100
|
+
id: "1",
|
|
101
|
+
title: "Hello",
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
it("returns document with only id when no bound fields present", () => {
|
|
105
|
+
const row = {
|
|
106
|
+
id: "1",
|
|
107
|
+
score: 0.9,
|
|
108
|
+
unrelated_column: "value",
|
|
109
|
+
};
|
|
110
|
+
const result = SEARCH_HIT.decode(row, "docs", binding);
|
|
111
|
+
expect(result.document).toEqual({ id: "1" });
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe("with TDocument generic", () => {
|
|
115
|
+
it("types document correctly", () => {
|
|
116
|
+
const row = {
|
|
117
|
+
id: "1",
|
|
118
|
+
score: 0.9,
|
|
119
|
+
title: "Hello",
|
|
120
|
+
views: 100,
|
|
121
|
+
};
|
|
122
|
+
const result = SEARCH_HIT.decode(row, "docs");
|
|
123
|
+
// TypeScript should allow accessing these fields
|
|
124
|
+
expect(result.document?.title).toBe("Hello");
|
|
125
|
+
expect(result.document?.views).toBe(100);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe("encode", () => {
|
|
130
|
+
it("throws not implemented", () => {
|
|
131
|
+
expect(() => SEARCH_HIT.encode({})).toThrow("SEARCH_HIT.encode: not implemented");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document.integration.test.d.ts","sourceRoot":"","sources":["../../../../src/pgvector/__tests__/integration/document.integration.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|