תארו לעצמכם חנות מסחר אלקטרוני עם 50,000 מוצרים שבה משתמשים לא מוצאים "אוזניות אלחוטיות" כי רק התאמות מדויקות בכותרת מוחזרות. אנחנו פותרים זאת עם חיפוש טקסט מלא (FTS). עבור קטלוג אלקטרוניקה של 120,000 פריטים (SKU), יישמנו Elasticsearch עם מנתח (stemmer) רוסי ופאקטות (facets). זמן החיפוש הממוצע ירד מ-3 שניות ל-0.1 שניות, וההמרה עלתה ב-30%. במשך 5 שנים, השלמנו מעל 15 פרויקטי FTS, הפחתנו את זמן החיפוש פי 10 והגדלנו את ההמרה ב-25%. חיסכון בעלויות תשתית מגיע ל-30% על ידי הורדת עומס ממסד הנתונים. להלן, נפרט כיצד לבחור וליישם חיפוש טקסט מלא—מ-PostgreSQL FTS ועד מנועים חיצוניים.
אילו בעיות פותר חיפוש טקסט מלא?
על נפחים גדולים, שאילתות LIKE גורמות לסריקת טבלאות מלאה. עם 100,000 מוצרים, זמן התגובה עולה על 5 שניות, מה שגורם לאובדן של עד 40% מהמשתמשים. FTS משתמש באינדקסים הפוכים (inverted indexes) כדי למצוא רשומות במילישניות. בנוסף, FTS מדרג תוצאות לפי רלוונטיות: כותרות מקבלות משקל גדול יותר מתיאורים, ורשומות חדשות מדורגות גבוה יותר. זה מגדיל את המרת החיפוש ב-25% ומפחית את העומס על מסד הנתונים.
באיזו ארכיטקטורה לבחור: FTS מובנה או מנוע חיצוני?
הבחירה תלויה בנפח הנתונים ובדרישות התכונות. עבור קטלוג טיפוסי של עד 100,000 רשומות, PostgreSQL FTS מספיק. עבור תרחישים מורכבים (חיפוש מטושטש, פאקטות, מילים נרדפות), יש צורך ב-Elasticsearch או Meilisearch.
PostgreSQL FTS: אפשרות מובנית
הגדרת סכמה:
ALTER TABLE products ADD COLUMN search_vector TSVECTOR GENERATED ALWAYS AS ( to_tsvector('russian', coalesce(title, '') || ' ' || coalesce(description, '') || ' ' || coalesce(brand, '') ) ) STORED;
CREATE INDEX idx_products_fts ON products USING GIN (search_vector);ALTER TABLE products ADD COLUMN search_vector TSVECTOR GENERATED ALWAYS AS ( to_tsvector('russian', coalesce(title, '') || ' ' || coalesce(description, '') || ' ' || coalesce(brand, '') ) ) STORED; CREATE INDEX idx_products_fts ON products USING GIN (search_vector); — העמודה מתעדכנת אוטומטית ב-INSERT/UPDATE, אין צורך בטריגר.
חיפוש עם דירוג ותקצירים:
-- Простой запрос
SELECT id, title, ts_rank(search_vector, query) AS rank,
ts_headline('russian', description, query, 'MaxWords=30, MinWords=15, StartSel=<b>, StopSel=</b>') AS excerpt
FROM products,
plainto_tsquery('russian', 'беспроводные наушники') AS query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 20;
-- websearch_to_tsquery: поддерживает "фразы", -исключения, OR
SELECT id, title
FROM products
WHERE search_vector @@ websearch_to_tsquery('russian', '"беспроводные наушники" -проводные')
ORDER BY ts_rank(search_vector, websearch_to_tsquery('russian', '"беспроводные наушники" -проводные')) DESC;GENERATED ALWAYS AS ... STORED יוצר תקציר עם התאמות מודגשות. פרטים נוספים ב-PostgreSQL.
Elasticsearch: מתי יש צורך במנוע חיצוני
ל-PostgreSQL FTS יש מגבלות: אין חיפוש מטושטש, אין מילים נרדפות מובנות, אין אגרגציות פאקטות. אם אלה נדרשות, השתמשו ב-Elasticsearch או OpenSearch.
סכמת אינדקס עם מנתח רוסי:
PUT /products {
"settings": {
"analysis": {
"analyzer": {
"russian_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "russian_stop", "russian_stemmer"]
}
},
"filter": {
"russian_stop": {
"type": "stop",
"stopwords": "_russian_"
},
"russian_stemmer": {
"type": "stemmer",
"language": "russian"
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "russian_analyzer",
"boost": 3
},
"brand": {
"type": "text",
"analyzer": "russian_analyzer",
"boost": 2
},
"description": {
"type": "text",
"analyzer": "russian_analyzer"
},
"category_id": {
"type": "keyword"
},
"price": {
"type": "double"
},
"status": {
"type": "keyword"
},
"created_at": {
"type": "date"
}
}
}
}חיפוש עם פאקטות ופילטרים:
POST /products/_search
{
"query": {
"bool": {
"must": {
"multi_match": {
"query": "беспроводные наушники",
"fields": ["title^3", "brand^2", "description"],
"type": "best_fields",
"fuzziness": "AUTO"
}
},
"filter": [
{
"term": {
"status": "published"
}
},
{
"range": {
"price": {
"gte": 1000,
"lte": 15000
}
}
}
]
}
},
"aggs": {
"by_brand": {
"terms": {
"field": "brand.keyword",
"size": 20
}
},
"price_stats": {
"stats": {
"field": "price"
}
}
},
"highlight": {
"fields": {
"title": {
"number_of_fragments": 0
},
"description": {
"fragment_size": 150,
"number_of_fragments": 3
}
}
},
"from": 0,
"size": 20
}סנכרון עם PostgreSQL באמצעות CDC (Debezium + Kafka) מבטיח שהנתונים מגיעים ל-ES גם בזמן תקלות שירות. התיעוד ב-Elasticsearch מכסה את כל התכונות.
כיצד להבטיח סנכרון נתונים בין PostgreSQL ל-Elasticsearch?
לסנכרון שינויים, אנו משתמשים ב-Change Data Capture (CDC) עם Debezium ו-Kafka. Debezium עוקב אחר שינויי WAL של PostgreSQL ומפרסם אירועים ל-Kafka, שממנו Elasticsearch (באמצעות Logstash או צרכן מותאם אישית) מקבל עדכונים. לחלופין, שירות API ישיר קורא מעת לעת שינויים לפי חותמת זמן. CDC אמין יותר: הוא לא מאבד נתונים בזמן תקלות ואינו דורש שאילתות נוספות למסד הנתונים. כדי להגדיר CDC, הפעילו רישום WAL (-- Простой запрос SELECT id, title, ts_rank(search_vector, query) AS rank, ts_headline('russian', description, query, 'MaxWords=30, MinWords=15, StartSel=<b>, StopSel=</b>' ) AS excerpt FROM products, plainto_tsquery('russian', 'беспроводные наушники') AS query WHERE search_vector @@ query ORDER BY rank DESC LIMIT 20; -- websearch_to_tsquery: поддерживает "фразы", -исключения, OR SELECT id, title FROM products WHERE search_vector @@ websearch_to_tsquery('russian', '"беспроводные наушники" -проводные') ORDER BY ts_rank(search_vector, websearch_to_tsquery('russian', '"беспроводные наушники" -проводные')) DESC; ) וצרו פרסום (publication) עבור הטבלאות.
למה ליישם חיפוש טקסט מלא מיד?
משתמשים עוזבים אם הם לא מוצאים מוצר תוך 3 שניות. חיפוש טקסט מלא מעלה את המרת החיפוש ב-25% ומפחית את העומס על מסד הנתונים פי 10. המהנדסים המוסמכים של Elastic אצלנו עוזרים לבחור את הפתרון האופטימלי עבור המחסנית שלכם. צרו קשר—נעריך את הפרויקט שלכם ונמליץ על הגישה הטובה ביותר.
איך אנחנו עושים את זה: מקרה בוחן
יישמנו חיפוש עבור קטלוג של 120,000 מוצרים ב-3 ימים. השתמשנו ב-Elasticsearch עם מנתח רוסי ופאקטות לפי מותג, מחיר וסטטוס. תוצאה: זמן החיפוש הממוצע ירד מ-3 שניות ל-0.1 שניות, וההמרה עלתה ב-30%.
תהליך העבודה: תוכנית שלב אחר שלב
| שלב | משך | תוצאה |
|---|---|---|
| אנליטיקה | 0.5 ימים | בדיקת סכמת DB, תרחישי חיפוש, שדות עדיפות |
| עיצוב | 0.5 ימים | בחירת מנוע (PostgreSQL FTS / Elasticsearch / Meilisearch) וסכמת אינדקס |
| יישום | 1–2 ימים | תצורה, קוד אינדוקס ושאילתות |
| בדיקות | 0.5 ימים | אימות רלוונטיות, ביצועים וכיסוי |
| פריסה | 0.5 ימים | הגדרת סנכרון (CDC או שירות) וניטור |
טעויות נפוצות
- שימוש ב-LIKE לחיפוש בטבלאות גדולות (איטי, ללא מורפולוגיה).
- אי מתן משקל לשדות (כותרת ותיאור מדורגים שווה).
- התעלמות מנורמליזציה (טיפול בסיומות מילים ורישיות).
השוואת מנועים
| תכונה | PostgreSQL FTS | Elasticsearch/OpenSearch | Meilisearch |
|---|---|---|---|
| זמן הגדרה | דקות | שעות–ימים | דקות |
| חיפוש מטושטש | באמצעות הרחבות | מובנה | מובנה |
| פאקטות | קשה | מובנה | מובנה |
| סנכרון | לא נדרש | CDC או סנכרון | CDC או סנכרון |
| תשתית | כבר קיימת | +שרת JVM | +שרת Go |
הגדרת מנתח נוסף ב-Meilisearch
ב-Meilisearch, פשוט הגדירו את שפת האינדקס בעת היצירה:
curl -X POST 'http://localhost:7700/indexes' \
-H 'Content-Type: application/json' \
-d '{ "uid": "products", "primaryKey": "id" }'לאחר מכן הגדירו שדות חיפוש ומשקלים באמצעות הגדרות PATCH. צרו קשר—נעריך את הפרויקט שלכם ונמליץ על הפתרון האופטימלי.







