Feature Spec: Full-Text Hybrid Search with SQLite FTS5
By Jay Griffin, Claude Sonnet 4.6*Summarized from a Claude.ai conversation· March 2, 2026
docsProject: jaygriff
🏷️ Tags:searchsqlitefts5specarchitecture
Replacing Fuse.js title-only search with server-side full-text search using SQLite FTS5 — paragraph-level results with highlighted snippet previews linking directly to headings.
Overview
Replace the current Fuse.js title-only search with a server-side full-text search powered by SQLite FTS5. Search results will return at the paragraph/section level with highlighted snippet previews, linking directly to the relevant heading within an article.
Why
- Current search only covers article titles via Fuse.js — misses content inside articles
- SQLite FTS5 is already a natural extension of the ongoing frontmatter migration
- FTS5's built-in
snippet()function returns highlighted match fragments out of the box - Server-side search means no large JSON blob shipped to the client
- One source of truth: frontmatter metadata + chunked content all in SQLite
Approach
1. Content Chunking (build time)
- Parse all
.mdand.tsxfiles insrc/content/ - Split content at heading boundaries (h2, h3)
- Slugify each heading to generate anchor IDs (pairs with
rehype-slug) - Each chunk stores:
article_slug,heading_id,heading_text,body_text - Insert chunks into SQLite alongside frontmatter
2. SQLite FTS5 Table
CREATE VIRTUAL TABLE search_fts USING fts5(
heading_text,
body_text,
content='chunks',
content_rowid='id'
);3. Search API Route
- Next.js API route:
/api/search?q=... - Queries FTS5, returns ranked chunk results
- Uses
snippet()to return highlighted text fragments
SELECT
slug,
heading_id,
heading_text,
snippet(search_fts, 1, '<mark>', '</mark>', '...', 20) as excerpt
FROM search_fts
JOIN chunks ON chunks.id = search_fts.rowid
WHERE search_fts MATCH ?
ORDER BY rank4. Search UI
- Replace Fuse.js with a
fetchcall to/api/search - Display results as cards: article title + heading + highlighted excerpt
- Each result links to
/blog/[slug]#[heading-id] - Wrap
<mark>in a Tailwind highlight class for styled matches
Prerequisites
- Finish frontmatter migration to SQLite
- Confirm
rehype-slug(or equivalent) is generating heading IDs on render
Implementation Steps
- Write build-time chunking script for
.mdfiles - Handle
.tsxcontent files (text extraction is trickier than plain markdown) - Create
chunkstable and FTS5 virtual table in SQLite schema - Populate chunks at build time / on content change
- Build
/api/searchroute - Update search UI component to use new API
- Add heading deep-link anchors to article pages (byproduct of chunking)
- Remove Fuse.js dependency
Notes
- TSX content files will need special handling — JSX isn't directly parseable for text extraction. May need to render to string first or maintain a plain-text/markdown source alongside.
- For future enhancement: layer in vector embeddings for semantic search (catches conceptual matches that keyword search misses). Client-side approximate search works fine up to a few hundred articles.
Related Posts
- Why I'm Finally Moving to SQLite (And Why I Waited Until Now)After over 60 articles in one month, my file-based content system is breaking down. Here's why I'm migrating to SQLite, why now, and why I didn't do this sooner.
- Content Layer: Current System DocumentationReference doc for the SQLite migration. Captures exactly how the current filesystem-based content system works, where the pain points are, and what each function actually does.
- LLM SEO Implementation RoadmapA practical guide to optimizing your site for AI-powered search and LLM discoverability - actionable strategies for appearing in ChatGPT, Claude, Perplexity, and other LLM-generated responses