libsql-search 0.9.1 → 0.10.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/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.9.1",
3
+ "version": "0.10.1",
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",