-
Notifications
You must be signed in to change notification settings - Fork 32
feat: Add support for cds.Vector on SQLite and Postgres
#1676
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 62 commits
c957c30
f5b18f0
77eaffd
4d577ba
fad7e12
8b636c5
937d410
415a5d8
1fc2404
dd6f20f
c7a0624
7828bcb
faebed1
86d0603
b4e58f1
530aa86
2034e6d
1efa2e5
6609105
cf7e643
76ecc98
f5dfb08
61700e7
3550dfb
cf78fa5
a20fa99
636f612
9b802e4
7bbebe0
29e93ca
1e119cf
99b8b47
3576232
88ffcc9
b30d66b
a525ab1
6e74619
a76461b
364c20b
cea1aed
7fd0133
365d8a3
f2c0c1b
e0d3a1b
cd98416
b98e412
e746d9a
36fd908
c3ba20c
2796628
b1b73d3
4f49b5b
998cef1
32e5583
cdbefdc
ff73234
4a82f1a
93512f8
a3ae0bb
c61a964
0940db2
43149fa
b4921e0
d69bd65
cf73757
bae2150
a1de2e3
ac24478
0ec572e
5e8b463
701f065
21098dd
004c277
841f92e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ const cds = require('@sap/cds') | |
| const crypto = require('crypto') | ||
| const { Writable, Readable } = require('stream') | ||
| const sessionVariableMap = require('./session.json') | ||
| const pgvector = require('pgvector/pg'); | ||
|
vkozyura marked this conversation as resolved.
Outdated
|
||
|
|
||
| const LOG = cds.log('sql|db') | ||
|
|
||
|
|
@@ -49,6 +50,14 @@ class PostgresService extends SQLService { | |
| try { | ||
| const dbc = new Client({ ...credentials, ...clientOptions }) | ||
| await dbc.connect() | ||
| // cds.Vector support for PG | ||
| try { | ||
| await dbc.query('CREATE EXTENSION IF NOT EXISTS vector') | ||
| await pgvector.registerTypes(dbc) | ||
| } catch (e) { | ||
| const LOG = cds.log('postgres') | ||
| LOG.debug('pgvector extension not available, skipping vector support:', e.message) | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should not be ran every
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. commented out with a revisit containing possible options - pg tests are currently disabled |
||
| dbc.open = true | ||
| dbc.on('end', () => { dbc.open = false }) | ||
| return dbc | ||
|
|
@@ -535,6 +544,7 @@ GROUP BY k | |
| DateTime: () => 'TIMESTAMP', | ||
| Timestamp: () => 'TIMESTAMP', | ||
| Map: () => 'JSONB', | ||
| Vector: () => 'vector', | ||
|
|
||
| // HANA Types | ||
| 'cds.hana.CLOB': () => 'BYTEA', | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| function cosineSimilarity(a, b) { | ||
| if (a == null || b == null) return null | ||
| let dot = 0, normA = 0, normB = 0 | ||
| for (let i = 0; i < a.length; i++) { | ||
| dot += a[i] * b[i] | ||
| normA += a[i] * a[i] | ||
| normB += b[i] * b[i] | ||
| } | ||
| const denom = Math.sqrt(normA) * Math.sqrt(normB) | ||
| return denom === 0 ? 0 : dot / denom | ||
| } | ||
|
|
||
| function l2Distance(a, b) { | ||
| if (a == null || b == null) return null | ||
| let sum = 0 | ||
| for (let i = 0; i < a.length; i++) { | ||
| const diff = a[i] - b[i] | ||
| sum += diff * diff | ||
| } | ||
| return Math.sqrt(sum) | ||
| } | ||
|
|
||
| function l2Normalize(v) { | ||
| if (v == null) return null | ||
| let norm = 0 | ||
| for (let i = 0; i < v.length; i++) norm += v[i] * v[i] | ||
| if (norm === 0) return v | ||
| norm = Math.sqrt(norm) | ||
| for (let i = 0; i < v.length; i++) v[i] /= norm | ||
| return v | ||
| } | ||
|
|
||
| /** | ||
| * Deterministic synchronous hash-based embedding function for SQLite. | ||
| * NOTE: If a synchronous embedding library becomes available for Node.js, | ||
| * it can be integrated here to replace the hash-based implementation. | ||
| */ | ||
| function hashEmbedding(text, dimensions = 384) { | ||
| if (text == null) return null | ||
| const vector = new Float32Array(dimensions) | ||
| const normalized = text.toLowerCase() | ||
| const ngramSize = 3 | ||
|
|
||
| if (normalized.length >= ngramSize) { | ||
| for (let i = 0; i <= normalized.length - ngramSize; i++) | ||
| project(ngramHash(normalized, i, ngramSize), vector, dimensions) | ||
| } else { | ||
| for (let i = 0; i < normalized.length; i++) | ||
| project(normalized.charCodeAt(i), vector, dimensions) | ||
| } | ||
| return Array.from(l2Normalize(vector)) | ||
| } | ||
|
|
||
| function ngramHash(text, start, len) { | ||
| let hash = 0x811c9dc5 | ||
| for (let i = start; i < start + len; i++) { | ||
| hash ^= text.charCodeAt(i) | ||
| hash = Math.imul(hash, 0x01000193) | ||
| } | ||
| return hash | ||
| } | ||
|
|
||
| function project(hash, vector, dimensions) { | ||
| for (let band = 0; band < 4; band++) { | ||
| const h = rehash(hash, band) | ||
| vector[Math.abs(h % dimensions)] += ((h >>> 16) & 1) === 0 ? 1.0 : -1.0 | ||
| } | ||
| } | ||
|
|
||
| function rehash(hash, band) { | ||
| let h = hash ^ Math.imul(band, 0x9e3779b9) | ||
| h ^= h >>> 16 | ||
| h = Math.imul(h, 0x45d9f3b) | ||
| h ^= h >>> 16 | ||
| return h | ||
| } | ||
|
|
||
| function toFloatArray(vector) { | ||
| if (vector == null) return null | ||
| if (vector instanceof Float32Array) return Array.from(vector) | ||
| if (Buffer.isBuffer(vector)) return JSON.parse(vector.toString('utf8')) | ||
| if (vector instanceof Uint8Array) return JSON.parse(Buffer.from(vector).toString('utf8')) | ||
| if (typeof vector === 'string') return JSON.parse(vector) | ||
| if (Array.isArray(vector)) return vector | ||
| throw new Error(`Unsupported vector type: ${typeof vector}`) | ||
| } | ||
|
|
||
| function fromFloatArray(arr, original) { | ||
| return original instanceof Float32Array ? new Float32Array(arr) : JSON.stringify(arr) | ||
| } | ||
|
|
||
| module.exports = { cosineSimilarity, l2Distance, l2Normalize, hashEmbedding, toFloatArray, fromFloatArray } | ||
|
vkozyura marked this conversation as resolved.
Outdated
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the source of
cds.env.ai?.embeddings?.remoteSource? If this comes from the previous PR or the AI plugin we should probably communicate this with java as well as they are looking into a global option as well.I would expect also an global setting for
model_and_versionso that it doesn't need to be hardcoded into models.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cds.env.ai.embeddings.remoteSource configuration is introduced by the @cap-js/ai.
Java stack has no ai property in CdsProperties.java. The Java test hardcodes the model.
The Parameter model_and_version is called embeddingModel in Java.
Suggestion: Sync with Java as follow-up