Embeddings vs Postgres Full-Text Search for Internal Knowledge
Vector search is the reflex for internal knowledge search, and Postgres full-text search often does the job with less to build. How each one works, where each tends to win, and how to test both on your own documents.
Teams building search over their own documents tend to reach for vector embeddings first. Postgres has had full-text search built in for years, and for many internal knowledge bases it does the job with less to build and less to run. The way to choose is to test both against the questions your staff actually ask.
How each one works
Postgres full-text search turns each document into a tsvector, a list of normalized words with their endings stripped, so "deploying" and "deployed" both match "deploy". A GIN index keeps lookups fast. Queries go through websearch_to_tsquery, which accepts what people type into a search box, quoted phrases and a minus sign for exclusion included, and you order the matches by ts_rank. If a document never uses the searcher's words, or a form of them, full-text search won't find it.
Embeddings turn each chunk of text into a vector, a long list of numbers from an embedding model, placed so that passages with similar meaning sit close together. The vectors live in a pgvector column. A query is embedded with the same model and compared by cosine distance, usually through an HNSW index. That's how a search for "customer who wants to cancel" can find a page about retention. It's also how a search can return a page on the right topic that doesn't answer the question.
Where each tends to win
Full-text search tends to do better when the searcher and the writer use the same words. Internal documents are full of them: product names, system names, error codes, form numbers, policy titles. Someone searching "hotfix deploy" wants the runbook that says "hotfix", and an exact match on that word is a strong signal. An embedding search can rank a general page about deployments above it, because that page is close in meaning to everything about deploying.
Embeddings tend to do better when the vocabulary doesn't line up. A new hire asks how to handle a customer who wants to leave, and the documents talk about retention, win-back offers and churn risk. Full-text search finds nothing, or finds the wrong page. Many embedding models also handle more than one language, while a Postgres text search configuration covers one language at a time.
Both fail when the answer isn't written down. Each one returns the closest document it has, and a confident wrong result does more damage than an empty one.
Run the comparison on your own documents
Published benchmarks are run on other people's documents. You'll learn more from a small test on your own, and most of the work is scoring.
- Collect 40 to 50 real questions from staff. Pull them from chat threads, help-desk tickets and the questions new hires ask. Don't write them yourself. People who know the documents phrase questions the way the documents are written, which tilts the test toward full-text search.
- Build both indexes over the same documents in the same database. Full-text on title and body. Embeddings on chunks of a few hundred words, with the document title repeated in each chunk so it keeps its context.
- Run every question through both and keep the top five results from each.
- Score each set of results from 0 to 3: 3 if the answer is in the results, 2 if the right document is there but the answer takes digging, 1 if the results are related, 0 if they aren't. Hide which system produced which results while scoring. If you can, have the person who would normally answer the question do the scoring.
- Record latency for each query, including the time to embed the query, which is a separate request to the embedding model. Note the index sizes, the setup time and the monthly cost of keeping embeddings current as documents change.
- Sort the questions into three groups: full-text did better, embeddings did better, neither found it. Reading the questions in each group is more useful than comparing average scores.
The hybrid pattern
If the first two groups are both big enough to matter, combine the two. A simple version:
- Run full-text search first.
- If the best full-text score falls below a threshold, tuned on your test questions, run the embedding search.
- If both come back weak, say there's no good match and offer a way to ask a person.
The other common approach runs both searches on every query and merges the two ranked lists, often with reciprocal rank fusion, which scores each document by where it ranks in each list. It needs an embedding request on every query, and there's no threshold to tune.
Either way, score the hybrid on the same test questions. Keep it only if it beats both single methods, because it adds a second index and a second code path to maintain.
What tends to break
Stale vectors. Documents change and their embeddings don't, unless re-embedding runs whenever a document is saved. Full-text search has the same problem if the tsvector is updated by hand. A generated column keeps it in step automatically.
Chunk size. A chunk that's too large blurs several topics into one vector. One that's too small loses the context that made it useful. Splitting on headings keeps each chunk about one thing.
Thresholds that drift. A cutoff tuned on this quarter's questions can stop working as documents and questions change. Re-run the test set after any large content change.
Permissions. A result the person isn't allowed to open is either a leak or a dead end. Apply the same access rules to both indexes, ideally in the database with row-level security.
How to measure it in production
Log every search: the query, the results shown and what the person clicked. A search with no click in the top five is a likely failure. Every couple of weeks, sort the failures the way you sorted the test questions: vocabulary mismatch, poor ranking, or a missing document. If most are vocabulary mismatch, add embeddings. If most are ranking problems, tune the weights and boost title matches. If most are missing documents, write them.
What to build first
One, full-text search on title and body, with a generated tsvector column and a GIN index. It's the smallest version that works, and if you're on Postgres it's already in your database.
Two, search logging, so failures show up.
Three, embeddings, only if the failed searches cluster around vocabulary mismatch. If they cluster around missing documents, the fix is writing those documents.
Get the next one.
Field notes from real deployments, written the way this one was. No schedule promised and nothing sold to you. Unsubscribe whenever.