latticesql 4.0.1 → 4.2.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 +51 -1
- package/dist/cli.js +7015 -2417
- package/dist/index.cjs +47576 -42575
- package/dist/index.d.cts +1874 -7
- package/dist/index.d.ts +1874 -7
- package/dist/index.js +47461 -42542
- package/docs/api-reference.md +67 -4
- package/docs/architecture.md +24 -0
- package/docs/assistant.md +23 -0
- package/docs/examples/dashboard.html +284 -0
- package/docs/importing.md +118 -0
- package/docs/retrieval.md +202 -0
- package/package.json +10 -4
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Retrieval, query & data primitives (v4.1)
|
|
2
|
+
|
|
3
|
+
latticesql 4.1 turns the library into a measurable, production-grade retrieval and
|
|
4
|
+
data substrate. Everything here is **additive and opt-in** — absent the opt-in,
|
|
5
|
+
`query()` / `count()` / `search()` behave byte-identically to 4.0. Every primitive
|
|
6
|
+
ships with unit (`:memory:` SQLite) + integration (real Postgres) + dialect-parity
|
|
7
|
+
tests.
|
|
8
|
+
|
|
9
|
+
## Measurable retrieval
|
|
10
|
+
|
|
11
|
+
### `evaluateRetrieval(queries, retriever, opts?)`
|
|
12
|
+
|
|
13
|
+
Standard IR metrics over **any** ranked retriever — `(query) => rankedRowIds`, so
|
|
14
|
+
it grades semantic, full-text, hybrid, graph, or an external service.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
const summary = await db.evaluateRetrieval(
|
|
18
|
+
[{ query: 'budget', relevant: ['doc1', 'doc7'] }],
|
|
19
|
+
async (q) => (await db.search('docs', q, { topK: 10 })).map((r) => String(r.row.id)),
|
|
20
|
+
{ k: 10, ks: [1, 5, 10] },
|
|
21
|
+
);
|
|
22
|
+
// summary.precisionAtK / recallAtK / mrr / ndcgAtK / map (+ perQuery, byK)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`detectRetrievalRegressions(baseline, candidate, tolerance)` turns it into a CI
|
|
26
|
+
gate — a retrieval change that lowers any metric past tolerance fails the build.
|
|
27
|
+
|
|
28
|
+
> **v4.2 — the gate can actually fail.** The golden corpus is now ~20 docs with
|
|
29
|
+
> deliberate cross-topic lexical overlap, so the real `search()` scores
|
|
30
|
+
> good-but-imperfect; the committed baseline is **generated** by running the real
|
|
31
|
+
> search (`npm run eval:baseline`) and is sub-perfect (`mrr ≈ 0.92`,
|
|
32
|
+
> `ndcg@3 ≈ 0.94`), never hand-authored. `npm run eval:gate` evaluates the current
|
|
33
|
+
> `search()` against that baseline and exits non-zero on any metric dropping past
|
|
34
|
+
> tolerance; it runs as a required CI step, and a suite test asserts the baseline
|
|
35
|
+
> still has headroom (`mrr < 1`) so the gate can't silently go blind.
|
|
36
|
+
|
|
37
|
+
### `lattice doctor` / `diagnoseRetrieval(opts?)`
|
|
38
|
+
|
|
39
|
+
Read-only health: per-table FTS + embedding coverage (soft-deleted rows excluded),
|
|
40
|
+
extension availability (FTS5, sqlite-vec, pgvector, pg_trgm), and severity-ranked
|
|
41
|
+
issues. `lattice doctor [--json]` exits non-zero on any error (deploy gate).
|
|
42
|
+
|
|
43
|
+
### `benchmarkRetrieval(opts?)` / `checkSlos(report, slos)`
|
|
44
|
+
|
|
45
|
+
Reproducible p50/p95/p99 latency for filtered query, FTS, vector, and aggregate,
|
|
46
|
+
plus ingest throughput + peak memory — on both dialects, at a configurable scale
|
|
47
|
+
(`LATTICE_BENCH_ROWS/QUERIES/DIM`). Ships in the package so buyers reproduce the
|
|
48
|
+
numbers; wire `checkSlos` as a CI SLO gate.
|
|
49
|
+
|
|
50
|
+
> **v4.2 — honest vector timing + an advisory SLO gate.** A Postgres integration
|
|
51
|
+
> test runs the benchmark against a real pgvector cluster and asserts the harness
|
|
52
|
+
> built the **native index before** the vector timing loop
|
|
53
|
+
> (`report.vectorIndexed === true`), so `vector.p95` reflects the indexed path,
|
|
54
|
+
> not the O(n) in-process scan; where pgvector is unavailable the test skips with a
|
|
55
|
+
> clear message rather than passing green-by-construction. `npm run slo:gate` runs
|
|
56
|
+
> the real benchmark at a committed scale and checks observed p95 latencies against
|
|
57
|
+
> committed thresholds — it is **advisory, never build-blocking** (shared CI
|
|
58
|
+
> runners are too latency-noisy to gate a merge on), and the output marks whether
|
|
59
|
+
> `vector.p95` reflects a native index or the in-process scan.
|
|
60
|
+
|
|
61
|
+
## Better search
|
|
62
|
+
|
|
63
|
+
### Chunked + contextual embeddings
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
db.define('docs', {
|
|
67
|
+
columns: { id: 'TEXT PRIMARY KEY', title: 'TEXT', body: 'TEXT' },
|
|
68
|
+
embeddings: {
|
|
69
|
+
fields: ['title', 'body'],
|
|
70
|
+
embed: myEmbedder,
|
|
71
|
+
chunker: semanticChunker({ maxChars: 1000, overlap: 100 }),
|
|
72
|
+
contextPrefix: (row) => String(row.title), // prepended to every chunk
|
|
73
|
+
modelId: 'text-embedding-3-small',
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Each row is embedded as several boundary-aware chunks → higher precision@k and
|
|
79
|
+
fewer tokens to a correct answer. `search()` returns the best-matching chunk
|
|
80
|
+
(`chunkIndex` + `matchedContent`), excludes soft-deleted rows, and throws
|
|
81
|
+
`EmbeddingDimensionMismatchError` if the model dimension changed without a re-embed.
|
|
82
|
+
`refreshEmbeddings(table, opts)` backfills missing / re-embeds stale / sweeps orphans.
|
|
83
|
+
|
|
84
|
+
### Indexed vector search
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
await db.buildVectorIndex('docs'); // pgvector HNSW (PG) / sqlite-vec (SQLite)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Opt-in per-table approximate-nearest-neighbor index built from the stored vectors;
|
|
91
|
+
`search()` uses it automatically when present, else the in-process scan (which
|
|
92
|
+
`doctor` reports). Requires the extension server-side (pgvector) or loaded
|
|
93
|
+
(sqlite-vec).
|
|
94
|
+
|
|
95
|
+
> **v4.2 — bounded retrieval reads.** `search()` / `hybridSearch()` clamp the
|
|
96
|
+
> caller's `topK` (`clampTopK`, `SEARCH_TOPK_MAX = 1000`) **before** the indexed
|
|
97
|
+
> arm over-fetches `topK * N` candidates, so a single large `topK` can't fan out
|
|
98
|
+
> into a whole-table read. For a table with **no** native index, the in-process
|
|
99
|
+
> cosine scan can be capped per-table with `embeddings.maxScanChunks`: when the
|
|
100
|
+
> scan would read more than that many stored chunk vectors it throws
|
|
101
|
+
> `EmbeddingScanTooLargeError` (telling you to add a pgvector index or raise the
|
|
102
|
+
> cap) rather than load them all into memory. It is **off by default** (unbounded
|
|
103
|
+
> scan — the historical behavior) and is **never silently truncated**, because a
|
|
104
|
+
> partial cosine scan would return incomplete, wrong results.
|
|
105
|
+
|
|
106
|
+
### Hybrid search + ranking + reranker
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const results = await db.hybridSearch('docs', 'q4 budget', {
|
|
110
|
+
topK: 10,
|
|
111
|
+
ranking: {
|
|
112
|
+
recency: { column: 'created_at', halfLifeDays: 30, weight: 1 },
|
|
113
|
+
reward: { weight: 0.5 },
|
|
114
|
+
},
|
|
115
|
+
reranker: myCrossEncoder, // optional; graceful fallback on failure
|
|
116
|
+
});
|
|
117
|
+
// each result carries .explain { rrf, vectorRank/Score, ftsRank/Score, rankingBoost, rerankerScore }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Reciprocal Rank Fusion (k=60) of the vector + full-text arms. `lattice search
|
|
121
|
+
"<q>" --table <t> --explain` shows the score breakdown. Full-text is now
|
|
122
|
+
relevance-ranked (`ts_rank` / `bm25`).
|
|
123
|
+
|
|
124
|
+
### Graph-augmented retrieval
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
await db.addEdge({ srcTable: 'docs', srcId: 'a', dstTable: 'docs', dstId: 'b', type: 'cites' });
|
|
128
|
+
await db.extractEdges({ srcTable: 'docs', fkColumn: 'parent_id', dstTable: 'docs' }); // zero-LLM
|
|
129
|
+
const results = await db.graphSearch('docs', 'q', { anchors: [{ table: 'docs', id: 'a' }] });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
A typed-edge graph (`__lattice_edges`) with bounded BFS (`traverseGraph`, depth ≤ 5,
|
|
133
|
+
node-capped) and adjacency boosting — relationship-aware retrieval that lifts rows
|
|
134
|
+
connected to your current-context entities.
|
|
135
|
+
|
|
136
|
+
## Query primitives
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// Bounded reads — guard against unbounded full-table loads
|
|
140
|
+
await db.query('t', { maxRows: 1000 }); // throws BoundedReadError if more match
|
|
141
|
+
new Lattice(path, { defaultMaxRows: 1000 }); // global default
|
|
142
|
+
|
|
143
|
+
// Projection — return only the columns you need
|
|
144
|
+
await db.query('t', { projection: ['id', 'name'] });
|
|
145
|
+
|
|
146
|
+
// OR/AND groups + jsonPath
|
|
147
|
+
await db.query('t', {
|
|
148
|
+
filters: [
|
|
149
|
+
{ col: 'status', op: 'eq', val: 'open' },
|
|
150
|
+
{
|
|
151
|
+
or: [
|
|
152
|
+
{ col: 'priority', op: 'gte', val: 3 },
|
|
153
|
+
{ col: 'pinned', op: 'eq', val: true },
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
{ col: 'meta', jsonPath: 'tier', op: 'eq', val: 'gold' },
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// SQL-side aggregation
|
|
161
|
+
await db.aggregate('orders', {
|
|
162
|
+
groupBy: ['status'],
|
|
163
|
+
aggregates: [
|
|
164
|
+
{ fn: 'count', as: 'n' },
|
|
165
|
+
{ fn: 'sum', col: 'total', as: 'revenue' },
|
|
166
|
+
],
|
|
167
|
+
having: [{ aggregate: 'n', op: 'gt', val: 10 }],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Keyset pagination — fast arbitrarily deep
|
|
171
|
+
const page = await db.queryPage('t', { orderBy: 'created_at', limit: 50, cursor });
|
|
172
|
+
|
|
173
|
+
// distinctOn — one row per group; include — batched relation expansion
|
|
174
|
+
await db.query('events', { distinctOn: 'user_id', orderBy: 'ts', orderDir: 'desc' });
|
|
175
|
+
await db.query('posts', { include: ['author'] }); // belongsTo → row; hasMany → array
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Governance, reliability, computed columns, cloud files
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
// Immutable provenance + trust gate
|
|
182
|
+
db.define('docs', { columns: {...}, provenance: true, trust: true });
|
|
183
|
+
await db.verifyRow('docs', id, 'alice'); // markRowForReview / rowsNeedingReview / verifiedRows
|
|
184
|
+
|
|
185
|
+
// Durable retry + online resumable migrations
|
|
186
|
+
await withRetry(() => db.insert(...)); // idempotent ops only
|
|
187
|
+
await applyChunkedMigration(db.adapter, { id, table, apply, batchSize: 1000 });
|
|
188
|
+
|
|
189
|
+
// Computed columns + materialized rollups
|
|
190
|
+
db.define('people', { columns: {...}, computed: {
|
|
191
|
+
full_name: { deps: ['first', 'last'], compute: (r) => `${r.first} ${r.last}` },
|
|
192
|
+
}});
|
|
193
|
+
db.define('posts', { columns: {...}, materializedRollups: {
|
|
194
|
+
comment_count: { sourceTable: 'comments', foreignKey: 'post_id', fn: 'count' },
|
|
195
|
+
}});
|
|
196
|
+
|
|
197
|
+
// Keyless cloud file-byte access (Postgres cloud)
|
|
198
|
+
await db.enableCloudFilePresigning({ bucket, region, accessKey, secretKey });
|
|
199
|
+
// members fetch bytes with zero config; the owner key never leaves the database.
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
See [CHANGELOG.md](../CHANGELOG.md) for the full 4.1 list.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "latticesql",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "Persistent structured memory for AI agent systems — pluggable SQLite or Postgres backend, LLM context bridge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,13 +31,16 @@
|
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsup",
|
|
33
33
|
"typecheck": "tsc --noEmit",
|
|
34
|
-
"lint": "eslint src tests",
|
|
35
|
-
"lint:fix": "eslint src tests --fix",
|
|
34
|
+
"lint": "eslint src tests scripts",
|
|
35
|
+
"lint:fix": "eslint src tests scripts --fix",
|
|
36
36
|
"format": "prettier --write .",
|
|
37
37
|
"format:check": "prettier --check .",
|
|
38
38
|
"check:generic": "bash scripts/check-generic.sh",
|
|
39
39
|
"test": "vitest run",
|
|
40
40
|
"test:watch": "vitest",
|
|
41
|
+
"eval:baseline": "vite-node scripts/eval-baseline.ts",
|
|
42
|
+
"eval:gate": "vite-node scripts/eval-gate.ts",
|
|
43
|
+
"slo:gate": "vite-node scripts/slo-gate.ts",
|
|
41
44
|
"test:coverage": "vitest run --coverage",
|
|
42
45
|
"test:e2e": "playwright test",
|
|
43
46
|
"docs": "typedoc --out docs-generated src/index.ts",
|
|
@@ -65,9 +68,12 @@
|
|
|
65
68
|
},
|
|
66
69
|
"optionalDependencies": {
|
|
67
70
|
"@aws-sdk/client-s3": "^3.1067.0",
|
|
71
|
+
"exceljs": "^4.4.0",
|
|
68
72
|
"pg": "^8.11.0",
|
|
73
|
+
"pgvector": "^0.2.0",
|
|
69
74
|
"playwright": "^1.48.0",
|
|
70
|
-
"sharp": "^0.33.5"
|
|
75
|
+
"sharp": "^0.33.5",
|
|
76
|
+
"sqlite-vec": "^0.1.6"
|
|
71
77
|
},
|
|
72
78
|
"devDependencies": {
|
|
73
79
|
"@eslint/js": "^9.0.0",
|