חיפוש אוטומטי השלמה ליישומי אינטרנט: יישום ורכיבים
לאחרונה עבדנו עם חנות מקוונת שהיה לה קטלוג של 200,000 מוצרים: משתמשים התלוננו על חיפוש איטי, והפתרון הסטנדרטי של WordPress לא עמד במשימה. יישמנו חיפוש אוטומטי השלמה מבוסס Elasticsearch עם חיפוש מטושטש וקאשינג — זמן ההקלדה ירד פי 3, ועומס השרת ירד ב-60%. טכנית, חיפוש אוטומטי השלמה משלב מספר משימות: חיפוש מטושטש מהיר באינדקס, debounce כדי למנוע עומס על השרת, טיפול נכון במקלדת (ARIA combobox), וביטול בקשות מיושנות. כל אחת נפתרת בנפרד; יחד הן יוצרות רכיב מלא. סיפקנו עשרות פרויקטים של חיפוש אוטומטי השלמה ואנחנו מכירים את המלכודות האופייניות.
אתגרים טכניים
במבט ראשון זה נראה פשוט: <input> עם רשימה נפתחת — וזהו. אבל בפועל, צריך:
- Debounce עם ביטול בקשות קודמות (כדי למנוע הצפת השרת). עיכוב אופטימלי — 250 אלפיות השנייה.
- חיפוש מטושטש (התאמה לא מדויקת) לסובלנות לשגיאות הקלדה. לדוגמה, שאילתה "iphon" תמצא את "iPhone".
- ניווט מקלדת נכון (חיצים, Enter, Escape) ומאפייני ARIA לנגישות.
- הדגשת התאמות בתוצאות כך שמשתמשים יראו שהשאילתה שלהם הובנה.
- קאשינג של שאילתות תכופות להפחתת זמן השהייה. קאש טיפוסי: 100 רשומות עם TTL של 30 שניות.
השוואת גישות: צד לקוח, צד שרת, והיברידית
| גישה | נפח נתונים | זמן תגובה | מורכבות יישום | ספריות אופייניות |
|---|---|---|---|---|
| צד לקוח | עד ~50,000 רשומות | מיידי (ללא השהיית רשת) | נמוכה | Fuse.js, MiniSearch |
| צד שרת | כל נפח | תלוי במהירות השרת והרשת | גבוהה | Elasticsearch, Typesense, PostgreSQL pg_trgm |
| היברידית | כל נפח | מהיר לשאילתות תכופות; לנדירות, כמו צד שרת | בינונית | כל אחת מהנ"ל + קאש לקוח |
חיפוש בצד לקוח מהיר פי 5–10 מחיפוש בצד שרת עבור קבוצות עד 50k רשומות. בחירת הגישה הנכונה היא קריטית לחוויית משתמש ולתקציב התשתית.
בחירת הגישה הנכונה: צד לקוח או צד שרת?
אם יש לך פחות מ-50,000 רשומות והנתונים משתנים לעתים רחוקות, צד לקוח יספק זמן השהייה מינימלי ויפחית עומס על השרת. עבור קטלוגים גדולים יותר או נתונים דינמיים, יש צורך בצד שרת עם קאש לקוח. אנחנו עוזרים לקבוע את האפשרות האופטימלית בשלב הבדיקה.
מה כלול בעבודה
כשאתה מזמין מאיתנו חיפוש אוטומטי השלמה, אתה מקבל:
- פתרון ארכיטקטוני: בחירת גישה לפי נפח נתונים ודרישות מהירות.
- רכיב מוכן עם ARIA combobox, ניווט מקלדת והדגשת התאמות.
- חלק שרת: אינדקס Elasticsearch או פונקציות SQL עם pg_trgm.
- קאש בצד לקוח (TTL 30 שניות, עד 100 רשומות) ואופטימיזציה של שאילתות.
- תיעוד אינטגרציה ופריסה.
- בדיקות במכשירים ניידים וברשת איטית.
- אחריות מקיפה: יש לנו מעל 5 שנות ניסיון בפיתוח אתרים וסיפקנו 50+ פרויקטים הקשורים לחיפוש. הפתרונות שלנו חוסכים ללקוחות בדרך כלל 30% במשאבי שרת.
כל הפרויקט מכוסה באחריות ל-6 חודשים — אם משהו נשבר, אנחנו מתקנים בחינם תוך 24 שעות.
תהליך יישום חיפוש אוטומטי השלמה
התהליך מורכב מארבעה שלבים:
- ניתוח נתונים ודרישות: הערכת נפח, תדירות עדכון, יעדי זמן השהייה.
- עיצוב ארכיטקטורה: בחירת גישה (צד לקוח, צד שרת, היברידית), הגדרת אינדקס.
- פיתוח רכיב: יישום חלק לקוח עם debounce (עיכוב 250 אלפיות השנייה), ביטול בקשות, ARIA combobox.
- אינטגרציה ובדיקות: חיבור חלק שרת, הגדרת קאש, בדיקות בנייד וברשת איטית.
כל שלב מסתיים בסקירת קוד ובדיקות יחידה.
יישום בצד לקוח
Hook בסיסי עם Debounce וביטול
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 לחיפוש אוטומטי השלמה עם ARIA Combobox
יישום נכון לפי תבנית ARIA combobox:
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">Поиск...</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>
);
} למה debounce של 250 אלפיות השנייה?
הערך 250 אלפיות השנייה הוא פשרה בין היענות לעומס. פחות מ-150 אלפיות השנייה — יותר מדי בקשות; יותר מ-400 אלפיות השנייה — המשתמש מרגיש בעיכוב. מחקרים מראים ש-debounce של 250 אלפיות השנייה מספק דיוק של 95% עם 40% פחות בקשות שרת בהשוואה ללא debounce.צד שרת עם Elasticsearch
באמצעות 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) });
}אינדקס עם שדה completion:
{
"mappings": {
"properties": {
"title": {
"type": "text",
"fields": {
"suggest": {
"type": "completion",
"analyzer": "standard"
},
"keyword": {
"type": "keyword"
}
}
}
}
}
} קאש בצד לקוח
const cache = new Map<string, { data: SearchResult[]; ts: number }>();
const TTL = 30_000; // 30 секунд
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;
} לוחות זמנים ועלות משוערים
| שלב | זמן | עלות משוערת |
|---|---|---|
| חיפוש אוטומטי השלמה פשוט (fetch + debounce, ללא ARIA) | 4–8 שעות | החל מ-$500 |
| רכיב מלא עם ARIA, הדגשה וקאש | 2–3 ימים | $1,500–$2,500 |
| הוספת אינדקס שרת Elasticsearch | +1–2 ימים | $1,000–$2,000 |
העלות מחושבת בנפרד עבור הפרויקט שלך. אנחנו מספקים אחריות ל-6 חודשים על הפונקציונליות המיושמת — אם מופיעה תקלה, אנחנו מתקנים תוך 24 שעות. הניסיון הרב שלנו בפיתוח אתרים (מעל 5 שנים) ו-50+ פרויקטים שהושלמו מבטיחים איכות. כל פתרון עובר סקירת קוד, בדיקות מהירות ובדיקות נגישות. משתמשים בדרך כלל רואים הצעות תוך 100 אלפיות השנייה בממוצע, ו-95% מהשאילתות מחזירות התאמות מדויקות.
הזמן יישום חיפוש אוטומטי השלמה לאתר שלך — נמצא את הפתרון האופטימלי. קבל ייעוץ לפרויקט שלך, צור קשר.







