@chatcops/store-postgres 0.0.0-stage → 0.2.0

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.
@@ -0,0 +1,147 @@
1
+ import { segmentWords } from '@chatcops/core';
2
+ /** Postgres pgvector-backed storage for embedded page chunks. */
3
+ export class PostgresVectorStore {
4
+ dimensions;
5
+ db;
6
+ table;
7
+ constructor(dimensions, db, table) {
8
+ this.dimensions = dimensions;
9
+ this.db = db;
10
+ this.table = table;
11
+ if (!Number.isInteger(dimensions) || dimensions < 1 || dimensions > 16_000) {
12
+ throw new Error(`dimensions must be a whole number from 1 to 16000; got ${dimensions}.`);
13
+ }
14
+ }
15
+ async upsert(records) {
16
+ for (const record of records)
17
+ this.checkVector(record.vector, `record ${record.id}`);
18
+ for (const record of records) {
19
+ const words = segmentWords(record.text).join(' ');
20
+ await this.db.query(`INSERT INTO ${this.table} (id, source_id, source, text, metadata, embedding, keyword_vector)
21
+ VALUES ($1, $2, $3::text::jsonb, $4, $5::text::jsonb, $6::text::vector, to_tsvector('simple', $7))
22
+ ON CONFLICT (id) DO UPDATE SET source_id = EXCLUDED.source_id, source = EXCLUDED.source,
23
+ text = EXCLUDED.text, metadata = EXCLUDED.metadata, embedding = EXCLUDED.embedding,
24
+ keyword_vector = EXCLUDED.keyword_vector`, [record.id, record.source.id, JSON.stringify(record.source), record.text, JSON.stringify(record.metadata ?? {}), toVector(record.vector), words]);
25
+ }
26
+ }
27
+ async query({ vector, topK, filter }) {
28
+ this.checkVector(vector, 'the query');
29
+ const limit = limitFor(topK);
30
+ if (!limit)
31
+ return [];
32
+ const params = [toVector(vector)];
33
+ const where = metadataWhere(filter, params);
34
+ params.push(limit);
35
+ const sql = `WITH nearest AS MATERIALIZED (
36
+ SELECT id, text, source, metadata, embedding <=> $1::text::vector AS distance
37
+ FROM ${this.table} ${where}
38
+ ORDER BY embedding <=> $1::text::vector
39
+ LIMIT $${params.length}
40
+ )
41
+ SELECT id, text, source, metadata, coalesce(1 - distance, 0)::float8 AS score
42
+ FROM nearest ORDER BY distance + 0, id`;
43
+ const hasFilter = Object.keys(filter ?? {}).length > 0;
44
+ const result = hasFilter
45
+ ? await inTransaction(this.db, async (tx) => {
46
+ await tx.query("SET LOCAL hnsw.iterative_scan = 'relaxed_order'");
47
+ return tx.query(sql, params);
48
+ })
49
+ : await this.db.query(sql, params);
50
+ return result.rows.map(asScoredRecord);
51
+ }
52
+ async keywordQuery({ text, topK, filter }) {
53
+ const limit = limitFor(topK);
54
+ const words = segmentWords(text).join(' ');
55
+ if (!limit || !words)
56
+ return [];
57
+ const params = [words];
58
+ const where = metadataWhere(filter, params);
59
+ params.push(limit);
60
+ const { rows } = await this.db.query(
61
+ // Any of the words, not all of them (plainto_tsquery would AND them):
62
+ // a question has more words than any one page. Each lexeme is quoted,
63
+ // so nothing in the text is read as tsquery syntax.
64
+ `WITH search AS (
65
+ SELECT to_tsquery('simple', string_agg(quote_literal(lexeme), ' | ')) AS query
66
+ FROM unnest(tsvector_to_array(to_tsvector('simple', $1::text))) AS lexeme
67
+ )
68
+ SELECT id, text, source, metadata, ts_rank(keyword_vector, search.query)::float8 AS score
69
+ FROM ${this.table}, search
70
+ WHERE keyword_vector @@ search.query${where ? ` AND ${where.slice('WHERE '.length)}` : ''}
71
+ ORDER BY score DESC, id LIMIT $${params.length}`, params);
72
+ return rows.map(asScoredRecord);
73
+ }
74
+ async deleteBySource(sourceId) {
75
+ await this.db.query(`DELETE FROM ${this.table} WHERE source_id = $1`, [sourceId]);
76
+ }
77
+ checkVector(vector, what) {
78
+ if (vector.length !== this.dimensions) {
79
+ throw new Error(`The vector for ${what} has ${vector.length} dimensions, but this store holds ${this.dimensions}.`);
80
+ }
81
+ if (!vector.every(Number.isFinite))
82
+ throw new Error(`The vector for ${what} has a value that isn't a finite number.`);
83
+ }
84
+ }
85
+ function toVector(vector) {
86
+ return `[${Array.from(vector).join(',')}]`;
87
+ }
88
+ function limitFor(topK) {
89
+ return Number.isFinite(topK) && topK >= 1 ? Math.floor(topK) : 0;
90
+ }
91
+ function metadataWhere(filter, params) {
92
+ if (!filter)
93
+ return '';
94
+ const clauses = [];
95
+ for (const [key, value] of Object.entries(filter)) {
96
+ params.push(key);
97
+ const keyParam = `$${params.length}`;
98
+ if (Array.isArray(value)) {
99
+ params.push(JSON.stringify(value));
100
+ clauses.push(`EXISTS (SELECT 1 FROM jsonb_array_elements($${params.length}::text::jsonb) AS wanted(value) WHERE metadata -> ${keyParam}::text = wanted.value)`);
101
+ }
102
+ else {
103
+ params.push(JSON.stringify(value));
104
+ clauses.push(`metadata -> ${keyParam}::text = $${params.length}::text::jsonb`);
105
+ }
106
+ }
107
+ return clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
108
+ }
109
+ function asScoredRecord(row) {
110
+ return {
111
+ id: row.id,
112
+ text: row.text,
113
+ source: row.source,
114
+ ...(row.metadata && Object.keys(row.metadata).length ? { metadata: row.metadata } : {}),
115
+ score: Number(row.score),
116
+ };
117
+ }
118
+ async function inTransaction(db, run) {
119
+ if (db.transaction)
120
+ return db.transaction(run);
121
+ const pool = db;
122
+ const fromPool = typeof pool.connect === 'function';
123
+ const client = fromPool ? await pool.connect() : db;
124
+ if (!fromPool && !client.connection) {
125
+ throw new Error('Filtered pgvector queries need a transaction-capable database connection for HNSW iterative scans.');
126
+ }
127
+ try {
128
+ await client.query('BEGIN');
129
+ const result = await run(client);
130
+ await client.query('COMMIT');
131
+ return result;
132
+ }
133
+ catch (error) {
134
+ try {
135
+ await client.query('ROLLBACK');
136
+ }
137
+ catch {
138
+ // Keep the query error if the connection has already been lost.
139
+ }
140
+ throw error;
141
+ }
142
+ finally {
143
+ if (fromPool)
144
+ client.release?.();
145
+ }
146
+ }
147
+ //# sourceMappingURL=vectors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vectors.js","sourceRoot":"","sources":["../src/vectors.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,67 @@
1
1
  {
2
2
  "name": "@chatcops/store-postgres",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Postgres storage for ChatCops: conversations, counters, keys and analytics events",
5
+ "keywords": [
6
+ "chatcops",
7
+ "chatbot",
8
+ "postgres",
9
+ "neon",
10
+ "storage"
11
+ ],
12
+ "homepage": "https://chat.codercops.com",
13
+ "bugs": "https://github.com/codercops/chatcops/issues",
14
+ "type": "module",
15
+ "main": "dist/index.js",
16
+ "types": "dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ }
22
+ },
23
+ "bin": {
24
+ "chatcops-postgres": "./dist/bin.js"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "peerDependencies": {
30
+ "pg": "^8.13.0",
31
+ "@chatcops/core": "^0.6.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "pg": {
35
+ "optional": true
36
+ }
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22",
40
+ "@types/pg": "^8.11.10",
41
+ "pg": "^8.13.1",
42
+ "postgres": "^3.4.5",
43
+ "typescript": "^5.7.0",
44
+ "vitest": "^4.1.11",
45
+ "@chatcops/core": "0.6.0"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/codercops/chatcops.git",
50
+ "directory": "packages/store-postgres"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "license": "MIT",
56
+ "engines": {
57
+ "node": ">=22"
58
+ },
59
+ "sideEffects": false,
60
+ "scripts": {
61
+ "build": "tsc",
62
+ "typecheck": "tsc --noEmit",
63
+ "test": "vitest run",
64
+ "test:watch": "vitest",
65
+ "clean": "rm -rf dist *.tsbuildinfo"
66
+ }
6
67
  }