מהו חיפוש פנים (Faceted Search) ולמה הוא חשוב?
אנו מפתחים סינון מוצרים שלא מאבד מכירות. דמיינו קטלוג עם 500 מחשבים ניידים: ללא סינון איכותי, המשתמש עוזב למתחרה. הפתרון שלנו הוא חיפוש פנים (מקור: ויקיפדיה): ערכי הסינון הזמינים מתעדכנים בהתאם לאלה שכבר נבחרו, והמשתמש תמיד יודע כמה פריטים נמצאים מאחורי כל ערך. זה שונה מהותית משאילתת WHERE פשוטה.
סינון טוב הוא שילוב של אינדקסי SQL, מטמון (caching) וסנכרון URL בצד הלקוח. לצוות שלנו יש מעל 10 שנות ניסיון במסחר אלקטרוני וביצענו מעל 50 פרויקטים לחנויות מקוונות. אנו מבטיחים שהמסנן עובד על קטלוגים של עד מיליון פריטים ללא פגיעה ב-LCP או INP. טעות נפוצה היא יישום סינון רק בצד הלקוח ללא סנכרון URL: המשתמש לא יכול לשתף קישור לרשימה המסוננת, ותנועת SEO אובדת. אנו מתכננים את המערכת כך שכל שילוב מסנן שנבחר בא לידי ביטוי בנתיב ה-URL או בפרמטרים. הלקוחות שלנו רואים בדרך כלל עלייה של 15% בהמרות, אשר עבור חנות עם הכנסות שנתיות של מיליון דולר מתורגמת להכנסה נוספת של 150,000 דולר.
אילו סוגי מסננים ניתן ליישם?
| סוג | רכיב UX | דוגמה | יישום טכני |
|---|---|---|---|
| בחירה מרובה | תיבות סימון | מותג: Apple, Samsung | WHERE brand IN (...) |
| בחירה יחידה | לחצני רדיו | מצב: חדש/משומש | WHERE condition = ... |
| טווח מספרי | מחוון עם שתי ידיות | מחיר: תלוי בטווח | WHERE price BETWEEN ... AND ... |
| טווח דרך שדות קלט | שדות "מ-" ו-"עד" | אלכסון: 13–15.6 אינץ' | WHERE diagonal BETWEEN ... |
| בוליאני | מתג | רק במלאי | WHERE stock > 0 |
| דירוג | כוכבים (≥N) | דירוג מ-4 | WHERE rating >= 4 |
| צבע | דוגמיות צבע | צבע: שחור, כסף | WHERE color IN (...) |
כיצד מיושם חיפוש פנים?
סינון מבוסס SQL
הגישה הפשוטה ביותר היא סינון דרך PostgreSQL. עובד עד כ-100,000 פריטים עם אינדוקס נכון.
-- Основной запрос с фильтрами
SELECT p.*
FROM products p
WHERE p.category_id = :cat
AND (:brands IS NULL OR p.brand = ANY(:brands::text[]))
AND (:price_min IS NULL OR p.price >= :price_min)
AND (:price_max IS NULL OR p.price <= :price_max)
AND (:in_stock IS NULL OR p.stock > 0)
ORDER BY p.sort_order
LIMIT 48 OFFSET :offset;
-- Агрегации для счётчиков (отдельный запрос на каждый фильтр)
SELECT brand, COUNT(*)
FROM products p
WHERE p.category_id = :cat
-- Все фильтры КРОМЕ brand
AND (:price_min IS NULL OR p.price >= :price_min)
GROUP BY brand;בעיית ה-SQL: עבור מוני ערכים נכונים, יש צורך בשאילתת אגרגציה נפרדת לכל מסנן, תוך החרגת אותו מסנן מהתנאים. עם 10 מסננים פעילים — 10 שאילתות נוספות. תחת עומס אמיתי, זה לא ניתן להרחבה.
Elasticsearch לחיפוש פנים
Elasticsearch פותר את הבעיה בשאילתה אחת באמצעות אגרגציות:
{
"query": {
"bool": {
"filter": [
{
"term": {
"category_id": 14
}
},
{
"terms": {
"brand": ["Apple", "Samsung"]
}
},
{
"range": {
"price": {
"gte": 5000,
"lte": 30000
}
}
}
]
}
},
"aggs": {
"brands": {
"filter": {
"bool": {
"filter": [
{
"term": {
"category_id": 14
}
},
{
"range": {
"price": {
"gte": 5000,
"lte": 30000
}
}
}
]
}
},
"aggs": {
"values": {
"terms": {
"field": "brand",
"size": 50
}
}
}
},
"price_range": {
"stats": {
"field": "price"
}
}
}
}כל אגרגציה (מותגים, זיכרון RAM, גודל מסך) משתמשת במסנן ללא התנאי שלה — זהו חיפוש פנים. שאילתה אחת מחזירה גם מוצרים וגם את כל המונים עבור כל המסננים.
בבדיקות עם קטלוג של 200,000 פריטים, Elasticsearch מבצע אגרגציות פי 10 מהר יותר מ-SQL. עומס מסד הנתונים יורד מכיוון שכל המונים מגיעים משאילתה אחת. עבור קטלוגים מעל 50,000 פריטים, Elasticsearch מציע הבדל איכותי במהירות ובעושר הפנים.
סנכרון URL
ה-URL צריך לשקף את מצב המסנן לשיתוף ו-SEO:
/noutbuki?brand=apple,samsung&ram=16&price_min=50000&price_max=100000&sort=price_asc בשינוי מסנן — pushState או replaceState ללא טעינת עמוד מחדש. בכניסה ישירה ל-URL — אתחול מצב המסנן מהפרמטרים. גישת SEO: שילובי מסננים פופולריים (מותג + קטגוריה) מוצגים כעמודים סטטיים נפרדים עם תוכן ייחודי ותגי canonical. עמודים עם שילובים נדירים מקבלים -- Основной запрос с фильтрами SELECT p.* FROM products p WHERE p.category_id = :cat AND (:brands IS NULL OR p.brand = ANY(:brands::text[])) AND (:price_min IS NULL OR p.price >= :price_min) AND (:price_max IS NULL OR p.price <= :price_max) AND (:in_stock IS NULL OR p.stock > 0) ORDER BY p.sort_order LIMIT 48 OFFSET :offset; -- Агрегации для счётчиков (отдельный запрос на каждый фильтр) SELECT brand, COUNT(*) FROM products p WHERE p.category_id = :cat -- Все фильтры КРОМЕ brand AND (:price_min IS NULL OR p.price >= :price_min) GROUP BY brand; .
יישום React בצד הלקוח
מצב המסנן מאוחסן ב-URL (מקור האמת) ומשוכפל במצב React:
type FilterState = {
brands: string[];
ram: number | null;
priceMin: number | null;
priceMax: number | null;
inStock: boolean;
sort: 'price_asc' | 'price_desc' | 'popularity' | 'rating';
};
function useFilters() {
const [searchParams, setSearchParams] = useSearchParams();
const filters = useMemo(() => parseFilters(searchParams), [searchParams]);
const setFilter = (key: keyof FilterState, value: unknown) => {
const next = { ...filters, [key]: value };
setSearchParams(buildParams(next), { replace: true });
};
return { filters, setFilter };
}בכל שינוי מסנן — debounce של 300ms, ולאחר מכן בקשת API. התוצאות מתעדכנות ללא טעינת עמוד מחדש.
אילו תכונות מתקדמות משפרות ביצועים?
מחוון מחיר עם היסטוגרמה
רכיב טווח המחירים הוא אתגר נפרד. דרישות:
- שתי ידיות (מינימום ומקסימום) שלא יכולות לחצות זו את זו
- קלט מקלדת עם אימות וקיצוץ לערכים חוקיים
- היסטוגרמת התפלגות מחירים מאחורי המחוון (מראה היכן מרוכזים הפריטים)
היסטוגרמה: אגרגציית היסטוגרמה של Elasticsearch עם מרווח = (מחיר מקסימלי - מחיר מינימלי) / 20. מוצגת דרך נתיב SVG או תרשים עמודות זעיר. רכיבים מוכנים: { "query": { "bool": { "filter": [ { "term": { "category_id": 14 } }, { "terms": { "brand": ["Apple", "Samsung"] } }, { "range": { "price": { "gte": 5000, "lte": 30000 } } } ] } }, "aggs": { "brands": { "filter": { "bool": { "filter": [ { "term": { "category_id": 14 } }, { "range": { "price": { "gte": 5000, "lte": 30000 } } } ] } }, "aggs": { "values": { "terms": { "field": "brand", "size": 50 } } } }, "price_range": { "stats": { "field": "price" } } } } , brands, ram. אפשרות Radix מועדפת לפרויקטים עם Tailwind-stack.
אופטימיזציית ביצועים
מטמון אגרגציות: תוצאות ספירת הפנים לא משתנות בכל בקשה. שמרו אגרגציות לכל קטגוריה עם קבוצות מסננים אופייניות ב-Redis למשך 5-10 דקות. בעדכון פריט, בטלו את מטמון הקטגוריה.
אינדקסי PostgreSQL:
-- Составной индекс для типичного запроса
CREATE INDEX ON products (category_id, brand, price)
WHERE status = 'active';
-- GIN-индекс для JSONB-атрибутов
CREATE INDEX ON products USING GIN (attributes);
טעינה עצלה של פנים: הצגת 5–7 ערכים ראשונים, כפתור "הצג הכל" טוען את השאר באמצעות בקשה נפרדת.
התאמה למובייל
במובייל, המסננים מוסתרים מאחורי כפתור "מסננים" → נפתח מגירה תחתונה במסך מלא (bottom sheet). בפנים אותם רכיבים אך עם אזורי מגע גדולים יותר. כפתור "החל" מקובע בתחתית. בעת לחיצה על החל, המגירה נסגרת והרשימה מתעדכנת.
כיצד ליישם חיפוש פנים ב-4 שלבים
שלב 1: ביקורת קטלוג
בדקו את תכונות המוצר ועקביות הנתונים. קבעו אילו תכונות ניתנות לסינון וסוגי הנתונים שלהן.
שלב 2: בחירת ערימת טכנולוגיה
בחרו בין SQL ל-Elasticsearch בהתבסס על גודל הקטלוג. עבור פחות מ-50 אלף פריטים, PostgreSQL; עבור גדולים יותר, Elasticsearch.
שלב 3: פיתוח ובדיקות
יישמו שאילתות backend, אגרגציות, מטמון ורכיבי frontend. בדקו עם זרימות משתמש אמיתיות ובדיקות עומס.
שלב 4: פריסה ואופטימיזציה
פרוסו עם ניטור. בצעו אופטימיזציה לאינדקסים והגדרות מטמון. הדריכו את הצוות על תחזוקת המערכת.
מסירה ותמיכה
מה כלול בעבודה
- תיעוד סכמת סינון: תיאורי אינדקס, אגרגציה ו-API
- הגדרת אינדקס ומטמון (Redis, Elasticsearch)
- רכיבי React בצד הלקוח עם סנכרון URL
- בדיקות עומס עד מיליון פריטים
- הדרכת צוות על חיפוש פנים
- 30 ימי תמיכה לאחר השקה
לוחות זמנים ועלויות פיתוח
- סינון SQL בסיסי (תיבות סימון ל-3–4 תכונות, טווח מחירים): 1–2 שבועות, החל מ-$5,000
- חיפוש פנים על Elasticsearch (מונים דינמיים, כל סוגי המסננים, סנכרון URL): 3–4 שבועות, החל מ-$10,000
- הוספת היסטוגרמת מחירים ומטמון אגרגציות: +שבוע, +$3,000
הבחירה בין SQL ל-Elasticsearch תלויה בגודל הקטלוג. עד 50,000 פריטים, גישת SQL מעוצבת היטב עובדת. מעל לכך, Elasticsearch מציע הבדל איכותי במהירות ובעושר הפנים.
השוואת טכנולוגיות: SQL לעומת Elasticsearch
| פרמטר | PostgreSQL | Elasticsearch |
|---|---|---|
| ביצועים | עד 50,000 פריטים ללא האטה | עד מיליון פריטים ללא האטה |
| אגרגציות | שאילתות N+1 לכל מסנן | שאילתה אחת לכל הפנים |
| מורכבות יישום | נמוכה (מסד נתונים מוכר) | בינונית (דורש קלאסטר נפרד) |
| דיוק מונים | מדויק אך איטי | מהיר אך עשוי להיות מקורב |
למה אנחנו לא ממליצים על MySQL לחיפוש פנים
MySQL מציעה תמיכה חלשה יותר באינדקסים מורכבים ו-JSONB בהשוואה ל-PostgreSQL, וחסרים בה אינדקסי GIN. עבור סינון פנים עם טווחים ומספר תנאים, PostgreSQL או Elasticsearch מספקים ביצועים טובים משמעותית.אנו מעריכים את הפרויקט שלכם תוך יום אחד. צרו קשר כדי לקבל ייעוץ על הגישה הטובה ביותר ולוחות זמנים מדויקים. פתרון הסינון הרב-פרמטרי שלנו הוטמע בלמעלה מ-50 חנויות מסחר אלקטרוני, והביא לעלייה ממוצעת של 15% בשיעורי ההמרה.







