Static site · no backend · search runs in your tab

Find the service
you actually need

Every service, certificate and payment of the portal — searchable by keyword, by meaning, or both at once. The ranking happens on this page: there is no search server.

loading the search engine…

popular:

    How this search works

    The four statements behind it — create, insert, search, and change — with the code this demo actually runs. It is ordinary SQL over a real database file; the WASM module that executes it here is ~660 KiB gzipped.

    step 1 · create the database

    One table, one vector column, two indexes

    A page is a row. The VECTOR(384) column holds each page's embedding, and the two indexes are what make retrieval fast: one for keyword ranking, one for nearest-vector lookup. Everything below is the engine's own dialect — SQLite's SQL plus vectors.

    CREATE TABLE pages (
        id INTEGER PRIMARY KEY,
        path TEXT, title TEXT, body TEXT,
        embedding VECTOR(384)          -- one vector per page
    );
    CREATE INDEX pages_body      ON pages (body);      -- full-text (BM25)
    CREATE INDEX pages_embedding ON pages (embedding); -- vectors (HNSW)

    In the browser this is one call: db.execute(sql). At build time the same statements run through the native Rust database — that is how site.inlay was written.

    step 2 · insert the pages

    Each page's text becomes a vector on the way in

    Parameters are bound as a JSON array — a nested array of numbers is a vector. The embedder here is the engine's stand-in (character trigrams, so it matches pages that spell alike); a real site puts its own model's output here instead, and the rest is unchanged.

    // one page, as this demo's build script runs it for all 45
    db.execute(
      "INSERT INTO pages (path, title, body, embedding) VALUES (?, ?, ?, ?)",
      JSON.stringify([
        "/services/passport/renew.html",
        "Renew a passport",
        body,                              // the page's full text
        Array.from(embed(body, dim)),      // ← the vector
      ]),
    );
    
    // write the retrieval indexes into the file itself, then ship it
    const bytes = db.export();           // checkpointed: BM25 + HNSW inside

    The exported bytes are the deployment: site.inlay is fetched like an image and opened with Database.open(bytes). That is the whole "backend" — a file on a web server.

    step 3 · search

    The query becomes a vector, then one statement ranks everything

    Your query text goes through the same embedder that built the index, so query and pages live in the same vector space. Then the ranking mode you picked changes one function call:

    bm25_score(body, ?) — keyword relevance, the classic search-engine score over the full-text index. Finds exact words: "renew", "passport".

    vector_score(embedding, ?) — semantic similarity against your query's vector, using the HNSW approximate-nearest-neighbour index. Finds pages about the same topic even when no query word appears: "help with rent" finds rent assistance.

    fuse(vector_score(…), bm25_score(…)) — hybrid: both scores are computed and combined with reciprocal rank fusion, which merges the two rankings rather than the two raw numbers, so a page that does well on either gets pulled up.

    
          

    That statement runs inside the WASM module in this tab against the database fetched at load — every keystroke-to-result hop is local. No search API exists to attack, log, rate-limit or take down.

    step 4 · change it — it is a real database

    Update, delete, and ship the result

    Nothing above is a snapshot trick: this is a mutable database. When a page's content changes, update the row and its vector; when a page disappears, delete it. The indexes maintain themselves incrementally — no rebuild.

    // a page's content changed — re-embed it, update the row
    db.execute(
      "UPDATE pages SET body = ?, embedding = ? WHERE path = ?",
      JSON.stringify([
        newBody,
        Array.from(embed(newBody, dim)),
        "/services/passport/renew.html",
      ]),
    );
    
    // a page was removed
    db.execute("DELETE FROM pages WHERE path = ?", JSON.stringify([path]));
    
    // persist: export() checkpoints first, so the bytes carry fresh indexes
    const updated = db.export();         // upload these bytes, done

    For a static site this maps directly onto publishing: content changed? Update the rows, export, upload. No index rebuild, no downtime, no cache invalidation beyond the one file.