Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@
- The format is based on [Keep a Changelog](https://keepachangelog.com/).
- This project adheres to [Semantic Versioning](https://semver.org/).

## Version 1.2.0 - tbd

### Added

- Add the `ai-sqlite` kind with a `VECTOR_EMBEDDING` function using ONNX Runtime and the `Xenova/all-MiniLM-L6-v2` model (384 dimensions)
- Downloads the pinned model revision on-demand from Hugging Face (~91MB), verifies its size and SHA-256, and caches it locally
- Supports both 3-parameter `(text, text_type, model_and_version)` and 4-parameter variants with `remote_source`
- Compatible with `SAP_GXY.20250407` and `SAP_GXY.20240715` model versions
- Synchronous execution suitable for SQLite user-defined functions
- **Note**: Produces 384-dimensional vectors (vs. 768 in SAP HANA) for efficiency in local development scenarios



## Version 1.1.0 - 2026-07-20

Expand Down
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,59 @@ resources:
type: org.cloudfoundry.managed-service
```

### 3. Local Vector Embeddings with SQLite

The `ai-sqlite` database kind extends `@cap-js/sqlite` with local semantic embeddings using an ONNX model.

#### Usage

Install the optional runtime dependencies:

```sh
npm add @cap-js/sqlite onnxruntime-node@1.20.1
```

`ai-sqlite` currently requires exactly `onnxruntime-node` 1.20.1 because synchronous SQLite functions need a version-specific native runtime API.

Select `ai-sqlite` for the database service:

```json
{
"cds": {
"requires": {
"db": "ai-sqlite"
}
}
}
```

The HANA-compatible SQL function can then be used in CQL:

```js
SELECT.from('Books').columns`
VECTOR_EMBEDDING(title, 'DOCUMENT', 'SAP_GXY.20250407') as embedding
`;
```

**Parameters:**
- `text` - Text to embed (`NULL` remains `NULL`; empty text returns a zero vector)
- `text_type` - Type of text, e.g., `'DOCUMENT'` (currently informational)
- `model_and_version` - Model identifier, e.g., `'SAP_GXY.20250407'` or `'SAP_GXY.20240715'`

**Returns:**
- JSON stringified array of embedding values (384 dimensions)

**Features:**
- **Initialization**: The ONNX model is loaded when the `ai-sqlite` service starts
- **Verified cache**: The pinned model revision is cached by default below the user's data directory; set `CDS_AI_MODEL_CACHE` to use a pre-provisioned cache root
- **Deterministic**: Same input always produces same output
- **Normalized vectors**: All embeddings are L2-normalized
- **Semantic similarity**: Embeddings capture text meaning for similarity search

**Error Handling:**
- Starting `ai-sqlite` fails if the ONNX model cannot be initialized
- Downloads are time-limited and accepted only when their expected size and SHA-256 match
- Throws if embedding generation fails

## Test the plugin locally

Expand Down
15 changes: 10 additions & 5 deletions lib/sqlite/AISQLiteService.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import SQLiteService from '@cap-js/sqlite';
import { initializeEmbedding, vector_embedding } from '../vector_embedding/index.js';

export default class AISQLiteService extends SQLiteService {
init() {
// add this.xyz here
async init() {
await initializeEmbedding();
return super.init();
}

get factory() {
const factory = super.factory;
factory._create = factory.create;
const create = factory.create;
factory.create = async (tenant) => {
const dbc = await factory._create(tenant);
// add dbc.xyz here
const dbc = await create(tenant);
const embedding = (input, textType, modelAndVersion) =>
input == null ? null : vector_embedding(String(input), textType, modelAndVersion);
const deterministic = { deterministic: true };
dbc.function('VECTOR_EMBEDDING', { ...deterministic, varargs: true }, embedding);
dbc.function('VECTOR_EMBEDDING', deterministic, embedding);
return dbc;
};
return factory;
Expand Down
93 changes: 93 additions & 0 deletions lib/vector_embedding/InferenceSession.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//
// Synchronous counterpart to onnxruntime-node's session handler. SQLite user
// defined functions cannot await the public asynchronous InferenceSession API.
import { createRequire } from 'module';

const require = createRequire(import.meta.url);
const SUPPORTED_ONNX_RUNTIME_VERSION = '1.20.1';
const runtimeVersion = require('onnxruntime-node/package.json').version;

if (runtimeVersion !== SUPPORTED_ONNX_RUNTIME_VERSION) {
throw new Error(
`Unsupported onnxruntime-node version ${runtimeVersion}; @cap-js/ai requires ${SUPPORTED_ONNX_RUNTIME_VERSION} because its synchronous SQLite integration uses the runtime's private native API.`
);
}

const ort = require('onnxruntime-node');
const binding = require('onnxruntime-node/dist/binding.js').binding;

class InferenceSession {
constructor(handler) {
this.handler = handler;
}

run(feeds) {
if (
typeof feeds !== 'object' ||
feeds === null ||
feeds instanceof ort.Tensor ||
Array.isArray(feeds)
) {
throw new TypeError(
"'feeds' must be an object that uses input names as keys and tensors as values."
);
}

for (const name of this.handler.inputNames) {
if (feeds[name] === undefined) throw new Error(`input '${name}' is missing in 'feeds'.`);
}

const fetches = Object.fromEntries(this.handler.outputNames.map((name) => [name, null]));
const results = this.handler.run(feeds, fetches, {});
const output = {};

for (const key in results) {
const result = results[key];
output[key] =
result instanceof ort.Tensor
? result
: new ort.Tensor(result.type, result.data, result.dims);
}

return output;
}

static async create(pathOrBuffer) {
if (typeof pathOrBuffer !== 'string' && !(pathOrBuffer instanceof Uint8Array)) {
throw new TypeError('Expected an ONNX model path or Uint8Array');
}
return new InferenceSession(new SynchronousSessionHandler(pathOrBuffer));
}
}

class SynchronousSessionHandler {
constructor(pathOrBuffer) {
this.session = new binding.InferenceSession();
if (typeof pathOrBuffer === 'string') {
this.session.loadModel(pathOrBuffer, {});
} else {
this.session.loadModel(
pathOrBuffer.buffer,
pathOrBuffer.byteOffset,
pathOrBuffer.byteLength,
{}
);
}
this.inputNames = this.session.inputNames;
this.outputNames = this.session.outputNames;
}

run(feeds, fetches, options) {
return this.session.run(feeds, fetches, options);
}

async dispose() {
this.session.dispose();
}
}

const { Tensor } = ort;

export { InferenceSession, Tensor };
Loading
Loading