npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

lucivy-wasm

v2.0.1

Published

Fast BM25 full-text search for browsers — WASM build of Lucivy with threading, OPFS persistence, and LUCE snapshot support

Readme

lucivy-wasm v2

Fast BM25 full-text search for browsers — WASM build with threading (emscripten pthreads), OPFS persistence, and snapshot/delta sync support. Runs in a Web Worker.

Try the live playground — runs entirely in your browser via WASM.

What's new in v2

  • SFX-only engine — all queries route through the Suffix FST, no legacy code paths
  • Distributed searchmerge_stats for multi-node BM25
  • Correct BM25 cross-shard — identical scores whether 1 shard or 4
  • 5 bindings — Python, Node.js, C++, WASM, Rust

Install

npm install lucivy-wasm

Requirements

  • COOP/COEP headers required for SharedArrayBuffer (threading):
    • Cross-Origin-Opener-Policy: same-origin
    • Cross-Origin-Embedder-Policy: require-corp
  • For GitHub Pages or environments without header control, use coi-serviceworker.

Quick start

import { Lucivy } from 'lucivy-wasm';

const lucivy = new Lucivy('./lucivy-worker.js');
await lucivy.ready;

const index = await lucivy.create('/my-index', {
    fields: [
        { name: 'title', type: 'text' },
        { name: 'body', type: 'text' },
    ],
    shards: 4,
});

await index.add(1, { title: 'Rust Programming', body: 'Systems programming with memory safety' });
await index.add(2, { title: 'Python Guide', body: 'Data science and web development' });
await index.commit();

const results = await index.search(
    { type: 'contains', field: 'body', value: 'program' },
    { highlights: true, fields: true }
);
for (const r of results) {
    console.log(r.docId, r.score, r.fields.title);
}

lucivy.terminate();

API

Lucivy (main class)

import { Lucivy } from 'lucivy-wasm';

const lucivy = new Lucivy('./lucivy-worker.js');
await lucivy.ready;

// Create a new index (config object with fields and optional shards)
const index = await lucivy.create('/my-index', {
    fields: [
        { name: 'title', type: 'text' },
        { name: 'body', type: 'text' },
    ],
    shards: 4,
});

// Open an existing index from OPFS
const index2 = await lucivy.open('/my-index');

// Import from a LUCE snapshot (Uint8Array)
const index3 = await lucivy.importSnapshot(snapshotData, '/restored');

// Terminate the worker (frees all WASM memory)
lucivy.terminate();

LucivyIndex

Add / update / delete

await index.add(1, { title: 'Hello', body: 'World' });

await index.addMany([
    { docId: 2, title: 'Foo', body: 'Bar' },
    { docId: 3, title: 'Baz', body: 'Qux' },
]);

await index.update(1, { title: 'Updated', body: 'Content' });
await index.remove(2);
await index.commit();
await index.drainMerges();  // wait for background segment merges

Search

All substring queries are cross-token: they match across token boundaries.

// Substring — matches "programming", "programmer", "getProgramHandle", etc.
const results = await index.search(
    { type: 'contains', field: 'body', value: 'program' },
    { highlights: true }
);

// Fuzzy substring (Levenshtein distance)
await index.search({ type: 'contains', field: 'body', value: 'mutx', distance: 1 });

// Regex substring — cross-token regex matching
await index.search({ type: 'contains', field: 'body', value: 'lock.*mutex', regex: true });

// Prefix / startsWith
await index.search({ type: 'startsWith', field: 'body', value: 'prog' });

// Multi-word search — each word as contains, combined with OR
await index.search({ type: 'contains_split', field: 'body', value: 'rust safety' });

// Multi-word with fuzzy distance
await index.search({ type: 'contains_split', field: 'body', value: 'memry safty', distance: 1 });

// Phrase — adjacent tokens in order
await index.search({ type: 'phrase', field: 'body', value: 'mutex lock' });

// Boolean
await index.search({
    type: 'boolean',
    must: [{ type: 'contains', field: 'body', value: 'rust' }],
    must_not: [{ type: 'contains', field: 'body', value: 'deprecated' }],
});

// Retrieve stored fields with results
const results2 = await index.search(
    { type: 'contains', field: 'body', value: 'rust' },
    { fields: true, limit: 10 }
);

// Pre-filtered by doc IDs
const results3 = await index.searchFiltered(
    { type: 'contains', field: 'body', value: 'rust' },
    [1, 3, 5],
    { highlights: true, fields: true }
);

Filtering on non-text fields:

await index.search({
    type: 'contains', field: 'body', value: 'lock',
    filters: [
        { field: 'category', op: 'eq', value: 'kernel' },
        { field: 'score', op: 'gte', value: 0.5 },
    ]
});

Filter ops: eq, ne, lt, lte, gt, gte, in, not_in, between, starts_with, contains.

Metadata

const count = await index.numDocs();
const schema = await index.schema();

Snapshots

// Export to Uint8Array (.luce format)
const snapshot = await index.exportSnapshot();

// Import from Uint8Array
const restored = await lucivy.importSnapshot(snapshot, '/restored');

Cleanup

await index.close();    // remove from tracking (OPFS files kept)
await index.destroy();  // remove from tracking + delete OPFS files
lucivy.terminate();     // kill worker, free all WASM memory

Note: Always call lucivy.terminate() when done. Individual close()/destroy() are instant and non-blocking. Actual WASM memory is reclaimed on terminate().

License

MIT