Skillshub algolia-performance-tuning
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/jeremylongshore/claude-code-plugins-plus-skills/algolia-performance-tuning" ~/.claude/skills/comeonoliver-skillshub-algolia-performance-tuning && rm -rf "$T"
manifest:
skills/jeremylongshore/claude-code-plugins-plus-skills/algolia-performance-tuning/SKILL.mdsource content
Algolia Performance Tuning
Overview
Algolia's edge infrastructure typically delivers search in < 50ms globally. When performance degrades, the causes are usually: oversized records, too many searchable attributes, unoptimized faceting, or missing client-side caching. This skill covers server-side and client-side optimizations.
Performance Baselines
| Metric | Good | Warning | Action Needed |
|---|---|---|---|
| Search latency (P50) | < 20ms | 20-100ms | > 100ms |
| Search latency (P95) | < 50ms | 50-200ms | > 200ms |
| Indexing time per 1K records | < 2s | 2-10s | > 10s |
| Record size (avg) | < 5KB | 5-50KB | > 50KB |
Instructions
Step 1: Optimize Record Size
import { algoliasearch } from 'algoliasearch'; const client = algoliasearch(process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_ADMIN_KEY!); // BAD: Full record with unnecessary data const badRecord = { objectID: '1', name: 'Running Shoes', full_html_description: '<div>...5000 chars of HTML...</div>', // Too big internal_notes: 'Supplier ref: ABC-123', // Not searchable all_reviews: [/* 200 reviews */], // Huge array }; // GOOD: Lean record for search const goodRecord = { objectID: '1', name: 'Running Shoes', description: 'Lightweight running shoes with cushioned sole', // Plain text, truncated category: 'shoes', brand: 'Nike', price: 129.99, rating: 4.5, review_count: 200, // Count, not full reviews in_stock: true, image_url: '/images/1.jpg', // URL, not base64 };
Step 2: Optimize Searchable Attributes
await client.setSettings({ indexName: 'products', indexSettings: { // Order matters: first attribute = highest priority in ranking // Fewer searchable attributes = faster search searchableAttributes: [ 'name', // Highest priority 'brand', 'category', 'unordered(description)', // unordered = position in attribute doesn't affect ranking ], // DON'T make IDs, URLs, or numeric fields searchable // unretrievableAttributes: fields searchable but never returned in hits // Use for fields users should match against but not see unretrievableAttributes: ['internal_tags'], // attributesToRetrieve: limit what comes back (smaller response = faster) attributesToRetrieve: ['name', 'brand', 'price', 'image_url', 'category'], }, });
Step 3: Optimize Faceting
await client.setSettings({ indexName: 'products', indexSettings: { attributesForFaceting: [ 'category', // Regular facet: counts computed 'brand', // Regular facet 'filterOnly(price)', // filterOnly: no counts = faster 'filterOnly(in_stock)', // Use for boolean/numeric filters 'filterOnly(created_at)', ], // filterOnly() saves CPU — use it when you don't need facet counts // searchable(brand) lets users search within facet values }, });
Step 4: Client-Side Response Caching
import { LRUCache } from 'lru-cache'; const searchCache = new LRUCache<string, any>({ max: 500, // Max cached queries ttl: 60 * 1000, // 1 minute TTL }); async function cachedSearch(query: string, filters?: string) { const cacheKey = `${query}|${filters || ''}`; const cached = searchCache.get(cacheKey); if (cached) return cached; const result = await client.searchSingleIndex({ indexName: 'products', searchParams: { query, filters, hitsPerPage: 20 }, }); searchCache.set(cacheKey, result); return result; }
Step 5: Query-Time Optimization Parameters
const { hits } = await client.searchSingleIndex({ indexName: 'products', searchParams: { query: 'laptop', // Reduce response size attributesToRetrieve: ['name', 'price', 'image_url'], // Only what UI needs attributesToHighlight: ['name'], // Fewer = faster attributesToSnippet: [], // Skip snippets if not used responseFields: ['hits', 'nbHits', 'page', 'nbPages'], // Skip unnecessary metadata // Limit processing hitsPerPage: 20, // Don't over-fetch maxValuesPerFacet: 10, // Limit facet values returned // Disable features you don't use // typoTolerance: false, // Uncomment if exact matching is fine // removeStopWords: false, // Keep stop words in query }, });
Step 6: Replica Strategy for Sort Orders
// Standard replicas share data but have their own ranking // Virtual replicas share data AND ranking config (less storage cost) await client.setSettings({ indexName: 'products', indexSettings: { replicas: [ 'virtual(products_price_asc)', // Virtual: cheaper, limited customization 'virtual(products_price_desc)', 'products_newest', // Standard: full ranking control ], }, }); // Virtual replica can only override: customRanking and ranking // Standard replica can override all settings
Performance Monitoring
async function measureSearchLatency(query: string, iterations = 10) { const latencies: number[] = []; for (let i = 0; i < iterations; i++) { const start = performance.now(); await client.searchSingleIndex({ indexName: 'products', searchParams: { query, hitsPerPage: 20 }, }); latencies.push(performance.now() - start); } latencies.sort((a, b) => a - b); console.log({ p50: latencies[Math.floor(iterations * 0.5)].toFixed(1), p95: latencies[Math.floor(iterations * 0.95)].toFixed(1), p99: latencies[Math.floor(iterations * 0.99)].toFixed(1), avg: (latencies.reduce((a, b) => a + b) / iterations).toFixed(1), }); }
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| P95 > 200ms | Oversized records | Trim records, use |
| Facet queries slow | Too many facet values | Use or |
| Indexing slow | Large batch + complex settings | Reduce batch size, simplify |
| Cache stampede | TTL expired, burst traffic | Use stale-while-revalidate pattern |
Resources
Next Steps
For cost optimization, see
algolia-cost-tuning.