These libraries provide full-text search capabilities directly within the browser or Node.js environment, eliminating the need for external search servers like Elasticsearch or Algolia for many use cases. lunr serves as the foundational, lightweight engine for static datasets. elasticlunr extends lunr with boolean logic and field boosting. flexsearch focuses on extreme speed and memory efficiency for large datasets. fuse.js specializes in fuzzy matching and typo tolerance rather than traditional inverted indices. js-search offers a simple, flexible wrapper around various indexing strategies. search-index provides a persistent, database-like search engine that supports real-time updates and complex querying.
Building search functionality directly into the frontend removes the latency and cost of external services. However, not all search libraries work the same way. Some rely on inverted indices for speed, others on vector space models for fuzzy matching, and some prioritize persistence over raw performance. Let's break down how elasticlunr, flexsearch, fuse.js, js-search, lunr, and search-index handle real-world engineering challenges.
The fundamental difference lies in how these libraries store and retrieve data. Most (lunr, elasticlunr, flexsearch, js-search, search-index) build an inverted index. This maps words to the documents they appear in, allowing for extremely fast lookups. fuse.js takes a different path, scanning records and calculating similarity scores, which is slower but better at handling typos.
lunr creates a lightweight inverted index optimized for static data. It tokenizes, stems, and stores references efficiently.
// lunr: Building a standard inverted index
const idx = lunr(function () {
this.field('title');
this.field('body');
this.ref('id');
docs.forEach(function (doc) {
this.add(doc);
}, this);
});
const results = idx.search('complex query');
elasticlunr uses the same core index structure as lunr but adds a query parser that supports boolean logic.
// elasticlunr: Indexing is identical to lunr
const idx = elasticlunr(function () {
this.field('title');
this.field('body');
this.ref('id');
docs.forEach(doc => this.add(doc));
});
// Querying supports boolean operators
const results = idx.search('+database -sql'); // Must have 'database', must not have 'sql'
flexsearch uses a proprietary index strategy that prioritizes memory efficiency and speed, often splitting tokens differently to maximize compression.
// flexsearch: Explicitly defined index with high compression
const idx = new FlexSearch.Document({
document: {
id: "id",
index: ["title", "body"]
}
});
idx.add({ id: 1, title: "Fast Search", body: "..." });
const results = idx.search("fast", { limit: 10 });
fuse.js does not build a traditional inverted index by default. It keeps the data in memory and calculates distances between the search term and the record fields.
// fuse.js: No index building required for basic usage
const fuse = new Fuse(docs, {
keys: ['title', 'body'],
threshold: 0.3 // Higher threshold = more fuzzy matches
});
const results = fuse.search('databse'); // Finds 'database' despite typo
js-search acts as a wrapper, allowing you to choose the index strategy (e.g., TsIndex, UnsortedIndex).
// js-search: Pluggable index strategy
const search = new JsSearch.Search('id');
search.indexStrategy = new JsSearch.TsIndexDocumentSearchStrategy();
search.sanitizer = new JsSearch.LowerCaseSanitizer();
search.tokenizer = new JsSearch.SplitTokenizer();
search.addDocuments(docs);
const results = await search.search('query');
search-index builds a persistent inverted index that can be serialized and stored, supporting dynamic updates.
// search-index: Creating a persistent store
const si = await searchIndex({ name: 'myIndex' });
await si.PUT(docs);
const results = await si.QUERY({ AND: ['search'] });
When your dataset grows from 100 items to 100,000, the choice of library becomes critical. flexsearch is engineered specifically for this scenario, often outperforming others by an order of magnitude in both indexing time and query speed.
flexsearch maintains consistent performance even with hundreds of thousands of documents due to its memory-efficient encoding.
// flexsearch: Optimized for massive datasets
const idx = new FlexSearch.Index({
encode: "icase",
tokenize: "forward",
resolution: 9
});
// Adding 100k items remains fast
largeDataset.forEach(item => idx.add(item.id, item.text));
const results = idx.search("term");
lunr and elasticlunr begin to show latency during the initial indexing phase with very large datasets, though query speed remains acceptable for moderate sizes (up to ~10k-20k docs).
// lunr: Indexing large arrays can block the main thread
const idx = lunr(function () {
this.ref('id');
this.field('text');
// Looping 100k items here may cause UI freeze without Web Workers
largeDataset.forEach(doc => this.add(doc));
});
fuse.js struggles significantly with large datasets because it often performs linear scans or heavy computation per query unless specific optimization thresholds are set.
// fuse.js: Performance degrades linearly with dataset size
const fuse = new Fuse(largeDataset, { keys: ['text'] });
// Searching 100k items can take hundreds of milliseconds
const results = fuse.search("term");
search-index handles large datasets well but relies on the underlying storage mechanism (e.g., LevelDB) which may introduce async overhead.
// search-index: Async operations prevent blocking but add latency
await si.PUT(largeDataset);
const results = await si.QUERY({ AND: ['term'] });
A major architectural fork is whether the index is built once (static) or updated continuously (dynamic). Most lightweight libraries assume the data never changes after initialization.
lunr, elasticlunr, and flexsearch generally expect you to rebuild the entire index if data changes. While flexsearch supports add and remove, frequent mutations can fragment the index or reduce performance.
// lunr: No native support for adding single documents after build
// You must re-run the entire lunr() function with the new dataset
const idx = lunr(function () {
// Must include ALL documents here
allDocs.forEach(doc => this.add(doc));
});
search-index is designed for mutability. You can add, delete, or update individual documents without rebuilding the whole store.
// search-index: True real-time updates
await si.PUT([{ id: 'new-doc', text: 'content' }]); // Add
await si.DEL(['old-doc-id']); // Remove
// Index is immediately consistent
js-search allows adding documents dynamically, but depending on the underlying strategy chosen, performance may vary.
// js-search: Supports dynamic addition
search.addDocument(newDoc);
// Performance depends on the specific IndexStrategy implementation used
Different applications need different query languages. A command palette needs fuzziness; a legal archive needs boolean logic.
fuse.js excels at fuzzy search out of the box. You configure a threshold, and it handles the rest.
// fuse.js: Built-in typo tolerance
const fuse = new Fuse(docs, {
keys: ['title'],
threshold: 0.4, // Allows significant character differences
distance: 100
});
const results = fuse.search('elastc'); // Matches 'elastic'
elasticlunr provides a rich query parser similar to Lucene, supporting field boosting and boolean operators.
// elasticlunr: Complex query syntax
const results = idx.search('title:search^2 +body:engine -deprecated');
// Searches 'search' in title (boosted 2x), requires 'engine' in body, excludes 'deprecated'
lunr supports simple wildcard queries and term boosting but lacks full boolean logic.
// lunr: Basic wildcards and boosting
const results = idx.search('engine*'); // Matches 'engines', 'engineering'
flexsearch supports complex queries via its query method but uses a specific syntax that differs from standard Lucene.
// flexsearch: Complex query via object notation
const results = idx.search({
query: "engine",
limit: 10,
suggest: true
});
For applications that need to save the search index to avoid re-indexing on every load, options are limited.
search-index is built on top of storage engines (like LevelDB) and persists data automatically. You can close the app and reopen it with the index intact.
// search-index: Automatic persistence
const si = await searchIndex({ name: 'cached-index' });
// Data survives page reloads or server restarts
lunr, elasticlunr, and flexsearch allow you to serialize the index to JSON, but you must manually handle saving and loading this JSON blob.
// lunr: Manual serialization
const serialized = idx.toJSON();
localStorage.setItem('searchIndex', JSON.stringify(serialized));
// Loading later
const saved = JSON.parse(localStorage.getItem('searchIndex'));
const idx = lunr.Index.load(saved);
fuse.js typically does not serialize an index since it often operates on raw data, though you can cache the instance.
// fuse.js: Usually re-instantiated with raw data
// No built-in toJSON() for an optimized index structure
const fuse = new Fuse(docs, options);
| Feature | lunr | elasticlunr | flexsearch | fuse.js | js-search | search-index |
|---|---|---|---|---|---|---|
| Primary Strength | Stability & Simplicity | Boolean Queries | Speed & Memory | Fuzzy Matching | Modularity | Persistence |
| Index Type | Inverted | Inverted | Custom Inverted | Vector/Scan | Pluggable | Persistent Inverted |
| Dynamic Updates | β (Rebuild needed) | β (Rebuild needed) | β οΈ (Limited) | β (Raw data) | β | β (Native) |
| Fuzzy Search | β οΈ (Wildcards only) | β οΈ (Wildcards only) | β (Configurable) | β (Excellent) | β οΈ (Depends on strategy) | β οΈ (Configurable) |
| Boolean Logic | β | β (Full) | β οΈ (Limited) | β | β οΈ (Limited) | β (Full) |
| Persistence | Manual (JSON) | Manual (JSON) | Manual (JSON) | N/A | Manual | Automatic |
lunr remains the safe, default choice for static sites like documentation or blogs where the content only changes when you deploy new code. It is stable, well-understood, and easy to implement.
elasticlunr is the direct upgrade if your users need to filter results using "AND" or "OR" logic, providing a more powerful search experience without sacrificing the lunr workflow.
flexsearch is the performance king. If you are building a search interface over a large dataset (e.g., a product catalog with 50k items) or targeting low-end mobile devices, this is the engine to use.
fuse.js lives in its own category. Use it when "finding what the user meant" is more important than strict keyword matching. It is ideal for command palettes, contact lists, or search bars where typos are common.
search-index is the heavy lifter for dynamic applications. If your users can create content that needs to be searchable immediately without a full page reload or server rebuild, this is the only robust client-side option.
js-search serves a niche where you need to experiment with different tokenizers or indexing strategies without rewriting your app logic, though it has seen less activity recently compared to lunr or flexsearch.
Final Thought: There is no single "best" library. The right choice depends entirely on whether your data is static or dynamic, how large it is, and whether your users type perfectly or make mistakes. Match the tool to the data behavior, not just the feature list.
Choose elasticlunr if you need the simplicity of lunr but require advanced query features like boolean operators (AND, OR, NOT) and field-specific boosting. It is ideal for documentation sites or blogs where users need to refine search results with specific criteria, and the dataset remains static after build time.
Choose flexsearch when performance is the primary constraint, specifically for large datasets (100k+ documents) or memory-constrained environments like mobile web apps. It is the best fit when you need near-instant search results and can tolerate a slightly different API structure in exchange for superior speed and compression.
Choose fuse.js if your primary goal is fuzzy matching, typo tolerance, or searching through small to medium-sized lists where exact token matching is less important than finding 'close enough' results. It is perfect for contact lists, command palettes, or scenarios where user input is often imprecise.
Choose js-search if you need a modular approach where you can swap out indexing strategies (e.g., using a specific tokenizer or stemmer) without changing your application logic. It suits projects that require a balance between simplicity and customization but do not need the extreme performance of flexsearch or the persistence of search-index.
Choose lunr for standard static site search needs where the dataset is fixed at build time and does not change until the next deployment. It is the most stable, widely adopted solution for generating search indices for blogs, documentation, and marketing sites with minimal configuration.
Choose search-index if your application requires real-time data updates (adding/removing documents without rebuilding the entire index) or needs to persist the index to disk/database. It is the only viable option in this list for dynamic applications where content changes frequently after the initial load.
Elasticlunr.js is a lightweight full-text search engine developed in JavaScript for browser search and offline search. Elasticlunr.js is developed based on Lunr.js, but more flexible than lunr.js. Elasticlunr.js provides Query-Time boosting, field search, more rational scoring/ranking methodology, fast computation speed and so on. Elasticlunr.js is a bit like Solr, but much smaller and not as bright, but also provide flexible configuration, query-time boosting, field search and other features.
A very simple search index can be created using the following scripts:
var index = elasticlunr(function () {
this.addField('title');
this.addField('body');
this.setRef('id');
});
Adding documents to the index is as simple as:
var doc1 = {
"id": 1,
"title": "Oracle released its latest database Oracle 12g",
"body": "Yestaday Oracle has released its new database Oracle 12g, this would make more money for this company and lead to a nice profit report of annual year."
}
var doc2 = {
"id": 2,
"title": "Oracle released its profit report of 2015",
"body": "As expected, Oracle released its profit report of 2015, during the good sales of database and hardware, Oracle's profit of 2015 reached 12.5 Billion."
}
index.addDoc(doc1);
index.addDoc(doc2);
Then searching is as simple:
index.search("Oracle database profit");
Also, you could do query-time boosting by passing in a configuration.
index.search("Oracle database profit", {
fields: {
title: {boost: 2},
body: {boost: 1}
}
});
This returns a list of matching documents with a score of how closely they match the search query:
[{
"ref": 1,
"score": 0.5376053707962494
},
{
"ref": 2,
"score": 0.5237481076838757
}]
If user do not want to store the original JSON documents, they could use the following setting:
var index = elasticlunr(function () {
this.addField('title');
this.addField('body');
this.setRef('id');
this.saveDocument(false);
});
Then elasticlunr.js will not store the JSON documents, this will reduce the index size, but also bring some inconvenience such as update a document or delete a document by document id or reference. Actually most of the time user will not udpate or delete a document from index.
API documentation is available, as well as a full working example.
Elasticlunr.js is developed based on Lunr.js, but more flexible than lunr.js. Elasticlunr.js provides Query-Time Boosting, Field Search, more rational scoring/ranking methodology, flexible configuration and so on. A bit like Solr, but much smaller and not as bright, but also provide flexible configuration, query-time boosting, field search, etc.
Simply include the elasticlunr.js source file in the page that you want to use it. Elasticlunr.js is supported in all modern browsers.
Browsers that do not support ES5 will require a JavaScript shim for Elasticlunr.js to work. You can either use Augment.js, ES5-Shim or any library that patches old browsers to provide an ES5 compatible JavaScript environment.
This part only contain important apects of elasticlunr.js, for the whole documentation, please go to API documentation.
When you first create a index instance, you need to specify which field you want to index. If you did not specify which field to index, then no field will be searchable for your documents. You could specify fields by:
var index = elasticlunr(function () {
this.addField('title');
this.addField('body');
this.setRef('id');
});
You could also set the document reference by this.setRef('id'), if you did not set document ref, elasticlunr.js will use 'id' as default.
You could do the above index setup as followings:
var index = elasticlunr();
index.addField('title');
index.addField('body');
index.setRef('id');
Also you could choose not store the original JSON document to reduce the index size by:
var index = elasticlunr();
index.addField('title');
index.addField('body');
index.setRef('id');
index.saveDocument(false);
Add document to index is very simple, just prepare you document in JSON format, then add it to index.
var doc1 = {
"id": 1,
"title": "Oracle released its latest database Oracle 12g",
"body": "Yestaday Oracle has released its new database Oracle 12g, this would make more money for this company and lead to a nice profit report of annual year."
}
var doc2 = {
"id": 2,
"title": "Oracle released its profit report of 2015",
"body": "As expected, Oracle released its profit report of 2015, during the good sales of database and hardware, Oracle's profit of 2015 reached 12.5 Billion."
}
index.addDoc(doc1);
index.addDoc(doc2);
If your JSON document contains field that not configured in index, then that field will not be indexed, which means that field is not searchable.
Elasticlunr.js support remove a document from index, just provide JSON document to elasticlunr.Index.prototype.removeDoc() function.
For example:
var doc = {
"id": 1,
"title": "Oracle released its latest database Oracle 12g",
"body": "Yestaday Oracle has released its new database Oracle 12g, this would make more money for this company and lead to a nice profit report of annual year."
}
index.removeDoc(doc);
Remove a document will remove each token of that document's each field from field-specified inverted index.
Elasticlunr.js support update a document in index, just provide JSON document to elasticlunr.Index.prototype.update() function.
For example:
var doc = {
"id": 1,
"title": "Oracle released its latest database Oracle 12g",
"body": "Yestaday Oracle has released its new database Oracle 12g, this would make more money for this company and lead to a nice profit report of annual year."
}
index.update(doc);
Elasticlunr.js provides flexible query configuration, supports query-time boosting and Boolean logic setting. You could setup a configuration tell elasticlunr.js how to do query-time boosting, which field to search in, how to do the boolean logic. Or you could just use it by simply provide a query string, this will aslo works perfectly because the scoring mechanism is very efficient.
Because elasticlunr.js has a very perfect scoring mechanism, so for most of your requirement, simple search would be easy to meet your requirement.
index.search("Oracle database profit");
Output is a results array, each element of results array is an Object contain a ref field and a score field.
ref is the document reference.
score is the similarity measurement.
Results array is sorted descent by score.
Setup which fields to search in by passing in a JSON configuration, and setup boosting for each search field. If you setup this configuration, then elasticlunr.js will only search the query string in the specified fields with boosting weight.
The scoring mechanism used in elasticlunr.js is very complex, please goto details for more information.
index.search("Oracle database profit", {
fields: {
title: {boost: 2},
body: {boost: 1}
}
});
Elasticlunr.js also support boolean logic setting, if no boolean logic is setted, elasticlunr.js use "OR" logic defaulty. By "OR" default logic, elasticlunr.js could reach a high Recall.
index.search("Oracle database profit", {
fields: {
title: {boost: 2},
body: {boost: 1}
},
bool: "OR"
});
Boolean model could be setted by global level such as the above setting or it could be setted by field level, if both global and field level contains a "bool" setting, field level setting will overwrite the global setting.
index.search("Oracle database profit", {
fields: {
title: {boost: 2, bool: "AND"},
body: {boost: 1}
},
bool: "OR"
});
The above setting will search title field by AND model and other fields by "OR" model.
Currently if you search in multiply fields, resutls from each field will be merged together to give the query results. In the future elasticlunr will support configuration that user could set how to combine the results from each field, such as "most_field" or "top_field".
Sometimes user want to expand a query token to increase RECALL, then user could set expand model to true by configuration, default is false. For example, user query token is "micro", and assume "microwave" and "microscope" are in the index, then is user choose expand the query token "micro" to increase RECALL, both "microwave" and "microscope" will be returned and search in the index. The query results from expanded tokens are penalized because they are not exactly the same as the query token.
index.search("micro", {
fields: {
title: {boost: 2, bool: "AND"},
body: {boost: 1}
},
bool: "OR",
expand: true
});
Field level expand configuration will overwrite global expand configuration.
index.search("micro", {
fields: {
title: {
boost: 2,
bool: "AND",
expand: false
},
body: {boost: 1}
},
bool: "OR",
expand: true
});
Elasticlunr.js contains some default stop words of English, such as:
Defaultly elasticlunr.js contains 120 stop words, user could decide not use these default stop words or add customized stop words.
You could remove default stop words simply as:
elasticlunr.clearStopWords();
User could add a list of customized stop words.
var customized_stop_words = ['an', 'hello', 'xyzabc'];
elasticlunr.addStopWords(customized_stop_words);
Elasticlunr support Node.js, you could use elastilunr in node.js as a node-module.
Install elasticlunr by:
npm install elasticlunr
then in your node.js project or in node.js console:
var elasticlunr = require('elasticlunr');
var index = elasticlunr(function () {
this.addField('title')
this.addField('body')
});
var doc1 = {
"id": 1,
"title": "Oracle released its latest database Oracle 12g",
"body": "Yestaday Oracle has released its new database Oracle 12g, this would make more money for this company and lead to a nice profit report of annual year."
}
var doc2 = {
"id": 2,
"title": "Oracle released its profit report of 2015",
"body": "As expected, Oracle released its profit report of 2015, during the good sales of database and hardware, Oracle's profit of 2015 reached 12.5 Billion."
}
index.addDoc(doc1);
index.addDoc(doc2);
index.search("Oracle database profit");
Default supported language of elasticlunr.js is English, if you want to use elasticlunr.js to index other language documents, then you need to use elasticlunr.js combined with lunr-languages.
Suppose you are using elasticlunr.js in browser for other languages, you could download the corresponding language support from lunr-languages, then include the scripts as:
<script src="lunr.stemmer.support.js"></script>
<script src="lunr.de.js"></script>
then, you could use elasticlunr.js as normal:
var index = elasticlunr(function () {
// use the language (de)
this.use(elasticlunr.de);
// then, the normal elasticlunr index initialization
this.addField('title')
this.addField('body')
});
Pay attention to the special code:
this.use(elasticlunr.de);
If you are using other language, such as es(Spanish), download the corresponding lunr.es.js file and lunr.stemmer.support.js, and change the above line to:
this.use(elasticlunr.es);
Suppose you are using elasticlunr.js in Node.js for other languages, you could download the corresponding language support from lunr-languages, put the files lunr.es.js file and lunr.stemmer.support.js in your project, then in your Node.js module, use elasticlunr.js as:
var elasticlunr = require('elasticlunr');
require('./lunr.stemmer.support.js')(elasticlunr);
require('./lunr.de.js')(elasticlunr);
var index = elasticlunr(function () {
// use the language (de)
this.use(elasticlunr.de);
// then, the normal elasticlunr index initialization
this.addField('title')
this.addField('body')
});
For more details, please go to lunr-languages.
See the CONTRIBUTING.mdown file.