Migrating to 3.0
bash-gres 3.0 replaces file-level search with chunk-granular search: hits are sections with line ranges, vectors live in a content-addressed cache filled by an explicit pass, and the embedder is batched. The database migration is additive and idempotent — existing data keeps working.
Agent Upgrade Prompt
Upgrading with a coding agent? Copy this prompt — it is self-contained and walks the agent through the whole migration, including verification. Prefer the details yourself? They follow below.
You are upgrading a host project from bash-gres 2.x to bash-gres 3.0.
The upgrade has two breaking API changes, one additive database migration,
and two one-time indexing passes. Work through the steps in order and verify
at the end. Read node_modules/bash-gres/README.md ("Search" section) whenever
you need the authoritative API surface.
## Breaking change 1 — searches are chunk-granular
textSearch() / semanticSearch() / hybridSearch() no longer return file-level
SearchResult { path, name, rank, snippet? }. They return section-level
ChunkSearchResult { path, startLine, endLine, headingPath, content, rank }.
hybridSearch's textWeight/vectorWeight options are gone (rankings are fused
with reciprocal-rank fusion); all three searches now take
{ path?, limit?, perFileCap? }.
Find every call site of the three search methods and update the consumers:
- ".name" no longer exists — derive it from path if needed.
- ".snippet" no longer exists — the hit IS the snippet: use .content, or
hydrate the exact section with
readFileLines(hit.path, { offset: hit.startLine,
limit: hit.endLine - hit.startLine + 1 }).
- Results may contain several hits per file (up to perFileCap, default 3) —
dedupe by path only if the old file-level behavior is genuinely required.
## Breaking change 2 — the embedder is batched
The PgFileSystem option embed: (text: string) => Promise<number[]> is now
embed: (texts: string[]) => Promise<number[][]> — one vector per input text,
same order. Most embedding APIs accept arrays natively; update the wrapper.
embeddingDimensions is unchanged. Embeddings are NO LONGER computed on
writeFile/appendFile — indexing is an explicit pass (step 4).
## Step 3 — database migration (additive, idempotent)
New tables: fs_blob_chunks (chunk index) and fs_chunk_embeddings (vector
cache). No existing table changes; older rows keep working.
- Native setup() users: re-run setup(client, { ...same options }) once —
it is idempotent and creates only what is missing.
- Drizzle users, in this order:
1. If the schema file destructures specific tables from createSchema(),
add fsBlobChunks (and fsChunkEmbeddings with vector search) to the
exports first — otherwise drizzle-kit generate sees nothing new and
emits an empty migration.
2. Run drizzle-kit generate (creates the tables).
3. Paste the ENTIRE output of generateMigrationSQL() into a new custom
migration (drizzle-kit generate --custom), ordered after the
table-creating one. Its statements are all idempotent (IF NOT EXISTS /
guarded DO blocks) — no diffing against existing migrations, the
already-applied statements no-op.
## Step 4 — enable chunking and run the two passes once
1. Add chunking: true (or chunking: { volatileFrontmatterKeys: [...] } if
files carry volatile front-matter like fetch timestamps) to every
PgFileSystem that writes or searches.
2. Index pre-existing content once per workspace:
await fs.backfillChunks() // chunk blobs written before 3.0
await fs.indexChunkEmbeddings() // fill the vector cache (needs embed)
Both are idempotent and version-scoped to the handle. Skip
indexChunkEmbeddings if the project only uses textSearch.
## Step 5 — verify
- Typecheck the project; fix every compile error at search call sites.
- Run one real query per search mode the project uses and check hits come
back with startLine/endLine populated.
- If searches throw with a message mentioning fs_blob_chunks,
fs_chunk_embeddings, or enableFullTextSearch, step 3 was incomplete — the
error text says exactly which setup flag or migration is missing.
Do not guess API shapes from memory: check node_modules/bash-gres/README.md
and the .d.ts files if anything here doesn't match the installed version.What Breaks
| 2.x | 3.0 |
|---|---|
Searches return file-level SearchResult { path, name, rank, snippet? } | Searches return section-level ChunkSearchResult { path, startLine, endLine, headingPath, content, rank } |
hybridSearch takes textWeight / vectorWeight | Reciprocal-rank fusion, no weights; all searches take { path?, limit?, perFileCap? } |
embed: (text) => Promise<number[]> | embed: (texts) => Promise<number[][]> (batch, one vector per text) |
Embeddings computed on writeFile | The write path never embeds — run indexChunkEmbeddings() explicitly |
1. Update Search Consumers
// 2.x
const results = await fs.textSearch("shipping")
for (const r of results) console.log(r.name, r.snippet)
// 3.0 — the hit is the snippet, with an exact address
const hits = await fs.textSearch("shipping")
for (const h of hits) console.log(`${h.path}:${h.startLine}-${h.endLine}`, h.content)
// need the section verbatim? hydrate the line range
const { content } = await fs.readFileLines(hits[0].path, {
offset: hits[0].startLine,
limit: hits[0].endLine - hits[0].startLine + 1,
})2. Batch the Embedder
// 2.x — one text per call
embed: async (text) => {
const res = await openai.embeddings.create({ model, input: text })
return res.data[0].embedding
}
// 3.0 — a batch per call, one vector per text
embed: async (texts) => {
const res = await openai.embeddings.create({ model, input: texts })
return res.data.map((d) => d.embedding)
}3. Migrate the Database
3.0 adds two tables — fs_blob_chunks (the chunk index) and fs_chunk_embeddings (the vector cache) — and no longer touches the old blob-level index. The migration is additive, and older bash-gres versions keep working against a migrated database. Native setup() users just re-run it once — it is idempotent. Drizzle users: if your schema file destructures specific tables from createSchema(), add fsBlobChunks (and fsChunkEmbeddings with vector search) to the exports first — otherwise drizzle-kit generate emits an empty migration. Then paste the entire output of generateMigrationSQL() into a new custom migration ordered after the table-creating one: every statement it emits is idempotent, so the already-applied ones no-op.
4. Index Existing Content
Enable chunking on your instances, then run two idempotent passes once per workspace. Both scope to the handle's current version:
const fs = new PgFileSystem({ db, workspaceId, chunking: true, embed, embeddingDimensions })
await fs.backfillChunks() // chunk content written before 3.0
await fs.indexChunkEmbeddings() // fill the vector cache (skip if BM25-only)