DanLevy.net

מדריך חיפוש טקסט ב‑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‑ים, וכל דבר שבו התשובה בינארית: התאמה או לא התאמה. אלו לא מרגישים כמו „חיפוש“, אך הם שייכים לדיון כי הדפוס הגרוע ביותר הוא להשתמש בחיפוש משועשעת או סמנטי לבעיות שיש להן תשובות נכונות.

הבחירה אינה קשורה למורכבות. היא קשורה להתאמת הכלי לצורת השאילתה.

Postgres search tool mapA comparison of pg_trgm, full-text search, pgvector, and hybrid search by input shape and query intent.Pick the search primitive by input shapeThe same Postgres table can support all four. The trick is matching the query to the text.Exact words matterMeaning mattersShort / structured textLong prose / chunksfuzzypg_trgmNames, addresses, titles, typos,autocomplete, partial strings.Orthographic similarity: spelling distance.similarpgvectorRelated items, duplicate tickets,recommendations from short descriptions.Embedding similarity: meaning distance.lexicalFull-text searchArticles, docs, logs, support contentwhere query words should appear.Lexemes, stemming, ranking, boolean filters.hybridFTS + pgvectorTechnical docs and RAG where users askconceptual questions plus exact symbols.Run both, fuse ranks with RRF.Start with query intent, then check text shape
The four Postgres search primitives mapped by query intent (exact vs. semantic) and text shape (structured vs. prose). The same table can carry all four indexes — the choice is per query, not per table.

מתי חיפוש טקסט מלא מנצח

חיפוש בטקסט חופשי למילות מפתח. פוסטים בבלוג, תיעוד, תיאורי מוצר, פניות תמיכה, מסמכים משפטיים. חיפוש טקסט מלא (FTS) נבנה במיוחד לצורת תוכן זו: אחזור מדורג על טקסט בשפה טבעית עם אינדקס.

שאילתות משתמש מבוססות מילות מפתח. משתמשים מקלידים מונח חיפוש, מסננים לפי תג, או גולשים לפי מילה. FTS מתמודד עם כוונה זו באופן טבעי ללא צורך בתשתית הטמעה.

תוצאות מדורגות ללא תלות חיצונית. אינדקסי FTS מהירים, דטרמיניסטיים, ואינם דורשים קריאות API. אותות הרלוונטיות נובעים מתדירות המונחים ומשוקלים לפי מיקום השדה.

סינון בוליאני לצד חיפוש. FTS משתלב באופן טבעי עם לוגיקת השאילתה הקיימת שלכם:

SELECT * FROM posts
WHERE 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 rank
FROM posts, to_tsquery('english', 'postgres & performance') query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 10;

setweight מקצה חשיבות: A (כותרת) עולה על B (גוף). זהו מודל הרלוונטיות השלם עבור רוב מקרי השימוש בחיפוש תוכן.

מה FTS לא מתמודד איתו היטב

מתי טריגרמים מנצחים (pg_trgm)

pg_trgm ממלא את הפער המביך שבו FTS נופל באופן עקבי.

FTS מחלק טקסט לליקסים ומבצע stemming. עבור פרוזה זה נכון. עבור שמות ומזהים קצרים זה לעיתים אינו:

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 score
FROM users
WHERE name % $1 -- אופרטור % = סף דמיון (ברירת מחדל 0.3)
ORDER BY score DESC
LIMIT 10;

האופרטור % משתמש ב‑pg_trgm.similarity_threshold (ברירת מחדל 0.3, טווח 0–1). עבור חיפוש שמות, 0.3–0.4 תופסים טעויות כתיב תוך שמירה על רעש נמוך.

השלמה אוטומטית, קידומת וחיפוש כולל

-- התאמת קידומת עבור השלמה אוטומטית. אינדקס GIN של טריגרם יכול לעזור,
-- אך אינדקס B‑tree לדפוסים עשוי להיות עדיף עבור קידומות משמאל בלבד.
SELECT name FROM users
WHERE name ILIKE $1 || '%'
ORDER BY name
LIMIT 10;
-- word_similarity להתאמות חלקיות בתוך מחרוזות ארוכות יותר
-- ("Johnson" בתוך "Andrew Johnson III")
SELECT id, name, word_similarity($1, name) AS score
FROM users
WHERE $1 <% name
ORDER BY score DESC
LIMIT 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_score
FROM posts
WHERE search_vector @@ to_tsquery('simple', $1)
OR title % $1
ORDER BY (ts_rank(search_vector, to_tsquery('simple', $1)) + similarity(title, $1)) DESC
LIMIT 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_refresh
BEFORE INSERT OR UPDATE OF title, body ON posts
FOR 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_refresh
BEFORE INSERT OR UPDATE OF name ON users
FOR 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, name
FROM users
WHERE name_search % unaccent($1)
ORDER BY similarity(name_search, unaccent($1)) DESC
LIMIT 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 rank
FROM posts
WHERE posts @@@ paradedb.fuzzy_phrase(field => 'title', value => 'postgres performnce')
ORDER BY rank DESC
LIMIT 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 ומרחק קוסינוס הם מטבעות שונים ולא ניתנים לחיבור אריתמטי.

חיפוש היברידי עם Reciprocal Rank Fusionשאילתה מתפצלת לחיפוש טקסט מלא ולחיפוש וקטורי, כל אחד מייצר דירוגים, ו‑RRF משלב אותם לרשימת תוצאות אחת.חיפוש היברידי הוא שני אותות כנים, ואז דירוג מאוחדאל תתמקד בממוצע ציונים גולמיים. דירוג FTS ומרחק קוסינוס הם מטבעות שונים.שאילתת משתמש”withRetrytimeout errors”FTS / BM25סמלים ומילים מדויקים1. תיעוד API2. מדריך Retrypgvectorשכנים מושגיים1. תקלות רשת2. מדריך Retryמיזוג RRFתן לכל תוצאה קרדיט עלהמיקום שלה בכל רשימה.1 / (60 + rank)תוצאות סופיותהתוצאה המובילה היא שבה מונחים מדויקיםוהמשמעות הסמנטית מתואמים.
שאילתה מתפצלת ל‑FTS ול‑pgvector במקביל. כל אחד מייצר רשימת דירוג משלו. RRF מדרג כל מסמך לפי מיקומו בכל רשימה ומסכם את הציונים — התוצאה מציגה מסמכים שהשני האותות מסכימים עליהם.
-- Hybrid search: FTS + pgvector merged with RRF
WITH 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_score
FROM fts FULL JOIN vec ON fts.id = vec.id
ORDER BY rrf_score DESC
LIMIT 10;

ברירת המחדל של 60 מסמכים לכל ענף (LIMIT 60) היא נקודת התחלה נפוצה. הרחיבו אותה אם הזיכרון (recall) נמוך; צמצמו אותה לשיפור המהירות.


מה הלאה

חיפוש טקסט ב‑Postgres מכסה הרבה, אך יש לו תקרה. כאשר משתמשים מתארים מה שהם רוצים במקום לקרוא לזה במפורש — “משהו שיעזור לי לישון בטיסה”, “מאמרים על ניפוי באגים וביטחון כמה מהנדס חדש” — חיפוש לקסיקלי וחיפוש טריגרם שניהם נכשלים.

זהו תחום של הטמעת וקטורים, חיפוש סמנטי, וארכיטקטורות היברידיות. מכוסה ב‑חיפוש וקטורי סמנטי ואסטרטגיות היברידיות.