CachedSearchService.java

package com.university.bookstore.search;

import java.util.List;
import java.util.Objects;

import com.university.bookstore.model.Material;
import com.university.bookstore.repository.MaterialRepository;

/**
 * Service that integrates Trie-based prefix search with LRU caching for optimal performance.
 * Provides fast prefix-based material searching with intelligent result caching.
 * 
 * <p>This service combines the efficiency of Trie data structures for prefix searching
 * with LRU caching to avoid repeated computation for frequently accessed queries.</p>
 * 
 * @author Navid Mohaghegh
 * @version 3.0
 * @since 2024-09-15
 */
public class CachedSearchService {
    
    private final MaterialTrie trie;
    private final SearchResultCache cache;
    private final MaterialRepository repository;
    
    /**
     * Creates a new cached search service.
     * 
     * @param repository the material repository to search
     * @param cacheSize the maximum number of cached search results
     */
    public CachedSearchService(MaterialRepository repository, int cacheSize) {
        this.repository = Objects.requireNonNull(repository, "Repository cannot be null");
        this.trie = new MaterialTrie();
        this.cache = new SearchResultCache(cacheSize);
        initializeTrie();
    }
    
    /**
     * Searches for materials by title prefix with caching.
     * 
     * @param prefix the title prefix to search for
     * @return list of materials matching the prefix
     */
    public List<Material> searchByPrefix(String prefix) {
        if (prefix == null || prefix.trim().isEmpty()) {
            return List.of();
        }
        
        String cacheKey = "prefix:" + prefix.toLowerCase().trim();
        
        // Check cache first
        var cached = cache.get(cacheKey);
        if (cached.isPresent()) {
            return cached.get();
        }
        
        // Perform search using trie
        List<Material> results = trie.searchByPrefix(prefix);
        
        // Cache results
        cache.put(cacheKey, results);
        
        return results;
    }
    
    /**
     * Searches for materials by title prefix with result limit and caching.
     * 
     * @param prefix the title prefix to search for
     * @param limit the maximum number of results to return
     * @return list of materials matching the prefix, limited to the specified count
     */
    public List<Material> searchByPrefixWithLimit(String prefix, int limit) {
        if (prefix == null || prefix.trim().isEmpty() || limit <= 0) {
            return List.of();
        }
        
        String cacheKey = "prefix:" + prefix.toLowerCase().trim() + ":limit:" + limit;
        
        // Check cache first
        var cached = cache.get(cacheKey);
        if (cached.isPresent()) {
            return cached.get();
        }
        
        // Perform search using trie
        List<Material> results = trie.searchByPrefixWithLimit(prefix, limit);
        
        // Cache results
        cache.put(cacheKey, results);
        
        return results;
    }
    
    /**
     * Adds a material to the search index and invalidates relevant cache entries.
     * 
     * @param material the material to add
     */
    public void addMaterial(Material material) {
        if (material == null) {
            throw new IllegalArgumentException("Material cannot be null");
        }
        
        trie.insert(material);
        invalidateCacheForMaterial(material);
    }
    
    /**
     * Removes a material from the search index and invalidates relevant cache entries.
     * 
     * @param material the material to remove
     */
    public void removeMaterial(Material material) {
        if (material == null) {
            return;
        }
        
        trie.remove(material);
        invalidateCacheForMaterial(material);
    }
    
    /**
     * Refreshes the search index from the repository.
     * Clears the cache to ensure consistency.
     */
    public void refreshIndex() {
        trie.clear();
        cache.clear();
        initializeTrie();
    }
    
    /**
     * Gets cache statistics for performance monitoring.
     * 
     * @return cache statistics
     */
    public SearchResultCache.CacheStats getCacheStats() {
        return cache.getStats();
    }
    
    /**
     * Gets the current size of the search index.
     * 
     * @return the number of materials in the index
     */
    public int getIndexSize() {
        return trie.size();
    }
    
    /**
     * Checks if the search index is empty.
     * 
     * @return true if no materials are indexed
     */
    public boolean isIndexEmpty() {
        return trie.isEmpty();
    }
    
    /**
     * Clears the search index and cache.
     */
    public void clear() {
        trie.clear();
        cache.clear();
    }
    
    /**
     * Initializes the trie with all materials from the repository.
     */
    private void initializeTrie() {
        List<Material> materials = repository.findAll();
        for (Material material : materials) {
            trie.insert(material);
        }
    }
    
    /**
     * Invalidates cache entries that might be affected by changes to a material.
     * 
     * @param material the material that was changed
     */
    private void invalidateCacheForMaterial(Material material) {
        String title = material.getTitle().toLowerCase();
        
        // Invalidate cache entries for all possible prefixes of the material's title
        for (int i = 1; i <= title.length(); i++) {
            String prefix = title.substring(0, i);
            String cacheKey = "prefix:" + prefix;
            cache.remove(cacheKey);
            
            // Also remove limit-based cache entries
            for (int limit = 10; limit <= 100; limit += 10) {
                String limitCacheKey = cacheKey + ":limit:" + limit;
                cache.remove(limitCacheKey);
            }
        }
    }
    
    @Override
    public String toString() {
        return String.format("CachedSearchService[IndexSize=%d, CacheSize=%d/%d, HitRatio=%.2f%%]",
            getIndexSize(),
            cache.size(),
            cache.getMaxSize(),
            getCacheStats().getHitRatio() * 100);
    }
}