Autocomplete Search for Web Applications: Implementation and Components

Autocomplete Search for Web Applications: Implementation and Components

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1283
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1238
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    981
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1029
  • image_website-sbh_0.webp
    Website development for SBH Partners
    1104
  • image_website-_0.webp
    Website development for Red Pear
    552

Autocomplete Search for Web Applications: Implementation and Components

We recently worked with an online store that had a catalog of 200,000 products: users complained about slow search, and the standard WordPress solution couldn't handle it. We implemented autocomplete based on Elasticsearch with fuzzy search and caching — input time dropped by 3x, and server load fell by 60%. Technically, autocomplete combines several tasks: fast fuzzy search across an index, debounce to avoid overloading the server, correct keyboard handling (ARIA combobox), and cancellation of outdated requests. Each is solved separately; together they form a full component. We've delivered dozens of autocomplete projects and know the typical pitfalls.

Technical Challenges

At first glance, it seems simple: an <input> with a dropdown list — and it's done. But in practice, you need:

  • Debounce with cancellation of previous requests (to avoid flooding the server). Optimal delay — 250 ms.
  • Fuzzy search (inexact matching) for tolerance to typos. For example, a query "iphon" would find "iPhone".
  • Correct keyboard navigation (arrows, Enter, Escape) and ARIA attributes for accessibility.
  • Highlighting matches in results so users see that their query was understood.
  • Caching frequent queries to reduce latency. Typical cache: 100 entries with a TTL of 30 seconds.

Approaches Comparison: Client-Side, Server-Side, and Hybrid

Approach Data Volume Response Time Implementation Complexity Typical Libraries
Client-side up to ~50,000 records Instant (no network latency) Low Fuse.js, MiniSearch
Server-side any Depends on backend speed and network High Elasticsearch, Typesense, PostgreSQL pg_trgm
Hybrid any Fast for frequent queries; for rare ones, like server-side Medium Any of the above + client cache

Client-side search is 5–10 times faster than server-side for sets up to 50k records. Choosing the right approach is critical for UX and infrastructure budget.

Choosing the Right Approach: Client-Side or Server-Side?

If you have fewer than 50,000 records and data rarely changes, client-side will give minimal latency and reduce server load. For larger catalogs or dynamic data, you need server-side with client caching. We help determine the optimal option during the audit phase.

What's Included in the Work

When you order autocomplete from us, you receive:

  • Architectural solution: choosing the approach based on data volume and speed requirements.
  • Ready component with ARIA combobox, keyboard navigation, and matching highlighting.
  • Server part: Elasticsearch index or SQL functions with pg_trgm.
  • Client-side caching (TTL 30 seconds, up to 100 entries) and query optimization.
  • Integration and deployment documentation.
  • Testing on mobile devices and slow network.
  • A comprehensive warranty: we have over 5 years of experience in web development and have delivered 50+ search-related projects. Our solutions typically save clients 30% on server resources.

The entire project is covered by a 6-month warranty — if something breaks, we fix it for free within 24 hours.

Autocomplete Implementation Process

The process consists of four stages:

  1. Data and requirements analysis: assess volume, update frequency, latency targets.
  2. Architecture design: choose approach (client-side, server-side, hybrid), define index.
  3. Component development: implement client part with debounce (250 ms delay), request cancellation, ARIA combobox.
  4. Integration and testing: connect server part, set up cache, test on mobile and slow network.

Each stage ends with a code review and unit tests.

Client-Side Implementation

Basic Hook with Debounce and Cancellation

import { useState, useEffect, useRef, useCallback } from 'react'; type SearchResult = { id: string; title: string; category?: string; url: string; }; function useAutocomplete( fetchFn: (query: string, signal: AbortSignal) => Promise<SearchResult[]>, delay = 250 ) { const [query, setQuery] = useState(''); const [results, setResults] = useState<SearchResult[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState<Error | null>(null); const abortRef = useRef<AbortController | null>(null); const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const search = useCallback((value: string) => { setQuery(value); if (timerRef.current) clearTimeout(timerRef.current); if (abortRef.current) abortRef.current.abort(); if (value.trim().length < 2) { setResults([]); return; } timerRef.current = setTimeout(async () => { const controller = new AbortController(); abortRef.current = controller; setLoading(true); setError(null); try { const data = await fetchFn(value, controller.signal); if (!controller.signal.aborted) setResults(data); } catch (err) { if (err instanceof Error && err.name !== 'AbortError') setError(err); } finally { if (!controller.signal.aborted) setLoading(false); } }, delay); }, [fetchFn, delay]); useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current); if (abortRef.current) abortRef.current.abort(); }, []); return { query, results, loading, error, search }; } 

React Autocomplete Component with ARIA Combobox

Correct implementation according to the ARIA combobox pattern:

import { useId, useRef, useState } from 'react'; interface AutocompleteProps { placeholder?: string; onSelect: (result: SearchResult) => void; fetchResults: (query: string, signal: AbortSignal) => Promise<SearchResult[]>; } export function Autocomplete({ placeholder, onSelect, fetchResults }: AutocompleteProps) { const id = useId(); const listId = `${id}-listbox`; const inputRef = useRef<HTMLInputElement>(null); const listRef = useRef<HTMLUListElement>(null); const { query, results, loading, search } = useAutocomplete(fetchResults); const [activeIndex, setActiveIndex] = useState(-1); const [open, setOpen] = useState(false); const isOpen = open && (results.length > 0 || loading); const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'ArrowDown': e.preventDefault(); setActiveIndex((i) => Math.min(i + 1, results.length - 1)); break; case 'ArrowUp': e.preventDefault(); setActiveIndex((i) => Math.max(i - 1, -1)); break; case 'Enter': if (activeIndex >= 0 && results[activeIndex]) { onSelect(results[activeIndex]); setOpen(false); setActiveIndex(-1); } break; case 'Escape': setOpen(false); setActiveIndex(-1); inputRef.current?.focus(); break; } }; return ( <div className="autocomplete" role="combobox" aria-expanded={isOpen} aria-haspopup="listbox"> <input ref={inputRef} type="search" placeholder={placeholder} value={query} aria-autocomplete="list" aria-controls={listId} aria-activedescendant={activeIndex >= 0 ? `${id}-option-${activeIndex}` : undefined} onChange={(e) => { search(e.target.value); setOpen(true); setActiveIndex(-1); }} onFocus={() => query.length >= 2 && setOpen(true)} onBlur={() => setTimeout(() => setOpen(false), 150)} onKeyDown={handleKeyDown} /> {isOpen && ( <ul ref={listRef} id={listId} role="listbox" className="autocomplete__dropdown"> {loading && <li role="option" aria-selected="false" className="autocomplete__loading">Searching...</li>} {results.map((result, index) => ( <li key={result.id} id={`${id}-option-${index}`} role="option" aria-selected={index === activeIndex} className={`autocomplete__option ${index === activeIndex ? 'autocomplete__option--active' : ''}`} onMouseDown={() => { onSelect(result); setOpen(false); }} onMouseEnter={() => setActiveIndex(index)} > <span> {(() => { if (!query.trim()) return <span>{result.title}</span>; const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const parts = result.title.split(new RegExp(`(${escaped})`, 'gi')); return parts.map((part, i) => part.toLowerCase() === query.toLowerCase() ? <mark key={i}>{part}</mark> : <span key={i}>{part}</span> ); })()} </span> {result.category && <span className="autocomplete__category">{result.category}</span>} </li> ))} </ul> )} </div> ); } 
Why 250 ms debounce? The value 250 ms is a compromise between responsiveness and load. Less than 150 ms — too many requests; more than 400 ms — user notices the delay. Research shows that a 250 ms debounce provides 95% accuracy with 40% fewer server requests compared to no debounce.

Server-Side with Elasticsearch

Using the Elasticsearch suggest API:

// POST /api/suggest async function suggestHandler(req: Request) { const { q } = await req.json(); if (!q || q.length < 2) return Response.json({ hits: [] }); const response = await esClient.search({ index: 'products', body: { suggest: { title_suggest: { prefix: q, completion: { field: 'title.suggest', size: 10, fuzzy: { fuzziness: 'AUTO' }, }, }, }, query: { multi_match: { query: q, fields: ['title^3', 'description', 'tags^2'], type: 'bool_prefix', }, }, _source: ['id', 'title', 'category', 'url', 'image'], size: 10, }, }); return Response.json({ hits: response.hits.hits.map((h) => h._source) }); } 

Index with completion field:

{ "mappings": { "properties": { "title": { "type": "text", "fields": { "suggest": { "type": "completion", "analyzer": "standard" }, "keyword": { "type": "keyword" } } } } } } 

Client-Side Caching

const cache = new Map<string, { data: SearchResult[]; ts: number }>(); const TTL = 30_000; // 30 seconds async function fetchWithCache(query: string, signal: AbortSignal) { const cached = cache.get(query); if (cached && Date.now() - cached.ts < TTL) return cached.data; const res = await fetch(`/api/suggest?q=${encodeURIComponent(query)}`, { signal }); const data = await res.json(); cache.set(query, { data: data.hits, ts: Date.now() }); if (cache.size > 100) cache.delete(cache.keys().next().value); return data.hits; } 

Estimated Timelines and Cost

Stage Time Estimated Cost
Simple autocomplete (fetch + debounce, no ARIA) 4–8 hours Starting at $500
Full component with ARIA, highlighting, and caching 2–3 days $1,500–$2,500
Adding Elasticsearch server index +1–2 days $1,000–$2,000

The cost is calculated individually for your project. We provide a 6-month warranty on implemented functionality — if a bug appears, we fix it within 24 hours. Our long-standing experience in web development (over 5 years) and 50+ completed projects guarantee quality. Each solution undergoes code review, speed testing, and accessibility testing. Users typically see suggestions within 100ms on average, and 95% of queries return accurate matches.

Order autocomplete implementation for your site — we'll find the optimal solution. Get a consultation on your project, contact us.