@an5/adapters 0.2.6 → 0.2.10
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/README.md +96 -2
- package/dist/an5Adapter.d.ts +57 -0
- package/dist/an5Adapter.js +263 -4
- package/dist/base/sql.js +16 -15
- package/dist/base/types.d.ts +6 -0
- package/dist/googlesheets/helpers.js +13 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -1
- package/dist/nbase/client.d.ts +92 -0
- package/dist/nbase/client.js +147 -0
- package/dist/nbase/index.d.ts +47 -0
- package/dist/nbase/index.js +80 -0
- package/dist/sqlite/engine.js +2 -1
- package/dotnet/An5Adapter.cs +41 -18
- package/dotnet/Base/Core.cs +56 -26
- package/dotnet/Postgres/PostgresEngine.cs +58 -30
- package/dotnet/Sqlite/SqliteEngine.cs +214 -0
- package/golang/base/where.go +32 -49
- package/golang/base/where_test.go +57 -0
- package/package.json +26 -11
- package/python/an5_adapter.py +74 -9
- package/python/base/__init__.py +1 -1
- package/python/base/metadata.py +25 -1
- package/python/base/sql.py +50 -21
- package/python/sqlite/__init__.py +3 -0
- package/python/sqlite/provider.py +83 -0
- package/python/table_client.py +53 -36
- package/rust/Cargo.toml +31 -0
- package/rust/README.md +71 -0
- package/rust/src/adapter.rs +1314 -0
- package/rust/src/base/dialect.rs +201 -0
- package/rust/src/base/metadata.rs +138 -0
- package/rust/src/base/mod.rs +17 -0
- package/rust/src/base/where_builder.rs +386 -0
- package/rust/src/lib.rs +75 -0
- package/typescript/src/an5Adapter.ts +300 -4
- package/typescript/src/base/sql.ts +15 -14
- package/typescript/src/base/types.ts +6 -0
- package/typescript/src/googlesheets/helpers.ts +11 -8
- package/typescript/src/index.ts +17 -0
- package/typescript/src/nbase/client.ts +214 -0
- package/typescript/src/nbase/index.ts +104 -0
- package/typescript/src/sqlite/engine.ts +2 -1
|
@@ -25,9 +25,7 @@ function matchWhere(row, where) {
|
|
|
25
25
|
if (key.includes('_') &&
|
|
26
26
|
value && typeof value === 'object' &&
|
|
27
27
|
!(value instanceof Date) &&
|
|
28
|
-
!
|
|
29
|
-
!value.not && !value.gte &&
|
|
30
|
-
!value.lte && !value.gt && !value.lt) {
|
|
28
|
+
!['equals', 'in', 'notIn', 'contains', 'startsWith', 'endsWith', 'not', 'gte', 'lte', 'gt', 'lt'].some(op => op in value)) {
|
|
31
29
|
Object.assign(cleanWhere, value);
|
|
32
30
|
}
|
|
33
31
|
else {
|
|
@@ -35,13 +33,16 @@ function matchWhere(row, where) {
|
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
35
|
for (const [key, value] of Object.entries(cleanWhere)) {
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
continue;
|
|
38
38
|
if (key === 'OR' && Array.isArray(value)) {
|
|
39
39
|
if (!value.some((v) => matchWhere(row, v)))
|
|
40
40
|
return false;
|
|
41
41
|
continue;
|
|
42
42
|
}
|
|
43
|
-
if (key === 'AND'
|
|
44
|
-
|
|
43
|
+
if (key === 'AND') {
|
|
44
|
+
const clauses = Array.isArray(value) ? value : [value];
|
|
45
|
+
if (!clauses.every((v) => matchWhere(row, v)))
|
|
45
46
|
return false;
|
|
46
47
|
continue;
|
|
47
48
|
}
|
|
@@ -65,21 +66,25 @@ function matchWhere(row, where) {
|
|
|
65
66
|
if (cellVal === null || cellVal === undefined)
|
|
66
67
|
return false;
|
|
67
68
|
}
|
|
69
|
+
else if (v.not && typeof v.not === 'object' && !(v.not instanceof Date) && !Array.isArray(v.not)) {
|
|
70
|
+
if (matchWhere(row, { [key]: v.not }))
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
68
73
|
else if (normalize(cellVal) == normalize(v.not))
|
|
69
74
|
return false;
|
|
70
75
|
}
|
|
71
76
|
if (v.equals !== undefined && normalize(cellVal) != normalize(v.equals))
|
|
72
77
|
return false;
|
|
73
78
|
if (v.contains !== undefined) {
|
|
74
|
-
if (
|
|
79
|
+
if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).includes(normalize(v.contains)))
|
|
75
80
|
return false;
|
|
76
81
|
}
|
|
77
82
|
if (v.startsWith !== undefined) {
|
|
78
|
-
if (
|
|
83
|
+
if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).startsWith(normalize(v.startsWith)))
|
|
79
84
|
return false;
|
|
80
85
|
}
|
|
81
86
|
if (v.endsWith !== undefined) {
|
|
82
|
-
if (
|
|
87
|
+
if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).endsWith(normalize(v.endsWith)))
|
|
83
88
|
return false;
|
|
84
89
|
}
|
|
85
90
|
if (v.gte !== undefined && !(Number(cellVal) >= Number(v.gte)))
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { An5Adapter, AdapterTableClient, createAn5Adapter, createAdapterProxy, executorFromAdapter, setAdapterMetadata, } from './an5Adapter';
|
|
2
2
|
export { parseWhere, buildOrderBy, quote } from './base/sql';
|
|
3
|
+
export { NBaseVectorClient, NBaseError, createNBaseVectorClient, createNBaseClient, parseNBaseConnectionString, buildVectorId, parseVectorId, } from './nbase';
|
|
4
|
+
export type { NBaseAdapterConfig, NBaseClientConfig, NBaseVector, NBaseSearchOptions, NBaseSearchResult, NBaseStats, } from './nbase';
|
|
3
5
|
export type { An5AdapterConfig, AdapterMetadata, Dialect, } from './an5Adapter';
|
|
4
6
|
export type { TypedAn5Adapter, AdapterAPI } from './typed';
|
|
5
7
|
export { getLlmConfig, setLlmConfig, getEmbeddingConfig, setEmbeddingConfig, resetAdapter, } from './config';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseSheetsConnectionString = exports.createAn5SheetsAdapter = exports.SheetsTableClient = exports.An5SheetsAdapter = exports.resetAdapter = exports.setEmbeddingConfig = exports.getEmbeddingConfig = exports.setLlmConfig = exports.getLlmConfig = exports.quote = exports.buildOrderBy = exports.parseWhere = exports.setAdapterMetadata = exports.executorFromAdapter = exports.createAdapterProxy = exports.createAn5Adapter = exports.AdapterTableClient = exports.An5Adapter = void 0;
|
|
3
|
+
exports.parseSheetsConnectionString = exports.createAn5SheetsAdapter = exports.SheetsTableClient = exports.An5SheetsAdapter = exports.resetAdapter = exports.setEmbeddingConfig = exports.getEmbeddingConfig = exports.setLlmConfig = exports.getLlmConfig = exports.parseVectorId = exports.buildVectorId = exports.parseNBaseConnectionString = exports.createNBaseClient = exports.createNBaseVectorClient = exports.NBaseError = exports.NBaseVectorClient = exports.quote = exports.buildOrderBy = exports.parseWhere = exports.setAdapterMetadata = exports.executorFromAdapter = exports.createAdapterProxy = exports.createAn5Adapter = exports.AdapterTableClient = exports.An5Adapter = void 0;
|
|
4
4
|
var an5Adapter_1 = require("./an5Adapter");
|
|
5
5
|
Object.defineProperty(exports, "An5Adapter", { enumerable: true, get: function () { return an5Adapter_1.An5Adapter; } });
|
|
6
6
|
Object.defineProperty(exports, "AdapterTableClient", { enumerable: true, get: function () { return an5Adapter_1.AdapterTableClient; } });
|
|
@@ -12,6 +12,14 @@ var sql_1 = require("./base/sql");
|
|
|
12
12
|
Object.defineProperty(exports, "parseWhere", { enumerable: true, get: function () { return sql_1.parseWhere; } });
|
|
13
13
|
Object.defineProperty(exports, "buildOrderBy", { enumerable: true, get: function () { return sql_1.buildOrderBy; } });
|
|
14
14
|
Object.defineProperty(exports, "quote", { enumerable: true, get: function () { return sql_1.quote; } });
|
|
15
|
+
var nbase_1 = require("./nbase");
|
|
16
|
+
Object.defineProperty(exports, "NBaseVectorClient", { enumerable: true, get: function () { return nbase_1.NBaseVectorClient; } });
|
|
17
|
+
Object.defineProperty(exports, "NBaseError", { enumerable: true, get: function () { return nbase_1.NBaseError; } });
|
|
18
|
+
Object.defineProperty(exports, "createNBaseVectorClient", { enumerable: true, get: function () { return nbase_1.createNBaseVectorClient; } });
|
|
19
|
+
Object.defineProperty(exports, "createNBaseClient", { enumerable: true, get: function () { return nbase_1.createNBaseClient; } });
|
|
20
|
+
Object.defineProperty(exports, "parseNBaseConnectionString", { enumerable: true, get: function () { return nbase_1.parseNBaseConnectionString; } });
|
|
21
|
+
Object.defineProperty(exports, "buildVectorId", { enumerable: true, get: function () { return nbase_1.buildVectorId; } });
|
|
22
|
+
Object.defineProperty(exports, "parseVectorId", { enumerable: true, get: function () { return nbase_1.parseVectorId; } });
|
|
15
23
|
var config_1 = require("./config");
|
|
16
24
|
Object.defineProperty(exports, "getLlmConfig", { enumerable: true, get: function () { return config_1.getLlmConfig; } });
|
|
17
25
|
Object.defineProperty(exports, "setLlmConfig", { enumerable: true, get: function () { return config_1.setLlmConfig; } });
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NBase — Neural Vector Database client.
|
|
3
|
+
*
|
|
4
|
+
* NBase is a vector database with HNSW/LSH/KNN indexing exposed over a REST
|
|
5
|
+
* API. an5 uses it to move similarity search out of the database process: the
|
|
6
|
+
* vectors live in NBase, the rows stay in the relational table, and the search
|
|
7
|
+
* results are hydrated from the table by the id kept in vector metadata.
|
|
8
|
+
*
|
|
9
|
+
* The client only uses the global `fetch`, so it works in Node 18+ and in the
|
|
10
|
+
* browser without pulling in a dependency.
|
|
11
|
+
*/
|
|
12
|
+
export interface NBaseVector {
|
|
13
|
+
id: string | number;
|
|
14
|
+
vector: number[];
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface NBaseSearchOptions {
|
|
18
|
+
/** Number of neighbours to return. */
|
|
19
|
+
k?: number;
|
|
20
|
+
/** Distance metric; NBase accepts `cosine` and `euclidean`. */
|
|
21
|
+
distanceMetric?: 'cosine' | 'euclidean';
|
|
22
|
+
/** `hnsw` uses the graph index, anything else uses the clustered path. */
|
|
23
|
+
method?: 'hnsw' | 'clustered';
|
|
24
|
+
efSearch?: number;
|
|
25
|
+
filters?: Record<string, unknown> | Array<Record<string, unknown>>;
|
|
26
|
+
includeMetadata?: boolean;
|
|
27
|
+
includeVectors?: boolean;
|
|
28
|
+
skipCache?: boolean;
|
|
29
|
+
searchTimeoutMs?: number;
|
|
30
|
+
rerank?: boolean;
|
|
31
|
+
rerankingMethod?: string;
|
|
32
|
+
rerankLambda?: number;
|
|
33
|
+
partitionIds?: string[];
|
|
34
|
+
}
|
|
35
|
+
export interface NBaseSearchResult {
|
|
36
|
+
id: string | number;
|
|
37
|
+
/** Distance from the query vector. Lower is more similar. */
|
|
38
|
+
dist: number;
|
|
39
|
+
metadata?: Record<string, unknown>;
|
|
40
|
+
vector?: number[];
|
|
41
|
+
dimension?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface NBaseClientConfig {
|
|
44
|
+
/** Base URL, e.g. `http://localhost:1307`. */
|
|
45
|
+
url: string;
|
|
46
|
+
/** Optional bearer token, for deployments behind an auth proxy. */
|
|
47
|
+
token?: string;
|
|
48
|
+
/** Per-request timeout in milliseconds. */
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
/** Injectable for tests and for runtimes without a global fetch. */
|
|
51
|
+
fetchImpl?: typeof fetch;
|
|
52
|
+
}
|
|
53
|
+
export interface NBaseStats {
|
|
54
|
+
vectorCount?: number;
|
|
55
|
+
metadataCount?: number;
|
|
56
|
+
memoryUsage?: number;
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
export declare class NBaseError extends Error {
|
|
60
|
+
readonly status?: number | undefined;
|
|
61
|
+
readonly body?: unknown;
|
|
62
|
+
constructor(message: string, status?: number | undefined, body?: unknown);
|
|
63
|
+
}
|
|
64
|
+
export declare class NBaseVectorClient {
|
|
65
|
+
private readonly config;
|
|
66
|
+
private readonly baseUrl;
|
|
67
|
+
private readonly fetchImpl;
|
|
68
|
+
private readonly timeoutMs;
|
|
69
|
+
constructor(config: NBaseClientConfig);
|
|
70
|
+
private request;
|
|
71
|
+
/** Liveness probe. NBase mounts its status routes at the server root. */
|
|
72
|
+
health(): Promise<{
|
|
73
|
+
status: string;
|
|
74
|
+
version?: string;
|
|
75
|
+
}>;
|
|
76
|
+
/** Vector count, memory use and other database statistics. */
|
|
77
|
+
stats(): Promise<NBaseStats>;
|
|
78
|
+
/** Adds or replaces vectors. NBase accepts a single vector or a bulk array. */
|
|
79
|
+
addVectors(vectors: NBaseVector[]): Promise<{
|
|
80
|
+
count: number;
|
|
81
|
+
ids?: Array<string | number>;
|
|
82
|
+
}>;
|
|
83
|
+
/** Nearest-neighbour search. Results are ordered by ascending `dist`. */
|
|
84
|
+
search(vector: number[], options?: NBaseSearchOptions): Promise<NBaseSearchResult[]>;
|
|
85
|
+
/** Reads a single vector. Returns undefined when the id is unknown. */
|
|
86
|
+
get(id: string | number): Promise<NBaseSearchResult | undefined>;
|
|
87
|
+
exists(id: string | number): Promise<boolean>;
|
|
88
|
+
delete(id: string | number): Promise<boolean>;
|
|
89
|
+
updateMetadata(id: string | number, metadata: Record<string, unknown>): Promise<boolean>;
|
|
90
|
+
}
|
|
91
|
+
/** Creates an NBase vector client. */
|
|
92
|
+
export declare function createNBaseVectorClient(config: NBaseClientConfig): NBaseVectorClient;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* NBase — Neural Vector Database client.
|
|
4
|
+
*
|
|
5
|
+
* NBase is a vector database with HNSW/LSH/KNN indexing exposed over a REST
|
|
6
|
+
* API. an5 uses it to move similarity search out of the database process: the
|
|
7
|
+
* vectors live in NBase, the rows stay in the relational table, and the search
|
|
8
|
+
* results are hydrated from the table by the id kept in vector metadata.
|
|
9
|
+
*
|
|
10
|
+
* The client only uses the global `fetch`, so it works in Node 18+ and in the
|
|
11
|
+
* browser without pulling in a dependency.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.NBaseVectorClient = exports.NBaseError = void 0;
|
|
15
|
+
exports.createNBaseVectorClient = createNBaseVectorClient;
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
17
|
+
class NBaseError extends Error {
|
|
18
|
+
constructor(message, status, body) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.status = status;
|
|
21
|
+
this.body = body;
|
|
22
|
+
this.name = 'NBaseError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.NBaseError = NBaseError;
|
|
26
|
+
class NBaseVectorClient {
|
|
27
|
+
constructor(config) {
|
|
28
|
+
this.config = config;
|
|
29
|
+
this.baseUrl = String(config.url).replace(/\/+$/, '');
|
|
30
|
+
const impl = config.fetchImpl ?? globalThis.fetch;
|
|
31
|
+
if (!impl) {
|
|
32
|
+
throw new NBaseError('No fetch implementation available. Node 18+ or a fetch polyfill is required.');
|
|
33
|
+
}
|
|
34
|
+
this.fetchImpl = impl;
|
|
35
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
36
|
+
}
|
|
37
|
+
async request(path, init) {
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
40
|
+
try {
|
|
41
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
42
|
+
...init,
|
|
43
|
+
signal: controller.signal,
|
|
44
|
+
headers: {
|
|
45
|
+
'content-type': 'application/json',
|
|
46
|
+
...(this.config.token ? { authorization: `Bearer ${this.config.token}` } : {}),
|
|
47
|
+
...init?.headers,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const text = await response.text();
|
|
51
|
+
let body;
|
|
52
|
+
try {
|
|
53
|
+
body = text ? JSON.parse(text) : undefined;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
body = text;
|
|
57
|
+
}
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const message = (body && typeof body === 'object' && 'error' in body
|
|
60
|
+
? String(body.error)
|
|
61
|
+
: undefined) ?? `HTTP ${response.status}`;
|
|
62
|
+
throw new NBaseError(`NBase request ${path} failed: ${message}`, response.status, body);
|
|
63
|
+
}
|
|
64
|
+
return body;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (err instanceof NBaseError)
|
|
68
|
+
throw err;
|
|
69
|
+
if (err?.name === 'AbortError') {
|
|
70
|
+
throw new NBaseError(`NBase request ${path} timed out after ${this.timeoutMs}ms`);
|
|
71
|
+
}
|
|
72
|
+
throw new NBaseError(`Could not reach NBase at ${this.baseUrl}: ${err instanceof Error ? err.message : String(err)}`);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Liveness probe. NBase mounts its status routes at the server root. */
|
|
79
|
+
async health() {
|
|
80
|
+
return this.request('/health');
|
|
81
|
+
}
|
|
82
|
+
/** Vector count, memory use and other database statistics. */
|
|
83
|
+
async stats() {
|
|
84
|
+
return this.request('/stats');
|
|
85
|
+
}
|
|
86
|
+
/** Adds or replaces vectors. NBase accepts a single vector or a bulk array. */
|
|
87
|
+
async addVectors(vectors) {
|
|
88
|
+
if (vectors.length === 0)
|
|
89
|
+
return { count: 0 };
|
|
90
|
+
const result = await this.request('/api/vectors', { method: 'POST', body: JSON.stringify({ vectors: vectors.length === 1 ? vectors[0] : vectors }) });
|
|
91
|
+
return { count: result.count ?? vectors.length, ...(result.addedIds ? { ids: result.addedIds } : {}) };
|
|
92
|
+
}
|
|
93
|
+
/** Nearest-neighbour search. Results are ordered by ascending `dist`. */
|
|
94
|
+
async search(vector, options = {}) {
|
|
95
|
+
const body = await this.request('/api/search', {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
query: vector,
|
|
99
|
+
k: options.k ?? 10,
|
|
100
|
+
...(options.distanceMetric ? { distanceMetric: options.distanceMetric } : {}),
|
|
101
|
+
...(options.method ? { method: options.method } : {}),
|
|
102
|
+
...(options.efSearch !== undefined ? { efSearch: options.efSearch } : {}),
|
|
103
|
+
...(options.filters ? { filters: options.filters } : {}),
|
|
104
|
+
...(options.includeMetadata !== undefined ? { includeMetadata: options.includeMetadata } : {}),
|
|
105
|
+
...(options.includeVectors !== undefined ? { includeVectors: options.includeVectors } : {}),
|
|
106
|
+
...(options.skipCache !== undefined ? { skipCache: options.skipCache } : {}),
|
|
107
|
+
...(options.searchTimeoutMs !== undefined ? { searchTimeoutMs: options.searchTimeoutMs } : {}),
|
|
108
|
+
...(options.rerank !== undefined ? { rerank: options.rerank } : {}),
|
|
109
|
+
...(options.rerankingMethod ? { rerankingMethod: options.rerankingMethod } : {}),
|
|
110
|
+
...(options.rerankLambda !== undefined ? { rerankLambda: options.rerankLambda } : {}),
|
|
111
|
+
...(options.partitionIds ? { partitionIds: options.partitionIds } : {}),
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
return body.results ?? [];
|
|
115
|
+
}
|
|
116
|
+
/** Reads a single vector. Returns undefined when the id is unknown. */
|
|
117
|
+
async get(id) {
|
|
118
|
+
try {
|
|
119
|
+
return await this.request(`/api/vectors/${encodeURIComponent(String(id))}`);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
if (err instanceof NBaseError && err.status === 404)
|
|
123
|
+
return undefined;
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async exists(id) {
|
|
128
|
+
const result = await this.request(`/api/vectors/${encodeURIComponent(String(id))}/exists`);
|
|
129
|
+
return result.exists === true;
|
|
130
|
+
}
|
|
131
|
+
async delete(id) {
|
|
132
|
+
await this.request(`/api/vectors/${encodeURIComponent(String(id))}`, { method: 'DELETE' });
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
async updateMetadata(id, metadata) {
|
|
136
|
+
await this.request(`/api/vectors/${encodeURIComponent(String(id))}/metadata`, {
|
|
137
|
+
method: 'PATCH',
|
|
138
|
+
body: JSON.stringify(metadata),
|
|
139
|
+
});
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
exports.NBaseVectorClient = NBaseVectorClient;
|
|
144
|
+
/** Creates an NBase vector client. */
|
|
145
|
+
function createNBaseVectorClient(config) {
|
|
146
|
+
return new NBaseVectorClient(config);
|
|
147
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NBase connection-string and adapter configuration.
|
|
3
|
+
*
|
|
4
|
+
* NBase is configured through the connection string alone:
|
|
5
|
+
*
|
|
6
|
+
* createAn5Adapter({ connectionString: 'nbase://localhost:1307' })
|
|
7
|
+
*
|
|
8
|
+
* Accepted forms are `nbase://host:port`, `nbase:host:port` and an explicit
|
|
9
|
+
* scheme, with optional `?token=…&timeoutMs=…&method=…` options. An adapter
|
|
10
|
+
* created this way is vector-only: it has no relational database, so a search
|
|
11
|
+
* returns the NBase hits instead of table rows.
|
|
12
|
+
*/
|
|
13
|
+
import { NBaseVectorClient, NBaseError, createNBaseVectorClient, type NBaseClientConfig, type NBaseVector, type NBaseSearchOptions, type NBaseSearchResult, type NBaseStats } from './client';
|
|
14
|
+
export interface NBaseAdapterConfig extends NBaseClientConfig {
|
|
15
|
+
url: string;
|
|
16
|
+
/**
|
|
17
|
+
* Metadata key that holds the relational row id. `an5Id` by default, which
|
|
18
|
+
* is what `indexTableVectors` writes.
|
|
19
|
+
*/
|
|
20
|
+
idField?: string;
|
|
21
|
+
/** Metadata key holding the model name, used to keep collections apart. */
|
|
22
|
+
modelField?: string;
|
|
23
|
+
/** Force the search method instead of letting NBase choose. */
|
|
24
|
+
method?: 'hnsw' | 'clustered';
|
|
25
|
+
}
|
|
26
|
+
/** True when the whole connection string points at NBase. */
|
|
27
|
+
export declare function isNBaseConnectionString(connectionString: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Parses an NBase connection string.
|
|
30
|
+
*
|
|
31
|
+
* Accepts `nbase:http://host:port`, `nbase://host:port` and
|
|
32
|
+
* `nbase:host:port`, with optional `?token=…&timeoutMs=…` options.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseNBaseConnectionString(connectionString: string): NBaseAdapterConfig;
|
|
35
|
+
/** Builds the vector id used to link a NBase vector back to a relational row. */
|
|
36
|
+
export declare function buildVectorId(model: string, id: string | number): string;
|
|
37
|
+
/** Splits a vector id produced by {@link buildVectorId}. */
|
|
38
|
+
export declare function parseVectorId(vectorId: string | number): {
|
|
39
|
+
model: string;
|
|
40
|
+
id: string;
|
|
41
|
+
};
|
|
42
|
+
export declare function createNBaseClient(config: NBaseAdapterConfig | string): {
|
|
43
|
+
client: NBaseVectorClient;
|
|
44
|
+
options: NBaseAdapterConfig;
|
|
45
|
+
};
|
|
46
|
+
export { NBaseVectorClient, NBaseError, createNBaseVectorClient };
|
|
47
|
+
export type { NBaseClientConfig, NBaseVector, NBaseSearchOptions, NBaseSearchResult, NBaseStats };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createNBaseVectorClient = exports.NBaseError = exports.NBaseVectorClient = void 0;
|
|
4
|
+
exports.isNBaseConnectionString = isNBaseConnectionString;
|
|
5
|
+
exports.parseNBaseConnectionString = parseNBaseConnectionString;
|
|
6
|
+
exports.buildVectorId = buildVectorId;
|
|
7
|
+
exports.parseVectorId = parseVectorId;
|
|
8
|
+
exports.createNBaseClient = createNBaseClient;
|
|
9
|
+
/**
|
|
10
|
+
* NBase connection-string and adapter configuration.
|
|
11
|
+
*
|
|
12
|
+
* NBase is configured through the connection string alone:
|
|
13
|
+
*
|
|
14
|
+
* createAn5Adapter({ connectionString: 'nbase://localhost:1307' })
|
|
15
|
+
*
|
|
16
|
+
* Accepted forms are `nbase://host:port`, `nbase:host:port` and an explicit
|
|
17
|
+
* scheme, with optional `?token=…&timeoutMs=…&method=…` options. An adapter
|
|
18
|
+
* created this way is vector-only: it has no relational database, so a search
|
|
19
|
+
* returns the NBase hits instead of table rows.
|
|
20
|
+
*/
|
|
21
|
+
const client_1 = require("./client");
|
|
22
|
+
Object.defineProperty(exports, "NBaseVectorClient", { enumerable: true, get: function () { return client_1.NBaseVectorClient; } });
|
|
23
|
+
Object.defineProperty(exports, "NBaseError", { enumerable: true, get: function () { return client_1.NBaseError; } });
|
|
24
|
+
Object.defineProperty(exports, "createNBaseVectorClient", { enumerable: true, get: function () { return client_1.createNBaseVectorClient; } });
|
|
25
|
+
/** Matches `nbase:`, `nbase://` and `nbase=http(s)://` at the start of a value. */
|
|
26
|
+
const NBASE_PREFIX = /^nbase:(?:\/\/)?/i;
|
|
27
|
+
/** True when the whole connection string points at NBase. */
|
|
28
|
+
function isNBaseConnectionString(connectionString) {
|
|
29
|
+
return NBASE_PREFIX.test(connectionString.trim());
|
|
30
|
+
}
|
|
31
|
+
/** Builds options from a URL and an optional query string. */
|
|
32
|
+
function buildOptions(url, query) {
|
|
33
|
+
const params = new URLSearchParams(query ?? '');
|
|
34
|
+
const trimmed = url.trim().replace(/\/+$/, '');
|
|
35
|
+
// `nbase://host:port` is shorthand for the same host over HTTP; an explicit
|
|
36
|
+
// scheme is used as-is so https deployments work.
|
|
37
|
+
const withScheme = !trimmed
|
|
38
|
+
? 'http://localhost:1307'
|
|
39
|
+
: /^[a-z]+:\/\//i.test(trimmed)
|
|
40
|
+
? trimmed
|
|
41
|
+
: `http://${trimmed}`;
|
|
42
|
+
return {
|
|
43
|
+
url: withScheme,
|
|
44
|
+
...(params.get('token') ? { token: params.get('token') } : {}),
|
|
45
|
+
...(params.get('timeoutMs') ? { timeoutMs: Number(params.get('timeoutMs')) } : {}),
|
|
46
|
+
...(params.get('idField') ? { idField: params.get('idField') } : {}),
|
|
47
|
+
...(params.get('modelField') ? { modelField: params.get('modelField') } : {}),
|
|
48
|
+
...(params.get('method') ? { method: params.get('method') } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Parses an NBase connection string.
|
|
53
|
+
*
|
|
54
|
+
* Accepts `nbase:http://host:port`, `nbase://host:port` and
|
|
55
|
+
* `nbase:host:port`, with optional `?token=…&timeoutMs=…` options.
|
|
56
|
+
*/
|
|
57
|
+
function parseNBaseConnectionString(connectionString) {
|
|
58
|
+
const raw = connectionString.trim();
|
|
59
|
+
if (!isNBaseConnectionString(raw)) {
|
|
60
|
+
throw new Error(`Not an NBase connection string: ${raw}`);
|
|
61
|
+
}
|
|
62
|
+
const [url, query] = raw.replace(NBASE_PREFIX, '').split('?');
|
|
63
|
+
return buildOptions(url ?? '', query);
|
|
64
|
+
}
|
|
65
|
+
/** Builds the vector id used to link a NBase vector back to a relational row. */
|
|
66
|
+
function buildVectorId(model, id) {
|
|
67
|
+
return `${model}:${id}`;
|
|
68
|
+
}
|
|
69
|
+
/** Splits a vector id produced by {@link buildVectorId}. */
|
|
70
|
+
function parseVectorId(vectorId) {
|
|
71
|
+
const raw = String(vectorId);
|
|
72
|
+
const separator = raw.indexOf(':');
|
|
73
|
+
if (separator === -1)
|
|
74
|
+
return { model: '', id: raw };
|
|
75
|
+
return { model: raw.slice(0, separator), id: raw.slice(separator + 1) };
|
|
76
|
+
}
|
|
77
|
+
function createNBaseClient(config) {
|
|
78
|
+
const options = typeof config === 'string' ? parseNBaseConnectionString(config) : config;
|
|
79
|
+
return { client: (0, client_1.createNBaseVectorClient)(options), options };
|
|
80
|
+
}
|
package/dist/sqlite/engine.js
CHANGED
|
@@ -15,7 +15,8 @@ class SqliteEngine {
|
|
|
15
15
|
throw new Error('better-sqlite3 package is required for SQLite support. Run: npm install better-sqlite3');
|
|
16
16
|
this.filePath = (adapterConfig.connectionString ?? '')
|
|
17
17
|
.replace(/^sqlite:\/\/\//i, '/')
|
|
18
|
-
.replace(/^sqlite:\/\//i, '')
|
|
18
|
+
.replace(/^sqlite:\/\//i, '')
|
|
19
|
+
.replace(/^sqlite:/i, '');
|
|
19
20
|
}
|
|
20
21
|
getDb() {
|
|
21
22
|
if (!this.db)
|
package/dotnet/An5Adapter.cs
CHANGED
|
@@ -25,6 +25,7 @@ namespace An5Orm
|
|
|
25
25
|
_engine = DialectDetector.Detect(connectionString) switch
|
|
26
26
|
{
|
|
27
27
|
Dialect.Postgres => new PostgresEngine(connectionString, commandTimeout),
|
|
28
|
+
Dialect.Sqlite => new SqliteEngine(connectionString, commandTimeout),
|
|
28
29
|
_ => new MssqlEngine(connectionString, commandTimeout)
|
|
29
30
|
};
|
|
30
31
|
}
|
|
@@ -44,8 +45,16 @@ namespace An5Orm
|
|
|
44
45
|
|
|
45
46
|
// ── Stored Procedure Execution ────────────────────────────────────────
|
|
46
47
|
|
|
48
|
+
private void ThrowIfNoStoredProcedures()
|
|
49
|
+
{
|
|
50
|
+
if (Dialect == Dialect.Sqlite)
|
|
51
|
+
throw new NotSupportedException(
|
|
52
|
+
"SQLite has no stored procedures. Run the statements directly with QueryRaw or ExecuteRaw.");
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
public List<T> QueryProc<T>(string procName, Dictionary<string, object> parameters = null) where T : new()
|
|
48
56
|
{
|
|
57
|
+
ThrowIfNoStoredProcedures();
|
|
49
58
|
var paramList = new List<string>();
|
|
50
59
|
if (parameters != null)
|
|
51
60
|
{
|
|
@@ -62,6 +71,7 @@ namespace An5Orm
|
|
|
62
71
|
|
|
63
72
|
public int ExecuteProc(string procName, Dictionary<string, object> parameters = null)
|
|
64
73
|
{
|
|
74
|
+
ThrowIfNoStoredProcedures();
|
|
65
75
|
var paramList = new List<string>();
|
|
66
76
|
if (parameters != null)
|
|
67
77
|
{
|
|
@@ -418,12 +428,27 @@ namespace An5Orm
|
|
|
418
428
|
|
|
419
429
|
private string Nolock => _dialect == Dialect.Mssql ? " WITH (NOLOCK)" : "";
|
|
420
430
|
|
|
431
|
+
/// <summary>
|
|
432
|
+
/// True for the dialects that paginate with LIMIT/OFFSET rather than
|
|
433
|
+
/// OFFSET ... FETCH NEXT, and have no SELECT TOP.
|
|
434
|
+
/// </summary>
|
|
435
|
+
private bool UsesLimitOffset => _dialect == Dialect.Postgres || _dialect == Dialect.Sqlite;
|
|
436
|
+
|
|
421
437
|
private string BuildPagination(string orderBy, int? skip, int? take)
|
|
422
438
|
{
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
439
|
+
// A skip on its own still has to skip. OFFSET without FETCH NEXT is
|
|
440
|
+
// valid on MSSQL, and on the LIMIT/OFFSET dialects an unbounded page
|
|
441
|
+
// is LIMIT -1; without this, `skip` was silently discarded whenever
|
|
442
|
+
// `take` was null.
|
|
443
|
+
if (take == null && skip == null) return "";
|
|
444
|
+
if (UsesLimitOffset)
|
|
445
|
+
{
|
|
446
|
+
if (take == null) return $" LIMIT -1 OFFSET {skip.Value}";
|
|
447
|
+
if (skip == null) return $" LIMIT {take.Value}";
|
|
448
|
+
return $" LIMIT {take.Value} OFFSET {skip.Value}";
|
|
449
|
+
}
|
|
426
450
|
var o = !string.IsNullOrEmpty(orderBy) ? $" ORDER BY {orderBy}" : " ORDER BY (SELECT NULL)";
|
|
451
|
+
if (take == null) return $"{o} OFFSET {skip.Value} ROWS";
|
|
427
452
|
return $"{o} OFFSET {skip ?? 0} ROWS FETCH NEXT {take.Value} ROWS ONLY";
|
|
428
453
|
}
|
|
429
454
|
|
|
@@ -432,17 +457,11 @@ namespace An5Orm
|
|
|
432
457
|
{
|
|
433
458
|
var sb = new StringBuilder($"SELECT * FROM {_tableName}{Nolock}");
|
|
434
459
|
if (!string.IsNullOrEmpty(whereClause)) sb.Append($" WHERE {whereClause}");
|
|
435
|
-
if (
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
else
|
|
442
|
-
{
|
|
443
|
-
if (!string.IsNullOrEmpty(orderBy)) sb.Append($" ORDER BY {orderBy}");
|
|
444
|
-
sb.Append(BuildPagination(orderBy, skip, take));
|
|
445
|
-
}
|
|
460
|
+
if (!string.IsNullOrEmpty(orderBy)) sb.Append($" ORDER BY {orderBy}");
|
|
461
|
+
// LIMIT precedes OFFSET, and neither carries MSSQL's `ROWS` keyword.
|
|
462
|
+
// Postgres tolerates either order, SQLite does not: `OFFSET n LIMIT m`
|
|
463
|
+
// is a syntax error there.
|
|
464
|
+
sb.Append(BuildPagination(orderBy, skip, take));
|
|
446
465
|
return _adapter.QueryRaw<T>(sb.ToString(), parameters);
|
|
447
466
|
}
|
|
448
467
|
|
|
@@ -457,7 +476,7 @@ namespace An5Orm
|
|
|
457
476
|
|
|
458
477
|
var cols = "*";
|
|
459
478
|
var keys = ArgsHelper.SelectedFields(args?.Select);
|
|
460
|
-
var
|
|
479
|
+
var usesLimitOffset = UsesLimitOffset;
|
|
461
480
|
|
|
462
481
|
if (keys.Count > 0 && !ArgsHelper.HasRelationSelect(args?.Select, An5Metadata.GetRelationsForModel(_modelName)))
|
|
463
482
|
{
|
|
@@ -468,7 +487,7 @@ namespace An5Orm
|
|
|
468
487
|
string query;
|
|
469
488
|
if (take != null && !hasSkip)
|
|
470
489
|
{
|
|
471
|
-
if (!
|
|
490
|
+
if (!usesLimitOffset)
|
|
472
491
|
{
|
|
473
492
|
query = $"SELECT TOP ({take.Value}) {cols} FROM {_tableName}{Nolock}";
|
|
474
493
|
if (!string.IsNullOrEmpty(whereSql)) query += $" WHERE {whereSql}";
|
|
@@ -484,7 +503,7 @@ namespace An5Orm
|
|
|
484
503
|
}
|
|
485
504
|
else if (hasSkip)
|
|
486
505
|
{
|
|
487
|
-
if (!
|
|
506
|
+
if (!usesLimitOffset)
|
|
488
507
|
{
|
|
489
508
|
query = $"SELECT {cols} FROM {_tableName}{Nolock}";
|
|
490
509
|
if (!string.IsNullOrEmpty(whereSql)) query += $" WHERE {whereSql}";
|
|
@@ -498,7 +517,7 @@ namespace An5Orm
|
|
|
498
517
|
if (!string.IsNullOrEmpty(whereSql)) query += $" WHERE {whereSql}";
|
|
499
518
|
if (!string.IsNullOrEmpty(orderSql)) query += $" {orderSql}";
|
|
500
519
|
if (take != null) query += $" LIMIT {take.Value}";
|
|
501
|
-
else query += " LIMIT ALL";
|
|
520
|
+
else query += _dialect == Dialect.Sqlite ? " LIMIT -1" : " LIMIT ALL";
|
|
502
521
|
query += $" OFFSET {skip.Value}";
|
|
503
522
|
}
|
|
504
523
|
}
|
|
@@ -988,6 +1007,10 @@ namespace An5Orm
|
|
|
988
1007
|
string vectorField = "Embedding", string distanceMetric = "cosine")
|
|
989
1008
|
{
|
|
990
1009
|
// 1. Primary path: Native database SQL vector query execution (MSSQL VECTOR_DISTANCE / Postgres pgvector)
|
|
1010
|
+
// SQLite has no vector operator, so it is skipped outright: building the
|
|
1011
|
+
// statement only to have it rejected costs a round trip and lands on the
|
|
1012
|
+
// in-memory path below anyway.
|
|
1013
|
+
if (_dialect != Dialect.Sqlite)
|
|
991
1014
|
try
|
|
992
1015
|
{
|
|
993
1016
|
var dim = vector.Count;
|