nix: tinysearch

packaging site-embedded search for nix

2024-08-11 / updated 2024-08-27

tinysearch is a static site-embedded search engine written in Rust by Matthias Endler. It consumes a fuse index (in my case, generated by Zola) and emits a WASM binary containing the index and search engine that can be loaded by the site and hooked up to an input element (see the search bar above).

The functionality is great, but it's incompatible with nix in its current state, as the tinysearch generator requires network access. In particular, it:

The bolded step requires network access because cargo build has to download dependencies. Nix tooling normally solves for this by vendoring dependencies using the crate's Cargo.lock and then invoking cargo build --offline, but it's not possible to configure this through the tinysearch -> wasm-pack -> wasm-bindgen stack.

nix port

I ported tinysearch support to Nix in a pair of packages:

# produces tinysearch index at ./store
$ tinysearch -m store search_index.en.json

I include it in my blog build like this:

# flake.nix -- details elided for concision
{
    inputs = {
        # ...
        tinysearch_nix.url = "git+https://pub.npry.dev/tinysearch_nix";
    };

    outputs = { flake-utils, ... } @ inputs: (flake-utils.lib.eachDefaultSystem (system: let
        pkgs = import nixpkgs {
            inherit system;

            overlays = [
                inputs.tinysearch_nix.overlays.default
            ];
        };

    in {
        packages.default = pkgs.callPackage ./blog.nix {};
    }));
}
# blog.nix
{
    # ...
    runCommand,

    tinysearch,
    tinysearch_engine,
}: runCommand "my_blog" {} ''
    # blog build elided

    blog_generate_index | \
        ${tinysearch.generate_index}/bin/tinysearch_generate_index > $out/tinysearch_index

    cp ${tinysearch_engine} $out/tinysearch_engine
    cp ${./search.js} $out/search.js
''

engine changes

I changed tinysearch_engine to not embed the search index; rather, the wasm blob is now just the search engine, and you dynamically load (possibly multiple) indices into it before using it. The result is less "batteries included" and requires another roundtrip to the server, but is more straightforwardly compatible with the Nix build process.1

Before:

import { search, default as init } from 'tinysearch';

// Init tinysearch engine.
await init();

// Submit a user query.
const results = search('user_search', 5);

After:

import { search, load_filters, default as init } from 'tinysearch';

const fetch_index = async (index_path) => {
    const resp = await fetch(index_path);
    const blob = await resp.blob();
    return await blob.bytes();
};

const [_, index_store] = await Promise.all([
    // Init tinysearch engine.
    init(),

    // Fetch the search index from the backend.
    fetch_index('/tinysearch_index'),
]);

// Load the index into tinysearch.
const filters = load_filters(index_store);

// Submit a user query.
const results = search(filters, 'user_search', 5);
1

And technically more flexible, in that you can load the engine and index separately — e.g. per-user site indices, dynamically updating the active index with up-to-date information, or loading multiple indices for distinct search domains.