pikelet-wasm 0.7.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/CHANGELOG.md +868 -0
- package/LICENSE +191 -0
- package/README.md +590 -0
- package/complete/builder.d.ts +82 -0
- package/complete/builder.mjs +527 -0
- package/complete/encoder-kernels/encoder.mjs +2 -0
- package/complete/encoder-kernels/encoder.node.mjs +2 -0
- package/complete/encoder-kernels/encoder.node.wasm +0 -0
- package/complete/encoder-kernels/encoder.wasm +0 -0
- package/complete/format.mjs +8 -0
- package/complete/index.d.ts +289 -0
- package/complete/index.mjs +1124 -0
- package/complete/inline-transformer.mjs +205 -0
- package/complete/lexical.mjs +264 -0
- package/complete/retrieval-abstention.mjs +119 -0
- package/complete/sources.mjs +186 -0
- package/complete/student-abstention.mjs +102 -0
- package/complete/student-embedder.mjs +231 -0
- package/complete/wordpiece.mjs +68 -0
- package/dist/engine.js +2 -0
- package/dist/engine.scalar.js +2 -0
- package/dist/engine.scalar.wasm +0 -0
- package/dist/engine.wasm +0 -0
- package/package.json +168 -0
- package/pancake-artifact-common.js +561 -0
- package/pancake-artifact-range.js +799 -0
- package/pancake-artifact-sketch.js +1040 -0
- package/pancake-artifact.d.ts +109 -0
- package/pancake-artifact.js +31 -0
- package/pancake-core.js +1268 -0
- package/pancake-errors.js +40 -0
- package/pancake-loader.js +66 -0
- package/pancake.d.ts +691 -0
- package/pancake.js +254 -0
- package/pancake.node.mjs +259 -0
- package/pancake.web.d.ts +50 -0
- package/pancake.web.mjs +88 -0
- package/pancake.workerd.mjs +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
# Pikelet
|
|
2
|
+
|
|
3
|
+
Pikelet is a search engine you host like a file.
|
|
4
|
+
|
|
5
|
+
> Renamed from **Pancake** (2026-09). Same project, same wire format —
|
|
6
|
+
> existing `.pancake` artifacts and their identities remain valid, and the
|
|
7
|
+
> old package names are deprecated pointers to `pikelet-wasm` and
|
|
8
|
+
> `pikelet`.
|
|
9
|
+
|
|
10
|
+
At the bottom there's a small WebAssembly HNSW vector engine that runs
|
|
11
|
+
anywhere JavaScript does — Node, browsers, Cloudflare Workers — with no
|
|
12
|
+
native dependencies. On top of it sit immutable search artifact formats:
|
|
13
|
+
files that package an index, corpus records, query interpretation,
|
|
14
|
+
calibration, and evaluation data, and that can be served from any storage
|
|
15
|
+
that answers byte-range requests.
|
|
16
|
+
|
|
17
|
+
Where this has been heading is the **complete search artifact**: one
|
|
18
|
+
`.pikelet` file that answers natural-language queries with hydrated,
|
|
19
|
+
confidence-scored results, using nothing but the file, range reads, and a
|
|
20
|
+
reader.
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
query text
|
|
24
|
+
-> artifact-carried query interpreter
|
|
25
|
+
-> vector search over compressed sketch/index data
|
|
26
|
+
+ BM25 over the embedded lexical index, rank-fused
|
|
27
|
+
-> lazy range reads for records
|
|
28
|
+
-> calibrated hydrated results
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The `pikelet-wasm` package carries both the vector engine and the artifact
|
|
32
|
+
runtime.
|
|
33
|
+
|
|
34
|
+
## Knowledge Packs
|
|
35
|
+
|
|
36
|
+
The same file doubles as a **knowledge pack** for LLMs. A `.pikelet` is a
|
|
37
|
+
portable, immutable corpus that contains its own query encoder, semantic
|
|
38
|
+
and lexical indexes, calibrated abstention, provenance, and integrity
|
|
39
|
+
metadata. Publish it as a static file; an MCP client attaches and queries
|
|
40
|
+
it directly over HTTP range requests — no index to rebuild, no vector
|
|
41
|
+
database to run, no embedding service to call:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx pikelet mcp install --client claude-code \
|
|
45
|
+
--pack https://github.com/mcn92/pancake/releases/download/artifact-wiki-inline-v4/pancake-wiki-inline.pancake#1b180adf4c6cebb2dcd5615256df6a25dac5fda8738dbbc11d60af86046f97f3
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then ask questions. That line pins 456,153 passages of Simple English
|
|
49
|
+
Wikipedia by content hash: the mount transfers a ~52 MiB resident slice
|
|
50
|
+
of the 649 MiB file, each question costs ~127 range reads, results carry
|
|
51
|
+
their provenance (pack identity, title, section, source), and a pack that
|
|
52
|
+
cannot answer says so instead of guessing. Details in the
|
|
53
|
+
pikelet README ("Attaching packs to an LLM") and `packs/`.
|
|
54
|
+
|
|
55
|
+
## What's in This Repo
|
|
56
|
+
|
|
57
|
+
Three layers, bottom to top:
|
|
58
|
+
|
|
59
|
+
1. **WASM vector engine**
|
|
60
|
+
- In-memory HNSW index with float32 and row-wise affine uint8 backends.
|
|
61
|
+
- JavaScript API: `Pikelet.create()`, `add()`, `search()`, `export()`,
|
|
62
|
+
`restore()`.
|
|
63
|
+
- Source in `src/`, package entrypoints at `pancake.js`,
|
|
64
|
+
`pancake.node.mjs`, `pancake.web.mjs`.
|
|
65
|
+
|
|
66
|
+
2. **Search artifacts**
|
|
67
|
+
- Immutable, integrity-checked files built from indexes and corpus data,
|
|
68
|
+
range-readable from local files, R2, S3, CDNs — anything that serves
|
|
69
|
+
byte ranges.
|
|
70
|
+
- One product format, one index layer beneath it:
|
|
71
|
+
- `.pikelet`: the complete one-file search artifact — the product.
|
|
72
|
+
- `.pancake-sketch`: its index segment (resident sketch plus lazy
|
|
73
|
+
rerank rows), also usable standalone; format 2 (the builder's
|
|
74
|
+
default) interleaves per-row digests with the rows, so every row a
|
|
75
|
+
query fetches is verified on that read.
|
|
76
|
+
- Two adjacent formats that are not search artifacts in the contract
|
|
77
|
+
sense:
|
|
78
|
+
- `.pnck`: the engine's snapshot envelope (`export()`/`restore()`
|
|
79
|
+
serialization), consumed by the compilers and by layer-1 users.
|
|
80
|
+
- `.pancake-range`: **deprecated** range-readable index profile. The
|
|
81
|
+
sketch geometry replaced it (5x faster over real networks at a
|
|
82
|
+
third of the size); readers stay supported for existing artifacts,
|
|
83
|
+
but no new ones should be built.
|
|
84
|
+
- Specs: [spec/SEARCH_ARTIFACT_CONTRACT.md](spec/SEARCH_ARTIFACT_CONTRACT.md),
|
|
85
|
+
[spec/SKETCH_PROFILE.md](spec/SKETCH_PROFILE.md), and
|
|
86
|
+
[spec/COMPLETE_PROFILE.md](spec/COMPLETE_PROFILE.md).
|
|
87
|
+
|
|
88
|
+
3. **Products and compilers**
|
|
89
|
+
- `pikelet/`: the fast path. One command compiles a docs
|
|
90
|
+
folder or a live site into a complete `.pikelet` file:
|
|
91
|
+
```bash
|
|
92
|
+
npx pikelet compile --source ./docs --out search.pikelet
|
|
93
|
+
```
|
|
94
|
+
or scaffolds a deployable search app:
|
|
95
|
+
```bash
|
|
96
|
+
npx pikelet create --name my-docs-search --source ./docs --no-deploy --yes
|
|
97
|
+
```
|
|
98
|
+
- `examples/05-one-file-search/`: the flagship path — the complete
|
|
99
|
+
`.pikelet` container, its compilers, and acceptance tests.
|
|
100
|
+
- `examples/04-static-wiki-pack/`: wiki-scale corpus preparation and the
|
|
101
|
+
sketch artifact pipeline.
|
|
102
|
+
- `examples/03-edge-docs-search/`: a Worker search app with a bundled
|
|
103
|
+
snapshot, distilled query encoder, and calibrated abstention.
|
|
104
|
+
|
|
105
|
+
## Why Pikelet Exists
|
|
106
|
+
|
|
107
|
+
Most vector search libraries assume a server process, native binaries, or a
|
|
108
|
+
database service. Pikelet is built for a different deployment shape:
|
|
109
|
+
|
|
110
|
+
- static files instead of always-on index servers;
|
|
111
|
+
- byte-range reads instead of whole-index downloads;
|
|
112
|
+
- readers for browsers, Workers, and Node;
|
|
113
|
+
- explicit artifact identity and digest verification;
|
|
114
|
+
- query interpretation and evaluation data carried inside the artifact;
|
|
115
|
+
- compact corpus-side vector representations that keep memory and transfer
|
|
116
|
+
costs sane.
|
|
117
|
+
|
|
118
|
+
The result is search you can host like an asset instead of operating like a
|
|
119
|
+
database.
|
|
120
|
+
|
|
121
|
+
## The Flagship: One-File Search
|
|
122
|
+
|
|
123
|
+
The complete profile (`.pikelet`) packs the five components of a search
|
|
124
|
+
application into one content-addressed file:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
index embedded .pancake-sketch artifact
|
|
128
|
+
corpus JSON records behind an offsets table
|
|
129
|
+
encoder query interpreter declaration or inline model data
|
|
130
|
+
calibration abstention / match-quality model
|
|
131
|
+
evaluation golden queries and expected behavior
|
|
132
|
+
lexical BM25 inverted index for hybrid retrieval (optional; readers
|
|
133
|
+
that predate it serve vector-only from the same file)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The reader verifies the manifest and eager segments, opens the sketch index
|
|
137
|
+
against a range source, and hydrates only the records the final results
|
|
138
|
+
need.
|
|
139
|
+
|
|
140
|
+
Query interpretation comes in three kinds. Each picks two of
|
|
141
|
+
**self-contained**, **small**, and **teacher-quality**, and gives up the
|
|
142
|
+
third — that trade-off, not history, is why all three exist:
|
|
143
|
+
|
|
144
|
+
- **kind 3: `inline-transformer-v1`** — self-contained + teacher quality.
|
|
145
|
+
The artifact carries the WordPiece vocab and quantized MiniLM teacher
|
|
146
|
+
weights as verified data; the reader ships the execution kernels. Costs
|
|
147
|
+
~25 MB of encoder no matter how small the corpus. This is the default:
|
|
148
|
+
`pikelet compile` builds kind 3.
|
|
149
|
+
- **kind 1: `student-inline-v1`** — self-contained + small. A
|
|
150
|
+
corpus-distilled student encoder (~1 MiB), pure-JS execution. The only
|
|
151
|
+
way a small docs corpus compiles to a file measured in single-digit MB;
|
|
152
|
+
quality is bounded by the distillation.
|
|
153
|
+
- **kind 2: `external-transformers-v1`** — small + teacher quality, but
|
|
154
|
+
not self-contained: the artifact pins a host-supplied encoder (model id,
|
|
155
|
+
pooling, normalization, max tokens) and verifies it against embedded
|
|
156
|
+
test vectors before serving. The escape hatch for encoders too big to
|
|
157
|
+
inline and hosts that already run one.
|
|
158
|
+
|
|
159
|
+
Kind 3 is the flagship: the file carries the corpus, index, tokenizer,
|
|
160
|
+
encoder weights, calibration, and evaluation data — the reader supplies
|
|
161
|
+
code, the artifact supplies everything else. Ask it about volcanoes and no
|
|
162
|
+
service, model host, or network dependency is involved beyond range reads
|
|
163
|
+
of the file itself.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
cd examples/05-one-file-search
|
|
167
|
+
node compile-wiki.mjs --inline-encoder
|
|
168
|
+
node compile.mjs --inspect pancake-wiki-inline.pancake
|
|
169
|
+
node test-inline.mjs
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Where the inline wiki artifact currently stands:
|
|
173
|
+
|
|
174
|
+
```text
|
|
175
|
+
456,153 records
|
|
176
|
+
~649 MiB complete artifact (680,029,254 bytes)
|
|
177
|
+
manifest identity: 1b180adf4c6cebb2dcd5615256df6a25dac5fda8738dbbc11d60af86046f97f3
|
|
178
|
+
recall@10: 95.2% at the recommended rerank (97.0% at C=600), against exact
|
|
179
|
+
fp32 brute-force ground truth on the 200-query pre-registered eval —
|
|
180
|
+
98.5% on hand-written natural-language questions, 94.8% on single-title
|
|
181
|
+
lookups (augmented retrieval: hybrid candidates, distance order)
|
|
182
|
+
serves over HTTP range reads: opens on ~52 MiB of the file, ~127 range
|
|
183
|
+
requests per query (cluster-ordered row layout + manifest fetch hints)
|
|
184
|
+
natural-language query with no host encoder option, ~110 ms/query end to
|
|
185
|
+
end locally (the resident scan auto-stages the engine's SIMD kernel at
|
|
186
|
+
this scale; ~540 ms on the pure-JS scan it falls back to)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Release asset (v4 — recovers the pack's canonical build: k-means
|
|
190
|
+
cluster-ordered rows and the 192-dim 2:1-pooled sketch, where v1–v3
|
|
191
|
+
embedded a 96-dim 4:1 sketch in unpermuted order and measured 82.4%; adds
|
|
192
|
+
the hybrid BM25 lexical segment and a format-2 sketch with per-row read
|
|
193
|
+
verification; earlier versions stay published for older checkouts):
|
|
194
|
+
`https://github.com/mcn92/pancake/releases/download/artifact-wiki-inline-v4/pancake-wiki-inline.pancake`
|
|
195
|
+
|
|
196
|
+
`node test-inline.mjs` downloads that file automatically when it is missing
|
|
197
|
+
locally, verifies the manifest identity, and then runs the acceptance
|
|
198
|
+
checks.
|
|
199
|
+
|
|
200
|
+
See [examples/05-one-file-search/README.md](examples/05-one-file-search/README.md)
|
|
201
|
+
for the full one-file walkthrough.
|
|
202
|
+
|
|
203
|
+
## The Design Choice Everything Hangs On
|
|
204
|
+
|
|
205
|
+
The engine's quantized backend stores corpus vectors as row-wise affine
|
|
206
|
+
uint8:
|
|
207
|
+
|
|
208
|
+
```text
|
|
209
|
+
value ~= offset[row] + scale[row] * byte
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
That one constraint propagates through the whole system:
|
|
213
|
+
|
|
214
|
+
- HNSW search keeps the query as float32 and scores it against compressed
|
|
215
|
+
corpus rows.
|
|
216
|
+
- Distance kernels fuse dequantization with dot/L2 accumulation.
|
|
217
|
+
- SIMD paths load bytes, widen lanes, apply scale/offset, and accumulate.
|
|
218
|
+
- Graph maintenance uses symmetric node-to-node distances derived from the
|
|
219
|
+
compressed rows and row statistics.
|
|
220
|
+
- Snapshots serialize compact scale/offset/qdata arrays.
|
|
221
|
+
- Sketch artifacts scan compressed resident rows and fetch only rerank
|
|
222
|
+
candidates.
|
|
223
|
+
- Range-backed readers keep the large corpus side compressed while queries
|
|
224
|
+
stay precise and transient.
|
|
225
|
+
|
|
226
|
+
This is why the engine work and the artifact work belong in one project:
|
|
227
|
+
the same corpus-side representation serves in-memory search, serialized
|
|
228
|
+
snapshots, resident sketches, range reads, and reranking.
|
|
229
|
+
|
|
230
|
+
## Install
|
|
231
|
+
|
|
232
|
+
From npm:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
npm install pancake-wasm
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
From a checkout:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
git clone https://github.com/mcn92/pancake.git
|
|
242
|
+
cd pancake
|
|
243
|
+
npm install
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The checkout includes prebuilt WASM in `dist/`, so you don't need to
|
|
247
|
+
rebuild the engine unless you edit `src/`.
|
|
248
|
+
|
|
249
|
+
## Runtime Entry Points
|
|
250
|
+
|
|
251
|
+
Published package:
|
|
252
|
+
|
|
253
|
+
```js
|
|
254
|
+
// Node.js CJS
|
|
255
|
+
const Pikelet = require('pikelet-wasm');
|
|
256
|
+
|
|
257
|
+
// Node.js ESM
|
|
258
|
+
import Pikelet from 'pikelet-wasm';
|
|
259
|
+
|
|
260
|
+
// Explicit Node/browser entries
|
|
261
|
+
import Pikelet from 'pikelet-wasm/node';
|
|
262
|
+
import Pikelet from 'pikelet-wasm/web';
|
|
263
|
+
|
|
264
|
+
// Search Artifact layer (sketch readers/builders; the deprecated
|
|
265
|
+
// .pancake-range reader stays exported for existing artifacts)
|
|
266
|
+
import { PancakeSketchArtifact, buildSketchArtifact } from 'pikelet-wasm/artifact';
|
|
267
|
+
|
|
268
|
+
// Complete one-file profile: reader (any runtime) and builder (Node)
|
|
269
|
+
import { openPikeletFile } from 'pikelet-wasm/complete';
|
|
270
|
+
import { assemblePancakeFile } from 'pikelet-wasm/complete/builder';
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Repository checkout:
|
|
274
|
+
|
|
275
|
+
```js
|
|
276
|
+
const Pikelet = require('./pancake.js');
|
|
277
|
+
import Pikelet from './pancake.node.mjs';
|
|
278
|
+
import Pikelet from './pancake.web.mjs';
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The browser entry expects the runtime or bundler to resolve
|
|
282
|
+
`dist/engine.wasm`.
|
|
283
|
+
|
|
284
|
+
## Engine Quick Start
|
|
285
|
+
|
|
286
|
+
Use the core engine when you already have vectors and want an in-memory ANN
|
|
287
|
+
index.
|
|
288
|
+
|
|
289
|
+
```js
|
|
290
|
+
import Pikelet from 'pikelet-wasm';
|
|
291
|
+
|
|
292
|
+
const index = await Pikelet.create({
|
|
293
|
+
dim: 384,
|
|
294
|
+
maxElements: 100000,
|
|
295
|
+
metric: 'cosine',
|
|
296
|
+
quantized: true,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const docVector = new Float32Array(384).fill(0.1); // your embedding here
|
|
300
|
+
const queryVector = new Float32Array(384).fill(0.1); // your query embedding here
|
|
301
|
+
|
|
302
|
+
const id = index.add(docVector);
|
|
303
|
+
|
|
304
|
+
const results = index.search(queryVector, 10);
|
|
305
|
+
// [{ id: 0, distance: 0.12 }, ...]
|
|
306
|
+
|
|
307
|
+
index.delete(id);
|
|
308
|
+
index.compact();
|
|
309
|
+
|
|
310
|
+
const snapshot = index.export();
|
|
311
|
+
const restored = await Pikelet.restore(snapshot, { maxElements: 100000 });
|
|
312
|
+
|
|
313
|
+
index.dispose();
|
|
314
|
+
restored.dispose();
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
If you have records with vectors:
|
|
318
|
+
|
|
319
|
+
```js
|
|
320
|
+
const rows = [
|
|
321
|
+
{ id: 'doc-1', vector: embedding1 },
|
|
322
|
+
{ id: 'doc-2', vector: embedding2 },
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
const { index, idMap } = await Pikelet.fromVectors(rows, {
|
|
326
|
+
metric: 'cosine',
|
|
327
|
+
quantized: true,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const hits = index.search(queryEmbedding, 5);
|
|
331
|
+
console.log(idMap.get(hits[0].id));
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
On Node entrypoints, file helpers are available:
|
|
335
|
+
|
|
336
|
+
```js
|
|
337
|
+
const { index, idMap } = await Pikelet.loadJsonFile('vectors.jsonl', {
|
|
338
|
+
metric: 'cosine',
|
|
339
|
+
vectorKey: 'embedding',
|
|
340
|
+
idKey: 'docId',
|
|
341
|
+
maxFileBytes: 64 * 1024 * 1024,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const restored = await Pikelet.loadSnapshotFile('index.pnck', {
|
|
345
|
+
dim: 384,
|
|
346
|
+
maxElements: 100000,
|
|
347
|
+
metric: 'cosine',
|
|
348
|
+
quantized: true,
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Complete Artifact Quick Start
|
|
353
|
+
|
|
354
|
+
Use the complete reader when you want a file that is itself the search
|
|
355
|
+
application data plane. To build one from your own docs, see the compile
|
|
356
|
+
command above (`npx pikelet compile`).
|
|
357
|
+
|
|
358
|
+
```js
|
|
359
|
+
import { openPikeletFile } from 'pikelet-wasm/complete'; // or './complete/index.mjs' in a checkout
|
|
360
|
+
|
|
361
|
+
const search = await openPikeletFile('examples/05-one-file-search/pancake-docs.pancake');
|
|
362
|
+
|
|
363
|
+
const out = await search.query('how do workers restore snapshots', { k: 5 });
|
|
364
|
+
|
|
365
|
+
console.log(out.matchQuality, out.confidence);
|
|
366
|
+
console.log(out.results[0].title);
|
|
367
|
+
|
|
368
|
+
await search.close();
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
For kind-3 artifacts, no host encoder is needed:
|
|
372
|
+
|
|
373
|
+
```js
|
|
374
|
+
const search = await openPikeletFile('pancake-wiki-inline.pancake');
|
|
375
|
+
const out = await search.query('how do volcanoes form', { k: 5 });
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
For kind-2 artifacts, pass `options.encodeQuery`; the reader runs it
|
|
379
|
+
against the declaration's verification vectors before serving and refuses
|
|
380
|
+
the open if they disagree (`info().encoderVerified` reports the outcome).
|
|
381
|
+
|
|
382
|
+
The reader also accepts a range source, which is how a `.pikelet` on R2 or
|
|
383
|
+
a CDN gets queried without downloading it:
|
|
384
|
+
|
|
385
|
+
```js
|
|
386
|
+
const search = await openPikeletFile({
|
|
387
|
+
size,
|
|
388
|
+
async read(offset, length) {
|
|
389
|
+
// return bytes for [offset, offset + length)
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
On integrity: every artifact-derived read is bounds-checked and budgeted
|
|
395
|
+
(`maxReadBytes`, `maxRecordBytes`). Format-2 files (`pancake-complete-v2`,
|
|
396
|
+
the builder's default) verify each hydrated record against a per-record
|
|
397
|
+
digest on its own range read (`info().corpusIntegrity`), and an embedded
|
|
398
|
+
format-2 sketch verifies each lazily fetched index row the same way
|
|
399
|
+
(`info().indexRowIntegrity`) — so a tampered transport fails the query
|
|
400
|
+
instead of skewing its results. Older files with a format-1 sketch commit
|
|
401
|
+
the lazy index rows to the identity but don't verify them per read; for
|
|
402
|
+
those, pass `verifyIndexVectors: true` (or call `verifyVectors()`) to
|
|
403
|
+
authenticate them in one full pass before trusting results from an
|
|
404
|
+
untrusted transport (spec/COMPLETE_PROFILE.md section 6).
|
|
405
|
+
|
|
406
|
+
### Attaching packs to an LLM
|
|
407
|
+
|
|
408
|
+
A complete artifact doubles as a portable knowledge pack: everything an
|
|
409
|
+
LLM needs to query a body of knowledge — corpus, indexes, encoder,
|
|
410
|
+
calibrated abstention, integrity commitments — in one immutable,
|
|
411
|
+
identity-pinned file. `pikelet mcp` serves packs over the
|
|
412
|
+
Model Context Protocol so Claude Code, Claude Desktop, or any agent
|
|
413
|
+
framework can attach them as a retrieval tool with one config line — no
|
|
414
|
+
vector database, no embedding service, no retrieval backend. Results
|
|
415
|
+
carry full provenance (pack, manifest identity, title, heading path,
|
|
416
|
+
source), and a pack that cannot answer says so instead of returning
|
|
417
|
+
plausible noise. See the pikelet README's
|
|
418
|
+
"Attaching packs to an LLM (MCP)" section.
|
|
419
|
+
|
|
420
|
+
## Demos
|
|
421
|
+
|
|
422
|
+
Start with [DEMOS.md](DEMOS.md) for the maintained demo path.
|
|
423
|
+
|
|
424
|
+
Useful commands:
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
npm run demo # synthetic engine validation
|
|
428
|
+
npm run demo:artifact # range artifact search
|
|
429
|
+
npm run demo:sketch # sketch artifact search/rerank path
|
|
430
|
+
npm run test:browser # browser smoke test
|
|
431
|
+
node examples/05-one-file-search/test-file.mjs
|
|
432
|
+
node examples/05-one-file-search/test-wiki.mjs
|
|
433
|
+
node examples/05-one-file-search/test-inline.mjs
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
The one-file example is the closest thing to the project's current center
|
|
437
|
+
of gravity:
|
|
438
|
+
|
|
439
|
+
```bash
|
|
440
|
+
cd examples/05-one-file-search
|
|
441
|
+
node compile.mjs
|
|
442
|
+
node test-file.mjs
|
|
443
|
+
node serve.mjs
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## API Snapshot
|
|
447
|
+
|
|
448
|
+
Core index:
|
|
449
|
+
|
|
450
|
+
```js
|
|
451
|
+
const index = await Pikelet.create(options);
|
|
452
|
+
index.add(vector);
|
|
453
|
+
index.addBatch(vectors);
|
|
454
|
+
index.search(query, k, options?);
|
|
455
|
+
index.searchFiltered(query, k, allowedIds, options?); // allowedIds: Set of ids
|
|
456
|
+
index.delete(id);
|
|
457
|
+
index.compact();
|
|
458
|
+
index.export();
|
|
459
|
+
index.dispose();
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Common options:
|
|
463
|
+
|
|
464
|
+
```js
|
|
465
|
+
{
|
|
466
|
+
dim: 384,
|
|
467
|
+
maxElements: 100000,
|
|
468
|
+
metric: 'cosine', // 'cosine' or 'l2'
|
|
469
|
+
quantized: true,
|
|
470
|
+
M: 12,
|
|
471
|
+
efConstruction: 75,
|
|
472
|
+
efSearch: 100,
|
|
473
|
+
seed: 108
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Snapshot helpers:
|
|
478
|
+
|
|
479
|
+
```js
|
|
480
|
+
await Pikelet.restore(bytes, options);
|
|
481
|
+
await Pikelet.loadSnapshotFile(path, options);
|
|
482
|
+
await Pikelet.loadJsonFile(path, options);
|
|
483
|
+
await Pikelet.fromVectors(rows, options);
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Range and sketch artifact APIs ship at `pikelet-wasm/artifact`; the
|
|
487
|
+
complete-profile reader and builder ship at `pikelet-wasm/complete` and
|
|
488
|
+
`pikelet-wasm/complete/builder`. The wiki-scale compilers and acceptance
|
|
489
|
+
tests live in `examples/05-one-file-search/`.
|
|
490
|
+
|
|
491
|
+
## Building From Source
|
|
492
|
+
|
|
493
|
+
You only need this if you change the C++ engine under `src/`.
|
|
494
|
+
|
|
495
|
+
Requirements:
|
|
496
|
+
|
|
497
|
+
- Node.js
|
|
498
|
+
- Emscripten SDK
|
|
499
|
+
|
|
500
|
+
Build:
|
|
501
|
+
|
|
502
|
+
```bash
|
|
503
|
+
npm run build
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
Build scalar and SIMD variants:
|
|
507
|
+
|
|
508
|
+
```bash
|
|
509
|
+
npm run build:all
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
The build emits `dist/engine.js`, `dist/engine.wasm`, and scalar variants.
|
|
513
|
+
|
|
514
|
+
## Tests
|
|
515
|
+
|
|
516
|
+
Core (`npm test` also runs the artifact and complete-profile conformance
|
|
517
|
+
suites):
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
npm test
|
|
521
|
+
npm run test:fuzz
|
|
522
|
+
npm run test:simd
|
|
523
|
+
npm run test:browser
|
|
524
|
+
npm run test:scaffold
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Complete profile, end to end:
|
|
528
|
+
|
|
529
|
+
```bash
|
|
530
|
+
node examples/05-one-file-search/test-file.mjs
|
|
531
|
+
node examples/05-one-file-search/test-wiki.mjs
|
|
532
|
+
node examples/05-one-file-search/test-inline.mjs
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
The inline wiki acceptance test downloads the large release artifact on
|
|
536
|
+
demand when `examples/05-one-file-search/pancake-wiki-inline.pancake` is
|
|
537
|
+
missing.
|
|
538
|
+
|
|
539
|
+
## Performance Notes
|
|
540
|
+
|
|
541
|
+
Performance is workload-dependent, but these are the knobs that matter:
|
|
542
|
+
|
|
543
|
+
- `quantized: true` for much smaller corpus-side vector storage.
|
|
544
|
+
- `efSearch` for the recall/latency tradeoff.
|
|
545
|
+
- sketch artifacts for depth-1 candidate selection before lazy rerank
|
|
546
|
+
reads.
|
|
547
|
+
- corpus layout and clustering for fewer remote read rounds.
|
|
548
|
+
- kind-3 inline encoders when deployment shouldn't depend on an external
|
|
549
|
+
ML runtime.
|
|
550
|
+
|
|
551
|
+
The older engine-level benchmarks are still useful for HNSW behavior. The
|
|
552
|
+
newer artifact-level benchmarks are more representative of what the project
|
|
553
|
+
is actually for: search from static files and range-readable storage.
|
|
554
|
+
|
|
555
|
+
The native baselines the engine benchmarks compare against (faiss-node,
|
|
556
|
+
hnswlib-node, usearch) are not dependencies of `pikelet-wasm`; install
|
|
557
|
+
them on demand with `cd benchmarks && npm install` before running those
|
|
558
|
+
scripts. See `benchmarks/`, `benchmark_results/`, and the example READMEs
|
|
559
|
+
for current numbers.
|
|
560
|
+
|
|
561
|
+
## Tradeoffs
|
|
562
|
+
|
|
563
|
+
Pikelet is a good fit when:
|
|
564
|
+
|
|
565
|
+
- you want search in JavaScript runtimes without native addons;
|
|
566
|
+
- you want static-file or edge deployment;
|
|
567
|
+
- you need reproducible, inspectable search artifacts;
|
|
568
|
+
- your corpus can be built offline;
|
|
569
|
+
- byte-range reads are cheaper than operating a dedicated search service.
|
|
570
|
+
|
|
571
|
+
It's the wrong tool when:
|
|
572
|
+
|
|
573
|
+
- you need high-rate online mutation;
|
|
574
|
+
- you need a multi-tenant database server;
|
|
575
|
+
- you require exact nearest-neighbor search at large scale;
|
|
576
|
+
- your deployment can already use a mature native ANN service with no
|
|
577
|
+
portability constraints.
|
|
578
|
+
|
|
579
|
+
## Status
|
|
580
|
+
|
|
581
|
+
The engine and the artifact readers/builders are published and usable
|
|
582
|
+
today (`pikelet-wasm`, with `pikelet` as the app scaffold).
|
|
583
|
+
The complete `.pikelet` profile's spec is still marked draft — Draft 2,
|
|
584
|
+
under review, not frozen — but its reader and builder ship in the package,
|
|
585
|
+
and it is the clearest expression of where the project is going: a search
|
|
586
|
+
engine packaged as one verified, range-readable file.
|
|
587
|
+
|
|
588
|
+
## License
|
|
589
|
+
|
|
590
|
+
Apache-2.0. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pikelet-wasm/complete/builder — Node-only assembly of complete .pancake
|
|
3
|
+
* artifacts (spec/COMPLETE_PROFILE.md). The wire-format constants here are
|
|
4
|
+
* the same objects the reader uses (complete/format.mjs).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export declare const MAGIC: number;
|
|
8
|
+
export declare const HEADER_BYTES: number;
|
|
9
|
+
export declare const TABLE_ENTRY_BYTES: number;
|
|
10
|
+
export declare const KINDS: Record<string, number>;
|
|
11
|
+
export declare const KIND_NAMES: Record<number, string>;
|
|
12
|
+
|
|
13
|
+
export declare function sha256(bytes: Uint8Array | Buffer): Buffer;
|
|
14
|
+
export declare function canonicalJson(value: unknown): string;
|
|
15
|
+
export declare const align16: (n: number) => number;
|
|
16
|
+
|
|
17
|
+
/** Manifest profile strings and the header formatVersion each one implies. */
|
|
18
|
+
export declare const PROFILE_V1: 'pancake-complete-v1';
|
|
19
|
+
export declare const PROFILE_V2: 'pancake-complete-v2';
|
|
20
|
+
export declare const FORMAT_VERSIONS: Record<string, number>;
|
|
21
|
+
/** Corpus layout name for format 2 (per-record digests behind a page table). */
|
|
22
|
+
export declare const CORPUS_LAYOUT_V2: 'records-v2';
|
|
23
|
+
export declare const DEFAULT_CORPUS_PAGE_RECORDS: number;
|
|
24
|
+
|
|
25
|
+
export declare function buildQueryInterpSegment(kind: number, encoderBytes: Buffer | Uint8Array, calibrationBytes: Buffer | Uint8Array): Buffer;
|
|
26
|
+
/** Corpus layout v1 (format 1): count + offsets + records, whole-segment integrity only. */
|
|
27
|
+
export declare function buildCorpusSegmentFromBuffers(records: Array<Buffer | Uint8Array>): Buffer;
|
|
28
|
+
/**
|
|
29
|
+
* Corpus layout v2 (format 2): count, pageRecords, offsets, a page table of
|
|
30
|
+
* SHA-256s over runs of record digests, one SHA-256 per record, then the
|
|
31
|
+
* records. Spread the returned `corpus` block into manifest.corpus.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildCorpusSegment(records: Array<Buffer | Uint8Array>, options?: { pageRecords?: number }): {
|
|
34
|
+
bytes: Buffer;
|
|
35
|
+
corpus: {
|
|
36
|
+
records: number;
|
|
37
|
+
layout: 'records-v2';
|
|
38
|
+
pageRecords: number;
|
|
39
|
+
pages: number;
|
|
40
|
+
recordDigest: 'sha256';
|
|
41
|
+
pageTableSha256: string;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export declare function buildInlineTransformerEncoderSegment(input: {
|
|
45
|
+
declaration: Record<string, unknown>;
|
|
46
|
+
vocabBytes: Buffer | Uint8Array;
|
|
47
|
+
weightBytes: Buffer | Uint8Array;
|
|
48
|
+
}): Buffer;
|
|
49
|
+
/**
|
|
50
|
+
* Writes the container. manifestFields.profile selects the header format
|
|
51
|
+
* version (PROFILE_V1 -> 1 with a v1 corpus, PROFILE_V2 -> 2 with a
|
|
52
|
+
* buildCorpusSegment() corpus); mismatches throw. Segments with a kind this
|
|
53
|
+
* spec does not name must carry an explicit kindNumber (readers skip them).
|
|
54
|
+
*/
|
|
55
|
+
export declare function assemblePancakeFile(
|
|
56
|
+
manifestFields: Record<string, unknown> & { profile: string; corpus: { records: number } & Record<string, unknown> },
|
|
57
|
+
segments: Array<{ kind: string; bytes: Buffer | Uint8Array; kindNumber?: number }>,
|
|
58
|
+
outPath: string
|
|
59
|
+
): { outPath: string; fileBytes: number; formatVersion: number; identity: string; manifest: Record<string, unknown> };
|
|
60
|
+
|
|
61
|
+
/** Measured recall-vs-rerank operating point (SKETCH_PROFILE.md section 5). */
|
|
62
|
+
export declare function measureRecommendedRerank(input: {
|
|
63
|
+
artifactModule: unknown;
|
|
64
|
+
sketchBytes: Uint8Array | ArrayBufferLike;
|
|
65
|
+
queryVectors?: Float32Array[] | null;
|
|
66
|
+
snapshotBytes?: Uint8Array | ArrayBufferLike | null;
|
|
67
|
+
k?: number;
|
|
68
|
+
targetRecall?: number;
|
|
69
|
+
maxQueries?: number;
|
|
70
|
+
sweep?: number[] | null;
|
|
71
|
+
}): Promise<{
|
|
72
|
+
recommendedRerank: number;
|
|
73
|
+
recall: number;
|
|
74
|
+
k: number;
|
|
75
|
+
targetRecall: number;
|
|
76
|
+
queries: number;
|
|
77
|
+
querySource: string;
|
|
78
|
+
curve: Array<{ rerank: number; recall: number }>;
|
|
79
|
+
}>;
|
|
80
|
+
|
|
81
|
+
/** Kind-3 kernel via the web glue with an explicit wasm binary (jiti-safe). */
|
|
82
|
+
export declare function loadInlineEncoderKernel(): Promise<() => unknown>;
|