libsql-search 0.10.1 → 0.11.1
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 +18 -15
- package/dist/index.cjs +17 -127
- package/dist/index.d.ts +10 -10
- package/dist/index.esm.js +17 -127
- package/dist/turso.cjs +90 -7
- package/dist/turso.d.ts +29 -8
- package/dist/turso.esm.js +90 -7
- package/docs/API.md +22 -17
- package/docs/INDEXING.md +13 -7
- package/docs/INTEGRATIONS.md +18 -24
- package/docs/MIGRATIONS.md +10 -10
- package/docs/PROVIDERS.md +3 -23
- package/docs/README.md +1 -1
- package/docs/TESTING.md +13 -12
- package/docs/TROUBLESHOOTING.md +0 -6
- package/docs/TURSO.md +55 -16
- package/package.json +1 -2
- package/docs/TROUBLESHOOTING-SHARP.md +0 -65
package/dist/turso.cjs
CHANGED
|
@@ -1,8 +1,65 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const MAX_QUERY_STATEMENT_CACHE_SIZE = 32;
|
|
3
4
|
function tursoAdapter(database) {
|
|
4
5
|
assertTursoDatabase(database);
|
|
5
|
-
|
|
6
|
+
const queryStatementsBySql = /* @__PURE__ */ new Map();
|
|
7
|
+
const liveQueryStatements = /* @__PURE__ */ new Set();
|
|
8
|
+
let disposed = false;
|
|
9
|
+
let disposePromise;
|
|
10
|
+
const assertUsable = () => {
|
|
11
|
+
if (disposed) {
|
|
12
|
+
throw new Error("This Turso adapter has been disposed");
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const closeQueryStatement = (entry) => {
|
|
16
|
+
entry.closePromise ??= closeStatement(entry.statement).finally(() => {
|
|
17
|
+
liveQueryStatements.delete(entry);
|
|
18
|
+
});
|
|
19
|
+
return entry.closePromise;
|
|
20
|
+
};
|
|
21
|
+
const retireQueryStatement = (entry) => {
|
|
22
|
+
if (queryStatementsBySql.get(entry.sql) === entry) {
|
|
23
|
+
queryStatementsBySql.delete(entry.sql);
|
|
24
|
+
}
|
|
25
|
+
entry.retired = true;
|
|
26
|
+
if (entry.pending === 0) {
|
|
27
|
+
void closeQueryStatement(entry);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const acquireQueryStatement = (sql) => {
|
|
31
|
+
assertUsable();
|
|
32
|
+
let entry = queryStatementsBySql.get(sql);
|
|
33
|
+
if (entry === void 0) {
|
|
34
|
+
entry = {
|
|
35
|
+
sql,
|
|
36
|
+
statement: database.prepare(sql),
|
|
37
|
+
tail: Promise.resolve(),
|
|
38
|
+
pending: 0,
|
|
39
|
+
retired: false
|
|
40
|
+
};
|
|
41
|
+
queryStatementsBySql.set(sql, entry);
|
|
42
|
+
liveQueryStatements.add(entry);
|
|
43
|
+
if (queryStatementsBySql.size > MAX_QUERY_STATEMENT_CACHE_SIZE) {
|
|
44
|
+
const oldest = queryStatementsBySql.values().next().value;
|
|
45
|
+
if (oldest !== void 0) {
|
|
46
|
+
retireQueryStatement(oldest);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
queryStatementsBySql.delete(sql);
|
|
51
|
+
queryStatementsBySql.set(sql, entry);
|
|
52
|
+
}
|
|
53
|
+
entry.pending += 1;
|
|
54
|
+
const turn = entry.tail;
|
|
55
|
+
let release;
|
|
56
|
+
const completion = new Promise((resolve) => {
|
|
57
|
+
release = resolve;
|
|
58
|
+
});
|
|
59
|
+
entry.tail = turn.then(() => completion);
|
|
60
|
+
return { entry, turn, release };
|
|
61
|
+
};
|
|
62
|
+
const adapter = {
|
|
6
63
|
libsqlSearchAdapter: true,
|
|
7
64
|
backend: "turso",
|
|
8
65
|
/**
|
|
@@ -12,15 +69,24 @@ function tursoAdapter(database) {
|
|
|
12
69
|
*/
|
|
13
70
|
supportsVectorIndex: false,
|
|
14
71
|
async executeDdl(sql) {
|
|
72
|
+
assertUsable();
|
|
15
73
|
await database.exec(sql);
|
|
16
74
|
},
|
|
17
75
|
async executeQuery(sql, args) {
|
|
18
|
-
const
|
|
76
|
+
const { entry, turn, release } = acquireQueryStatement(sql);
|
|
77
|
+
await turn;
|
|
19
78
|
try {
|
|
20
|
-
const rows = await (args === void 0 ? statement.all() : statement.all(args));
|
|
79
|
+
const rows = await (args === void 0 ? entry.statement.all() : entry.statement.all(args));
|
|
21
80
|
return rows;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
retireQueryStatement(entry);
|
|
83
|
+
throw error;
|
|
22
84
|
} finally {
|
|
23
|
-
|
|
85
|
+
entry.pending -= 1;
|
|
86
|
+
release();
|
|
87
|
+
if (entry.retired && entry.pending === 0) {
|
|
88
|
+
await closeQueryStatement(entry);
|
|
89
|
+
}
|
|
24
90
|
}
|
|
25
91
|
},
|
|
26
92
|
/**
|
|
@@ -37,6 +103,7 @@ function tursoAdapter(database) {
|
|
|
37
103
|
* this turns N+1 prepares into exactly 2 regardless of corpus size.
|
|
38
104
|
*/
|
|
39
105
|
async executeAtomicWrite(statements) {
|
|
106
|
+
assertUsable();
|
|
40
107
|
const preparedBySql = /* @__PURE__ */ new Map();
|
|
41
108
|
const prepareOnce = (sql) => {
|
|
42
109
|
let prepared = preparedBySql.get(sql);
|
|
@@ -62,16 +129,32 @@ function tursoAdapter(database) {
|
|
|
62
129
|
throw error;
|
|
63
130
|
} finally {
|
|
64
131
|
for (const prepared of preparedBySql.values()) {
|
|
65
|
-
closeStatement(prepared);
|
|
132
|
+
await closeStatement(prepared);
|
|
66
133
|
}
|
|
67
134
|
preparedBySql.clear();
|
|
68
135
|
}
|
|
136
|
+
},
|
|
137
|
+
dispose() {
|
|
138
|
+
disposePromise ??= (async () => {
|
|
139
|
+
disposed = true;
|
|
140
|
+
for (const entry of [...queryStatementsBySql.values()]) {
|
|
141
|
+
retireQueryStatement(entry);
|
|
142
|
+
}
|
|
143
|
+
await Promise.all(
|
|
144
|
+
[...liveQueryStatements].map(async (entry) => {
|
|
145
|
+
await entry.tail;
|
|
146
|
+
await closeQueryStatement(entry);
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
})();
|
|
150
|
+
return disposePromise;
|
|
69
151
|
}
|
|
70
152
|
};
|
|
153
|
+
return adapter;
|
|
71
154
|
}
|
|
72
|
-
function closeStatement(statement) {
|
|
155
|
+
async function closeStatement(statement) {
|
|
73
156
|
try {
|
|
74
|
-
statement.close?.();
|
|
157
|
+
await statement.close?.();
|
|
75
158
|
} catch {
|
|
76
159
|
}
|
|
77
160
|
}
|
package/dist/turso.d.ts
CHANGED
|
@@ -86,11 +86,21 @@ interface DatabaseAdapter {
|
|
|
86
86
|
* import { tursoAdapter } from 'libsql-search/turso';
|
|
87
87
|
* import { createTable, indexContent, search } from 'libsql-search';
|
|
88
88
|
*
|
|
89
|
-
* const
|
|
89
|
+
* const database = await connect('./local.db');
|
|
90
|
+
* const client = tursoAdapter(database);
|
|
91
|
+
* const embeddingOptions = {
|
|
92
|
+
* provider: 'openai-compatible' as const,
|
|
93
|
+
* baseUrl: 'https://embeddings.example.com/v1',
|
|
94
|
+
* model: 'bge-large-en-v1.5',
|
|
95
|
+
* dimensions: 1024
|
|
96
|
+
* };
|
|
90
97
|
*
|
|
91
|
-
* await createTable(client);
|
|
92
|
-
* await indexContent({ client, contentPath: './content' });
|
|
93
|
-
* const results = await search({ client, query: 'vector search' });
|
|
98
|
+
* await createTable(client, 'articles', 1024);
|
|
99
|
+
* await indexContent({ client, contentPath: './content', embeddingOptions });
|
|
100
|
+
* const results = await search({ client, query: 'vector search', embeddingOptions });
|
|
101
|
+
*
|
|
102
|
+
* await client.dispose();
|
|
103
|
+
* await database.close();
|
|
94
104
|
* ```
|
|
95
105
|
*
|
|
96
106
|
* @module libsql-search/turso
|
|
@@ -113,8 +123,9 @@ interface TursoStatement {
|
|
|
113
123
|
*
|
|
114
124
|
* Not closing leaks roughly 10 KB of native memory per prepare on
|
|
115
125
|
* `@tursodatabase/database`, which the garbage collector does not reclaim
|
|
116
|
-
* because it is not JavaScript heap.
|
|
117
|
-
*
|
|
126
|
+
* because it is not JavaScript heap. The adapter therefore closes
|
|
127
|
+
* transaction-local statements immediately and query statements on safe LRU
|
|
128
|
+
* eviction or disposal.
|
|
118
129
|
*/
|
|
119
130
|
close?(): unknown;
|
|
120
131
|
}
|
|
@@ -130,6 +141,16 @@ interface TursoDatabase {
|
|
|
130
141
|
exec(sql: string): unknown;
|
|
131
142
|
prepare(sql: string): TursoStatement;
|
|
132
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* A Turso-backed adapter with an explicit prepared-statement disposal hook.
|
|
146
|
+
*
|
|
147
|
+
* `dispose()` is terminal: it waits for queued query calls, closes every
|
|
148
|
+
* cached query statement, and rejects later adapter operations. It does not
|
|
149
|
+
* close the caller-owned {@link TursoDatabase} handle.
|
|
150
|
+
*/
|
|
151
|
+
interface TursoAdapter extends DatabaseAdapter {
|
|
152
|
+
dispose(): Promise<void>;
|
|
153
|
+
}
|
|
133
154
|
/**
|
|
134
155
|
* Wrap a `@tursodatabase/database` handle so this library's functions can use
|
|
135
156
|
* it.
|
|
@@ -150,7 +171,7 @@ interface TursoDatabase {
|
|
|
150
171
|
* This preserves the guarantee that a failed rebuild leaves the previous
|
|
151
172
|
* index intact.
|
|
152
173
|
*/
|
|
153
|
-
declare function tursoAdapter(database: TursoDatabase):
|
|
174
|
+
declare function tursoAdapter(database: TursoDatabase): TursoAdapter;
|
|
154
175
|
|
|
155
176
|
export { tursoAdapter };
|
|
156
|
-
export type { DatabaseAdapter, TursoDatabase, TursoStatement };
|
|
177
|
+
export type { DatabaseAdapter, TursoAdapter, TursoDatabase, TursoStatement };
|
package/dist/turso.esm.js
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
|
+
const MAX_QUERY_STATEMENT_CACHE_SIZE = 32;
|
|
1
2
|
function tursoAdapter(database) {
|
|
2
3
|
assertTursoDatabase(database);
|
|
3
|
-
|
|
4
|
+
const queryStatementsBySql = /* @__PURE__ */ new Map();
|
|
5
|
+
const liveQueryStatements = /* @__PURE__ */ new Set();
|
|
6
|
+
let disposed = false;
|
|
7
|
+
let disposePromise;
|
|
8
|
+
const assertUsable = () => {
|
|
9
|
+
if (disposed) {
|
|
10
|
+
throw new Error("This Turso adapter has been disposed");
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const closeQueryStatement = (entry) => {
|
|
14
|
+
entry.closePromise ??= closeStatement(entry.statement).finally(() => {
|
|
15
|
+
liveQueryStatements.delete(entry);
|
|
16
|
+
});
|
|
17
|
+
return entry.closePromise;
|
|
18
|
+
};
|
|
19
|
+
const retireQueryStatement = (entry) => {
|
|
20
|
+
if (queryStatementsBySql.get(entry.sql) === entry) {
|
|
21
|
+
queryStatementsBySql.delete(entry.sql);
|
|
22
|
+
}
|
|
23
|
+
entry.retired = true;
|
|
24
|
+
if (entry.pending === 0) {
|
|
25
|
+
void closeQueryStatement(entry);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const acquireQueryStatement = (sql) => {
|
|
29
|
+
assertUsable();
|
|
30
|
+
let entry = queryStatementsBySql.get(sql);
|
|
31
|
+
if (entry === void 0) {
|
|
32
|
+
entry = {
|
|
33
|
+
sql,
|
|
34
|
+
statement: database.prepare(sql),
|
|
35
|
+
tail: Promise.resolve(),
|
|
36
|
+
pending: 0,
|
|
37
|
+
retired: false
|
|
38
|
+
};
|
|
39
|
+
queryStatementsBySql.set(sql, entry);
|
|
40
|
+
liveQueryStatements.add(entry);
|
|
41
|
+
if (queryStatementsBySql.size > MAX_QUERY_STATEMENT_CACHE_SIZE) {
|
|
42
|
+
const oldest = queryStatementsBySql.values().next().value;
|
|
43
|
+
if (oldest !== void 0) {
|
|
44
|
+
retireQueryStatement(oldest);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
queryStatementsBySql.delete(sql);
|
|
49
|
+
queryStatementsBySql.set(sql, entry);
|
|
50
|
+
}
|
|
51
|
+
entry.pending += 1;
|
|
52
|
+
const turn = entry.tail;
|
|
53
|
+
let release;
|
|
54
|
+
const completion = new Promise((resolve) => {
|
|
55
|
+
release = resolve;
|
|
56
|
+
});
|
|
57
|
+
entry.tail = turn.then(() => completion);
|
|
58
|
+
return { entry, turn, release };
|
|
59
|
+
};
|
|
60
|
+
const adapter = {
|
|
4
61
|
libsqlSearchAdapter: true,
|
|
5
62
|
backend: "turso",
|
|
6
63
|
/**
|
|
@@ -10,15 +67,24 @@ function tursoAdapter(database) {
|
|
|
10
67
|
*/
|
|
11
68
|
supportsVectorIndex: false,
|
|
12
69
|
async executeDdl(sql) {
|
|
70
|
+
assertUsable();
|
|
13
71
|
await database.exec(sql);
|
|
14
72
|
},
|
|
15
73
|
async executeQuery(sql, args) {
|
|
16
|
-
const
|
|
74
|
+
const { entry, turn, release } = acquireQueryStatement(sql);
|
|
75
|
+
await turn;
|
|
17
76
|
try {
|
|
18
|
-
const rows = await (args === void 0 ? statement.all() : statement.all(args));
|
|
77
|
+
const rows = await (args === void 0 ? entry.statement.all() : entry.statement.all(args));
|
|
19
78
|
return rows;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
retireQueryStatement(entry);
|
|
81
|
+
throw error;
|
|
20
82
|
} finally {
|
|
21
|
-
|
|
83
|
+
entry.pending -= 1;
|
|
84
|
+
release();
|
|
85
|
+
if (entry.retired && entry.pending === 0) {
|
|
86
|
+
await closeQueryStatement(entry);
|
|
87
|
+
}
|
|
22
88
|
}
|
|
23
89
|
},
|
|
24
90
|
/**
|
|
@@ -35,6 +101,7 @@ function tursoAdapter(database) {
|
|
|
35
101
|
* this turns N+1 prepares into exactly 2 regardless of corpus size.
|
|
36
102
|
*/
|
|
37
103
|
async executeAtomicWrite(statements) {
|
|
104
|
+
assertUsable();
|
|
38
105
|
const preparedBySql = /* @__PURE__ */ new Map();
|
|
39
106
|
const prepareOnce = (sql) => {
|
|
40
107
|
let prepared = preparedBySql.get(sql);
|
|
@@ -60,16 +127,32 @@ function tursoAdapter(database) {
|
|
|
60
127
|
throw error;
|
|
61
128
|
} finally {
|
|
62
129
|
for (const prepared of preparedBySql.values()) {
|
|
63
|
-
closeStatement(prepared);
|
|
130
|
+
await closeStatement(prepared);
|
|
64
131
|
}
|
|
65
132
|
preparedBySql.clear();
|
|
66
133
|
}
|
|
134
|
+
},
|
|
135
|
+
dispose() {
|
|
136
|
+
disposePromise ??= (async () => {
|
|
137
|
+
disposed = true;
|
|
138
|
+
for (const entry of [...queryStatementsBySql.values()]) {
|
|
139
|
+
retireQueryStatement(entry);
|
|
140
|
+
}
|
|
141
|
+
await Promise.all(
|
|
142
|
+
[...liveQueryStatements].map(async (entry) => {
|
|
143
|
+
await entry.tail;
|
|
144
|
+
await closeQueryStatement(entry);
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
})();
|
|
148
|
+
return disposePromise;
|
|
67
149
|
}
|
|
68
150
|
};
|
|
151
|
+
return adapter;
|
|
69
152
|
}
|
|
70
|
-
function closeStatement(statement) {
|
|
153
|
+
async function closeStatement(statement) {
|
|
71
154
|
try {
|
|
72
|
-
statement.close?.();
|
|
155
|
+
await statement.close?.();
|
|
73
156
|
} catch {
|
|
74
157
|
}
|
|
75
158
|
}
|
package/docs/API.md
CHANGED
|
@@ -113,7 +113,7 @@ Indexes Markdown files from a directory on disk.
|
|
|
113
113
|
interface IndexerOptions {
|
|
114
114
|
client: Client | DatabaseAdapter;
|
|
115
115
|
contentPath: string;
|
|
116
|
-
embeddingOptions
|
|
116
|
+
embeddingOptions: EmbeddingOptions;
|
|
117
117
|
fileExtensions?: string[];
|
|
118
118
|
exclude?: string[];
|
|
119
119
|
tableName?: string;
|
|
@@ -187,7 +187,16 @@ class IndexingError extends Error {
|
|
|
187
187
|
import { indexContent, IndexingError } from "libsql-search";
|
|
188
188
|
|
|
189
189
|
try {
|
|
190
|
-
await indexContent({
|
|
190
|
+
await indexContent({
|
|
191
|
+
client,
|
|
192
|
+
contentPath: "./content",
|
|
193
|
+
embeddingOptions: {
|
|
194
|
+
provider: "openai-compatible",
|
|
195
|
+
baseUrl: process.env.EMBEDDING_BASE_URL!,
|
|
196
|
+
model: "bge-large-en-v1.5",
|
|
197
|
+
dimensions: 1024,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
191
200
|
} catch (error) {
|
|
192
201
|
if (error instanceof IndexingError) {
|
|
193
202
|
console.error(error.phase, error.failures);
|
|
@@ -212,7 +221,7 @@ interface SearchOptions {
|
|
|
212
221
|
query: string;
|
|
213
222
|
limit?: number;
|
|
214
223
|
tableName?: string;
|
|
215
|
-
embeddingOptions
|
|
224
|
+
embeddingOptions: EmbeddingOptions;
|
|
216
225
|
candidates?: number;
|
|
217
226
|
exact?: boolean;
|
|
218
227
|
}
|
|
@@ -246,7 +255,7 @@ Controls how many rows the index returns for the exact re-rank. It must be an in
|
|
|
246
255
|
|
|
247
256
|
```ts
|
|
248
257
|
// Trade query cost for recall on a large corpus
|
|
249
|
-
const results = await search({ client, query, limit: 10, candidates: 200 });
|
|
258
|
+
const results = await search({ client, query, embeddingOptions, limit: 10, candidates: 200 });
|
|
250
259
|
```
|
|
251
260
|
|
|
252
261
|
`candidates` has no effect when `exact` is `true` — that path scans every row — but it is **still validated**. `search({ exact: true, limit: 10, candidates: 5 })` throws, exactly as it would on the index path. Validity does not depend on which path a call happens to take.
|
|
@@ -262,7 +271,7 @@ Related exported constants:
|
|
|
262
271
|
Set `exact: true` to bypass the index and score every row in the table.
|
|
263
272
|
|
|
264
273
|
```ts
|
|
265
|
-
const results = await search({ client, query, exact: true });
|
|
274
|
+
const results = await search({ client, query, embeddingOptions, exact: true });
|
|
266
275
|
```
|
|
267
276
|
|
|
268
277
|
This is the guaranteed-exact path: it computes `vector_distance_cos` for every row with a non-`NULL` embedding, sorts by `(distance, id)`, and trims to `limit`. Cost grows linearly with table size, so it is intended for small corpora, correctness checks against the index path, and tables that have no vector index.
|
|
@@ -330,8 +339,7 @@ All retrieval helpers validate `tableName` before executing SQL, and all of them
|
|
|
330
339
|
|
|
331
340
|
```ts
|
|
332
341
|
interface EmbeddingOptions {
|
|
333
|
-
provider
|
|
334
|
-
| "local"
|
|
342
|
+
provider:
|
|
335
343
|
| "cloudflare"
|
|
336
344
|
| "mistral"
|
|
337
345
|
| "gemini"
|
|
@@ -353,7 +361,7 @@ interface EmbeddingOptions {
|
|
|
353
361
|
|
|
354
362
|
Important option rules:
|
|
355
363
|
|
|
356
|
-
- `provider`
|
|
364
|
+
- `provider` is required; every provider is an external service
|
|
357
365
|
- `maxLength` defaults to `8000`
|
|
358
366
|
- `timeoutMs` defaults to `30000`
|
|
359
367
|
- `model` is only used by `openai-compatible`
|
|
@@ -364,7 +372,6 @@ Important option rules:
|
|
|
364
372
|
|
|
365
373
|
Dimension rules:
|
|
366
374
|
|
|
367
|
-
- local: fixed `384`
|
|
368
375
|
- Cloudflare: fixed `1024`
|
|
369
376
|
- Mistral: fixed `1024`
|
|
370
377
|
- Gemini: default `3072`, allowed integer range `128-3072`
|
|
@@ -373,7 +380,7 @@ Dimension rules:
|
|
|
373
380
|
|
|
374
381
|
See [Provider matrix and credential rules](./PROVIDERS.md) for the canonical provider table.
|
|
375
382
|
|
|
376
|
-
### `generateEmbedding(text, options
|
|
383
|
+
### `generateEmbedding(text, options)`
|
|
377
384
|
|
|
378
385
|
Generates one embedding vector.
|
|
379
386
|
|
|
@@ -385,15 +392,15 @@ const embedding = await generateEmbedding("deploy docs", {
|
|
|
385
392
|
});
|
|
386
393
|
```
|
|
387
394
|
|
|
388
|
-
### `generateEmbeddings(texts, options
|
|
395
|
+
### `generateEmbeddings(texts, options)`
|
|
389
396
|
|
|
390
397
|
Generates an ordered batch of embeddings.
|
|
391
398
|
|
|
392
|
-
- empty batches return `[]` without
|
|
399
|
+
- empty batches return `[]` without configuring credentials or making a provider call
|
|
393
400
|
- OpenAI batches above `2048` inputs are rejected before network work
|
|
394
401
|
- `openai-compatible` batches are chunked sequentially according to `batchSize`
|
|
395
402
|
|
|
396
|
-
### `createEmbeddingProvider(options
|
|
403
|
+
### `createEmbeddingProvider(options)`
|
|
397
404
|
|
|
398
405
|
Creates a provider client with immutable metadata and an `embed(texts, options?)` method.
|
|
399
406
|
|
|
@@ -413,7 +420,7 @@ Provider clients return a rich `EmbeddingBatchResult`; the compatibility helpers
|
|
|
413
420
|
|
|
414
421
|
Hosted provider clients are scoped to their current options. The library does not reuse a Cloudflare, Mistral, Gemini, or OpenAI client across different credential sets or configurations.
|
|
415
422
|
|
|
416
|
-
### `getEmbeddingProviderMetadata(options
|
|
423
|
+
### `getEmbeddingProviderMetadata(options)`
|
|
417
424
|
|
|
418
425
|
Returns the same metadata exposed by `createEmbeddingProvider(options).metadata` without resolving hosted-provider credentials.
|
|
419
426
|
|
|
@@ -422,7 +429,6 @@ Metadata shape:
|
|
|
422
429
|
```ts
|
|
423
430
|
interface EmbeddingProviderMetadata {
|
|
424
431
|
name:
|
|
425
|
-
| "local"
|
|
426
432
|
| "cloudflare"
|
|
427
433
|
| "mistral"
|
|
428
434
|
| "gemini"
|
|
@@ -451,7 +457,6 @@ Batch interpretation:
|
|
|
451
457
|
interface EmbeddingBatchResult {
|
|
452
458
|
embeddings: number[][];
|
|
453
459
|
provider:
|
|
454
|
-
| "local"
|
|
455
460
|
| "cloudflare"
|
|
456
461
|
| "mistral"
|
|
457
462
|
| "gemini"
|
|
@@ -474,7 +479,7 @@ Validates provider responses before they reach the database:
|
|
|
474
479
|
|
|
475
480
|
### `padEmbedding(embedding, targetDimensions)`
|
|
476
481
|
|
|
477
|
-
Pads or truncates a vector to the target width. This is exported for compatibility and migration workflows
|
|
482
|
+
Pads or truncates a vector to the target width. This is exported for compatibility and migration workflows; provider adapters otherwise validate and preserve the vectors returned by their external service.
|
|
478
483
|
|
|
479
484
|
### `prepareTextForEmbedding(fields)`
|
|
480
485
|
|
package/docs/INDEXING.md
CHANGED
|
@@ -18,13 +18,18 @@ The slug is derived from the file path relative to `contentPath`.
|
|
|
18
18
|
`indexContent()` replaces the whole target table:
|
|
19
19
|
|
|
20
20
|
```ts
|
|
21
|
+
const embeddingOptions = {
|
|
22
|
+
provider: "openai-compatible" as const,
|
|
23
|
+
baseUrl: process.env.EMBEDDING_BASE_URL!,
|
|
24
|
+
model: "bge-large-en-v1.5",
|
|
25
|
+
dimensions: 1024,
|
|
26
|
+
};
|
|
27
|
+
|
|
21
28
|
await indexContent({
|
|
22
29
|
client,
|
|
23
30
|
contentPath: "./content",
|
|
24
|
-
tableName: "
|
|
25
|
-
embeddingOptions
|
|
26
|
-
provider: "local",
|
|
27
|
-
},
|
|
31
|
+
tableName: "articles_bge_1024",
|
|
32
|
+
embeddingOptions,
|
|
28
33
|
});
|
|
29
34
|
```
|
|
30
35
|
|
|
@@ -69,7 +74,7 @@ Both are governed by `failurePolicy` like any other build failure, so they abort
|
|
|
69
74
|
import { indexContent, IndexingError } from "libsql-search";
|
|
70
75
|
|
|
71
76
|
try {
|
|
72
|
-
await indexContent({ client, contentPath: "./content" });
|
|
77
|
+
await indexContent({ client, contentPath: "./content", embeddingOptions });
|
|
73
78
|
} catch (error) {
|
|
74
79
|
if (error instanceof IndexingError) {
|
|
75
80
|
for (const failure of error.failures) {
|
|
@@ -87,6 +92,7 @@ By default one bad file aborts the whole rebuild. To index everything that can b
|
|
|
87
92
|
const result = await indexContent({
|
|
88
93
|
client,
|
|
89
94
|
contentPath: "./content",
|
|
95
|
+
embeddingOptions,
|
|
90
96
|
failurePolicy: "skip",
|
|
91
97
|
});
|
|
92
98
|
|
|
@@ -105,6 +111,7 @@ An empty source directory throws by default, because silently leaving stale rows
|
|
|
105
111
|
await indexContent({
|
|
106
112
|
client,
|
|
107
113
|
contentPath: "./content",
|
|
114
|
+
embeddingOptions,
|
|
108
115
|
allowEmptyIndex: true,
|
|
109
116
|
});
|
|
110
117
|
```
|
|
@@ -173,7 +180,6 @@ Many projects wire indexing into a dedicated script and call it before their sit
|
|
|
173
180
|
|
|
174
181
|
## Runtime Notes
|
|
175
182
|
|
|
176
|
-
- local embeddings may download and cache a model on the first run
|
|
177
183
|
- Node users need `@libsql/client` installed alongside the package, at `^0.15.0 || ^0.17.0`; the packaged build is smoke-tested against both arms (`0.15.15` and `0.17.4`), which covers table and vector-index creation. `batch()` rollback behaves identically on both at the contract level, though its error text differs — see [Version differences](./TROUBLESHOOTING.md#libsqlclient-version-differences). Upgrading the client is not a prerequisite for upgrading this package. Deno/JSR users are not covered by that range and should pin the client themselves — see [Install](../README.md#install)
|
|
178
|
-
-
|
|
184
|
+
- all embedding providers send indexed or queried text to external services; this library never loads a model in-process
|
|
179
185
|
- the repository validates package build and `deno check`, but indexing still depends on filesystem access
|
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -55,13 +55,6 @@ These presets keep credentials out of the source file while making dimensions an
|
|
|
55
55
|
|
|
56
56
|
```ts
|
|
57
57
|
const providerPresets = {
|
|
58
|
-
local: {
|
|
59
|
-
tableName: "articles_local_384",
|
|
60
|
-
dimensions: 384,
|
|
61
|
-
embeddingOptions: {
|
|
62
|
-
provider: "local" as const,
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
58
|
cloudflare: {
|
|
66
59
|
tableName: "articles_cf_bgem3_1024",
|
|
67
60
|
dimensions: 1024,
|
|
@@ -137,9 +130,13 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
137
130
|
client,
|
|
138
131
|
query,
|
|
139
132
|
limit,
|
|
140
|
-
tableName: "
|
|
133
|
+
tableName: "articles_tei_1024",
|
|
141
134
|
embeddingOptions: {
|
|
142
|
-
provider: "
|
|
135
|
+
provider: "openai-compatible",
|
|
136
|
+
baseUrl: process.env.EMBEDDING_BASE_URL!,
|
|
137
|
+
model: process.env.EMBEDDING_MODEL!,
|
|
138
|
+
dimensions: 1024,
|
|
139
|
+
apiKey: process.env.EMBEDDING_API_KEY,
|
|
143
140
|
intent: "query",
|
|
144
141
|
},
|
|
145
142
|
});
|
|
@@ -162,14 +159,14 @@ const client = createClient({
|
|
|
162
159
|
});
|
|
163
160
|
|
|
164
161
|
export async function getStaticPaths() {
|
|
165
|
-
const articles = await getAllArticles(client, "
|
|
162
|
+
const articles = await getAllArticles(client, "articles_tei_1024");
|
|
166
163
|
|
|
167
164
|
return articles.map((article) => ({
|
|
168
165
|
params: { slug: article.slug },
|
|
169
166
|
}));
|
|
170
167
|
}
|
|
171
168
|
|
|
172
|
-
const article = await getArticleBySlug(client, "guides/getting-started", "
|
|
169
|
+
const article = await getArticleBySlug(client, "guides/getting-started", "articles_tei_1024");
|
|
173
170
|
```
|
|
174
171
|
|
|
175
172
|
## Next.js Route Handler
|
|
@@ -191,9 +188,13 @@ export async function POST(request: NextRequest) {
|
|
|
191
188
|
client,
|
|
192
189
|
query,
|
|
193
190
|
limit,
|
|
194
|
-
tableName: "
|
|
191
|
+
tableName: "articles_tei_1024",
|
|
195
192
|
embeddingOptions: {
|
|
196
|
-
provider: "
|
|
193
|
+
provider: "openai-compatible",
|
|
194
|
+
baseUrl: process.env.EMBEDDING_BASE_URL!,
|
|
195
|
+
model: process.env.EMBEDDING_MODEL!,
|
|
196
|
+
dimensions: 1024,
|
|
197
|
+
apiKey: process.env.EMBEDDING_API_KEY,
|
|
197
198
|
intent: "query",
|
|
198
199
|
},
|
|
199
200
|
});
|
|
@@ -214,7 +215,7 @@ const client = createClient({
|
|
|
214
215
|
});
|
|
215
216
|
|
|
216
217
|
export async function generateStaticParams() {
|
|
217
|
-
const articles = await getAllArticles(client, "
|
|
218
|
+
const articles = await getAllArticles(client, "articles_tei_1024");
|
|
218
219
|
|
|
219
220
|
return articles.map((article) => ({
|
|
220
221
|
slug: article.slug,
|
|
@@ -223,7 +224,7 @@ export async function generateStaticParams() {
|
|
|
223
224
|
|
|
224
225
|
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
|
225
226
|
const { slug } = await params;
|
|
226
|
-
const article = await getArticleBySlug(client, slug, "
|
|
227
|
+
const article = await getArticleBySlug(client, slug, "articles_tei_1024");
|
|
227
228
|
|
|
228
229
|
return <article>{article?.title}</article>;
|
|
229
230
|
}
|
|
@@ -241,13 +242,6 @@ const client = createClient({
|
|
|
241
242
|
});
|
|
242
243
|
|
|
243
244
|
const providerPresets = {
|
|
244
|
-
local: {
|
|
245
|
-
tableName: "articles_local_384",
|
|
246
|
-
dimensions: 384,
|
|
247
|
-
embeddingOptions: {
|
|
248
|
-
provider: "local" as const,
|
|
249
|
-
},
|
|
250
|
-
},
|
|
251
245
|
cloudflare: {
|
|
252
246
|
tableName: "articles_cf_bgem3_1024",
|
|
253
247
|
dimensions: 1024,
|
|
@@ -299,7 +293,7 @@ const providerPresets = {
|
|
|
299
293
|
},
|
|
300
294
|
} as const;
|
|
301
295
|
|
|
302
|
-
const provider = process.env.EMBEDDING_PROVIDER ?? "
|
|
296
|
+
const provider = process.env.EMBEDDING_PROVIDER ?? "openai-compatible";
|
|
303
297
|
|
|
304
298
|
if (!(provider in providerPresets)) {
|
|
305
299
|
throw new Error(
|
|
@@ -349,4 +343,4 @@ await generateEmbeddings(["doc one", "doc two"], {
|
|
|
349
343
|
});
|
|
350
344
|
```
|
|
351
345
|
|
|
352
|
-
The repository test suite should not require real provider credentials. See [Testing guidance](./TESTING.md) for
|
|
346
|
+
The repository test suite should not require real provider credentials. See [Testing guidance](./TESTING.md) for embedding-service mocks, Gemini SDK mocks, and validation-before-network assertions.
|