Guide to Implementing Caching for Optimized Typesense Search Performance

356 views

Caching is a vital strategy to optimize the performance and reduce the load on your Typesense search engine. By caching frequently requested search results, you can minimize response times and improve the overall user experience. Here’s a comprehensive guide on implementing caching for Typesense:

Why Cache?

  • Reduced Latency: Caching reduces the need to perform expensive operations repeatedly, thereby decreasing response times.
  • Load Reduction: Reduces the number of requests hitting your Typesense server, making it more scalable and resilient.
  • Improved User Experience: Faster responses lead to a smoother and more responsive application.

Implementing Caching in Typesense

1. Choose a Caching Method and Store

You can use libraries such as node-cache for in-memory caching in a Node.js application, or use more robust solutions like Redis for distributed caching.

a. In-Memory Caching with node-cache

Install node-cache:

npm install node-cache

Initialize and configure the cache:

const NodeCache = require('node-cache');
const searchCache = new NodeCache({ stdTTL: 300, checkperiod: 320 }); // 5 minutes TTL

b. Distributed Caching with Redis

Install Redis client for Node.js:

npm install redis

Initialize and configure the Redis client:

const redis = require('redis');
const client = redis.createClient();

2. Cache Search Results

You can wrap your search logic to check the cache before hitting Typesense and cache the results afterward.

a. Using node-cache

const performSearch = async (query, queryBy) => {
  const cacheKey = `${query}:${queryBy}`;
  const cachedResult = searchCache.get(cacheKey);

  if (cachedResult) {
    console.log('Serving from cache');
    return cachedResult;
  }

  const result = await client.collections('books').documents().search({
    q: query,
    query_by: queryBy
  });

  if (result) {
    searchCache.set(cacheKey, result);
  }

  return result;
};

b. Using Redis

const performSearch = async (query, queryBy) => {
  const cacheKey = `${query}:${queryBy}`;

  // Check Redis cache
  const cachedData = await new Promise((resolve, reject) => {
    client.get(cacheKey, (err, data) => {
      if (err) reject(err);
      resolve(data);
    });
  });

  if (cachedData) {
    console.log('Serving from Redis cache');
    return JSON.parse(cachedData);
  }

  // Perform search query
  const result = await client.collections('books').documents().search({
    q: query,
    query_by: queryBy
  });

  // Store in Redis cache
  if (result) {
    client.set(cacheKey, JSON.stringify(result), 'EX', 300); // Expire in 5 minutes
  }

  return result;
};

3. Cache Invalidations

To ensure the cache doesn't serve stale data:

  • TTL (Time To Live): Set an appropriate TTL for cache entries.
  • Manual Invalidation: Invalidate the cache when data changes significantly.
  • Cache Busting: Add versioning or timestamps to cache keys if the underlying data changes frequently.

4. Monitoring and Metrics

Monitor the hit/miss ratio of your cache to understand its effectiveness:

  • Hit Rate: Percentage of requests served from the cache.
  • Miss Rate: Percentage of requests served by making a new call to Typesense.

For node-cache

console.log(searchCache.getStats());

For Redis

Use Redis commands to monitor performance:

redis-cli info stats

Example Application Structure

Initialization (Setup Typesense and Redis)

// typesenseClient.js
const Typesense = require('typesense');
const typesenseClient = new Typesense.Client({
  nodes: [{ host: 'localhost', port: '8108', protocol: 'http' }],
  apiKey: 'xyz',
  connectionTimeoutSeconds: 2
});

module.exports = typesenseClient;
// redisClient.js
const redis = require('redis');
const redisClient = redis.createClient();

redisClient.on('error', (err) => {
  console.error('Redis error: ', err);
});

module.exports = redisClient;

Performing Search

// searchService.js
const typesenseClient = require('./typesenseClient');
const redisClient = require('./redisClient');

const performSearch = async (query, queryBy) => {
  const cacheKey = `${query}:${queryBy}`;

  const cachedData = await new Promise((resolve, reject) => {
    redisClient.get(cacheKey, (err, data) => {
      if (err) reject(err);
      resolve(data);
    });
  });

  if (cachedData) {
    console.log('Serving from Redis cache');
    return JSON.parse(cachedData);
  }

  const result = await typesenseClient.collections('books').documents().search({
    q: query,
    query_by: queryBy
  });

  if (result) {
    redisClient.set(cacheKey, JSON.stringify(result), 'EX', 300); // Expire in 5 minutes
  }

  return result;
};

module.exports = performSearch;

By implementing caching using either node-cache for simple use cases or Redis for more complex scenarios, you can dramatically improve the performance of your Typesense-powered search in a Node.js application. This approach ensures quick response times and a smooth user experience.