How MiniSearch Search Caching Works with IndexedDB, TTL, and Max Entry Limits

MiniSearch caches text and image query results in IndexedDB via the Dexie wrapper, automatically expiring entries after a 15-minute TTL and pruning stores to a maximum of 100 entries to prevent unbounded storage growth.

The felladrin/minisearch repository implements a sophisticated client-side caching layer that accelerates repeated searches while rigorously managing browser storage quotas. Understanding the MiniSearch search caching mechanism reveals how the application balances query performance against resource constraints through time-based expiration and hard entry limits configured in client/modules/search.ts.

IndexedDB Cache Architecture and Configuration

MiniSearch persists query results using IndexedDB through the Dexie library, creating distinct object stores for text and image searches. The caching behavior is governed by two configurable policies defined as constants at the top of client/modules/search.ts.

TTL (time-to-live) dictates how long a cached result remains valid. By default, this is set to 15 minutes (15 * 60 * 1000 ms) at line 11.

Maximum entries establishes a hard ceiling on the number of items each store may retain. The default limit of 100 entries is defined at line 13.

These constraints ensure that the cache delivers fast results for recent queries without consuming excessive disk space.

Inserting Entries and Pruning Strategy

When a search completes, the executeCachedSearch function delegates result storage to cacheResult. This method writes a record containing the query hash, result payload, and current timestamp:

await store.put({ key, results, timestamp: Date.now() });

To avoid expensive cleanup operations on every write, MiniSearch employs a batched pruning strategy. The module maintains an internal _cacheWriteCount counter, triggering a size-based cleanup every PRUNE_INTERVAL writes (defaulting to 10):

if (this._cacheWriteCount % CACHE_CONFIG.PRUNE_INTERVAL === 0) {
    this.pruneCache(storeName);
}

This logic appears in the cacheResult method around line 75 of client/modules/search.ts.

Enforcing the Maximum Entry Limit

The pruneCache function (implemented around line 100) proactively maintains the entry ceiling. It first counts the current rows in the specified store; if the count exceeds cacheConfig.maxEntries, it calculates the excess and removes the oldest entries by timestamp:

const count = await store.count();
if (count > maxEntries) {
    const excess = count - maxEntries;
    const oldestEntries = await store.orderBy("timestamp")
                                      .limit(excess)
                                      .primaryKeys();
    await store.bulkDelete(oldestEntries);
}

This ensures the cache never grows beyond the configured limit, preserving storage for recent, relevant queries.

TTL-Based Expiration and Validation

Freshness enforcement operates through two mechanisms. First, before any cache lookup, executeCachedSearch invokes cleanExpiredCache to purge stale entries. This function scans for records older than Date.now() - cacheConfig.ttl and bulk deletes them:

const expiredItems = await store.where("timestamp")
                                .below(currentTime - timeToLive)
                                .toArray();
await store.bulkDelete(expiredItems.map(item => item.key));

Additionally, when reading cached results via getCachedResult, the module validates the entry's age:

const fresh = Date.now() - cachedItem.timestamp < cacheConfig.ttl;

If the entry is fresh, MiniSearch returns the cached results and increments a hit metric. Otherwise, it treats the lookup as a cache miss and executes a fresh search against the remote API.

Runtime Configuration and Statistics

Both cache policies can be adjusted dynamically without restarting the application. The searchService.updateCacheConfig method (around line 85) accepts new TTL and max entry values, validating that inputs are non-negative before applying them:

if (newConfig.ttl !== undefined && newConfig.ttl < 0) …
if (newConfig.maxEntries !== undefined && newConfig.maxEntries < 0) …
Object.assign(cacheConfig, newConfig);

Changes take effect immediately, influencing subsequent cleanup cycles and pruning operations. Developers can also inspect cache performance through getCacheStats(), which exposes hit rates and current configuration settings.

Practical Implementation Examples

Automatic Cache Utilization

Search functions automatically leverage the cache when available:

import { searchText, searchImages } from "./modules/search";

// First call misses cache and queries the remote API
const textResults = await searchText("open source AI");

// Subsequent call within 15 minutes hits the cache instantly
const cachedText = await searchText("open source AI");

Adjusting Cache Policies

Reduce TTL and increase capacity for high-traffic scenarios:

import { searchServiceInstance } from "./modules/search";

// Set TTL to 5 minutes and allow 200 entries per store
searchServiceInstance.updateCacheConfig({ 
  ttl: 5 * 60 * 1000, 
  maxEntries: 200 
});

Cache Maintenance Operations

Clear the cache entirely or inspect statistics:

// Clear all cached entries
await searchServiceInstance.clearSearchCache();

// View hit rates and configuration
const stats = searchServiceInstance.getCacheStats();
console.log({
  textHitRate: stats.textHitRate,
  imageHitRate: stats.imageHitRate,
  config: stats.config,
});

Summary

  • Storage Backend: MiniSearch uses IndexedDB via Dexie, with separate stores for text and image search history.
  • Default Limits: Entries expire after 15 minutes (TTL) and stores cap at 100 items (max entries), defined in client/modules/search.ts.
  • Write Behavior: Every 10 writes (PRUNE_INTERVAL), the system prunes excess entries by age, keeping only the newest results.
  • Read Validation: Lookups automatically purge expired items and validate freshness before returning cached data.
  • Dynamic Control: Applications can adjust TTL and entry limits at runtime via updateCacheConfig, with changes applying immediately to subsequent operations.

Frequently Asked Questions

How does MiniSearch handle cache storage limits in the browser?

MiniSearch enforces a configurable maximum entry limit (default 100) per IndexedDB store. When inserts exceed this threshold, the pruneCache function automatically deletes the oldest entries by timestamp, ensuring the cache never grows beyond the specified bound regardless of query volume.

Can I change the cache expiration time after initialization?

Yes. The searchService.updateCacheConfig method allows runtime modification of the TTL value. Changes apply immediately to subsequent cache reads and cleanup cycles, though existing entries are only evaluated against the new TTL during their next access or during the periodic expiration sweep.

What happens when the cache reaches the maximum number of entries?

Once a store contains the maximum configured entries (100 by default), MiniSearch removes the oldest records to make room for new results. This occurs every 10 write operations (PRUNE_INTERVAL), preventing performance degradation from excessive pruning while maintaining strict storage boundaries.

How can I verify if my searches are hitting the cache?

Call searchService.getCacheStats() to retrieve current metrics, including textHitRate and imageHitRate. These values indicate the percentage of recent queries served from IndexedDB versus remote API calls, helping you tune the TTL and entry limits for your specific usage patterns.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →