Introduction
Display real-time suggestions as users type in the search box — optimize the backend with debouncing, trie structures, and Redis caching. Generate designs with Claude Code.
CLAUDE.md Autocomplete Rules
## Autocomplete Design Rules
### Frontend
- Debounce: 300ms (don't hit API on every keystroke)
- Minimum characters: start suggesting at 2+
- Cancel: cancel old requests with AbortController
### Backend
- Manage prefix indexes with Redis Sorted Set (ZSet)
- Use Elasticsearch completion suggester in production
- Cache suggestion results in Redis (TTL: 5 minutes)
- Return max 10 results
Generated Autocomplete Implementation
// src/search/autocomplete.ts
export class AutocompleteService {
async indexProduct(product: { id: string; name: string; popularityScore: number }): Promise<void> {
const normalizedName = product.name.toLowerCase();
const pipeline = redis.pipeline();
for (let i = 2; i <= normalizedName.length; i++) {
const prefix = normalizedName.slice(0, i);
pipeline.zadd(`autocomplete:prefix:${prefix}`, { score: product.popularityScore, member: `${normalizedName}::${product.id}` });
pipeline.expire(`autocomplete:prefix:${prefix}`, 86400);
}
await pipeline.exec();
}
async getSuggestions(prefix: string, limit = 10): Promise<SuggestionResult[]> {
const normalizedPrefix = prefix.toLowerCase().trim();
if (normalizedPrefix.length < 2) return [];
const cacheKey = `suggestions:${normalizedPrefix}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const members = await redis.zrangebyscore(`autocomplete:prefix:${normalizedPrefix}`, '-inf', '+inf', 'WITHSCORES', 'LIMIT', '0', String(limit * 2));
const suggestions: SuggestionResult[] = [];
for (let i = 0; i < members.length && suggestions.length < limit; i += 2) {
const [name, productId] = members[i].split('::');
const product = await this.getProductFromCache(productId);
if (!product) continue;
suggestions.push({
id: productId, name: product.name, score: parseFloat(members[i + 1]),
highlight: this.highlightMatch(product.name, normalizedPrefix),
});
}
await redis.set(cacheKey, JSON.stringify(suggestions), { EX: 300 });
return suggestions;
}
private highlightMatch(name: string, prefix: string): string {
const start = name.toLowerCase().indexOf(prefix);
if (start === -1) return name;
return name.slice(0, start) + `<mark>${name.slice(start, start + prefix.length)}</mark>` + name.slice(start + prefix.length);
}
}
Frontend Debounce (React)
export function SearchBox() {
const [query, setQuery] = useState('');
const [suggestions, setSuggestions] = useState<SuggestionResult[]>([]);
const abortRef = useRef<AbortController | null>(null);
useEffect(() => {
if (query.length < 2) { setSuggestions([]); return; }
const timer = setTimeout(async () => {
abortRef.current?.abort();
abortRef.current = new AbortController();
try {
const res = await fetch(`/api/search/suggest?q=${encodeURIComponent(query)}`, { signal: abortRef.current.signal });
const data = await res.json();
setSuggestions(data.suggestions);
} catch (err) {
if ((err as Error).name !== 'AbortError') console.error(err);
}
}, 300); // 300ms debounce
return () => clearTimeout(timer);
}, [query]);
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search products..." />
{suggestions.length > 0 && (
<ul>{suggestions.map(s => <li key={s.id} dangerouslySetInnerHTML={{ __html: s.highlight }} />)}</ul>
)}
</div>
);
}
Summary
Design Autocomplete with Claude Code:
- CLAUDE.md — 300ms debounce, minimum 2 characters, Redis/ES index management
- Redis ZSet for prefix indexes (popularity score ranking)
- Elasticsearch completion suggester for production-scale suggestions
- AbortController to cancel stale requests (prevent race conditions)
Review search system designs with **Code Review Pack (¥980)* using /code-review at prompt-works.jp*
myouga (@myougatheaxo) — Axolotl VTuber.
Top comments (0)