lunr review
Lunr 2.3.9 builds a full-text index over JavaScript objects and returns ranked document references for matching terms. It supports field restrictions, field and term boosts, wildcards, fuzzy edit distance, and required or prohibited terms. The index stays in memory and can be serialized, which makes Lunr useful for offline documentation and other static collections. It does not store the original records or update a finished index in place. There is no newer current-version behavior to report: npm 2.3.9 was published in August 2020, and the repository has not been pushed since July 2024.
Lunr still fits prebuilt offline search over a modest, mostly static corpus. New work that needs updates, TypeScript packaging, or product-search features should start elsewhere.
We installed it
| Install | ✓ · 0.7s | 1 package on disk · 2 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | 8.6 KB | gzipped (30.1 KB minified), bundled with esbuild |
| Types | — | no TypeScript types found |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does lunr install cleanly?
Yes. In a fresh container with an empty cache, npm install lunr finished in 0.7s, leaving 1 package and 2 MB on disk. npm audit reported no known vulnerabilities.
How much does lunr add to a browser bundle?
8.6 KB gzipped (30.1 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does lunr work with both ESM and CommonJS?
Yes. Both import 'lunr' and require('lunr') worked in Node 22 in our run. The package is published as CommonJS.
Does lunr include TypeScript types?
No type declarations were found in our install, so TypeScript users need their own declarations.
lunr or minisearch: which should you use?
minisearch: Use it for client-side search that needs document additions, removals, autocomplete, and fuzzy matching. Lunr still fits prebuilt offline search over a modest, mostly static corpus.
When should you not use lunr?
Documents change while the application is running. A built Lunr index has no normal add or remove workflow, so updates require a rebuild.
Use it if
- A static documentation or help corpus needs search that continues working without a server or network connection.
- You can build the index during deployment, ship it as an asset, and retain a separate map of source records.
- BM25 ranking, field boosts, fuzzy terms, and a compact query language cover the search experience you need.
- The collection is small enough for its serialized index and lookup structures to live in each user's memory.
- Documents change while the application is running. A built Lunr index has no normal add or remove workflow, so updates require a rebuild.
- A large corpus would make the browser download, parse, and retain the complete index. The official guide warns that building large indexes can make a page unresponsive.
- You need bundled TypeScript declarations or a modern ESM package contract. Our install found neither types nor an exports map, and the package is CommonJS.
- Search needs facets, synonyms managed at runtime, analytics, access filters, typo tuning, or live ingestion. Those are search-service concerns outside Lunr's API.
- You expect active package releases. Version 2.3.9 dates to 2020, with 130 open issues and pull requests and no repository push after July 2024.
Setup reality
We installed lunr 2.3.9 in a clean Node 22 Bookworm container. npm completed in 0.7 seconds, left one package on disk, and used 2 MB. Lunr has no direct or peer dependencies and is 1192 KB unpacked. npm audit found no known vulnerabilities. It is CommonJS with no exports map; require and ESM import both worked in our test. No TypeScript declarations were present. Our browser bundle was 30.1 KB minified and 8.6 KB gzipped.
Define a unique ref field and every searchable field before adding documents. Lunr keeps references, scores, and match metadata, while your application keeps titles, URLs, excerpts, and permissions. A typo in a fielded query can throw because the named field must exist. Bare terms are optional and combine like OR; prefixes such as plus and minus change term presence. Wildcards bypass stemming, and a leading wildcard can be expensive.
Building on page load blocks the main thread as the corpus grows. The documented alternative is to build under Node, serialize the index with JSON.stringify, compress it for transport, and load it with lunr.Index.load. This removes browser build work but still costs download, JSON parse time, and resident memory. Rebuild whenever documents, fields, boosts, or pipeline functions change.
English stemming and stop words ship in core. Other languages use plugins such as lunr-languages, loaded in the required order before the index is built. A serialized index depends on registered pipeline functions being available when it is loaded. Treat user text as data through the programmatic query API if punctuation should not invoke Lunr's query grammar.
Patterns
Index a document array build-index
const lunr = require('lunr')
const documents = [
{ id: '1', title: 'Moonlight', body: 'Light reflected by the moon' },
{ id: '2', title: 'Sunrise', body: 'The sun above the horizon' },
]
const index = lunr(function () {
this.ref('id')
this.field('title')
this.field('body')
documents.forEach((doc) => this.add(doc))
})The ref value must be unique. Register every searchable field before adding documents.
Join references to source records resolve-results
const records = new Map(documents.map((doc) => [doc.id, doc]))
const matches = index.search('moon').map(({ ref, score, matchData }) => ({
record: records.get(ref),
score,
matchData,
}))Lunr results do not contain the original object. Keep records in a separate map, store, or database.
Weight title matches higher boost-title-field
const index = lunr(function () {
this.ref('id')
this.field('title', { boost: 8 })
this.field('body')
documents.forEach((doc) => this.add(doc))
})An index-time field boost affects every search. Use query boosts when importance changes by screen or request.
Restrict a term to the title search-field
const results = index.search('title:moon')An unknown field name raises a query error. Do not insert arbitrary user-selected field names without validation.
Match a term prefix search-prefix
const results = index.search('astro*')Wildcard terms are not stemmed. Leading wildcards such as *astro are slower because more vocabulary must be inspected.
Allow one edit search-fuzzy
const results = index.search('javascript~1')Higher edit distances widen matches and cost more. Short queries can become noisy even at a distance of one.
Require two terms and reject one require-terms
const results = index.search('+javascript +testing -browser')Several bare terms use OR. Prefix each required term with plus when every term must occur.
Raise one query term's score boost-term
const results = index.search('javascript^10 testing')The boost changes ranking. It does not make the other bare term mandatory.
Avoid parsing user punctuation query-programmatically
const results = index.query((query) => {
query.term(userTerm, {
fields: ['title'],
presence: lunr.Query.presence.REQUIRED,
boost: 5,
})
query.term('legacy', { presence: lunr.Query.presence.PROHIBITED })
})The query object gives code direct control over fields and presence without treating user punctuation as syntax.
Write an index during the build serialize-index
const fs = require('node:fs')
fs.writeFileSync(
'public/search-index.json',
JSON.stringify(index),
)Recreate this artifact after any document, field, boost, or pipeline change. Keep source records separately.
Load a prebuilt browser index load-index
import lunr from 'lunr'
const data = await fetch('/search-index.json').then((response) => response.json())
const index = lunr.Index.load(data)
const results = index.search('offline')Prebuilding removes indexing work from the page, but download, parse, and in-memory costs remain.
Install the French pipeline index-french
const lunr = require('lunr')
require('lunr-languages/lunr.stemmer.support')(lunr)
require('lunr-languages/lunr.fr')(lunr)
const index = lunr(function () {
this.use(lunr.fr)
this.ref('id')
this.field('text')
documents.forEach((doc) => this.add(doc))
})Install lunr-languages separately and load stemmer support before the language plugin.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| minisearch | npm | Use it for client-side search that needs document additions, removals, autocomplete, and fuzzy matching. |
| flexsearch | npm | Use it when browser indexing and lookup speed matter more than Lunr's small, familiar API. |
| fuse.js | npm | Use it for fuzzy matching over a modest array without maintaining a full-text index. |
More data guides
numpy · fsspec · pandas · sqlalchemy · pyarrow · lxml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

