libsql-search 0.9.1 → 0.10.0
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 +2 -0
- package/dist/index.cjs +91 -39
- package/dist/index.d.ts +107 -8
- package/dist/index.esm.js +91 -39
- package/dist/turso.cjs +96 -0
- package/dist/turso.d.ts +156 -0
- package/dist/turso.esm.js +94 -0
- package/docs/API.md +28 -3
- package/docs/README.md +1 -0
- package/docs/TURSO.md +230 -0
- package/package.json +16 -3
package/docs/API.md
CHANGED
|
@@ -52,6 +52,25 @@ It also exports these types:
|
|
|
52
52
|
- [Migration and reindexing guide](./MIGRATIONS.md)
|
|
53
53
|
- [Indexing and operational behavior](./INDEXING.md)
|
|
54
54
|
- [Testing guidance](./TESTING.md)
|
|
55
|
+
- [Turso Database backend](./TURSO.md)
|
|
56
|
+
|
|
57
|
+
## The `client` argument
|
|
58
|
+
|
|
59
|
+
Every function that talks to a database takes a `client`. It accepts either:
|
|
60
|
+
|
|
61
|
+
- a `@libsql/client` `Client` — the default, and the only backend the main entry point references, or
|
|
62
|
+
- an adapter from a backend entry point, currently `tursoAdapter()` from `libsql-search/turso`
|
|
63
|
+
|
|
64
|
+
`IndexerOptions["client"]` and `SearchOptions["client"]` are typed as
|
|
65
|
+
`Client | DatabaseAdapter`, so existing `Client`-typed code needs no change. The
|
|
66
|
+
main entry point exports no new symbol for this and never imports a backend
|
|
67
|
+
package other than `@libsql/client`.
|
|
68
|
+
|
|
69
|
+
The adapter carries a capability flag that changes two behaviors on a backend
|
|
70
|
+
without an approximate-nearest-neighbor vector index: `createTable()` skips the
|
|
71
|
+
vector index, and `search()` uses the exact path automatically. Both are
|
|
72
|
+
described in the [Turso Database backend guide](./TURSO.md), which is currently
|
|
73
|
+
the only backend where they apply.
|
|
55
74
|
|
|
56
75
|
## `createTable(client, tableName?, dimensions?)`
|
|
57
76
|
|
|
@@ -61,6 +80,10 @@ Creates the search table and supporting indexes.
|
|
|
61
80
|
await createTable(client);
|
|
62
81
|
```
|
|
63
82
|
|
|
83
|
+
On a backend with no vector index support, the `<tableName>_embedding_idx`
|
|
84
|
+
vector index is skipped; the table, the folder index, and the slug index are
|
|
85
|
+
still created. On `@libsql/client` it is never skipped.
|
|
86
|
+
|
|
64
87
|
Defaults:
|
|
65
88
|
|
|
66
89
|
- `tableName`: `"articles"`
|
|
@@ -88,7 +111,7 @@ Indexes Markdown files from a directory on disk.
|
|
|
88
111
|
|
|
89
112
|
```ts
|
|
90
113
|
interface IndexerOptions {
|
|
91
|
-
client: Client;
|
|
114
|
+
client: Client | DatabaseAdapter;
|
|
92
115
|
contentPath: string;
|
|
93
116
|
embeddingOptions?: EmbeddingOptions;
|
|
94
117
|
fileExtensions?: string[];
|
|
@@ -185,7 +208,7 @@ Generates a query embedding and performs vector similarity search.
|
|
|
185
208
|
|
|
186
209
|
```ts
|
|
187
210
|
interface SearchOptions {
|
|
188
|
-
client: Client;
|
|
211
|
+
client: Client | DatabaseAdapter;
|
|
189
212
|
query: string;
|
|
190
213
|
limit?: number;
|
|
191
214
|
tableName?: string;
|
|
@@ -244,6 +267,8 @@ const results = await search({ client, query, exact: true });
|
|
|
244
267
|
|
|
245
268
|
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.
|
|
246
269
|
|
|
270
|
+
A backend with no vector index at all takes this path whether or not `exact` is set, because there is no index path for it to fall back from. That is the case for `@tursodatabase/database`; see the [Turso Database backend guide](./TURSO.md).
|
|
271
|
+
|
|
247
272
|
### Missing Vector Index
|
|
248
273
|
|
|
249
274
|
If the target table has no `<tableName>_embedding_idx`, the default path throws an error naming the missing index and pointing at `createTable()` and `exact: true`. libSQL's own message for this case ("failed to parse vector index parameters") says nothing about a missing index, so it is preserved as the thrown error's `cause` rather than surfaced directly.
|
|
@@ -297,7 +322,7 @@ Returns articles in a specific folder.
|
|
|
297
322
|
|
|
298
323
|
Returns distinct folder names from the index.
|
|
299
324
|
|
|
300
|
-
All retrieval helpers validate `tableName` before executing SQL.
|
|
325
|
+
All retrieval helpers validate `tableName` before executing SQL, and all of them accept either client kind.
|
|
301
326
|
|
|
302
327
|
## Embedding Helpers
|
|
303
328
|
|
package/docs/README.md
CHANGED
|
@@ -8,6 +8,7 @@ This directory holds the longer-form reference material for `libsql-search`.
|
|
|
8
8
|
- [API reference](./API.md): exported functions, option shapes, and result data
|
|
9
9
|
- [Indexing and operations](./INDEXING.md): content layout, rebuild behavior, and search quality notes
|
|
10
10
|
- [Testing guidance](./TESTING.md): CI-safe mocks and no-live-call policy
|
|
11
|
+
- [Turso Database backend](./TURSO.md): experimental exact-search-only support for the in-process `@tursodatabase/database` client
|
|
11
12
|
- [Troubleshooting](./TROUBLESHOOTING.md): known install and runtime issues
|
|
12
13
|
- [Releasing](./RELEASING.md): maintainer release workflow
|
|
13
14
|
|
package/docs/TURSO.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Turso Database backend (experimental)
|
|
2
|
+
|
|
3
|
+
`libsql-search` can run against the in-process [`@tursodatabase/database`](https://www.npmjs.com/package/@tursodatabase/database)
|
|
4
|
+
client in addition to `@libsql/client`.
|
|
5
|
+
|
|
6
|
+
**This backend is experimental, and search on it is exact-only.** Read
|
|
7
|
+
[What is different on Turso](#what-is-different-on-turso) before adopting it —
|
|
8
|
+
the difference is architectural, not a rough edge that will be smoothed out by
|
|
9
|
+
an upgrade.
|
|
10
|
+
|
|
11
|
+
`@libsql/client` remains the default and the only backend the main entry point
|
|
12
|
+
knows about. Nothing on this page affects you if you do not import
|
|
13
|
+
`libsql-search/turso`.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
`@tursodatabase/database` is an **optional** peer dependency, declared as
|
|
18
|
+
`^0.7.0 || ^0.8.0`. It is not installed, resolved, or imported unless you ask
|
|
19
|
+
for it. Both lines behave identically on every dimension this adapter depends
|
|
20
|
+
on; the disjunction is a caret-per-minor range for the same reason
|
|
21
|
+
`@libsql/client`'s is.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm add libsql-search @tursodatabase/database
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install libsql-search @tursodatabase/database
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
Wrap the connected handle with `tursoAdapter()` and pass the result as `client`
|
|
34
|
+
everywhere you would otherwise pass a libSQL client.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { connect } from "@tursodatabase/database";
|
|
38
|
+
import { tursoAdapter } from "libsql-search/turso";
|
|
39
|
+
import { createTable, indexContent, search } from "libsql-search";
|
|
40
|
+
|
|
41
|
+
// connect() is async. Awaiting it is not optional — tursoAdapter() rejects a
|
|
42
|
+
// pending Promise with a message saying so, because it is the most common
|
|
43
|
+
// first-use mistake.
|
|
44
|
+
const database = await connect("./local.db");
|
|
45
|
+
const client = tursoAdapter(database);
|
|
46
|
+
|
|
47
|
+
await createTable(client, "articles", 384);
|
|
48
|
+
|
|
49
|
+
await indexContent({
|
|
50
|
+
client,
|
|
51
|
+
contentPath: "./content",
|
|
52
|
+
embeddingOptions: { provider: "local" },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const results = await search({
|
|
56
|
+
client,
|
|
57
|
+
query: "how do I deploy my docs site",
|
|
58
|
+
limit: 5,
|
|
59
|
+
embeddingOptions: { provider: "local" },
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Use `":memory:"` instead of a file path for an ephemeral database.
|
|
64
|
+
|
|
65
|
+
`createTable`, `indexContent`, `search`, `getAllArticles`, `getArticleBySlug`,
|
|
66
|
+
`getArticlesByFolder`, and `getFolders` all accept the adapter. The option and
|
|
67
|
+
result shapes — `SearchOptions`, `SearchResult`, `IndexerOptions`,
|
|
68
|
+
`IndexResult` — are identical on both backends.
|
|
69
|
+
|
|
70
|
+
## What is different on Turso
|
|
71
|
+
|
|
72
|
+
### There is no ANN vector index, so search is a full scan
|
|
73
|
+
|
|
74
|
+
Turso Database implements the vector *functions* — `vector()`, `vector32()`,
|
|
75
|
+
`vector_distance_cos()` — but not the approximate-nearest-neighbor index that
|
|
76
|
+
libSQL exposes through `libsql_vector_idx()` and `vector_top_k()`. Attempting
|
|
77
|
+
either produces a parse error and a "no such table: vector_top_k" respectively.
|
|
78
|
+
|
|
79
|
+
Two consequences, both handled for you:
|
|
80
|
+
|
|
81
|
+
- `createTable()` creates the table, the folder index, and the slug index, and
|
|
82
|
+
**skips** the vector index. It does not skip it on `@libsql/client`.
|
|
83
|
+
- `search()` runs the exact full-scan path automatically. You do not pass
|
|
84
|
+
`exact: true`, and passing it changes nothing.
|
|
85
|
+
|
|
86
|
+
**Search cost is therefore linear in the number of indexed rows.** For a
|
|
87
|
+
personal site or a docs set this is generally fine and often faster than an
|
|
88
|
+
index probe. For a large corpus, use `@libsql/client`, which is not affected by
|
|
89
|
+
any of this.
|
|
90
|
+
|
|
91
|
+
This is a design direction rather than an unshipped feature:
|
|
92
|
+
|
|
93
|
+
- [`tursodatabase/turso` #832](https://github.com/tursodatabase/turso/issues/832),
|
|
94
|
+
the DiskANN port, is open, unassigned, and in the Backlog milestone.
|
|
95
|
+
- [`tursodatabase/turso` #3778](https://github.com/tursodatabase/turso/issues/3778)
|
|
96
|
+
was closed as *completed* by shipping SIMD-accelerated **exact** vector search,
|
|
97
|
+
with the maintainer noting that "fast exact search is what many use cases
|
|
98
|
+
actually want".
|
|
99
|
+
- The project's `COMPAT.md` lists the vector functions as supported and has no
|
|
100
|
+
row for `libsql_vector_idx` or `vector_top_k` at all.
|
|
101
|
+
|
|
102
|
+
One thing worth knowing, because it costs people an afternoon: Turso's own
|
|
103
|
+
**AI & Embeddings** documentation page markets `vector_top_k` as a "Turso and
|
|
104
|
+
libSQL" feature without distinguishing the two engines. If you arrived expecting
|
|
105
|
+
it to work on `@tursodatabase/database`, that page is why. It does not.
|
|
106
|
+
|
|
107
|
+
### Index replacement uses a transaction, not a batch
|
|
108
|
+
|
|
109
|
+
`indexContent()` builds every document in memory and then replaces the table
|
|
110
|
+
contents in one atomic write, so a failed rebuild leaves the previous index
|
|
111
|
+
intact. That guarantee holds identically on both backends, but the primitive
|
|
112
|
+
behind it is inverted between them:
|
|
113
|
+
|
|
114
|
+
| | `@libsql/client` | `@tursodatabase/database` |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| `batch(statements, "write")` | atomic | **not atomic** |
|
|
117
|
+
| `transaction()` | breaks in-memory clients | atomic, but **deferred** — see below |
|
|
118
|
+
|
|
119
|
+
Turso's `batch()` is not transactional: a batch that fails part way through
|
|
120
|
+
leaves the statements before the failure committed. On this backend the
|
|
121
|
+
replacement therefore runs inside an explicit `BEGIN IMMEDIATE` / `COMMIT`, with
|
|
122
|
+
an explicit `ROLLBACK` on failure. If you are extending this library, do not
|
|
123
|
+
"simplify" the Turso path to use `batch()` — it would silently convert a failed
|
|
124
|
+
rebuild into a half-destroyed index.
|
|
125
|
+
|
|
126
|
+
**Nor is `transaction()` the shortcut it looks like.** Turso's `transaction()`
|
|
127
|
+
is better-sqlite3-style: it *returns a wrapped function* rather than executing.
|
|
128
|
+
`await db.transaction(fn)` resolves to that function and never runs `fn` — no
|
|
129
|
+
error, no rows written. That is why this adapter issues `BEGIN IMMEDIATE`
|
|
130
|
+
itself.
|
|
131
|
+
|
|
132
|
+
This one is genuinely dangerous to get wrong, because it fails green. Rewriting
|
|
133
|
+
`executeAtomicWrite()` as `await database.transaction(async () => { ... })`
|
|
134
|
+
throws nothing and **still passes every test in the suite**, including the one
|
|
135
|
+
asserting that a failed rebuild leaves the previous index intact — an index that
|
|
136
|
+
was never touched trivially satisfies "unchanged". The reindex becomes a silent
|
|
137
|
+
no-op in production while CI stays green. `tests/turso-database.test.ts` pins
|
|
138
|
+
the deferred-execution behavior directly for this reason.
|
|
139
|
+
|
|
140
|
+
## Platform support
|
|
141
|
+
|
|
142
|
+
`@tursodatabase/database` ships prebuilt native binaries for these targets only:
|
|
143
|
+
|
|
144
|
+
- `darwin-arm64` (Apple Silicon)
|
|
145
|
+
- `linux-x64-gnu`
|
|
146
|
+
- `linux-arm64-gnu`
|
|
147
|
+
- `win32-x64-msvc`
|
|
148
|
+
|
|
149
|
+
Two gaps follow from that list, and Turso's documentation does not address
|
|
150
|
+
either one, so this is an observation about the published binaries rather than a
|
|
151
|
+
statement of their support policy:
|
|
152
|
+
|
|
153
|
+
- **No musl target.** Alpine-based images are not covered by a prebuilt binary.
|
|
154
|
+
- **No `darwin-x64`.** Intel Macs are not covered either.
|
|
155
|
+
|
|
156
|
+
`@libsql/client` has no such constraint, which is another reason it stays the
|
|
157
|
+
default.
|
|
158
|
+
|
|
159
|
+
The integration suite in `tests/turso-database.test.ts` skips itself — with a
|
|
160
|
+
warning naming the platform and the reason — anywhere the native module fails to
|
|
161
|
+
load, rather than failing the run. The capability-driven behavior it covers
|
|
162
|
+
(skipping the vector index, forcing the exact search path, routing the
|
|
163
|
+
replacement through the atomic write) is additionally covered on every platform
|
|
164
|
+
by `tests/database.test.ts`, which uses a stub adapter and no native code.
|
|
165
|
+
|
|
166
|
+
Tests never require Turso Cloud credentials, a URL, or a token. Everything runs
|
|
167
|
+
against an in-process `:memory:` database.
|
|
168
|
+
|
|
169
|
+
## Deno and JSR
|
|
170
|
+
|
|
171
|
+
The JSR package exports only the main entry point. `libsql-search/turso` is not
|
|
172
|
+
available there.
|
|
173
|
+
|
|
174
|
+
That is a scope decision, not a technical limit. `src/turso.ts` imports nothing
|
|
175
|
+
from `@tursodatabase/database` — it accepts the handle through a structural
|
|
176
|
+
`TursoDatabase` interface declared in this package — so exposing it on JSR would
|
|
177
|
+
not force Deno to resolve a Node-native package. It is left off because the
|
|
178
|
+
backend is experimental and the native binaries target Node platforms. The entry
|
|
179
|
+
point is still type-checked by `deno task check` so the claim above stays true.
|
|
180
|
+
|
|
181
|
+
## Type compatibility
|
|
182
|
+
|
|
183
|
+
`IndexerOptions["client"]` and `SearchOptions["client"]` accept either a
|
|
184
|
+
`@libsql/client` `Client` or an adapter. Existing `Client`-typed code compiles
|
|
185
|
+
unchanged, and the main entry point exports no new symbol and references no
|
|
186
|
+
Turso type.
|
|
187
|
+
|
|
188
|
+
`tursoAdapter()` returns a `DatabaseAdapter`, and that type is exported from
|
|
189
|
+
`libsql-search/turso` so you can name it:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import { tursoAdapter, type DatabaseAdapter } from "libsql-search/turso";
|
|
193
|
+
|
|
194
|
+
let client: DatabaseAdapter;
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
It is exported from the subpath only. The main entry point does not export it,
|
|
198
|
+
so `libsql-search`'s public surface is unchanged for everyone else. An export
|
|
199
|
+
modifier is not a structural member, so the asymmetry does not affect
|
|
200
|
+
assignability between the two entry points.
|
|
201
|
+
|
|
202
|
+
`libsql-search` and `libsql-search/turso` are bundled separately, so each ships
|
|
203
|
+
its own structural copy of the adapter declaration. `pnpm check:dist-types`
|
|
204
|
+
compiles an adapter from the subpath against the client type from the main entry
|
|
205
|
+
to prove the two stay interchangeable, and runs as part of
|
|
206
|
+
`pnpm validate:package`.
|
|
207
|
+
|
|
208
|
+
## If you are extending this adapter
|
|
209
|
+
|
|
210
|
+
Three things in `src/turso.ts` look like noise and are not. Each has a
|
|
211
|
+
regression test; none of them fails loudly at runtime if removed.
|
|
212
|
+
|
|
213
|
+
**Prepared statements must be closed.** A statement holds native memory that the
|
|
214
|
+
garbage collector cannot reclaim, because it is not JavaScript heap. Every
|
|
215
|
+
`prepare()` is released with `close()` in a `finally`. Measured through the
|
|
216
|
+
built bundle, 60 000 `executeQuery()` calls on one handle grow RSS by ~570 MB
|
|
217
|
+
without the close and ~150 MB with it. `search()` issues exactly one query, so
|
|
218
|
+
an SSR site calling it per request is the case that turns this from untidy into
|
|
219
|
+
an OOM. Inside `executeAtomicWrite()` the cached statements are released only
|
|
220
|
+
*after* `COMMIT` or `ROLLBACK` — a statement stays bound to the transaction
|
|
221
|
+
while it is open.
|
|
222
|
+
|
|
223
|
+
**`BEGIN IMMEDIATE` sits outside the `try`.** If it were inside, a `BEGIN` that
|
|
224
|
+
fails because another rebuild already holds the write lock would fall into the
|
|
225
|
+
`catch` and issue a `ROLLBACK`, ending the *other* call's in-flight transaction
|
|
226
|
+
and destroying a good rebuild that was about to commit. Failing to start a
|
|
227
|
+
transaction must never end one.
|
|
228
|
+
|
|
229
|
+
**Do not reach for `batch()` or `transaction()`.** Both are covered above; both
|
|
230
|
+
fail silently rather than loudly.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libsql-search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Semantic search for static sites using libSQL/Turso with multi-provider embeddings",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.34.5",
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"import": "./dist/index.esm.js",
|
|
13
13
|
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./turso": {
|
|
16
|
+
"types": "./dist/turso.d.ts",
|
|
17
|
+
"import": "./dist/turso.esm.js",
|
|
18
|
+
"require": "./dist/turso.cjs"
|
|
14
19
|
}
|
|
15
20
|
},
|
|
16
21
|
"files": [
|
|
@@ -29,9 +34,10 @@
|
|
|
29
34
|
"test:coverage": "vitest run --coverage",
|
|
30
35
|
"test:release-plan": "node --test ./scripts/plan-release.node-test.mjs",
|
|
31
36
|
"check:deno": "deno task check",
|
|
37
|
+
"check:dist-types": "node ./scripts/check-dist-types.mjs",
|
|
32
38
|
"smoke:package": "node ./scripts/smoke-package.mjs",
|
|
33
39
|
"validate:release-version": "node ./scripts/validate-release-version.mjs",
|
|
34
|
-
"validate:package": "tsc --noEmit && rollup -c && rollup -c rollup.dts.config.js && node ./scripts/smoke-package.mjs",
|
|
40
|
+
"validate:package": "tsc --noEmit && rollup -c && rollup -c rollup.dts.config.js && node ./scripts/check-dist-types.mjs && node ./scripts/smoke-package.mjs",
|
|
35
41
|
"validate:release": "pnpm audit --audit-level moderate && pnpm test:release-plan && pnpm test:coverage && pnpm validate:package && pnpm check:deno",
|
|
36
42
|
"prepublishOnly": "pnpm validate:release"
|
|
37
43
|
},
|
|
@@ -60,7 +66,13 @@
|
|
|
60
66
|
"node": ">=22.12.0"
|
|
61
67
|
},
|
|
62
68
|
"peerDependencies": {
|
|
63
|
-
"@libsql/client": "^0.15.0 || ^0.17.0"
|
|
69
|
+
"@libsql/client": "^0.15.0 || ^0.17.0",
|
|
70
|
+
"@tursodatabase/database": "^0.7.0 || ^0.8.0"
|
|
71
|
+
},
|
|
72
|
+
"peerDependenciesMeta": {
|
|
73
|
+
"@tursodatabase/database": {
|
|
74
|
+
"optional": true
|
|
75
|
+
}
|
|
64
76
|
},
|
|
65
77
|
"dependencies": {
|
|
66
78
|
"@huggingface/transformers": "4.2.0",
|
|
@@ -70,6 +82,7 @@
|
|
|
70
82
|
"@libsql/client": "^0.17.4",
|
|
71
83
|
"@rollup/plugin-commonjs": "^29.0.3",
|
|
72
84
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
85
|
+
"@tursodatabase/database": "^0.7.2",
|
|
73
86
|
"@types/node": "^24.13.3",
|
|
74
87
|
"@vitest/coverage-v8": "4.1.11",
|
|
75
88
|
"@vitest/ui": "^4.1.11",
|