@remit/search-service 0.0.13 → 0.0.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/search-service",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -24,17 +24,12 @@
24
24
  "./sqlite-vec": {
25
25
  "types": "./src/sqlite-vec.ts",
26
26
  "default": "./src/sqlite-vec.ts"
27
- },
28
- "./pgvector": {
29
- "types": "./src/pgvector.ts",
30
- "default": "./src/pgvector.ts"
31
27
  }
32
28
  },
33
29
  "scripts": {
34
30
  "test:typecheck": "tsgo --noEmit",
35
- "test:run": "node --env-file=../../localhost-test-unit.env --import tsx --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=82 --test 'src/**/*.test.ts'",
31
+ "test:run": "node $NODE_TEST_FLAGS --env-file=../../localhost-test-unit.env --import tsx --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=90 --test 'src/**/*.test.ts'",
36
32
  "test:integ:local": "RUN_INTEG_TESTS=1 node --env-file=../../localhost-test-unit.env --import tsx --test 'src/backends/sqlite-vec.integ.test.ts'",
37
- "test:integ:pg": "RUN_INTEG_TESTS=1 node --env-file=../../localhost-test-unit.env --import tsx --test 'src/backends/pgvector.integ.test.ts'",
38
33
  "test:integ:model": "RUN_INTEG_TESTS=1 node --env-file=../../localhost-test-unit.env --import tsx --test 'src/semantic-search.integ.test.ts'",
39
34
  "test": "npm run test:typecheck && npm run test:run"
40
35
  },
@@ -44,14 +39,12 @@
44
39
  "@remit/api-openapi-types": "*",
45
40
  "better-sqlite3": "^12.11.1",
46
41
  "p-limit": "^6.2.0",
47
- "pg": "^8.0.0",
48
42
  "sqlite-vec": "^0.1.9",
49
43
  "zod": "*",
50
44
  "@aws-sdk/client-bedrock-runtime": "*",
51
45
  "@aws-sdk/client-s3vectors": "*",
52
46
  "@smithy/types": "^4.16.1",
53
- "@types/better-sqlite3": "^7.6.13",
54
- "@types/pg": "^8.20.0"
47
+ "@types/better-sqlite3": "^7.6.13"
55
48
  },
56
49
  "devDependencies": {
57
50
  "@remit/storage-service": "*",
@@ -24,8 +24,8 @@ export interface VectorStoreService {
24
24
  getByMessage(messageId: string): Promise<VectorRecord[]>;
25
25
  /**
26
26
  * Release any held connections (e.g. a pooled database client). Optional — the
27
- * in-memory and file backends hold nothing; the pgvector backend closes its
28
- * pool so a short-lived process (a test, a one-shot reindex) can exit cleanly.
27
+ * in-memory backend holds nothing; a pooled backend closes its pool so a
28
+ * short-lived process (a test, a one-shot reindex) can exit cleanly.
29
29
  */
30
30
  close?(): Promise<void>;
31
31
  }
@@ -5,7 +5,7 @@
5
5
  * native extension.
6
6
  *
7
7
  * Gated behind RUN_INTEG_TESTS because it loads the native better-sqlite3 and
8
- * sqlite-vec binaries, matching the pgvector integration suite.
8
+ * sqlite-vec binaries.
9
9
  *
10
10
  * npm run test:integ:local -w packages/search-service
11
11
  */
package/src/from-env.ts CHANGED
@@ -2,7 +2,6 @@ import type { DataType } from "@huggingface/transformers";
2
2
  import { BedrockEmbeddingService } from "./backends/bedrock.js";
3
3
  import type { VectorStoreService } from "./backends/memory.js";
4
4
  import { createMemoryVectorStore } from "./backends/memory.js";
5
- import { createPgVectorStore } from "./backends/pgvector.js";
6
5
  import { createS3VectorsBackend } from "./backends/s3-vectors.js";
7
6
  import { createSqliteVectorStore } from "./backends/sqlite-vec.js";
8
7
  import {
@@ -55,27 +54,19 @@ const parseDtype = (): DataType | undefined => {
55
54
  * composes a SearchService (the API, the search-index worker, the local
56
55
  * indexing shim) so the selection rule lives in one place:
57
56
  *
58
- * - `DATA_BACKEND=postgres` + `PG_CONNECTION_URL` set → pgvector (Postgres parity).
59
57
  * - `LOCAL_VECTORDB_PATH` set → persistent sqlite-vec (local dev).
60
58
  * - `S3_VECTORS_BUCKET_NAME` + `S3_VECTORS_INDEX_NAME` set → S3 Vectors (prod).
61
59
  * - otherwise → in-memory store (unit tests / default).
62
60
  *
63
61
  * `dimensions` should be the embedding service's dimension count. When a
64
- * dimension-typed store is selected (sqlite-vec's vec0 table, pgvector's
65
- * `VECTOR(n)` column), it is created with that dimension so the store and
62
+ * dimension-typed store is selected (sqlite-vec's vec0 table), it is created
63
+ * with that dimension so the store and
66
64
  * embedder always agree instead of failing confusingly at insert time (e.g. a
67
65
  * 64-dim deterministic embedder writing into a 384-wide column).
68
66
  */
69
67
  export const buildVectorStoreFromEnv = (
70
68
  dimensions?: number,
71
69
  ): VectorStoreService => {
72
- const pgConnectionUrl = process.env.PG_CONNECTION_URL;
73
- if (process.env.DATA_BACKEND === "postgres" && pgConnectionUrl) {
74
- return createPgVectorStore({
75
- connectionString: pgConnectionUrl,
76
- dimensions,
77
- });
78
- }
79
70
  const localPath = process.env.LOCAL_VECTORDB_PATH;
80
71
  if (localPath) {
81
72
  return createSqliteVectorStore({ path: localPath, dimensions });
@@ -96,9 +87,9 @@ export const buildVectorStoreFromEnv = (
96
87
  * Select an embedder from the environment, mirroring `buildVectorStoreFromEnv`:
97
88
  *
98
89
  * - `SEARCH_EMBEDDING_PROVIDER=local` → Transformers.js model (local dev). The
99
- * model is `SEARCH_EMBEDDING_MODEL_ID` (default MiniLM); the Postgres-parity
100
- * stack points it at a multilingual MiniLM so the ~50% non-English mail corpus
101
- * embeds well. Both models are 384-dim, so the pgvector column is stable.
90
+ * model is `SEARCH_EMBEDDING_MODEL_ID` (default MiniLM); the self-host stack
91
+ * points it at a multilingual MiniLM so the ~50% non-English mail corpus
92
+ * embeds well. Both models are 384-dim, so the vector column is stable.
102
93
  * - `SEARCH_EMBEDDING_PROVIDER=bedrock` → Bedrock Titan (prod).
103
94
  * - otherwise → deterministic bag-of-words embedder (unit tests / default).
104
95
  *
@@ -1,174 +0,0 @@
1
- /**
2
- * Exercises the pgvector store against a real local Postgres with the `vector`
3
- * extension (the pg-parity container). Proves upsert, cosine ranking,
4
- * multi-condition scoped filtering, content-hash lookup, and delete — the
5
- * multi-condition filter is the case that silently emptied results on S3
6
- * Vectors, so it is tested explicitly here.
7
- *
8
- * Gated behind RUN_INTEG_TESTS. Point PG_CONNECTION_URL at a database whose
9
- * `vector` extension is enabled (default: local remit_test).
10
- *
11
- * npm run test:integ:pg -w packages/search-service
12
- */
13
- import assert from "node:assert";
14
- import { randomUUID } from "node:crypto";
15
- import { after, before, describe, test } from "node:test";
16
- import pg from "pg";
17
- import type { ChunkMetadata, VectorRecord } from "../types.js";
18
- import type { VectorStoreService } from "./memory.js";
19
- import { createPgVectorStore } from "./pgvector.js";
20
-
21
- const RUN = process.env.RUN_INTEG_TESTS === "1";
22
- const CONNECTION_STRING =
23
- process.env.PG_CONNECTION_URL ??
24
- "postgresql://remit:remit@localhost:5432/remit_test";
25
-
26
- const DIMENSIONS = 4;
27
-
28
- const meta = (
29
- over: Partial<ChunkMetadata> & { messageId: string },
30
- ): ChunkMetadata => ({
31
- threadId: "t-1",
32
- accountConfigId: "acc-1",
33
- mailboxIds: ["mb-1"],
34
- chunkType: "body",
35
- sentDate: 1000,
36
- isRead: false,
37
- hasAttachment: false,
38
- hasStars: false,
39
- ...over,
40
- });
41
-
42
- const record = (
43
- chunkId: string,
44
- vector: number[],
45
- over: Partial<ChunkMetadata> & { messageId: string },
46
- ): VectorRecord => ({ chunkId, vector, metadata: meta(over) });
47
-
48
- describe("pgvector store (integration)", { skip: !RUN }, () => {
49
- const table = `message_embedding_test_${randomUUID().replace(/-/g, "")}`;
50
- let store: VectorStoreService;
51
- let adminPool: pg.Pool;
52
-
53
- before(() => {
54
- adminPool = new pg.Pool({ connectionString: CONNECTION_STRING });
55
- store = createPgVectorStore({
56
- connectionString: CONNECTION_STRING,
57
- dimensions: DIMENSIONS,
58
- tableName: table,
59
- });
60
- });
61
-
62
- after(async () => {
63
- await adminPool.query(`DROP TABLE IF EXISTS ${table}`);
64
- await adminPool.end();
65
- });
66
-
67
- test("upsert then query ranks by cosine similarity", async () => {
68
- await store.upsert([
69
- record("c-x", [1, 0, 0, 0], { messageId: "m-x", contentHash: "hx" }),
70
- record("c-y", [0, 1, 0, 0], { messageId: "m-y", contentHash: "hy" }),
71
- record("c-z", [0.9, 0.1, 0, 0], { messageId: "m-z", contentHash: "hz" }),
72
- ]);
73
-
74
- const matches = await store.query({ vector: [1, 0, 0, 0], topK: 3 });
75
-
76
- assert.equal(matches[0].chunkId, "c-x");
77
- assert.equal(matches[1].chunkId, "c-z");
78
- assert.equal(matches[2].chunkId, "c-y");
79
- assert.ok(matches[0].score > matches[1].score);
80
- assert.ok(matches[0].score > 0.99);
81
- });
82
-
83
- test("multi-condition scoped filter narrows to the matching partition", async () => {
84
- await store.upsert([
85
- record("s-a", [1, 0, 0, 0], {
86
- messageId: "m-a",
87
- accountConfigId: "acc-A",
88
- mailboxIds: ["inbox"],
89
- isRead: true,
90
- }),
91
- record("s-b", [1, 0, 0, 0], {
92
- messageId: "m-b",
93
- accountConfigId: "acc-A",
94
- mailboxIds: ["archive"],
95
- isRead: true,
96
- }),
97
- record("s-c", [1, 0, 0, 0], {
98
- messageId: "m-c",
99
- accountConfigId: "acc-B",
100
- mailboxIds: ["inbox"],
101
- isRead: true,
102
- }),
103
- record("s-d", [1, 0, 0, 0], {
104
- messageId: "m-d",
105
- accountConfigId: "acc-A",
106
- mailboxIds: ["inbox"],
107
- isRead: false,
108
- }),
109
- ]);
110
-
111
- const matches = await store.query({
112
- vector: [1, 0, 0, 0],
113
- topK: 10,
114
- filter: { accountConfigId: "acc-A", mailboxId: "inbox", isRead: true },
115
- });
116
-
117
- const ids = matches.map((m) => m.chunkId);
118
- assert.deepEqual(ids, ["s-a"]);
119
- });
120
-
121
- test("existingContentHashes returns stored hashes only for known keys", async () => {
122
- const hashes = await store.existingContentHashes(["c-x", "c-y", "missing"]);
123
- assert.equal(hashes.get("c-x"), "hx");
124
- assert.equal(hashes.get("c-y"), "hy");
125
- assert.equal(hashes.has("missing"), false);
126
- });
127
-
128
- test("upsert overwrites an existing chunk in place", async () => {
129
- await store.upsert([
130
- record("c-x", [0, 0, 0, 1], { messageId: "m-x", contentHash: "hx2" }),
131
- ]);
132
- const hashes = await store.existingContentHashes(["c-x"]);
133
- assert.equal(hashes.get("c-x"), "hx2");
134
- });
135
-
136
- test("getByMessage returns every chunk of a message with its vector and metadata", async () => {
137
- await store.upsert([
138
- record("g-sub", [1, 0, 0, 0], {
139
- messageId: "m-get",
140
- chunkType: "subject",
141
- contentHash: "hg1",
142
- }),
143
- record("g-body", [0, 1, 0, 0], {
144
- messageId: "m-get",
145
- chunkType: "body",
146
- contentHash: "hg2",
147
- }),
148
- record("g-other", [0, 0, 1, 0], { messageId: "m-other" }),
149
- ]);
150
-
151
- const records = await store.getByMessage("m-get");
152
-
153
- const byId = new Map(records.map((r) => [r.chunkId, r]));
154
- assert.equal(records.length, 2, "only the message's own chunks");
155
- assert.deepEqual(byId.get("g-sub")?.vector, [1, 0, 0, 0]);
156
- assert.deepEqual(byId.get("g-body")?.vector, [0, 1, 0, 0]);
157
- assert.equal(byId.get("g-sub")?.metadata.chunkType, "subject");
158
- assert.equal(byId.get("g-body")?.metadata.messageId, "m-get");
159
- });
160
-
161
- test("getByMessage returns an empty array for an unknown message", async () => {
162
- assert.deepEqual(await store.getByMessage("m-absent"), []);
163
- });
164
-
165
- test("delete removes every chunk of a message", async () => {
166
- await store.upsert([
167
- record("d-1", [1, 0, 0, 0], { messageId: "m-del" }),
168
- record("d-2", [0, 1, 0, 0], { messageId: "m-del" }),
169
- ]);
170
- await store.delete({ messageId: "m-del" });
171
- const hashes = await store.existingContentHashes(["d-1", "d-2"]);
172
- assert.equal(hashes.size, 0);
173
- });
174
- });
@@ -1,306 +0,0 @@
1
- import type { Pool, PoolClient } from "pg";
2
- import type {
3
- ChunkMetadata,
4
- VectorMatch,
5
- VectorQuery,
6
- VectorQueryFilter,
7
- VectorRecord,
8
- } from "../types.js";
9
- import type { VectorStoreService } from "./memory.js";
10
- import { runtimeImport } from "./runtime-import.js";
11
-
12
- type PgModule = {
13
- default: { Pool: new (config: { connectionString: string }) => Pool };
14
- };
15
-
16
- /**
17
- * pgvector-backed vector store for the Postgres-parity stack. Vectors live in
18
- * the same Postgres as their message rows (one row per chunk), so there is no
19
- * write amplification and scoped queries are plain SQL WHERE clauses combined
20
- * with the ANN ordering — the equality/range filters run in the same query as
21
- * the KNN instead of post-filtering a fixed top-k (which silently drops recall).
22
- *
23
- * `mailboxId` is a single-value scalar because the indexing pipeline always
24
- * writes a one-element `mailboxIds`; membership reduces to equality — same as
25
- * the sqlite-vec backend.
26
- *
27
- * The store self-provisions its table and indexes on first use (the `vector`
28
- * extension must already be enabled on the database — the local pg container
29
- * does this at startup). This mirrors the sqlite-vec backend, which owns its
30
- * own CREATE, and keeps the embedding table out of the drizzle schema that the
31
- * extension-less embedded-postgres test harness pushes.
32
- */
33
- const DEFAULT_DIMENSIONS = 384;
34
- const DEFAULT_TABLE = "message_embedding";
35
-
36
- const createTableSql = (table: string, dimensions: number): string => `
37
- CREATE TABLE IF NOT EXISTS ${table} (
38
- chunk_id TEXT PRIMARY KEY,
39
- message_id TEXT NOT NULL,
40
- account_config_id TEXT NOT NULL,
41
- mailbox_id TEXT NOT NULL,
42
- chunk_type TEXT NOT NULL,
43
- sent_date BIGINT NOT NULL,
44
- is_read BOOLEAN NOT NULL,
45
- has_attachment BOOLEAN NOT NULL,
46
- has_stars BOOLEAN NOT NULL,
47
- embedding VECTOR(${dimensions}) NOT NULL,
48
- metadata JSONB NOT NULL
49
- );
50
- `;
51
-
52
- const createIndexesSql = (table: string): string[] => [
53
- `CREATE INDEX IF NOT EXISTS ${table}_message_id_idx ON ${table} (message_id);`,
54
- `CREATE INDEX IF NOT EXISTS ${table}_account_config_id_idx ON ${table} (account_config_id);`,
55
- `CREATE INDEX IF NOT EXISTS ${table}_embedding_hnsw_idx ON ${table} USING hnsw (embedding vector_cosine_ops);`,
56
- ];
57
-
58
- const toVectorLiteral = (vector: number[]): string => `[${vector.join(",")}]`;
59
-
60
- // pgvector renders a `vector` column as `[1,2,3]`; parse it back to a number[]
61
- // for the anchor pooling read path (getByMessage).
62
- const parseVectorLiteral = (literal: string): number[] => {
63
- const inner = literal.trim().replace(/^\[/, "").replace(/\]$/, "");
64
- if (inner.length === 0) return [];
65
- return inner.split(",").map((n) => Number(n));
66
- };
67
-
68
- interface WhereClause {
69
- sql: string;
70
- params: unknown[];
71
- }
72
-
73
- const buildFilterClause = (
74
- filter: VectorQueryFilter | undefined,
75
- nextParamIndex: number,
76
- ): WhereClause => {
77
- const conditions: string[] = [];
78
- const params: unknown[] = [];
79
- let i = nextParamIndex;
80
- if (!filter) return { sql: "", params };
81
-
82
- if (filter.accountConfigId !== undefined) {
83
- conditions.push(`account_config_id = $${i++}`);
84
- params.push(filter.accountConfigId);
85
- }
86
- if (filter.mailboxId !== undefined) {
87
- conditions.push(`mailbox_id = $${i++}`);
88
- params.push(filter.mailboxId);
89
- }
90
- if (filter.chunkType !== undefined) {
91
- conditions.push(`chunk_type = $${i++}`);
92
- params.push(filter.chunkType);
93
- }
94
- if (filter.category !== undefined) {
95
- conditions.push(`metadata->>'category' = $${i++}`);
96
- params.push(filter.category);
97
- }
98
- if (filter.hasAttachment !== undefined) {
99
- conditions.push(`has_attachment = $${i++}`);
100
- params.push(filter.hasAttachment);
101
- }
102
- if (filter.hasStars !== undefined) {
103
- conditions.push(`has_stars = $${i++}`);
104
- params.push(filter.hasStars);
105
- }
106
- if (filter.isRead !== undefined) {
107
- conditions.push(`is_read = $${i++}`);
108
- params.push(filter.isRead);
109
- }
110
- if (filter.sentDateRange?.from !== undefined) {
111
- conditions.push(`sent_date >= $${i++}`);
112
- params.push(Math.trunc(filter.sentDateRange.from));
113
- }
114
- if (filter.sentDateRange?.to !== undefined) {
115
- conditions.push(`sent_date <= $${i++}`);
116
- params.push(Math.trunc(filter.sentDateRange.to));
117
- }
118
-
119
- return {
120
- sql: conditions.length > 0 ? ` AND ${conditions.join(" AND ")}` : "",
121
- params,
122
- };
123
- };
124
-
125
- export interface PgVectorStoreConfig {
126
- connectionString: string;
127
- dimensions?: number;
128
- tableName?: string;
129
- }
130
-
131
- export const createPgVectorStore = (
132
- config: PgVectorStoreConfig,
133
- ): VectorStoreService => {
134
- const dimensions = config.dimensions ?? DEFAULT_DIMENSIONS;
135
- const table = config.tableName ?? DEFAULT_TABLE;
136
- let poolPromise: Promise<Pool> | null = null;
137
-
138
- const getPool = async (): Promise<Pool> => {
139
- if (poolPromise) return poolPromise;
140
- poolPromise = (async () => {
141
- const pg = await runtimeImport<PgModule>("pg");
142
- const pool = new pg.default.Pool({
143
- connectionString: config.connectionString,
144
- });
145
- await pool.query(createTableSql(table, dimensions));
146
- for (const sql of createIndexesSql(table)) {
147
- await pool.query(sql);
148
- }
149
- return pool;
150
- })();
151
- return poolPromise;
152
- };
153
-
154
- const upsert = async (vectors: VectorRecord[]): Promise<void> => {
155
- if (vectors.length === 0) return;
156
- const pool = await getPool();
157
- const client = await pool.connect();
158
- try {
159
- await client.query("BEGIN");
160
- for (const record of vectors) {
161
- const m = record.metadata;
162
- await client.query(
163
- `
164
- INSERT INTO ${table} (
165
- chunk_id, message_id, account_config_id, mailbox_id, chunk_type,
166
- sent_date, is_read, has_attachment, has_stars, embedding, metadata
167
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::vector, $11::jsonb)
168
- ON CONFLICT (chunk_id) DO UPDATE SET
169
- message_id = EXCLUDED.message_id,
170
- account_config_id = EXCLUDED.account_config_id,
171
- mailbox_id = EXCLUDED.mailbox_id,
172
- chunk_type = EXCLUDED.chunk_type,
173
- sent_date = EXCLUDED.sent_date,
174
- is_read = EXCLUDED.is_read,
175
- has_attachment = EXCLUDED.has_attachment,
176
- has_stars = EXCLUDED.has_stars,
177
- embedding = EXCLUDED.embedding,
178
- metadata = EXCLUDED.metadata
179
- `,
180
- [
181
- record.chunkId,
182
- m.messageId,
183
- m.accountConfigId,
184
- m.mailboxIds[0] ?? "",
185
- m.chunkType,
186
- Math.trunc(m.sentDate),
187
- m.isRead,
188
- m.hasAttachment,
189
- m.hasStars,
190
- toVectorLiteral(record.vector),
191
- JSON.stringify(m),
192
- ],
193
- );
194
- }
195
- await client.query("COMMIT");
196
- } catch (error) {
197
- await client.query("ROLLBACK");
198
- throw error;
199
- } finally {
200
- client.release();
201
- }
202
- };
203
-
204
- const query = async (params: VectorQuery): Promise<VectorMatch[]> => {
205
- const pool = await getPool();
206
- const vectorLiteral = toVectorLiteral(params.vector);
207
- const { sql: filterSql, params: filterParams } = buildFilterClause(
208
- params.filter,
209
- 3,
210
- );
211
- // Iterative index scan keeps a selective metadata filter from under-filling
212
- // the top-k: pgvector re-scans the HNSW graph until topK matches pass the
213
- // WHERE instead of stopping at the first ef_search candidates.
214
- const client: PoolClient = await pool.connect();
215
- try {
216
- await client.query("BEGIN");
217
- await client.query("SET LOCAL hnsw.iterative_scan = relaxed_order");
218
- const result = await client.query<{
219
- chunk_id: string;
220
- score: number;
221
- metadata: ChunkMetadata;
222
- }>(
223
- `
224
- SELECT chunk_id, metadata, 1 - (embedding <=> $1::vector) AS score
225
- FROM ${table}
226
- WHERE TRUE${filterSql}
227
- ORDER BY embedding <=> $1::vector
228
- LIMIT $2
229
- `,
230
- [vectorLiteral, params.topK, ...filterParams],
231
- );
232
- await client.query("COMMIT");
233
- return result.rows.map((row) => ({
234
- chunkId: row.chunk_id,
235
- score: Number(row.score),
236
- metadata: row.metadata,
237
- }));
238
- } catch (error) {
239
- await client.query("ROLLBACK");
240
- throw error;
241
- } finally {
242
- client.release();
243
- }
244
- };
245
-
246
- const existingContentHashes = async (
247
- chunkIds: string[],
248
- ): Promise<Map<string, string>> => {
249
- const out = new Map<string, string>();
250
- if (chunkIds.length === 0) return out;
251
- const pool = await getPool();
252
- const result = await pool.query<{
253
- chunk_id: string;
254
- content_hash: string | null;
255
- }>(
256
- `SELECT chunk_id, metadata->>'contentHash' AS content_hash
257
- FROM ${table} WHERE chunk_id = ANY($1::text[])`,
258
- [chunkIds],
259
- );
260
- for (const row of result.rows) {
261
- if (row.content_hash !== null) out.set(row.chunk_id, row.content_hash);
262
- }
263
- return out;
264
- };
265
-
266
- const getByMessage = async (messageId: string): Promise<VectorRecord[]> => {
267
- const pool = await getPool();
268
- const result = await pool.query<{
269
- chunk_id: string;
270
- embedding: string;
271
- metadata: ChunkMetadata;
272
- }>(
273
- `SELECT chunk_id, embedding::text AS embedding, metadata
274
- FROM ${table} WHERE message_id = $1`,
275
- [messageId],
276
- );
277
- return result.rows.map((row) => ({
278
- chunkId: row.chunk_id,
279
- vector: parseVectorLiteral(row.embedding),
280
- metadata: row.metadata,
281
- }));
282
- };
283
-
284
- const del = async (filter: { messageId: string }): Promise<void> => {
285
- const pool = await getPool();
286
- await pool.query(`DELETE FROM ${table} WHERE message_id = $1`, [
287
- filter.messageId,
288
- ]);
289
- };
290
-
291
- const close = async (): Promise<void> => {
292
- if (!poolPromise) return;
293
- const pool = await poolPromise;
294
- poolPromise = null;
295
- await pool.end();
296
- };
297
-
298
- return {
299
- upsert,
300
- query,
301
- existingContentHashes,
302
- getByMessage,
303
- delete: del,
304
- close,
305
- };
306
- };
package/src/pgvector.ts DELETED
@@ -1,4 +0,0 @@
1
- export {
2
- createPgVectorStore,
3
- type PgVectorStoreConfig,
4
- } from "./backends/pgvector.js";