docusaurus-openai-search
Version:
AI-powered search plugin for Docusaurus - extends Algolia search with intelligent keyword generation and RAG-based answers
140 lines (139 loc) • 4.04 kB
JavaScript
import { getLogger } from './logger';
/**
* Simple in-memory cache for AI responses
*/
export class ResponseCache {
constructor() {
this.cache = new Map();
this.logger = getLogger();
this.maxSize = 100; // Configurable size limit
}
static getInstance() {
if (!ResponseCache.instance) {
ResponseCache.instance = new ResponseCache();
}
return ResponseCache.instance;
}
/**
* Reset the singleton instance (for cleanup/testing)
*/
static reset() {
if (ResponseCache.instance) {
ResponseCache.instance.clear();
ResponseCache.instance = null;
}
}
/**
* Get cached response for a query
*/
getCached(query, ttl) {
const normalized = this.normalizeQuery(query);
const cached = this.cache.get(normalized);
if (cached && Date.now() - cached.timestamp < ttl * 1000) {
this.logger.log('Cache hit for query:', query);
return cached;
}
if (cached) {
this.logger.log('Cache expired for query:', query);
this.cache.delete(normalized);
}
return null;
}
/**
* Cache a response
*/
set(query, response, queryAnalysis, documents) {
const normalized = this.normalizeQuery(query);
// Enforce size limits - remove oldest entries if cache is full
if (this.cache.size >= this.maxSize) {
this.evictOldest();
}
this.cache.set(normalized, {
response,
timestamp: Date.now(),
queryAnalysis,
documents
});
this.logger.log('Cached response for query:', query);
}
/**
* Clear all cached responses
*/
clear() {
this.cache.clear();
this.logger.log('Cache cleared');
}
/**
* Get cache size
*/
size() {
return this.cache.size;
}
/**
* Get maximum cache size
*/
getMaxSize() {
return this.maxSize;
}
/**
* Remove expired entries from cache
*/
cleanupExpired(ttl) {
const now = Date.now();
let removedCount = 0;
for (const [key, value] of this.cache.entries()) {
if (now - value.timestamp > ttl * 1000) {
this.cache.delete(key);
removedCount++;
}
}
if (removedCount > 0) {
this.logger.log(`Cleaned up ${removedCount} expired cache entries`);
}
}
/**
* Get all cache keys (for debugging/monitoring)
*/
getKeys() {
return Array.from(this.cache.keys());
}
/**
* Evict oldest entries when cache is full
*/
evictOldest() {
if (this.cache.size === 0)
return;
let oldestKey = null;
let oldestTime = Date.now();
// Find the oldest entry
for (const [key, value] of this.cache.entries()) {
if (value.timestamp < oldestTime) {
oldestTime = value.timestamp;
oldestKey = key;
}
}
if (oldestKey) {
this.cache.delete(oldestKey);
this.logger.log('Evicted oldest cache entry to make room for new entry');
}
}
/**
* Normalize query for consistent caching
*/
normalizeQuery(query) {
// Normalize query to increase cache hits:
// - Convert to lowercase
// - Trim whitespace
// - Replace multiple spaces with single space
// - Remove punctuation and special characters
// - Sort words alphabetically (so "react integrate" matches "integrate react")
return query
.toLowerCase()
.trim()
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
.replace(/[^\w\s]/g, '') // Remove non-alphanumeric characters
.split(' ')
.sort() // Sort words to handle different word orders
.join(' ');
}
}