PostgreSQL fulltext-sökning räcker för många appar. Använd en enkel beslutsregel, en startfråga och en indexeringschecklista för att veta när du behöver lägga till en sökmotor.

De flesta ber inte om “fulltext-sökning”. De vill ha en sökruta som känns snabb och hittar det de menade på första sidan. Om resultaten är långsamma, brusiga eller konstigt ordnade bryr sig inte användarna om du använde PostgreSQL fulltext-sökning eller en separat motor — de slutar bara lita på sökningen.
Det är ett beslut: behåll sök i Postgres eller lägg till en dedikerad sökmotor. Målet är inte perfekt relevans. Det är en stabil baseline som går snabbt att leverera, är enkel att köra och tillräckligt bra för hur appen faktiskt används.
För många appar räcker PostgreSQL fulltext-sökning länge. Om du har några textfält (titel, beskrivning, anteckningar), grundläggande rankning och en eller två filter (status, kategori, tenant) kan Postgres hantera det utan extra infrastruktur. Du får färre rörliga delar, enklare backup och färre incidenter av typen “varför är sökningen nere men appen uppe?”.
”Tillräcklig” betyder oftast att du når tre mål samtidigt:
Ett konkret exempel: en SaaS-dashboard där användare söker projekt efter namn och anteckningar. Om en query som “onboarding checklist” returnerar rätt projekt bland topp 5, på under en sekund, och ni inte konstant finjusterar analyzers eller reindexerar, då är det “tillräckligt”. När ni inte kan nå dessa mål utan att lägga på komplexitet är det dags att fundera på “inbyggd sökning vs sökmotor”.
Team beskriver ofta sök i funktioner, inte i utfall. Det smarta är att översätta varje funktion till vad det kostar att bygga, justera och hålla tillförlitligt.
Tidiga förfrågningar låter ofta så här: feltolerans, facets och filter, highlights, “smart” ranking och autocomplete. För en första version, skilj måste-ha från trevligt-att-ha. En grundläggande sökruta behöver oftast bara hitta relevanta poster, hantera vanliga ordformer (plural, tempus), respektera enkla filter och förbli snabb när tabellen växer. Det är precis där PostgreSQL fulltext-sökning brukar passa.
Postgres glänser när ditt innehåll finns i vanliga textfält och du vill ha sök nära din data: hjälpartiklar, blogginlägg, supportärenden, interna docs, produktnamn och -beskrivningar eller anteckningar på kundposter. Det är oftast “hitta rätt post”-problem, inte “bygg en sökprodukt”.
Trevligt-att-ha är där komplexiteten smyger in. Feltolerans och rik autocomplete skjuter ofta mot extra verktyg. Facets är möjliga i Postgres, men om du vill ha många facets, djup analys och omedelbara counts över jättelika dataset blir en dedikerad motor mer attraktiv.
Den dolda kostnaden är sällan licensavgiften. Det är det andra systemet. När du lägger till en sökmotor får du också data-synk och backfills (och buggarna de skapar), övervakning och uppgraderingar, “varför visar sökningen gammal data?”-supportarbete och två uppsättningar relevansknep.
Om ni är osäkra: börja med Postgres, leverera något enkelt, och lägg bara till en motor när ett tydligt krav inte kan mötas.
Använd en tre-facksregel. Om ni klarar alla tre, stanna med PostgreSQL fulltext-sökning. Om ni misslyckas med en av dem kraftigt, överväg en dedikerad sökmotor.
Ett säkert startsteg är enkelt: leverera en baseline i Postgres, logga långsamma queries och “inga resultat”-sökningar, och bestäm sedan. Många appar växer aldrig ur det och ni undviker att köra och synka ett andra system för tidigt.
Röda flaggor som pekar mot en dedikerad motor:
Gröna flaggor för att stanna i Postgres:
PostgreSQL fulltext-sökning är ett inbyggt sätt att omvandla text till något databasen kan söka snabbt utan att skanna varje rad. Det fungerar bäst när ditt innehåll redan lever i Postgres och du vill ha snabb, bra och förutsägbar drift.
Det finns tre delar värda att känna till:
ts_rank (eller ts_rank_cd) för att placera mer relevanta rader först.Språkkonfiguration spelar roll eftersom den ändrar hur Postgres behandlar ord. Med rätt konfig kan “running” och “run” matcha (stemming) och vanliga stoppord ignoreras. Med fel konfig kan sök kännas trasig eftersom normal användarfomulering inte längre matchar det som indexerats.
Prefix-matchning är funktionen folk ofta vill ha för “typeahead”-känsla, som att matcha “dev” till “developer”. I Postgres FTS görs det vanligtvis med en prefix-operator (t.ex. term:*). Det kan förbättra upplevd kvalitet, men ökar ofta arbetet per query, så behandla det som en valfri uppgradering, inte som standard.
Vad Postgres inte försöker vara: en komplett sökplattform med alla funktioner. Om ni behöver fuzzy stavningskorrigering, avancerad autocomplete, learning-to-rank, komplexa analyzers per fält eller distribuerad indexering över många noder är ni utanför inbyggda komfortzonen. För många appar ger PostgreSQL fulltext-sökning ändå det mesta användarna förväntar sig med betydligt färre rörliga delar.
Här är en liten, realistisk form för innehåll du vill söka:
-- Minimal example table
CREATE TABLE articles (
id bigserial PRIMARY KEY,
title text NOT NULL,
body text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
En bra baseline för PostgreSQL fulltext-sökning är: bygg en fråga från vad användaren skrev, filtrera rader först (när du kan), och ranka de återstående matcherna.
-- $1 = user search text, $2 = limit, $3 = offset
WITH q AS (
SELECT websearch_to_tsquery('english', $1) AS query
)
SELECT
a.id,
a.title,
a.updated_at,
ts_rank_cd(
setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(a.body, '')), 'B'),
q.query
) AS rank
FROM articles a
CROSS JOIN q
WHERE
a.updated_at >= now() - interval '2 years' -- example safe filter
AND (
setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(a.body, '')), 'B')
) @@ q.query
ORDER BY rank DESC, a.updated_at DESC, a.id DESC
LIMIT $2 OFFSET $3;
Några detaljer som sparar tid senare:
WHERE innan ranking (status, tenant_id, datumintervall). Du rankar färre rader, så det förblir snabbt.\n- Lägg alltid till en tie-breaker i ORDER BY (som updated_at, sedan id). Det håller pagineringen stabil när många resultat har samma rank.\n- Använd websearch_to_tsquery för användarinmatning. Den hanterar citationstecken och enkla operatorer på ett sätt användare förväntar sig.När den här baslinjen fungerar, flytta to_tsvector(...)-uttrycket till en lagrad kolumn. Det undviker att räkna om det vid varje query och gör indexering enkel.
De flesta historier om “PostgreSQL fulltext-sökning är långsam” handlar om en sak: databasen bygger sökdokumentet vid varje query. Åtgärda det först genom att lagra en förbyggd tsvector och indexera den.
tsvector: generated column eller trigger?En generated column är enklast när ditt sökdokument byggs från kolumner i samma rad. Den hålls korrekt automatiskt och är svår att glömma vid uppdateringar.
Använd en trigger-maintained tsvector när dokumentet beror på relaterade tabeller (t.ex. kombinera en produktpost med dess kategorinamn), eller när du vill ha anpassad logik som är svår att uttrycka som en enda genererad expression. Triggers lägger till rörliga delar, så håll dem små och testa dem.
Skapa en GIN-index på tsvector-kolumnen. Det är baslinjen som gör PostgreSQL fulltext-sökning omedelbar för typisk app-sök.
En setup som fungerar för många appar:
tsvector i samma tabell som raderna du söker oftast.\n- Lägg till en GIN-index på den tsvector-kolumnen.\n- Se till att din fråga använder @@ mot den lagrade tsvector, inte to_tsvector(...) som räknas ut i farten.\n- Överväg VACUUM (ANALYZE) efter stora backfills så planner förstår indexet.Att hålla vektorn i samma tabell är vanligtvis snabbare och enklare. En separat söktabell kan vara meningsfull om bas-tabellen är mycket write-heavy eller om du indexerar ett kombinerat dokument som spänner över många tabeller och vill uppdatera det enligt egen tidsplan.
Partiella index kan hjälpa när du bara söker en undergrupp av rader, som status = 'active', en enda tenant i en multi-tenant-app eller ett specifikt språk. De minskar indexstorleken och kan snabba upp sökningar, men bara om dina queries alltid innehåller samma filter.
Du kan få överraskande bra resultat med PostgreSQL fulltext-sökning om du håller relevansreglerna enkla och förutsägbara.
Det enklaste är fältviktning: träffar i titel bör räknas mer än träffar långt ner i kroppen. Bygg en kombinerad tsvector där titel är viktad högre än beskrivning, och ranka med ts_rank eller ts_rank_cd.
Om du vill att “färskt” eller “populärt” ska flyta upp, gör det försiktigt. En liten bonus är okej, men låt det inte skriva över textrelevans. Ett praktiskt mönster är: ranka efter text först, bryt sedan lika-poäng med färskhet, eller lägg till en takad bonus så en irrelevant ny post inte slår en äldre perfekt match.
Synonymer och frasmatchning är där förväntningarna ofta skiljer sig. Synonymer är inte automatiska; du får dem bara om du lägger till en thesaurus eller anpassat dictionary, eller om du expanderar query-termer själv (t.ex. behandla “auth” som “authentication”). Frasmatchning är inte heller standard: vanliga queries matchar ord var som helst, inte “den exakta frasen”. Om användare skriver in citerade fraser eller långa frågor, överväg phraseto_tsquery eller websearch_to_tsquery för att bättre matcha hur folk söker.
Blandat språk kräver ett beslut. Om ni vet språk per dokument, spara det och generera tsvector med rätt konfiguration (English, Russian, etc.). Om ni inte vet det är en säker fallback att indexera med simple-konfigurationen (ingen stemming), eller behåll två vektorer: en språk-specifik när känd, en simple för allt.
För att validera relevans, håll det litet och konkret:
Detta räcker vanligtvis för PostgreSQL fulltext-sökning i app-sökfält som “templates”, “docs” eller “projects”.
De flesta historier om “PostgreSQL fulltext-sökning är långsam eller irrelevant” kommer från några få undvikbara misstag. Att åtgärda dem är oftast enklare än att lägga till ett nytt söksystem.
En vanlig fälla är att behandla tsvector som ett beräknat värde som förblir korrekt av sig självt. Om du lagrar tsvector i en kolumn men inte uppdaterar den vid varje insert/update kommer resultaten att se slumpmässiga ut eftersom indexet inte längre matchar texten. Om du beräknar to_tsvector(...) i frågan kan resultaten vara korrekta men långsammare, och du förlorar fördelen med ett dedikerat index.
Ett annat sätt att försämra prestanda är att ranka innan du minskar kandidatmängden. ts_rank är användbart, men det bör vanligtvis köras efter att Postgres använt indexet för att hitta matchande rader. Om du beräknar rank för en enorm del av tabellen (eller joinar mot andra tabeller först) kan du förvandla en snabb sökning till en tabellscanning.
Folk förväntar sig också att “contains”-sök ska bete sig som LIKE '%term%'. Ledande wildcards maps inte väl till FTS eftersom FTS bygger på ord (lexemes), inte godtyckliga substrängar. Om du behöver substring-sök för produktkoder eller partiella ID:n, använd ett annat verktyg för det fallet (t.ex. trigram-indexering) istället för att klandra FTS.
Prestandaproblem kommer ofta från resultathantering, inte matchning. Två mönster att se upp för:
OFFSET-paginering, vilket får Postgres att hoppa över fler och fler rader ju längre du bläddrar.\n- Obundna resultatuppsättningar, där query kan returnera tiotusentals rader.Driftproblem är viktiga också. Indexbloat kan byggas upp efter många uppdateringar, och reindex kan vara dyrt om du väntar tills saker redan är smärtsamma. Mät verkliga querytider (och kolla EXPLAIN ANALYZE) före och efter ändringar. Utan siffror är det lätt att “fixa” PostgreSQL fulltext-sökning men göra det värre på annat sätt.
Innan du skyller på PostgreSQL fulltext-sök, kör dessa kontroller. De flesta “Postgres-sök är långsamt eller irrelevant”-buggar kommer från saknade grunder, inte från funktionen själv.
Bygg en riktig tsvector: lagra den i en generated eller underhållen kolumn (inte beräknad vid varje query), använd rätt språkconfig (english, simple, etc.) och applicera vikter om du mixar fält (title > subtitle > body).
Normalisera vad du indexerar: håll bullriga fält (ID:n, boilerplate, navigeringstext) utanför tsvector och trimma stora blobbar om användare aldrig söker dem.
Skapa rätt index: lägg en GIN-index på tsvector-kolumnen och bekräfta att den används i EXPLAIN. Om bara en delmängd är sökbar (t.ex. status = 'published') kan ett partiellt index minska storlek och snabba upp läsningar.
Håll tabeller friska: döda tuples kan sakta ner index-scans. Regelbunden vacuuming är viktig, särskilt på ofta uppdaterat innehåll.
Ha en reindex-plan: stora migrationer eller uppblåsta index behöver ibland ett kontrollerat reindex-fönster.
När data och index ser bra ut, fokusera på query-formen. PostgreSQL FTS är snabb när den kan begränsa kandidatmängden tidigt.
Filtrera först, ranka sedan: applicera strikta filter (tenant, language, published, category) innan ranking. Att ranka tusentals rader du senare kastar är slöseri.
Använd stabil sortering: sortera efter rank och sedan en tie-breaker som updated_at eller id så resultat inte hoppar mellan uppdateringar.
Undvik “query gör allt”: om du behöver fuzzy matching eller feltolerans, gör det medvetet (och mät). Tvinga inte fram sekventiella skann.
Testa riktiga queries: samla topp 20-sökningar, kontrollera relevans för hand och behåll en liten förväntad-resultatlåda för att upptäcka regressioner.
Övervaka långsamma vägar: logga långsamma queries, granska EXPLAIN (ANALYZE, BUFFERS) och håll koll på indexstorlek och cache-hit-rate så du ser när tillväxt ändrar beteendet.
Ett SaaS help center är en bra start eftersom målet är enkelt: hjälpa folk att hitta artikeln som svarar på deras fråga. Ni har några tusen artiklar, varje med titel, kort sammanfattning och brödtext. De flesta skriver 2–5 ord som “reset password” eller “billing invoice”.
Med PostgreSQL fulltext-sökning kan detta kännas klart oväntat snabbt. Du sparar en tsvector för kombinerade fält, lägger en GIN-index och rankar efter relevans. Framgång ser ut som: resultat kommer under 100 ms, topp 3-resultat är vanligtvis korrekta och du behöver inte övervaka systemet hela tiden.
Sen växer produkten. Support vill filtrera på produktområde, plattform (web, iOS, Android) och plan (free, pro, business). Dokumentförfattare vill ha synonymer, “menade du” och bättre stavningshantering. Marketing vill ha analys som “topp-sökningar utan resultat”. Trafiken ökar och sök blir en av de mest trafikerade endpoints.
Det är signalerna att en dedikerad sökmotor kan vara värd kostnaden:
En praktisk migrationsväg är att behålla Postgres som source of truth även efter att ni lagt till en motor. Börja med att logga sökqueries och inga-resultat-fall, kör en asynkron sync-jobb som kopierar bara sökbara fält till det nya indexet. Kör båda parallellt ett tag och växla gradvis istället för att satsa allt från dag ett.
Om er sökning mest är “hitta dokument som innehåller dessa ord” och datasetet inte är massivt, räcker PostgreSQL fulltext-sökning oftast. Börja där, få det att fungera, och lägg bara till en dedikerad motor när ni kan namnge den saknade funktionen eller skalningsproblemet.
En sammanfattning att ha i bakhuvudet:
tsvector, lägga en GIN-index och dina rankingbehov är grundläggande.\n- Leverera en starter query och en index-setup, mät verklig latenstid och “hittade folk det?”\n- Justera relevans med små, uppenbara ändringar (vikter, språkconfig, queryparsing), inte en stor omskrivning.\n- Planera en sökmotor bara när du når tydliga luckor (autocomplete, stark feltolerans, faceting) eller växtsignaler (storlek, load).Ett praktiskt nästa steg: implementera starter-queryn och indexet från tidigare avsnitt, logga några enkla mätvärden i en vecka. Följ p95-querytid, långsamma queries och ett enkelt success-signal som “sök -> klick -> ingen omedelbar bounce” (även en grundläggande eventräknare hjälper). Du ser snabbt om ni behöver bättre ranking eller bara bättre UX (filter, highlighting, bättre utdrag).
Starta planeringen av en dedikerad sökmotor när en av dessa blir ett verkligt krav (inte ett trevligt-att-ha): stark autocomplete eller instant-sök på varje tangenttryckning i skala, hög feltolerans och stavningskorrigering, facets och aggregationer över många fält med snabba counts, avancerade relevansverktyg (synonymuppsättningar, learning-to-rank, per-query boosts), eller långvarig hög belastning och stora index som är svåra att hålla snabba.
Om du vill gå snabbt i appen kan Koder.ai (koder.ai) vara ett praktiskt sätt att prototypa sök-UI och API via chat, och sen iterera säkert med snapshots och rollback medan du mäter vad användarna faktiskt gör.
PostgreSQL fulltext-sökning är “tillräcklig” när du kan träffa tre mål samtidigt:
Om du kan uppfylla dessa med en lagrad tsvector + en GIN-index, är du oftast i ett utmärkt läge.
Standardrekommendationen är att börja med PostgreSQL fulltext-sökning. Det går snabbare att leverera, håller data och sök nära varandra och undviker att bygga och underhålla en separat indexeringspipeline.
Gå över till en dedikerad sökmotor när du har ett tydligt krav som Postgres inte hanterar bra (högkvalitativ felstavningshantering, avancerad autocomplete, tunga facets eller sökload som konkurrerar med databasen).
En enkel regel: stanna i Postgres om du klarar dessa tre kontroller:
Om du misslyckas hårt på någon (särskilt typos/autocomplete eller hög söktrafik), överväg en dedikerad motor.
Använd Postgres FTS när sökningen mest är “hitta rätt post” över ett par fält som title/body/notes, med enkla filter (tenant, status, kategori).
Det passar bra för help centers, interna docs, tickets, artikel-/bloggsök och SaaS-dashboards där användare söker projektnamn och anteckningar.
En bra baselinefråga brukar:
websearch_to_tsquery för användarinmatning.\n- Filtrera enkla begränsningar först (tenant/status/date).\n- Matcha med @@ mot en lagrad tsvector.\n- Sortera med ts_rank/ts_rank_cd plus en stabil tie-breaker som .Spara en förberäknad tsvector och lägg till en GIN-index. Då slipper du räkna ut to_tsvector(...) vid varje förfrågan.
Praktisk setup:
tsvector i samma tabell som du frågar.\n- Skapa en GIN-index på den kolumnen.\n- Se till att din fråga använder tsvector_column @@ tsquery.Använd en generated column när sökdokumentet byggs från kolumner i samma rad (enkelt och svårt att glömma).
Använd en trigger-maintained tsvector när texten beror på relaterade tabeller eller du behöver anpassad logik.
Standardval: generated column först, triggers bara när du verkligen behöver kors-tabell-komposition.
Börja med förutsägbar relevans:
Validera sedan med en liten uppsättning riktiga queries och förväntade toppresultat.
FTS är ord-baserat, inte substring-baserat, så det beter sig inte som LIKE '%term%' för godtyckliga delsträngar.
Om du behöver substring-sök (ID:n, koder, fragment) hantera det separat (vanligtvis med trigram-indexering) istället för att tvinga FTS att göra något det inte är designat för.
Signaler att ni vuxit ur Postgres FTS:
Praktisk väg: behåll Postgres som source-of-truth och börja med asynkron indexering när kravet är tydligt.
updated_at, idDet håller resultaten relevanta, snabba och stabila för paginering.
Det här är den vanligaste åtgärden när sök känns långsam.