Skip to main content

Replacing search via Algolia with SQLite WASM in 41 minutes

· 4 min read
Cody
Product @ Ascend

From hackathon conversation to merged PR.

At Ascend's quarterly hackathon this week, Seth, one of our engineers, walked over to my desk. Somehow the conversation landed on website search. Our docs site was using Algolia — not a bad product, but it's an external dependency with API keys, a crawling pipeline to maintain, and free-tier branding requirements. I'd recently built a personal project with client-side SQLite search and knew the approach worked well. With AI coding agents, knowing something is possible and roughly how to do it means the implementation is typically trivial.

So I opened a terminal and started a Claude Code session.

The conversation

Here's roughly how it went, with timestamps. The whole thing took 29 minutes from first prompt to PR, 41 minutes to merge (including CI).

2:45 PM: I set the direction:

I'd like to rip algolia out of ascend-docs. We want to take an approach similar to my OSS project zorto. That is, at build, we build a "search.db" — simple sqlite db. Then, in the browser, we use sqlite wasm to query it. Plan out this work, clarifying anything with me as needed.

Claude Code explored the existing Algolia setup and the reference implementation in parallel, then wrote out a plan.

2:53 PM: Claude starts implementing. It creates the build-time plugin, the browser search component, updates the Docusaurus config, removes Algolia dependencies, and deletes ~700 lines of Algolia code. Dependencies installed, build passes, 327 docs indexed.

3:02 PM: First issue. I notice the generated search.db is 4 MB:

4MB seems big for our site...can you check into that?

Claude investigates — the database was storing both raw content and a pre-lowered copy (content_lower). It drops the redundant column and uses LOWER(content) at query time. 4.2 MB → 2.2 MB. It also switches from better-sqlite3 (native Node addon, was causing version mismatch errors) to sql.js (pure WASM) for the build-time generation.

During this it briefly tries truncating content to 1000 characters per page. I catch it:

whooaaaa I don't think we should truncate content.

It reverts immediately.

3:06 PM: I ask about search ranking:

how are we doing the weighting? references lower than how-to/concepts?

Claude adds section-weighted scoring: getting-started > how-to > concepts > reference > support.

3:08 PM: I run /simplify, which launches three parallel review agents (reuse, quality, efficiency) against the diff. This catches several issues: duplicated utility functions between the new search plugin and the existing llms-txt.ts plugin, a race condition in database initialization, stale search results on rapid typing. Claude extracts shared utilities into doc-utils.ts and fixes the component bugs.

3:13 PM: I say "open a PR." Claude commits, pushes, creates the PR.

3:14 PM: PR is up. 730 additions, 909 deletions. Net negative lines — removed a dependency and added a feature.

3:26 PM: CI passes, PR merged.

The architecture

The approach is straightforward:

Build time: A Docusaurus plugin (search-index.ts) walks the MDX source files, strips markup, and writes a SQLite database (search.db) using sql.js. Each page gets a row with its URL, title, description, full-text content, and section category. The database ships as a static asset — Cloudflare Pages serves it with brotli/gzip compression, bringing the ~2.2 MB file well under 1 MB over the wire.

Browser: A React component (SqliteSearch.tsx) loads the database using sql.js's WASM build. On Cmd+K, a modal opens and queries the database with LIKE matching across title, description, and content. Results are ranked by match location (title > description > content) and section weight. No network requests, no API keys, no external service.

Shared utilities: MDX parsing, frontmatter extraction, and content stripping are shared between the search index plugin and the existing llms-txt.ts plugin via a common doc-utils.ts module.

Takeaways

The entire effort — removing Algolia, building the SQLite replacement, code review, PR — took 29 minutes of wall-clock time. The key ingredients:

  1. Knowing the approach works. I'd done this before. I didn't need to research feasibility — I just needed to describe the architecture and let the agent implement it.
  2. An agent that can explore and act. Claude Code read the existing codebase, understood the Docusaurus plugin system, wrote the implementation, caught its own mistakes (the native module issue), and responded to feedback (the truncation revert, the section weighting).
  3. Tight feedback loops. I was previewing the site locally while Claude was coding. When something was wrong (database too big, content truncated), I said so immediately. The agent course-corrected in seconds.

One external dependency removed. Zero ongoing maintenance for search infrastructure. A better search experience for users. Cool!