@statewalker/indexer-pglite 0.2.1 → 0.2.2
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 +68 -29
- package/package.json +11 -7
package/README.md
CHANGED
|
@@ -1,53 +1,92 @@
|
|
|
1
1
|
# @statewalker/indexer-pglite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## What it is
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
An `Indexer` stored in [PGlite](https://github.com/electric-sql/pglite) (PostgreSQL compiled to WASM). Full-text sub-indexes use PostgreSQL `tsvector` columns with a GIN index and `ts_rank_cd` scoring; vector sub-indexes use pgvector with an HNSW cosine index. It runs in the browser, in Node and in workers.
|
|
6
|
+
|
|
7
|
+
## Why it exists
|
|
8
|
+
|
|
9
|
+
It gives real PostgreSQL text search (stemming per language) and an approximate nearest-neighbour vector index without a server. With an IndexedDB or file `dataDir`, data persists without a separate save step.
|
|
10
|
+
|
|
11
|
+
## How to use
|
|
6
12
|
|
|
7
13
|
```sh
|
|
8
|
-
pnpm add @statewalker/indexer-pglite
|
|
14
|
+
pnpm add @statewalker/indexer-pglite
|
|
9
15
|
```
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
`@electric-sql/pglite` and `@electric-sql/pglite-pgvector` are dependencies of this package. Add them to your own `package.json` only if you create the `PGlite` instance yourself.
|
|
18
|
+
|
|
19
|
+
One entry point, `@statewalker/indexer-pglite`:
|
|
20
|
+
|
|
21
|
+
- `createPGLiteIndexer(options?): Promise<Indexer>` — `options.db?: PGlite`.
|
|
22
|
+
- `PGLiteIndexerOptions` — the options type.
|
|
23
|
+
|
|
24
|
+
## Examples
|
|
25
|
+
|
|
26
|
+
In-memory database owned by the indexer:
|
|
12
27
|
|
|
13
28
|
```ts
|
|
14
29
|
import { createPGLiteIndexer } from "@statewalker/indexer-pglite";
|
|
30
|
+
import { newFullTextAccess, setFullTextConfig } from "@statewalker/indexer-fulltext";
|
|
15
31
|
|
|
16
|
-
// Uses an in-memory PGlite instance by default.
|
|
17
32
|
const indexer = await createPGLiteIndexer();
|
|
18
33
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
const params = { name: "docs" };
|
|
35
|
+
setFullTextConfig(params, "q", { language: "en" });
|
|
36
|
+
const index = await indexer.createIndex(params);
|
|
37
|
+
|
|
38
|
+
const fts = newFullTextAccess("q");
|
|
39
|
+
await fts.get(index).addDocument([{ path: "/docs/a", blockId: "1", content: "running dogs" }]);
|
|
40
|
+
|
|
41
|
+
const request = { topK: 10 };
|
|
42
|
+
fts.setQuery(request, { queries: ["run"] }); // stemmed: matches "running"
|
|
43
|
+
for await (const r of index.search(request)) {
|
|
44
|
+
console.log(r.path, r.blockId, fts.getResult(r)?.score); // ts_rank_cd
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await indexer.close(); // also closes the PGlite instance it created
|
|
30
48
|
```
|
|
31
49
|
|
|
32
|
-
|
|
50
|
+
Your own persistent database:
|
|
33
51
|
|
|
34
|
-
|
|
52
|
+
```ts
|
|
53
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
54
|
+
import { vector } from "@electric-sql/pglite-pgvector";
|
|
55
|
+
import { createPGLiteIndexer } from "@statewalker/indexer-pglite";
|
|
56
|
+
|
|
57
|
+
const db = await PGlite.create({ dataDir: "idb://my-app", extensions: { vector } });
|
|
58
|
+
const indexer = await createPGLiteIndexer({ db });
|
|
59
|
+
// indexer.close() does not close db; call db.close() yourself
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Internals
|
|
35
63
|
|
|
36
|
-
|
|
64
|
+
### The vector extension must be loaded
|
|
65
|
+
|
|
66
|
+
At startup the indexer runs `CREATE EXTENSION IF NOT EXISTS vector`. When it creates the database itself it passes `extensions: { vector }`. A `PGlite` you pass in must be created with the same extension, otherwise this statement fails and the indexer is not created.
|
|
67
|
+
|
|
68
|
+
### Tables
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
__indexer_manifest(name, config)
|
|
72
|
+
idx_<index>_docs(doc_id SERIAL, path UNIQUE) path -> doc_id
|
|
73
|
+
idx_<index>_<sub>_fts(doc_id, block_id, content, content_tsv TSVECTOR GENERATED, metadata) + GIN
|
|
74
|
+
idx_<index>_<sub>_vec(doc_id, block_id, embedding vector(dim)) + HNSW vector_cosine_ops
|
|
75
|
+
```
|
|
37
76
|
|
|
38
|
-
|
|
77
|
+
`content_tsv` is a generated column, so the full-text index is always current; there is no rebuild step.
|
|
39
78
|
|
|
40
|
-
|
|
41
|
-
- `idx_<prefix>_fts(doc_id, block_id, content, content_tsv TSVECTOR GENERATED ..., metadata)` + `USING GIN (content_tsv)`.
|
|
42
|
-
- `idx_<prefix>_vec(doc_id, block_id, embedding vector(dim))` + `USING hnsw (embedding vector_cosine_ops)`.
|
|
79
|
+
### Queries, scores and languages
|
|
43
80
|
|
|
44
|
-
|
|
81
|
+
- A full-text query is lowercased, split on whitespace, stripped of tsquery operators (`& | ! ( ) : ' " \`) and joined with `|` (OR). Score is `ts_rank_cd`. With several `queries` in one sub-query, each block keeps its best score across them (scores are not summed).
|
|
82
|
+
- Vector `score` is `1 - (embedding <=> query)` (cosine). Embeddings are sent as text literals cast to `vector(dim)`.
|
|
83
|
+
- `language` accepts ISO-639-1 codes (`en`, `fr`, `de`, `es`, `it`, `pt`, `nl`, `ru`, `sv`, `no`, `da`, `fi`, `hu`, `ro`, `tr`), `simple`, or the matching Postgres config names (`english`, ...). Anything else throws `pglite FTS: unsupported language "<x>" (must be an ISO-639-1 code or a Postgres text-search config name)`. The value is put into SQL, which is why it is checked against this list.
|
|
84
|
+
- Default retrieval depth is `topK = 100` per sub-query.
|
|
45
85
|
|
|
46
|
-
|
|
86
|
+
### Dependencies
|
|
47
87
|
|
|
48
|
-
-
|
|
49
|
-
- `PGLiteIndexerOptions` — option type.
|
|
88
|
+
`@electric-sql/pglite` and `@electric-sql/pglite-pgvector` (database and vector extension), `@statewalker/indexer-core` (`createSqlIndexer` and the SQL retrievers), `@statewalker/indexer-api`, `@statewalker/indexer-fulltext`, `@statewalker/indexer-vector`.
|
|
50
89
|
|
|
51
|
-
##
|
|
90
|
+
## License
|
|
52
91
|
|
|
53
|
-
|
|
92
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/indexer-pglite",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "PGlite-backed indexer implementation: full-text + vector search using in-browser Postgres.",
|
|
7
|
-
"
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/statewalker/statewalker-search/issues"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/statewalker/statewalker-search/tree/main/packages/indexer-pglite#readme",
|
|
8
11
|
"author": {
|
|
9
12
|
"name": "Mikhail Kotelnikov",
|
|
10
13
|
"email": "mikhail.kotelnikov@gmail.com"
|
|
@@ -12,7 +15,8 @@
|
|
|
12
15
|
"license": "MIT",
|
|
13
16
|
"repository": {
|
|
14
17
|
"type": "git",
|
|
15
|
-
"url": "git+
|
|
18
|
+
"url": "git+https://github.com/statewalker/statewalker-search.git",
|
|
19
|
+
"directory": "packages/indexer-pglite"
|
|
16
20
|
},
|
|
17
21
|
"exports": {
|
|
18
22
|
".": {
|
|
@@ -29,10 +33,10 @@
|
|
|
29
33
|
"dependencies": {
|
|
30
34
|
"@electric-sql/pglite": "^0.5.8",
|
|
31
35
|
"@electric-sql/pglite-pgvector": "^0.0.9",
|
|
32
|
-
"@statewalker/indexer-api": "^0.1.
|
|
33
|
-
"@statewalker/indexer-core": "^0.1.
|
|
34
|
-
"@statewalker/indexer-fulltext": "^0.1.
|
|
35
|
-
"@statewalker/indexer-vector": "^0.1.
|
|
36
|
+
"@statewalker/indexer-api": "^0.1.2",
|
|
37
|
+
"@statewalker/indexer-core": "^0.1.2",
|
|
38
|
+
"@statewalker/indexer-fulltext": "^0.1.2",
|
|
39
|
+
"@statewalker/indexer-vector": "^0.1.2"
|
|
36
40
|
},
|
|
37
41
|
"devDependencies": {
|
|
38
42
|
"rimraf": "^6.1.3",
|