elasticlunr vs flexsearch vs fuse.js vs lunr vs search-index
Client-Side Search Engines: Architecture, Performance, and Selection Guide
elasticlunrflexsearchfuse.jslunrsearch-indexSimilar Packages:

Client-Side Search Engines: Architecture, Performance, and Selection Guide

These five libraries provide full-text search capabilities directly in the browser or Node.js environment, eliminating the need for a dedicated search server like Elasticsearch for many use cases. lunr is the foundational, lightweight engine that inspired several forks. elasticlunr extends lunr with better field weighting and boolean logic. flexsearch is a modern, high-performance engine optimized for speed and memory efficiency, often outperforming others on large datasets. fuse.js specializes in fuzzy matching, making it ideal for typo-tolerant search rather than strict full-text indexing. search-index offers a persistent, database-like approach with support for faceting and complex queries, suitable for applications requiring data durability across sessions.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
elasticlunr02,067-7710 years agoMIT
flexsearch013,7712.33 MB36a year agoApache-2.0
fuse.js020,437417 kB121 days agoApache-2.0
lunr09,203-1306 years agoMIT
search-index01,422693 kB6a year agoMIT

Client-Side Search Engines: Architecture, Performance, and Selection Guide

When building modern web applications, adding search functionality often leads to a critical architectural decision: do you spin up a heavy backend service like Elasticsearch, or do you handle it directly in the client? For many applications—documentation sites, dashboards, and content-heavy SPAs—client-side search is faster, cheaper, and simpler. The JavaScript ecosystem offers several robust options, each with a distinct philosophy. Let's break down how lunr, elasticlunr, flexsearch, fuse.js, and search-index actually work under the hood.

🏗️ Core Architecture: Inverted Index vs. Fuzzy Matching

The fundamental difference lies in how these libraries store and retrieve data. Most full-text engines use an inverted index, while others rely on pattern matching.

lunr builds a classic inverted index. It tokenizes your documents, stems words (e.g., "running" becomes "run"), and maps terms to document IDs. It is designed to be built once and queried many times.

// lunr: Build an inverted index
const idx = lunr(function () {
  this.field('title');
  this.field('body');
  this.ref('id');

  documents.forEach(function (doc) {
    this.add(doc);
  }, this);
});

// Querying is exact match based on tokens
const results = idx.search('complex architecture');

elasticlunr follows the same inverted index architecture as lunr but modifies the scoring algorithm to support more granular field boosting and boolean operators.

// elasticlunr: Supports boolean logic in query string
const idx = elasticlunr(function () {
  this.field('title');
  this.field('category');
  this.ref('id');
  documents.forEach(doc => this.add(doc));
});

// Explicit AND/OR logic
const results = idx.search('+architecture +database');

flexsearch uses a proprietary indexing strategy that prioritizes speed and memory efficiency over standard stemming. It breaks text into contexts and uses a highly optimized lookup table, allowing it to handle massive datasets without blocking the main thread.

// flexsearch: High-performance context indexing
const index = new FlexSearch.Document({
  document: {
    id: "id",
    index: ["title", "body"]
  }
});

index.add({ id: 1, title: "Fast Search", body: "Optimized for speed" });

// Extremely fast query execution
const results = index.search("speed", { limit: 10 });

fuse.js does not use an inverted index. Instead, it performs a bitwise comparison of patterns against your data array at query time. This makes it incredibly flexible for fuzzy matching but computationally expensive for large datasets.

// fuse.js: No index build step, searches array directly
const fuse = new Fuse(documents, {
  keys: ['title', 'tags'],
  threshold: 0.3 // Higher threshold = more fuzzy matches
});

// Handles typos naturally
const results = fuse.search('architectur'); // Matches "architecture"

search-index creates a persistent inverted index that can be serialized and stored (e.g., in LocalStorage or a file). It treats the index more like a database that can be incrementally updated.

// search-index: Persistent, updatable index
const si = await searchIndex({ name: 'my-index' });

await si.PUT([
  { _id: 1, title: 'Node Basics', category: 'backend' },
  { _id: 2, title: 'React Hooks', category: 'frontend' }
]);

// Complex queries with facets
const results = await si.QUERY({ AND: ['node'], FACETS: ['category'] });

⚡ Performance at Scale: Small Lists vs. Big Data

Performance is where the trade-offs become obvious. If you have 500 items, all of these work fine. If you have 50,000, the choice matters.

lunr and elasticlunr perform well up to about 10,000–20,000 documents. Beyond that, the initial index build time and memory usage can cause noticeable lag in the browser.

// lunr: Build time increases linearly with document count
// Suitable for ~10k docs. Beyond this, consider offloading build to CI.
const startTime = performance.now();
lunr(function() { /* add 20k docs */ });
console.log(`Build took: ${performance.now() - startTime}ms`);

flexsearch is engineered for scale. It can index hundreds of thousands of documents in seconds and query them in milliseconds. It is the only choice here for truly large client-side datasets.

// flexsearch: Optimized for 100k+ documents
const largeIndex = new FlexSearch.Index();
// Adds 100k items efficiently without freezing UI
for(let i=0; i<100000; i++) largeIndex.add(i, `Document ${i}`);

// Queries remain instant
largeIndex.search("Document");

fuse.js slows down significantly as the array grows because it scans the dataset (or large portions of it) for every query. It is best kept under 5,000 items for a smooth user experience.

// fuse.js: Query time degrades with list size
// Good for < 5k items. Avoid for massive catalogs.
const fuse = new Fuse(largeArrayOfObjects, { keys: ['name'] });
// Each search iterates through the list logic
const result = fuse.search("query");

search-index has a higher overhead for setup but handles dynamic updates better than lunr. Its query speed is comparable to lunr, but its strength is avoiding full re-indexing when data changes.

// search-index: Efficient incremental updates
// No need to rebuild the whole index for one change
await si.PUT([{ _id: 999, title: 'New Item' }]); 
// Query performance remains stable
const res = await si.QUERY({ AND: ['item'] });

🔍 Query Capabilities: Fuzzy vs. Boolean vs. Faceted

Different applications need different types of search logic.

fuse.js is the king of fuzzy search. If your users make typos or you need to match partial strings aggressively, this is the tool. It returns a similarity score for every result.

// fuse.js: Built-in typo tolerance
const fuse = new Fuse(items, { threshold: 0.4 });
// "iphon" matches "iPhone" with a high score
const results = fuse.search("iphon"); 
console.log(results[0].score); // e.g., 0.15 (lower is better)

elasticlunr excels at boolean logic. It allows users to construct complex queries like "must have this word BUT NOT that word," which standard lunr struggles with.

// elasticlunr: Advanced boolean operators
// Find docs with "react" AND "hooks" but NOT "class"
const results = idx.search('+react +hooks -class');

search-index supports faceting and filtering. You can group results by categories, tags, or numeric ranges, similar to an e-commerce filter sidebar.

// search-index: Faceted search
const response = await si.QUERY({
  AND: ['laptop'],
  FACETS: ['brand', 'price_range']
});
// Returns results plus aggregation counts
console.log(response.FACETS); 

lunr and flexsearch focus on relevance scoring based on term frequency. They are great for "Google-like" search bars where the user types a few keywords and expects the most relevant documents first.

// lunr: Simple relevance scoring
const results = idx.search("performance tuning");
// Results sorted by internal TF-IDF score automatically

// flexsearch: Contextual relevance
const results = index.search("performance", { enrich: true });
// Returns document data along with match context

🔄 Data Lifecycle: Static vs. Dynamic

How often does your data change? This dictates whether you can pre-build an index or need a dynamic one.

lunr, elasticlunr, and flexsearch are typically static. You build the index at build time (e.g., during Webpack/Vite compilation) and ship the JSON file to the client. Updating the index requires rebuilding the whole bundle or fetching a new JSON file.

// Typical workflow for lunr/flexsearch in a static site
// 1. Build script runs during CI/CD
const indexJSON = idx.toJSON();
fs.writeFileSync('search-index.json', JSON.stringify(indexJSON));

// 2. Client loads the pre-built index
fetch('/search-index.json')
  .then(res => res.json())
  .then(json => { const idx = lunr.Index.load(json); });

search-index is dynamic. It allows you to add, delete, or update documents on the fly in the browser or Node environment. It can persist these changes to storage.

// search-index: Dynamic updates in the browser
// User adds a new note in a PWA
await si.PUT([{ _id: 'note-1', text: 'New idea' }]);

// Later, user edits it
await si.PUT([{ _id: 'note-1', text: 'Updated idea' }]);

// Index reflects changes immediately without reload

fuse.js is ephemeral. It holds the raw data in memory. If your data changes, you just update the array and create a new Fuse instance. There is no serialization step.

// fuse.js: Re-instantiate on data change
let currentData = getData();
let fuse = new Fuse(currentData, { keys: ['title'] });

// Data updates? Just make a new instance
currentData = getUpdatedData();
fuse = new Fuse(currentData, { keys: ['title'] });

🛑 Deprecation and Maintenance Warning

Before starting a new project, check the maintenance status. elasticlunr has seen very little activity in recent years. While it still works, it may lack compatibility with modern build tools or contain unpatched bugs. For new projects requiring boolean logic, consider using lunr with custom scoring or switching to flexsearch if performance is key. Always verify the current repository status on npm or GitHub before committing to a dependency.

📊 Summary: Feature Matrix

Featurelunrelasticlunrflexsearchfuse.jssearch-index
Index TypeInvertedInvertedContext/InvertedPattern MatchPersistent Inverted
Best ForStatic SitesBoolean QueriesLarge DatasetsFuzzy/Typo ToleranceDynamic/Local DB
Max Data~20k docs~20k docs100k+ docs~5k docsLimited by Storage
Fuzzy SearchBasicBasicConfigurableExcellentConfigurable
Dynamic Updates❌ (Rebuild)❌ (Rebuild)❌ (Rebuild)✅ (Re-init)✅ (Incremental)
Persistence

💡 Final Recommendation

Choosing the right search library depends entirely on your data shape and user needs.

  • Building a documentation site? Stick with lunr. It's stable, small, and integrates easily with static site generators.
  • Need to handle typos in a product filter? Use fuse.js. Its fuzzy matching is unmatched for user-friendly experiences on small datasets.
  • Searching through a massive dataset (100k+ rows)? flexsearch is the only viable option for client-side performance.
  • Building a local-first app or PWA? search-index gives you the database-like features you need to manage data locally.
  • Need complex boolean queries? elasticlunr offers this, but weigh the risk of lower maintenance against your specific requirements.

By matching the tool to your specific constraints, you can deliver a fast, responsive search experience without the overhead of a backend server.

How to Choose: elasticlunr vs flexsearch vs fuse.js vs lunr vs search-index

  • elasticlunr:

    Choose elasticlunr if you need the simplicity of lunr but require more advanced field boosting and boolean query logic (AND/OR/NOT) without adding significant complexity. It is a solid middle-ground for documentation sites where users need to filter results by category or title specifically. However, be aware that it is a fork and may lag behind the main lunr ecosystem in community support.

  • flexsearch:

    Choose flexsearch when performance is your primary constraint, especially for large datasets (100k+ documents) or when running on low-power devices. Its unique indexing strategy allows for extremely fast query times and low memory footprint. It is the best choice for enterprise dashboards or data-heavy applications where lunr might feel sluggish, provided you can adapt to its specific configuration syntax.

  • fuse.js:

    Choose fuse.js if your primary goal is fuzzy matching to handle user typos, partial matches, or searching through small to medium-sized lists of objects (like a contact list or product catalog). It is not a traditional inverted-index search engine, so it struggles with very large datasets, but it excels at 'did you mean?' functionality and ranking results by similarity score out of the box.

  • lunr:

    Choose lunr for standard static site search needs where simplicity, stability, and a small bundle size are paramount. It is the most mature and widely adopted solution for generating a pre-built index at build time. If your data fits in memory and you don't need complex boolean logic or extreme performance, lunr offers the most straightforward integration with the least amount of boilerplate.

  • search-index:

    Choose search-index if you need a persistent search database that can be updated dynamically without rebuilding the entire index from scratch. It supports faceting, tagging, and complex filtering, making it suitable for applications that behave more like a local database than a static search index. It is ideal for Node.js backends or Progressive Web Apps (PWAs) that need to store and query data locally over time.

README for elasticlunr

Elasticlunr.js

Build Status npm version GitHub license

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.

Why You Need Lightweight Offline Search?

  1. In some system, you don't want to deploy any complex full-text search engine(such as Lucence, Elasticsearch, Sphinx, etc.), you only want to provide some static web pages and provide search functionality , then you could build index in previous and load index in client side(such as Browser).
  2. Provide offline search functionality. For some documents, user usually download these documents, you could build index and put index in the documents package, then provide offline search functionality.
  3. For some limited or restricted network, such WAN or LAN, offline search is a better choice.
  4. For mobile device, Iphone or Android phone, network traffic maybe very expensive, then provide offline search is a good choice.
  5. If you want to provide search functionality in your Node.js system, and you don't want to use a complex system, or you only need to support thousands of documents, then Elasticlunr.js is what you want to use.

Key Features Comparing with Lunr.js

  1. Query-Time Boosting, you don't need to setup boosting weight in index building procedure, Query-Time Boosting make it more flexible that you could try different boosting scheme.
  2. More Rational Scoring Mechanism, Elasticlunr.js use quite the same scoring mechanism as Elasticsearch, and also this scoring mechanism is used by lucene.
  3. Field-Search, you could choose which field to index and which field to search.
  4. Boolean Model, you could set which field to search and the boolean model for each query token, such as "OR", "AND".
  5. Combined Boolean Model, TF/IDF Model and the Vector Space Model, make the results ranking more reliable.
  6. Fast, Elasticlunr.js removed TokenCorpus and Vector from lunr.js, by using combined model there is no need to compute the vector of a document and query string to compute similarity of query and matched document, this improve the search speed significantly.
  7. Small Index Size, Elasticlunr.js did not store TokenCorpus because there is no need to compute query vector and document vector, then the index file is small, and also user could choose if they need to store the origianl JSON doc, if user care more about the index size, they could choose not store the original JSON doc, this could reduce the index size significantly. This is especially helpful when elasticlunr.js is used as offline search. The index size is about half size of lunr.js index file.

Example

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.

Description

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.

Installation

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.

Documentation

This part only contain important apects of elasticlunr.js, for the whole documentation, please go to API documentation.

1. Build Index

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);

2. Add document to index

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.

3. Remove document from index

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.

4. Update a document in 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);

5. Query from Index

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.

5.1 Simple Query

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.

5.2 Configuration Query

5.2.1 Query-Time Boosting

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}
    }
});

5.2.2 Boolean Model

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".

5.2.3 Token Expandation

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
});

6. Add customized stop words

Elasticlunr.js contains some default stop words of English, such as:

  • a
  • about
  • an
  • all
  • also
  • and
  • any
  • but
  • the
  • ...

Defaultly elasticlunr.js contains 120 stop words, user could decide not use these default stop words or add customized stop words.

6.1 Remove default stop words

You could remove default stop words simply as:

elasticlunr.clearStopWords();

6.2 Add customized stop words

User could add a list of customized stop words.

var customized_stop_words = ['an', 'hello', 'xyzabc'];
elasticlunr.addStopWords(customized_stop_words);

7. Use elasticlunr in Node.js

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");

Other Languages

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.

Other languages example in Browser

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);

Other languages example in Node.js

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.

Contributing

See the CONTRIBUTING.mdown file.