DEV Community

Cover image for How I Implemented Real-Time Content Search in Node.js with 50ms Latency (No Caching Required)
Kuldeep Tiwari
Kuldeep Tiwari

Posted on

1

How I Implemented Real-Time Content Search in Node.js with 50ms Latency (No Caching Required)

Building a High-Performance Search API

In today's digital world, fast and accurate search functionality is a must-have for any application. Whether you're building a fitness app, an e-learning platform, or a content-heavy website, users expect search results in milliseconds. In this guide, I'll walk you through how to implement a high-performance search API using Node.js, Express, FlexSearch, and Fuse.js. This solution delivers search results in under 50ms without caching, making it perfect for real-time applications.

Who Should Use This Guide?

This guide is ideal for:

  • Developers building applications with large datasets that require fast search capabilities.
  • Startups looking to implement scalable and efficient search functionality without relying on third-party services.
  • Tech enthusiasts interested in learning how to combine FlexSearch and Fuse.js for hybrid search solutions.
  • SEO-conscious developers who want to optimize their applications for performance and user experience.

Why FlexSearch and Fuse.js?

  • FlexSearch: A highly optimized full-text search library that supports advanced indexing and querying. It's perfect for structured data and offers features like boosting, tokenization, and contextual search.
  • Fuse.js: A lightweight fuzzy-search library that excels at finding approximate matches. It's great for handling typos and partial queries.

By combining these two libraries, you get the best of both worlds: precision and flexibility.

Can I use only FlexSearch?

While FlexSearch is incredibly powerful for fast and precise full-text search, it has limitations when it comes to handling typos, spelling mistakes, or partial queries. This is where Fuse.js shines. Fuse.js is a lightweight fuzzy-search library designed to handle approximate matches, making it perfect for accommodating user errors or variations in search terms.

By combining FlexSearch for structured, high-performance search and Fuse.js for fuzzy matching, you create a hybrid search solution that delivers both speed and flexibility. FlexSearch ensures that exact or near-exact queries are handled efficiently, while Fuse.js acts as a fallback to catch queries with typos or incomplete terms. This combination ensures a seamless and user-friendly search experience, even when users don't type perfectly.

For example, if a user searches for "meditation" but accidentally types "meditaion," FlexSearch might not return results, but Fuse.js can still find relevant matches. This dual approach ensures your search functionality is both robust and forgiving, catering to real-world user behavior.

Step-by-Step Implementation

1. Set Up Your Node.js Project

Start by initializing a new Node.js project and installing the required dependencies:

mkdir fast-search-api
cd fast-search-api
npm init -y
npm install express express-async-handler flexsearch fuse.js dotenv mysql2
Enter fullscreen mode Exit fullscreen mode

2. Configure Environment Variables

Create a .env file to store your database credentials and other configurations:

DB_HOST=localhost
DB_USER=root
DB_PASSWORD=yourpassword
DB_NAME=yourdatabase
Enter fullscreen mode Exit fullscreen mode

3. Initialize FlexSearch and Fuse.js

Create a searchController.js file and set up the search indices. Here's a generalized version of how to initialize FlexSearch and Fuse.js:

import FlexSearch from 'flexsearch';
import Fuse from 'fuse.js';

// Initialize FlexSearch index
const flexIndex = new FlexSearch.Document({
  document: {
    id: "id",
    index: ["name", "description", "category", "tags"], // Fields to index
  },
  tokenize: "forward",
  resolution: 10,
  minlength: 2,
  threshold: 1,
  depth: 3,
  boost: {
    name: 2, // Boost name field for higher relevance
    category: 1.5,
  },
});

// Initialize Fuse.js with options for fuzzy matching
const fuseOptions = {
  includeScore: true,
  threshold: 0.4,
  keys: ["name", "description", "category"], // Fields to search
};

let fuseIndex = null;
Enter fullscreen mode Exit fullscreen mode

4. Load Data into the Indices

To make the search functional, you need to load your data into the indices. Here's how you can do it:

const loadDataIntoIndex = async () => {
  try {
    const data = await fetchDataFromDatabase(); // Fetch data from your database
    data.forEach(item => flexIndex.add(item)); // Add data to FlexSearch
    fuseIndex = new Fuse(data, fuseOptions); // Initialize Fuse.js with data
    console.log("Data loaded into both FlexSearch and Fuse.js indices");
  } catch (error) {
    console.error("Error loading data into indices:", error);
  }
};

// Initialize the search indices
const initializeSearchIndices = async () => {
  await loadDataIntoIndex();
};

initializeSearchIndices();
Enter fullscreen mode Exit fullscreen mode

5. Create the Search Endpoint

Now, let's create an Express route to handle search queries. Here's a generalized version:

import express from 'express';
import asyncHandler from 'express-async-handler';

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

// Search endpoint
app.get('/search', asyncHandler(async (req, res) => {
  const searchQuery = req.query.q?.trim();
  if (!searchQuery) return res.status(200).json([]);

  try {
    let searchResults;

    // Perform FlexSearch query
    searchResults = flexIndex.search(searchQuery, {
      limit: 100, // Limit the number of results
      enrich: true,
      suggest: true,
    });

    // Fallback to Fuse.js if no results
    if (searchResults.flat().length === 0 && fuseIndex) {
      const fuseResults = fuseIndex.search(searchQuery);
      searchResults = fuseResults.map(result => ({
        field: 'name',
        result: [result.item.id],
      }));
    }

    // Fetch detailed results from the database
    const detailedResults = await fetchDetailedResults(searchResults);
    res.status(200).json(detailedResults);
  } catch (error) {
    res.status(500).json({ error: "Internal Server Error" });
  }
}));

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

6. Fetch Detailed Results

After performing the search, you'll need to fetch detailed results from your database. Here's a generalized example:

const fetchDetailedResults = async (searchResults) => {
  const uniqueIds = [...new Set(searchResults.flatMap(result => result.result))];
  if (uniqueIds.length === 0) return [];

  // Fetch detailed data from the database
  const query = `SELECT * FROM your_table WHERE id IN (${uniqueIds.join(",")})`;
  const [results] = await db.query(query);

  return results;
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

By following this guide, you've built a lightning-fast search API using Node.js, Express, FlexSearch, and Fuse.js. This solution is perfect for applications requiring real-time search with high accuracy and low latency. Whether you're a developer, startup, or tech enthusiast, this implementation will help you deliver a seamless search experience to your users.

Final Thoughts

  • FlexSearch and Fuse.js are powerful tools that can be tailored to your specific use case.
  • Always test your search functionality with real-world data to ensure optimal performance.
  • Consider adding caching (e.g., Redis) for even faster response times in high-traffic scenarios.

Happy coding! 🚀

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more