
Narsil
The search engine that scales with your data.
Narsil is an open source search engine with full-text, vector, hybrid, and geosearch on one typed index. You can embed Narsil in your application process, where the engine searches without a network hop, or deploy it as a standalone search server with a REST API, a write-ahead log, and bulk NDJSON ingest. Both forms contain the same engine, which stores indexes in one cross-language binary format (.nrsl), so you can load an index from one form into the other.
The engine partitions large indexes across workers and merges the results of the partitions into a single ranked list. This TypeScript package is the reference implementation of the .nrsl format, and a Rust crate and a Go module hold the same engine for programs in those languages.
On BEIR SciFact, Narsil's BM25 ranking scores 0.681 nDCG@10, which is within 0.002 of the Anserini reference of 0.679. Elasticsearch scores 0.679 and OpenSearch scores 0.679 on the same queries. On SciFact, Narsil answers 4,200 keyword queries a second at its peak, and OpenSearch follows with 3,027. On DBpedia entities 1M with every engine held at 0.99 recall, Narsil answers 670 vector queries a second at its peak, while OpenSearch leads with 869. With each engine's recommended production settings on the same set, Narsil answers 1,154 vector queries a second at its peak, and OpenSearch follows with 1,111. Narsil has the highest peak throughput in 16 of the 20 keyword, vector, and hybrid comparisons in the newest recorded run: 8 of 12 with every engine on equal settings, and all 8 with each engine's recommended production settings. The harness recorded these figures on 2026-09-28 in run 20260928T002255Z. GCP c3-standard-8 hardware in us-central1-a, us-east1-b, and us-west1-a hosted the engines. You'll find every dataset, every engine's settings, and the full method in BENCHMARKS.md.
Try it in your browser at narsil.sondelali.com/demo. Read the full documentation at narsil.sondelali.com/docs.
narsil is the sword of Elendil in Tolkien's Lord of the Rings, shattered into shards and later reforged. The name fits the design, because the engine splits your data into partitions that it persists one by one, then merges their results into one ranked list for each query.
Contents
- Install
- Add Narsil with a coding agent
- Quick start
- Features
- Documentation
- Examples
- Distribution
- Search quality
- Runtime support
- Other languages
- License
Install
pnpm add @delali/narsil
Narsil works in Node.js 22 or newer, and in Bun, Deno, and browsers. The Runtime support table covers each runtime.
On an arm64 or x64 machine under macOS, Linux, or Windows, the package manager also installs Narsil's native search core in C as an optional dependency, so the engine analyses text and answers keyword and vector searches through it. If you omit optional dependencies, Narsil does that work through WebAssembly and TypeScript, with the same results, as it always does in a browser.
Add Narsil with a coding agent
The Narsil skill tells a coding agent whether to embed Narsil or serve it, which packages to install alongside it, and which server settings keep a public address safe. Install the skill into the coding agents on your machine with the skills CLI:
npx skills add assetcorp/narsil
The skill has the agent read the Narsil version that your project installs, and it sends the agent to that version's types and documentation. On the agents page, you can copy a prompt that sets a coding agent to work on one of six search tasks.
Quick start
import { createNarsil } from '@delali/narsil'
const narsil = await createNarsil()
await narsil.createIndex('products', {
schema: {
title: 'string',
description: 'string',
price: 'number',
inStock: 'boolean',
category: 'enum',
tags: 'string[]',
},
language: 'english',
})
await narsil.insert('products', {
title: 'Mechanical Keyboard',
description: 'Cherry MX Brown switches with PBT keycaps and USB-C connection',
price: 129.99,
inStock: true,
category: 'electronics',
tags: ['peripherals', 'typing', 'mechanical'],
})
const results = await narsil.query('products', {
term: 'mechanical keyboard',
filters: {
fields: {
inStock: { eq: true },
price: { lte: 200 },
},
},
boost: { title: 2.0 },
limit: 10,
})
Every hit holds the document, its id, and its BM25 score. Beside the hits, results.count holds the total number of matching documents, results.elapsed holds the query time in milliseconds, and results.coverage counts the partitions behind the answer, so you can tell when a cluster answers from part of its data.
Features
Search. For full-text search, the engine scores with BM25 and supports field boosting, fuzzy matching through bounded Levenshtein distance, search as you type through last-word prefix matching, and term-coverage and score thresholds. You can combine a query with filters, facets, sorting, grouping, cursor pagination, and pinned results. On a field that you declare for pattern search, a filter can find text anywhere inside a value with contains, test the whole value against a wildcard pattern, look for a match of a regular expression, and accept a typo or two in an eq test. With partial-word search, a query word also matches inside longer words, so a search for proof finds waterproof.
Vector and hybrid retrieval. For vector search, the engine compares vectors by cosine similarity, dot product, or Euclidean distance. It scans a field exactly until the field holds 1,024 vectors, a count that you can change, after which it builds an HNSW graph with optimised scalar quantization on by default. Outside a browser, on an arm64 or x64 machine under macOS, Linux, or Windows, the engine searches that graph through a native core in C, which returns the same results as its WebAssembly search. For hybrid search, the engine fuses BM25 and vector rankings through reciprocal rank fusion or linear blending. Embedding adapters turn text into vectors on insert and query, through OpenAI, local Transformers.js models, or an adapter that you write. A query can rerank its top hits with a cross-encoder, through a Cohere-compatible rerank service, a local Transformers.js model, or an adapter that you write.
Geosearch. Geo filters match documents by radius, using Haversine or Vincenty distance, or by polygon containment. You can combine a geo filter with every other query feature.
Storage. Persistence adapters store indexes on the filesystem, in IndexedDB, in memory, or in a backend that you write. With durability on, the engine keeps a write-ahead log with periodic checkpoints and recovers automatically after a crash. You can stream a whole index into a portable snapshot archive, or export its documents as JSON Lines to move the index across releases. With device sync, a phone, a laptop, or a browser tab keeps a copy of one index from a server, which it searches and writes while it is offline.
Scale. The engine routes each document to a partition by a deterministic hash. When you call rebalance(), the engine reshapes the partitions online and buffers incoming writes in a write-ahead queue. Once an index holds 1,000 documents, the engine serves keyword queries from worker copies on half its worker threads, while the HTTP server receives requests on the threads that hold those copies.
Operations. The HTTP server subpath wraps an engine in a REST API with health probes, bulk NDJSON import, snapshot and restore endpoints, and task-based long operations. The client subpath calls every one of those routes from a browser or from Node under the engine's own method names. The React subpath turns the read methods into hooks that call either a client or an engine in the same page. For observability, you have events, plugins, and memory reporting. Language modules cover the languages that the guide lists, including Swahili, Yoruba, Hausa, and other African languages, and each module is a separate entry point.
Documentation
| Guide | What it covers |
|---|---|
| Configuration | Every createNarsil option, worker tuning, analysis rebuilds, and the tokenizer cache |
| Indexes and documents | Schemas, index management, inserts, reads, updates, removals, and batch operations |
| Full-text search | Term queries, fuzzy matching, prefix completion, thresholds, highlighting, scoring modes, and suggestions |
| Filters, facets, and pagination | Field, array, presence, and geo filters, facet counts, sorting, grouping, cursors, and pinning |
| Vector search | Vector fields, distance metrics, HNSW promotion, quantization, the native search core, and graph maintenance |
| Hybrid search | Reciprocal rank fusion and linear blending of text and vector rankings |
| Geosearch | Radius and polygon filters, and the two distance formulas |
| Embedding adapters | Automatic embedding on insert and query, named adapters, the bundled ones, and custom ones |
| Reranking | Reordering the top hits with a cross-encoder, the bundled rerank adapters, and custom ones |
| Persistence and durability | Storage backends, the write-ahead log, checkpoints, recovery, snapshots, document exports, and indexes from an earlier release |
| Partitions and workers | Partition routing, online rebalancing, worker copies, and multi-instance invalidation |
| Language support | Every language module, analysis revisions and rebuilds, and named tokenizers and stop words |
| HTTP server | Wrapping an engine in a REST API, every route it serves, and long-running tasks |
| Cluster mode | Multi-node indexes: nodes and roles, replication, routed writes, distributed searches and reads, and the calls that fail in a cluster |
| Client | Reaching a server from a browser or Node, following a task, and the codes it raises |
| Python client | Reaching a server or a cluster from Python, in blocking and async forms, and the failover rules |
| Go client | Reaching a server or a cluster from Go, streaming snapshots, and the failover rules |
| React Native | The engine inside an iOS or Android app, where the index lives, snapshots, memory, and errors |
| Device sync | A copy of one index on a device that searches and writes offline, the sync routes, conflicts, and the snapshot |
| Change capture | Following every insert, update, and removal on an index from a backend service, the feed, filters, and resyncs |
| Popular query suggestions | Whole searches that people ran and clicked, counted per person, built into a suggestions index, and the privacy trade-offs |
| React | The hooks over a client or an engine in the page, one shared request per key, and loading a corpus from a component |
| Observability | Plugin hooks, engine events, and memory reporting |
| Errors | The codes you handle most often, and the three sets that NarsilError.code covers |
The specification defines the .nrsl format, the analysis pipeline, and the replication invariants that every implementation must uphold.
Examples
| Example | What it shows |
|---|---|
| HTTP server | The launcher serves the engine as a REST service with durability, API-key auth, and Docker packaging, while every setting comes from the environment. |
| Browser | The app embeds the engine in a browser with IndexedDB persistence and Web Worker search. |
| Server app | The app calls the HTTP server through the client SDK and the React hooks to load corpora as import tasks. In its Ask view, you can ask questions about those corpora. |
Distribution
@delali/narsil/distribution holds Narsil's multi-node cluster mode: nodes and roles, replication, coordinator adapters for etcd and in-process testing, TCP and gRPC transports with mutual TLS, and distributed query routing. A cluster node can create, drop, clear, write, update, search, list, count, and suggest across every partition, and it can change the replication factor and the partition count of an index. A node with a durability directory keeps its id there across restarts, and the etcd coordinator connects over TLS with a user and a password. A node that the HTTP server fronts reports the health of the cluster and the node's Prometheus metrics. The cluster dashboard example starts three nodes against etcd and shows the state of each partition while you cut the network links. The cluster guide covers the API, including the packages that the etcd coordinator and the gRPC transport each need. For the contract that every implementation must uphold, read packages/spec/distribution.
Search quality
We measure ranking quality on the BEIR SciFact corpus, which holds 5,183 documents and 300 queries with human relevance judgements. We compare Narsil with Orama and MiniSearch in one process. All three use the same 33 Lucene English stop words and default BM25 parameters, although each one stems English differently. Narsil has the top nDCG@10 of the three.
What these metrics mean:
- nDCG@10 measures whether the most relevant documents appear near the top of the results. A score of 1.0 means a perfect ranking, while 0.0 means that no relevant document appears in the top 10.
- P@10 is the fraction of the top 10 results that are relevant.
- MAP tracks precision at every rank where a relevant document appears. A higher MAP means that relevant documents cluster near the top of the ranking.
- MRR measures how soon the first relevant result appears. A higher MRR means that the first relevant document appears nearer the top.
Continuous integration executes a separate SciFact regression test on the same corpus. That test fails the build when ranking quality drops below calibrated thresholds.
Reproduce these scores with pnpm --filter benchmarks bench -- --tiers relevance. BENCHMARKS.md holds the full quality, throughput, and latency tables for all three engines.
Narsil also works as a search server. On the BEIR datasets, we compare the server with Elasticsearch, OpenSearch, Qdrant, Weaviate, Typesense, and Meilisearch across keyword, vector, and hybrid retrieval. See the full benchmarks for those results.
Runtime support
| Runtime | Concurrency | Persistence | Invalidation |
|---|---|---|---|
| Node.js | worker_threads | Filesystem | Adapter-based |
| Bun | worker_threads | Filesystem | Adapter-based |
| Deno | Web Workers | Filesystem | BroadcastChannel |
| Browser | Web Workers | IndexedDB | BroadcastChannel |
The browser example shows an embedded engine with IndexedDB persistence, while the server app example shows the same interface over the client SDK and the React hooks.
Other languages
| Package | What it does |
|---|---|
delali-narsil | The Rust crate creates, changes, and searches an index inside a Rust program. |
github.com/assetcorp/narsil/packages/go/engine | The Go engine creates, changes, and searches an index inside a Go program. |
github.com/assetcorp/narsil/packages/go | The Go client calls a Narsil server, or every node of a cluster, from Go. |
narsil-client | The Python client calls a Narsil server, or every node of a cluster, from Python. |
@delali/narsil-react-native | The driver puts the engine inside an iOS or Android app. |
The Rust crate and the Go engine save every file in the same layouts as this package, so this package can open an index that either one saves, and either one can open an index that this package saves. Only this package holds the HTTP server and cluster mode.
License
Apache-2.0