מדריך חיפוש טקסט ב‑Postgres 2026
כל כלי החיפוש שכבר קיימים במאגר שלך, ומתי כל אחד מהם מצדיק את קיומו.
רוב הצוותים משתמשים בכלי חיפוש אחד של Postgres. צוותים שמכירים את שלושת האפשרויות משיגים חיפוש טוב יותר עם פחות מורכבות — ומונעים את המסלול היקר לשירות חיפוש ייעודי שהם עדיין לא צריכים.
המדריך הזה מכסה את כל האפשרויות הטבעיות של Postgres: מה כל אחת עושה, מתי היא מתאימה, ואיך לשלב אותן.
שלושת הכלים
חיפוש טקסט מלא (tsvector / GIN index) הוא לקסיקלי. הוא מחלק טקסט ללקסמות, מבצע סטמינג ומשווה שאילתות מול האינדקס. „Running“ ו‑„runs“ מתמזגים לאותה לקסמה. כך גם „dog“ ו‑„dogs“. פונקציית הדירוג (ts_rank) מתגמלת מסמכים שבהם מונחי השאילתה מופיעים בתדירות גבוהה או במיקום בולט.
טריגרמים (pg_trgm) מחלקים מחרוזות לחתיכות חופפות של שלושה תווים ומודדים כמה חתיכות משותפות לשתי המחרוזות. „Dan“ → " da", "dan", "an ". „Micheal“ ו‑„Michael“ חולקים את רוב ה‑trigrams, ולכן הדמיון גבוה. זה עושה את pg_trgm מצוין להתאמת שמות משועשעת, סובלנות לשגיאות כתיב והשלמת אוטומט — התחום שבו FTS מתפקד בחסר.
אינדקסים של התאמה מדויקת (B‑tree, hash) מטפלים במפתחות ראשיים, כתובות אימייל, מזהים, SKU‑ים, וכל דבר שבו התשובה בינארית: התאמה או לא התאמה. אלו לא מרגישים כמו „חיפוש“, אך הם שייכים לדיון כי הדפוס הגרוע ביותר הוא להשתמש בחיפוש משועשעת או סמנטי לבעיות שיש להן תשובות נכונות.
הבחירה אינה קשורה למורכבות. היא קשורה להתאמת הכלי לצורת השאילתה.
מתי חיפוש טקסט מלא מנצח
חיפוש בטקסט חופשי למילות מפתח. פוסטים בבלוג, תיעוד, תיאורי מוצר, פניות תמיכה, מסמכים משפטיים. חיפוש טקסט מלא (FTS) נבנה במיוחד לצורת תוכן זו: אחזור מדורג על טקסט בשפה טבעית עם אינדקס.
שאילתות משתמש מבוססות מילות מפתח. משתמשים מקלידים מונח חיפוש, מסננים לפי תג, או גולשים לפי מילה. FTS מתמודד עם כוונה זו באופן טבעי ללא צורך בתשתית הטמעה.
תוצאות מדורגות ללא תלות חיצונית. אינדקסי FTS מהירים, דטרמיניסטיים, ואינם דורשים קריאות API. אותות הרלוונטיות נובעים מתדירות המונחים ומשוקלים לפי מיקום השדה.
סינון בוליאני לצד חיפוש. FTS משתלב באופן טבעי עם לוגיקת השאילתה הקיימת שלכם:
SELECT * FROM postsWHERE search_vector @@ to_tsquery('english', 'postgres & performance') AND category = 'tutorial' AND published_at > NOW() - INTERVAL '6 months';הגדרת FTS
-- עמודה מחושבת שומרת את האינדקס מעודכן אוטומטיתALTER TABLE posts ADD COLUMN search_vector tsvector GENERATED ALWAYS AS ( setweight(to_tsvector('english', coalesce(title, '')), 'A') || setweight(to_tsvector('english', coalesce(body, '')), 'B') ) STORED;
CREATE INDEX posts_search_idx ON posts USING GIN (search_vector);
-- שאילתהSELECT title, ts_rank(search_vector, query) AS rankFROM posts, to_tsquery('english', 'postgres & performance') queryWHERE search_vector @@ queryORDER BY rank DESCLIMIT 10;setweight מקצה חשיבות: A (כותרת) עולה על B (גוף). זהו מודל הרלוונטיות השלם עבור רוב מקרי השימוש בחיפוש תוכן.
מה FTS לא מתמודד איתו היטב
- שגיאות כתיב בשאילתות — „javascipt” לא יתאים ל‑„javascript”
- שמות אישיים, כתובות, שמות עצם משולבים שלא מתמתחים באופן צפוי
- חיפוש קידומת/השלמה אוטומטית ללא תצורה מיוחדת
- שאילתות שבהן המשתמש מתאר מושג במקום לקרוא לו שם
מתי טריגרמים מנצחים (pg_trgm)
pg_trgm ממלא את הפער המביך שבו FTS נופל באופן עקבי.
FTS מחלק טקסט לליקסים ומבצע stemming. עבור פרוזה זה נכון. עבור שמות ומזהים קצרים זה לעיתים אינו:
- שמות אישיים (“Dan Levy” → stemming שונה בהתאם למילון והגדרת השפה)
- שמות חברות, כתובות, כותרות מוצר שבהן האיות המדויק חשוב
- שאילתות עם שגיאות כתיב — “Micheal Jordan”, “Amaon”, “javascipt”
- השלמה אוטומטית / חיפוש קידומת
- התאמת מחרוזות חלקיות (“son” תואם ל-”Johnson”, “Anderson”)
pg_trgm הוא גם בלתי תלוי בשפה, מה שמסייע לשמות מרקעים לשוניים מגוונים. ב‑FTS נדרש קונפיגורציית מילון לכל שפה.
חיפוש שם מעורפל
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX users_name_trgm_idx ON users USING GIN (name gin_trgm_ops);
-- מוצא "Micheal Jordan" כשמחפשים "Michael Jordan"SELECT id, name, similarity(name, $1) AS scoreFROM usersWHERE name % $1 -- אופרטור % = סף דמיון (ברירת מחדל 0.3)ORDER BY score DESCLIMIT 10;האופרטור % משתמש ב‑pg_trgm.similarity_threshold (ברירת מחדל 0.3, טווח 0–1). עבור חיפוש שמות, 0.3–0.4 תופסים טעויות כתיב תוך שמירה על רעש נמוך.
השלמה אוטומטית, קידומת וחיפוש כולל
-- התאמת קידומת עבור השלמה אוטומטית. אינדקס GIN של טריגרם יכול לעזור,-- אך אינדקס B‑tree לדפוסים עשוי להיות עדיף עבור קידומות משמאל בלבד.SELECT name FROM usersWHERE name ILIKE $1 || '%'ORDER BY nameLIMIT 10;
-- word_similarity להתאמות חלקיות בתוך מחרוזות ארוכות יותר-- ("Johnson" בתוך "Andrew Johnson III")SELECT id, name, word_similarity($1, name) AS scoreFROM usersWHERE $1 <% nameORDER BY score DESCLIMIT 10;אינדקס GIN של טריגרם שימושי במיוחד לשאילתות ILIKE '%pattern%' של חיפוש כולל ולחיפוש עמיד לשגיאות כתיב — תבניות שבדרך כלל גורמות לסריקות של כל הטבלה ללא אינדקס טריגרם.
מתי לבחור ב‑pg_trgm על פני FTS
| תרחיש | שימוש |
|---|---|
| חיפוש שם אדם/חברה עם שגיאות כתיב | pg_trgm |
| השלמה אוטומטית / חיפוש קידומת | pg_trgm (או FTS עם שאילתות קידומת) |
| מחרוזות קצרות, מזהים, קודים | pg_trgm |
| מאמרים פרוזיים, תיעוד, כרטיסיות | FTS |
| הודעות יומן למילות מפתח | FTS |
| חיפוש שמות רב‑לשוני | pg_trgm (ללא תלות בשפה) |
מתי חיפוש התאמה מדויקת ב‑SQL מנצח
חלק מבעיות ה„חיפוש” אינן חיפוש כלל.
„מצא את המשתמש עם האימייל dan@example.com” הוא בדיקת שוויון. „מצא את ההזמנה ORD-12345” הוא חיפוש מפתח ראשי. „הצג פוסטים בקטגוריית tutorial ממוינים לפי תאריך” הוא שאילתת סינון. כל אלה מתבצעים בצורה מיטבית בעזרת אינדקסי B‑tree או hash.
שימוש ב‑FTS או ב‑טריגרמים כאן מוסיף מורכבות מבלי לשפר את הדיוק — ובמקרים של מזהים מדויקים, התאמה חלקית היא גרועה יותר מאי‑התאמה.
CREATE INDEX users_email_idx ON users (email);
-- חיפוש מדויק: מהיר וללא דו‑משמעותSELECT id, name FROM users WHERE email = $1;הלקח הרחב: חיפוש משוער לבעיות שיש להן תשובה נכונה הוא טעות קטגורית. הוא מחזיר משהו — שעשוי להיות בטוח לא נכון.
שילוב הכלים הללו
הכלים האלו משתלבים בצורה נקייה. אין צורך לבחור רק אחד.
FTS + pg_trgm לתיבת חיפוש שמסבילה שגיאות כתיב במילות מפתח:
-- דמיון טריגרם על הכותרת קולט שגיאות; ts_rank מטפל ברלוונטיות הגוףSELECT id, title, ts_rank(search_vector, to_tsquery('simple', $1)) AS fts_rank, similarity(title, $1) AS trgm_scoreFROM postsWHERE search_vector @@ to_tsquery('simple', $1) OR title % $1ORDER BY (ts_rank(search_vector, to_tsquery('simple', $1)) + similarity(title, $1)) DESCLIMIT 10;FTS + unaccent לתוכן בינלאומי:
-- מסיר סימני ניקוד כך ש-"José" תתאים ל-"Jose"CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE TEXT SEARCH CONFIGURATION public.simple_unaccent (COPY = pg_catalog.simple);
ALTER TEXT SEARCH CONFIGURATION public.simple_unaccent ALTER MAPPING FOR hword, hword_part, word WITH unaccent, simple;
ALTER TABLE posts ADD COLUMN search_vector tsvector;
CREATE TRIGGER posts_search_vector_refreshBEFORE INSERT OR UPDATE OF title, body ON postsFOR EACH ROW EXECUTE FUNCTION tsvector_update_trigger(search_vector, 'public.simple_unaccent', title, body);unaccent + pg_trgm לחיפוש שמות בינלאומי:
ALTER TABLE users ADD COLUMN name_search text;
CREATE FUNCTION users_name_search_refresh()RETURNS trigger LANGUAGE plpgsql AS $$BEGIN NEW.name_search := unaccent(coalesce(NEW.name, '')); RETURN NEW;END;$$;
CREATE TRIGGER users_name_search_refreshBEFORE INSERT OR UPDATE OF name ON usersFOR EACH ROW EXECUTE FUNCTION users_name_search_refresh();
CREATE INDEX users_name_search_trgm_idx ON users USING GIN (name_search gin_trgm_ops);
SELECT id, nameFROM usersWHERE name_search % unaccent($1)ORDER BY similarity(name_search, unaccent($1)) DESCLIMIT 10;דוגמאות הטריגרים נמנעות משימוש ב‑unaccent() בתוך עמודות מחוללות או ביטויי אינדקס, שם כללי ה‑immutability של PostgreSQL רלוונטיים. אם אתם עוטפים את unaccent() בפונקציה בלתי‑משתנה משלהם, תעדו שמדובר בקבלת סיכון של עדכונים/הגדרות.
הרחבות בולטות
pg_trgm מגיע עם רוב הפצות PostgreSQL אך דורש הפעלה מפורשת. הוא הבסיס להתאמה גסה של מחרוזות ב‑Postgres.
unaccent מסיר סימני ניקוד לפני אינדוקס ושאילתה. משתלב היטב עם pg_trgm ו‑FTS לתוכן בשפות אירופיות. מגיע עם PostgreSQL.
pg_bigm מרחיב את גישת ה‑trigram ל‑bigrams (קטעים של שני תווים), מה שמשפר משמעותית תוצאות עבור שפות CJK (סינית, יפנית, קוריאנית) שבהן pg_trgm מתפקד פחות. יש להתקין בנפרד; אינו חלק מההפצה.
pg_search (מ‑ParadeDB) מחליף את ערמת GIN / tsvector הסטנדרטית באינדקס BM25 מבוסס Tantivy. זה מספק דירוג BM25 (לעיתים טוב יותר מ‑ts_rank), התאמה גסה בתוך שאילתות FTS, חיפוש מפולח, והאינדוקס מהיר משמעותית בטבלאות גדולות. זו דרך שדרוג “drop‑in” כאשר ה‑FTS הרגיל מתחיל להיתקל במגבלות דירוג או ביצועים.
-- pg_search: חיפוש טקסט מלא BM25 עם התאמה גסהCREATE INDEX posts_bm25_idx ON posts USING bm25 (id, title, body) WITH (key_field = 'id', text_fields = '{"title": {}, "body": {}}');
-- שאילתה עם דירוג BM25 + התאמה גסה (תופסת "javascipt")SELECT id, title, paradedb.score(id) AS rankFROM postsWHERE posts @@@ paradedb.fuzzy_phrase(field => 'title', value => 'postgres performnce')ORDER BY rank DESCLIMIT 10;pgvector מוסיף אחסון וקטורים צפופים וחיפוש דמיון. זה הכלי המתאים כאשר משתמשים מתארים מה שהם רוצים במקום לשמות אותו — חיפוש סמנטי, RAG, המלצות תוכן קשור, שאילתות רב‑לשוניות. מכוסה לעומק ב-חיפוש וקטורים סמנטיים ואסטרטגיות היברידיות.
טבלת החלטות
| מה שאתה מחפש | מומלץ |
|---|---|
| מאמרים, תיעוד, כרטיסים | FTS |
| שמות אנשים/חברות עם שגיאות כתיב | pg_trgm |
| השלמה אוטומטית, חיפוש קידומת | pg_trgm |
| קודים קצרים, מזהים | pg_trgm |
| הודעות יומן למילות מפתח | FTS |
| שמות בינלאומיים | pg_trgm + unaccent |
| תוכן גדול, דירוג משופר | pg_search (ParadeDB BM25) |
| מפתחות ראשיים, אימיילים מדויקים, מזהים | אינדקס B‑tree |
| תאריכים, טווחים, רשימות ממוינות | אינדקס B‑tree |
| הרשאות, קטגוריות, מסננים | תנאי WHERE רגיל |
| שאלות, פרפרזות, מושגים | pgvector (ראו מאמר הבא) |
כאשר יש ספק: מחרוזות קצרות עם וריאציות איות → trigrams. פרוזה ארוכה לשאילתות מילות‑מפתח → FTS. מזהים מובנים → אינדקסים רגילים. שאילתות מושגיות או בשפה טבעית → pgvector.
חיפוש היברידי: שני אותות, דירוג אחד
כאשר שאילתה כמו "withRetry timeout errors" מגיעה לתיבת חיפוש, היא נושאת שני סוגי כוונה: שמות סימבוליים מדויקים שהמשתמש יודע (withRetry) ותיאור מושגי (timeout errors). שום פרימיטיב יחיד אינו מכסה את שניהם. הרצת FTS וחיפוש וקטורי במקביל — ואז מיזוג רשימות הדירוג עם Reciprocal Rank Fusion — עושה זאת.
RRF מדרג כל תוצאה כ‑1 / (60 + rank) בכל רשימה ומסכם את הציונים בין הרשימות. הקבוע 60 מדכא את היתרון של דירוגים גבוהים, כך שתוצאה שמקבלת מקום שני בשתי הרשימות יכולה לנצח תוצאה שמנצחת ברשימה אחת ומפספסת לחלוטין את השנייה. החשוב, RRF לעולם לא ממוצע ציונים גולמיים בין השיטות — דירוג FTS ומרחק קוסינוס הם מטבעות שונים ולא ניתנים לחיבור אריתמטי.
-- Hybrid search: FTS + pgvector merged with RRFWITH fts AS ( SELECT id, ts_rank(search_vector, query) AS score, ROW_NUMBER() OVER (ORDER BY ts_rank(search_vector, query) DESC) AS rank FROM docs, to_tsquery('english', 'withRetry & timeout') query WHERE search_vector @@ query LIMIT 60),vec AS ( SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $embedding) AS rank FROM docs ORDER BY embedding <=> $embedding LIMIT 60)SELECT COALESCE(fts.id, vec.id) AS id, (COALESCE(1.0 / (60 + fts.rank), 0) + COALESCE(1.0 / (60 + vec.rank), 0)) AS rrf_scoreFROM fts FULL JOIN vec ON fts.id = vec.idORDER BY rrf_score DESCLIMIT 10;ברירת המחדל של 60 מסמכים לכל ענף (LIMIT 60) היא נקודת התחלה נפוצה. הרחיבו אותה אם הזיכרון (recall) נמוך; צמצמו אותה לשיפור המהירות.
מה הלאה
חיפוש טקסט ב‑Postgres מכסה הרבה, אך יש לו תקרה. כאשר משתמשים מתארים מה שהם רוצים במקום לקרוא לזה במפורש — “משהו שיעזור לי לישון בטיסה”, “מאמרים על ניפוי באגים וביטחון כמה מהנדס חדש” — חיפוש לקסיקלי וחיפוש טריגרם שניהם נכשלים.
זהו תחום של הטמעת וקטורים, חיפוש סמנטי, וארכיטקטורות היברידיות. מכוסה ב‑חיפוש וקטורי סמנטי ואסטרטגיות היברידיות.