Rockbox Zig's search just got a significant overhaul. I replaced the embedded Tantivy full-text search engine with Typesense, an open-source, self-hosted search server. This post covers what changed, why we made the switch, and how the new system works end-to-end.
The Old Approach: Tantivy
The previous search was powered by Tantivy, a Rust full-text search library that runs embedded inside the Rockbox process. The implementation lived in a dedicated /crates/search/ crate and maintained separate on-disk indexes for each entity type:
Tracks
Albums
Artists
Liked tracks / liked albums
Files
Indexes were stored on the filesystem under ~/.config/rockbox.org/indexes/ using Tantivy's MmapDirectory. Each entity type had its own schema definition, and search queries ran against these local indexes synchronously inside the Rockbox server process.
The problems:
Searches took long enough that the macOS client needed a loading spinner and an
isLoadingstate variable just to indicate work was happening.Tantivy indexes lived inside the process — scaling or offloading that work required non-trivial changes.
Running the full-text indexing and query logic inside the server added overhead during library scans.
The New Approach: Typesense
Typesense is a standalone search server written in C++ that exposes a clean REST API. Instead of embedding search logic in the Rockbox process, we now run Typesense as a sidecar process and talk to it over HTTP.
How Typesense is provisioned
The new /crates/typesense/ crate handles setup. On startup, it:
- 1.
Checks whether
typesense-serveris already on$PATH. - 2.
If not, downloads the correct binary for the current OS/arch from
https://dl.typesense.org/releases/{version}/typesense-server-{os}-{arch}.tar.gz(default version:30.1). - 3.
Stores data in
~/.config/rockbox.org/typesense/. - 4.
Generates (or reuses) a UUID-based API key stored at
~/.config/rockbox.org/typesense/api-key. - 5.
Starts the server on port
8109(configurable viaRB_TYPESENSE_PORT).
Both macOS (darwin) and Linux are supported. The whole provisioning step is transparent — users don't need to install or configure anything separately.
Collection schemas
Three Typesense collections mirror the main entity types:
Tracks
path : string, unique document ID
title, artist, album, album_artist: string, searchable
genre, composer: string
bitrate, frequency, filesize, length: int
track_number, disc_number, year: int
md5: string, content hash
album_art, artist_id, album_id, genre_id: string, optional
Default sort: title. Query fields: title, artist, album, path.
Albums
title, artist: string, searchable
year: int
md5, artist_id: string
album_art, label: string, optional
Artists
| Field | Type | Notes |
|---|---|---|
| name | string | searchable |
| bio, image | string | optional |
Data Flow
Library scan and indexing
When Rockbox scans your music library it:
- 1.
Walks the music directory recursively looking for supported audio formats (mp3, flac, ogg, m4a, aac, wav, wv, mpc, aiff, opus, ape, wma, and more).
- 2.
Extracts metadata and album art using
rockbox_sysFFI calls, running up to 8 files concurrently. - 3.
Persists everything to a local SQLite database.
- 4.
Fetches all tracks, albums, and artists from SQLite, converts them to the Typesense types, serialises to JSONL (JSON Lines), and batch-imports via:
POST /collections/{collection}/documents/import?action=upsert
X-TYPESENSE-API-KEY: <key>
The upsert action means re-scanning the library is idempotent — existing documents are updated in place rather than duplicated.
Search queries
The HTTP search handler sits at:
GET /search?q={query}
It fans out to three parallel Typesense requests, one per collection, using query_by=title,artist,album,path for tracks, query_by=title,artist,label for albums, and query_by=name for artists. The responses are merged and returned as a single JSON payload:
{
"tracks": [...],
"albums": [...],
"artists": [...]
}
The same data is also available through the GraphQL API as a SearchResults type with tracks, albums, artists, liked_tracks, and liked_albums fields.
What Changed in the Client
The most visible sign of the improvement is what we were able to remove from the macOS app. With Tantivy, searches were slow enough to require explicit loading UI:
// Before
@Published var isLoading: Bool = false
Typesense returns results fast enough that the loading state and spinner are simply gone. The macOS SearchManager now fires a 300 ms debounced query and the results appear — no spinner, no progress view.
Configuration Reference
RB_TYPESENSE_API_KEY: auto-generated UUID, Authentication key RB_TYPESENSE_PORT: 8109, Port for the Typesense server RB_TYPESENSE_VERSION: 30.1, Version to download if not in PATH ROCKBOX_UPDATE_LIBRARY: Set to 1 to force a library rescan on startup
You can also trigger a rescan with index rebuild at any time via the HTTP API:
POST /system/scan_library?rebuild_index=true
Summary
| | Tantivy (before) | Typesense (after) |
|---------------|----------------|-------------------------|
| Deployment | Embedded in process | Sidecar server (auto-provisioned) |
| Storage | ~/.config/rockbox.org/indexes/ | ~/.config/rockbox.org/typesense/ |
| Index format | Tantivy segments (mmap) | Typesense native |
| Import format | Tantivy documents | JSONL batch via REST |
| Search API | In-process function call | HTTP REST |
| Client loading UI | Required | Removed |
| Platform | Rust only | OS binary (macOS + Linux) |
The migration removes an entire crate from the project, replaces it with a thin HTTP client, and delivers noticeably faster results. Typesense stays self-hosted and open-source, so there are no external dependencies or data leaving your machine.
The full implementation is available at github.com/tsirysndr/rockbox-zig.