-
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
Open
vkozyura
wants to merge
73
commits into
main
Choose a base branch
from
feat/vector-simplified
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
73 commits
Select commit
Hold shift + click to select a range
c957c30
Init
schiwekM f5b18f0
ESM -> CJS
schiwekM 77eaffd
Merge branch 'main' into feat/vector-parity
schiwekM 4d577ba
Get rid of unused code
schiwekM fad7e12
Replace synckit with own impl
schiwekM 8b636c5
Merge branch 'main' into feat/vector-parity
schiwekM 937d410
Simplify embeddings logic
schiwekM 415a5d8
Add tests
schiwekM 1fc2404
Merge branch 'main' into feat/vector-parity
schiwekM dd6f20f
Update cql-functions.js
schiwekM c7a0624
Update sqlite-vector-worker.js
schiwekM 7828bcb
Update index.cds
schiwekM faebed1
Merge branch 'main' into feat/vector-parity
schiwekM 86d0603
Generating vectors without WorkerThreads
schiwekM b4e58f1
Update embedding.js
schiwekM 530aa86
Merge branch 'main' into feat/vector-parity
schiwekM 2034e6d
Add PG compatibility
schiwekM 1efa2e5
Update index.js
schiwekM 6609105
Own parity impl to avoid dependency
schiwekM cf7e643
Merge branch 'main' into feat/vector-parity
schiwekM 76ecc98
Update index.js
schiwekM f5dfb08
Remove unsupported functions
schiwekM 61700e7
Merge branch 'main' into feat/vector-parity
schiwekM 3550dfb
Update cql-functions.js
schiwekM cf78fa5
Update functions.test.js
schiwekM a20fa99
Update index.js
schiwekM 636f612
Fix faulty test setup
schiwekM 9b802e4
Merge branch 'main' into feat/vector-parity
schiwekM 7bbebe0
Update pg-stack.yml
schiwekM 29e93ca
Fix
schiwekM 1e119cf
Update vectors.cds
schiwekM 99b8b47
manual cleanup of sqlite vector implementation
BobdenOs 3576232
optional onnx runtime dependency
BobdenOs 88ffcc9
Attempt to convince eslint that TextDecoder is an esm global variable
BobdenOs b30d66b
allow onnx runtime dependency to be not installed
BobdenOs a525ab1
still do nothing in the catch block
BobdenOs 6e74619
refactor: simplify vector embedding implementation
vkozyura a76461b
refactor: extract shared vector math functions
vkozyura 364c20b
refactor: deduplicate VECTOR_EMBEDDING error message
vkozyura cea1aed
refactor: reduce comments and simplify code
vkozyura 7fd0133
test: add vector embedding tests
vkozyura 365d8a3
fix: update vector tests to work without chai closeTo
vkozyura f2c0c1b
simplify: use synchronous hash-based VECTOR_EMBEDDING
vkozyura e0d3a1b
docs: update hash embedding comment
vkozyura cd98416
docs: move NOTE to hashEmbedding function
vkozyura b98e412
refactor: inline vector function definitions in SQLiteService
vkozyura e746d9a
test: remove redundant hash-based embedding tests
vkozyura 36fd908
test: update VECTOR_EMBEDDING test to verify it works
vkozyura c3ba20c
test: consolidate vector tests into functions.test.js
vkozyura 2796628
Merge branch 'main' into feat/vector-simplified
vkozyura b1b73d3
fix: reset package-lock.json to main (remove onnxruntime-node)
vkozyura 4f49b5b
fix: update package-lock.json with pgvector
vkozyura 998cef1
fix: use Buffer instead of TextDecoder for eslint
vkozyura 32e5583
fix: skip vector tests on HANA Express (no REAL_VECTOR support)
vkozyura cdbefdc
fix: rename describeIf to describeSkipIf for clarity
vkozyura ff73234
fix: remove unused vectors test data
vkozyura 4a82f1a
fix: use early return pattern for HANA skip (cds.db not available at …
vkozyura 93512f8
fix: run vector tests only for SQLite (postgres cast not working)
vkozyura a3ae0bb
fix: add Vector type to postgres TypeMap, enable vector tests for pos…
vkozyura c61a964
fix: run vector tests only for SQLite (postgres pgvector not availabl…
vkozyura 0940db2
fix: set locale in pgvector docker to match collation
vkozyura 43149fa
fix: enable vector tests for postgres (skip only HANA)
vkozyura b4921e0
fix: skip vector tests for non-SQLite (HANA/Postgres issues)
vkozyura d69bd65
fix: revert to postgres:16-alpine (pgvector image has different colla…
vkozyura cf73757
docs: add comment about pgvector image for vector support
vkozyura bae2150
fix: remove pg vector dependency and init
vkozyura a1de2e3
refactor: delete index.js
vkozyura ac24478
move tests to sqlite/test/vector.test.js
vkozyura 0ec572e
Merge branch 'main' into feat/vector-simplified
vkozyura 5e8b463
add pg support
vkozyura 701f065
fix tests
vkozyura 21098dd
fix tests
vkozyura 004c277
move sqlite function to file end
vkozyura File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| const cds = require('../../test/cds.js') | ||
| const { Client } = require('pg') | ||
| const os = require('os') | ||
|
|
||
| describe('vector functions', () => { | ||
| const { expect } = cds.test(__dirname + '/../../test/compliance/resources') | ||
|
|
||
| // Setup pgvector extension and fake vector_embedding function | ||
| beforeAll(async () => { | ||
| const testDb = process.env.TRAVIS_JOB_ID || process.env.GITHUB_RUN_ID || os.userInfo().username || 'test_db' | ||
|
|
||
| // First connect to postgres db to create extension in template1 and ensure test db exists | ||
| const adminClient = new Client({ | ||
| host: 'localhost', | ||
| port: 5432, | ||
| user: 'postgres', | ||
| password: 'postgres', | ||
| database: 'postgres' | ||
| }) | ||
| await adminClient.connect() | ||
| await adminClient.query('CREATE EXTENSION IF NOT EXISTS vector') | ||
|
|
||
| // Create test database if it doesn't exist | ||
| try { | ||
| await adminClient.query(`CREATE DATABASE "${testDb}"`) | ||
| } catch (e) { | ||
| // Database might already exist | ||
| } | ||
| await adminClient.end() | ||
|
|
||
| // Now connect to the test database and set up extension + function | ||
| const client = new Client({ | ||
| host: 'localhost', | ||
| port: 5432, | ||
| user: 'postgres', | ||
| password: 'postgres', | ||
| database: testDb | ||
| }) | ||
| await client.connect() | ||
| await client.query('CREATE EXTENSION IF NOT EXISTS vector') | ||
| await client.query(` | ||
| CREATE OR REPLACE FUNCTION public.vector_embedding(model text, input text) | ||
| RETURNS text AS $$ | ||
| SELECT CASE WHEN input IS NULL THEN NULL | ||
| ELSE (SELECT json_agg(sin(i * hashtext(input)::float8 / 1000))::text | ||
| FROM generate_series(1, 384) i) | ||
| END; | ||
| $$ LANGUAGE SQL IMMUTABLE; | ||
| `) | ||
| await client.end() | ||
| }) | ||
|
|
||
| describe('COSINE_SIMILARITY', () => { | ||
| test('identical vectors return 1', async () => { | ||
| const res = await SELECT.from('complex.associations.Books') | ||
| .columns`cosine_similarity(cast('[1, 0, 0]' as cds.Vector), cast('[1, 0, 0]' as cds.Vector)) as similarity` | ||
| expect(res[0].similarity).to.eq(1) | ||
| }) | ||
| test('orthogonal vectors return 0', async () => { | ||
| const res = await SELECT.from('complex.associations.Books') | ||
| .columns`cosine_similarity(cast('[1, 0, 0]' as cds.Vector), cast('[0, 1, 0]' as cds.Vector)) as similarity` | ||
| expect(res[0].similarity).to.eq(0) | ||
| }) | ||
| test('opposite vectors return -1', async () => { | ||
| const res = await SELECT.from('complex.associations.Books') | ||
| .columns`cosine_similarity(cast('[1, 0, 0]' as cds.Vector), cast('[-1, 0, 0]' as cds.Vector)) as similarity` | ||
| expect(res[0].similarity).to.eq(-1) | ||
| }) | ||
| }) | ||
|
|
||
| describe('L2DISTANCE', () => { | ||
| test('identical vectors return 0', async () => { | ||
| const res = await SELECT.from('complex.associations.Books') | ||
| .columns`l2distance(cast('[1, 0, 0]' as cds.Vector), cast('[1, 0, 0]' as cds.Vector)) as distance` | ||
| expect(res[0].distance).to.eq(0) | ||
| }) | ||
| test('unit vectors distance', async () => { | ||
| const res = await SELECT.from('complex.associations.Books') | ||
| .columns`l2distance(cast('[1, 0, 0]' as cds.Vector), cast('[0, 1, 0]' as cds.Vector)) as distance` | ||
| expect(Math.abs(res[0].distance - Math.SQRT2) < 0.0001).to.eq(true) | ||
| }) | ||
| test('known distance', async () => { | ||
| const res = await SELECT.from('complex.associations.Books') | ||
| .columns`l2distance(cast('[0, 0, 0]' as cds.Vector), cast('[3, 4, 0]' as cds.Vector)) as distance` | ||
| expect(res[0].distance).to.eq(5) | ||
| }) | ||
| }) | ||
|
|
||
| describe('L2NORMALIZE', () => { | ||
| test('normalizes to unit length', async () => { | ||
| const res = await SELECT.from('complex.associations.Books') | ||
| .columns`l2normalize(cast('[3, 4, 0]' as cds.Vector)) as normalized` | ||
| const normalized = JSON.parse(res[0].normalized) | ||
| expect(Math.abs(normalized[0] - 0.6) < 0.0001).to.eq(true) | ||
| expect(Math.abs(normalized[1] - 0.8) < 0.0001).to.eq(true) | ||
| expect(normalized[2]).to.eq(0) | ||
| }) | ||
| test('already normalized unchanged', async () => { | ||
| const res = await SELECT.from('complex.associations.Books') | ||
| .columns`l2normalize(cast('[1, 0, 0]' as cds.Vector)) as normalized` | ||
| const normalized = JSON.parse(res[0].normalized) | ||
| expect(normalized[0]).to.eq(1) | ||
| expect(normalized[1]).to.eq(0) | ||
| }) | ||
| }) | ||
|
|
||
| describe('VECTOR_EMBEDDING', () => { | ||
| test('computes embedding', async () => { | ||
| const res = await SELECT.from('complex.associations.Books') | ||
| .columns`VECTOR_EMBEDDING('model', title) as embedding` | ||
| .limit(1) | ||
| const embedding = JSON.parse(res[0].embedding) | ||
| expect(Array.isArray(embedding)).to.eq(true) | ||
| expect(embedding.length).to.eq(384) | ||
| }) | ||
| test('deterministic', async () => { | ||
| const res = await SELECT.from('complex.associations.Books') | ||
| .columns`vector_embedding('model', 'test') as e1, vector_embedding('model', 'test') as e2` | ||
| expect(res[0].e1).to.eq(res[0].e2) | ||
| }) | ||
| test('different inputs different outputs', async () => { | ||
| const res = await SELECT.from('complex.associations.Books') | ||
| .columns`vector_embedding('model', 'hello') as e1, vector_embedding('model', 'world') as e2` | ||
| expect(res[0].e1).to.not.eq(res[0].e2) | ||
| }) | ||
| test('null returns null', async () => { | ||
| const res = await SELECT.from('complex.associations.Books') | ||
| .columns`vector_embedding('model', null) as embedding` | ||
| expect(res[0].embedding).to.eq(null) | ||
| }) | ||
| }) | ||
| }) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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