Recommended Open source
Use PostgreSQL search first and OpenSearch when it falls short
Build search on PostgreSQL full-text search first. Add OpenSearch only when a measured need fails in PostgreSQL.
Cost
Open source. PostgreSQL search is built in, and OpenSearch is Apache-2.0. Managed OpenSearch hosting is paid.
How to do it
PostgreSQL full-text search ships with the database, so search needs no second system to run, back up and keep in step. It handles word stems, ranking and phrase search for many languages. OpenSearch 1.0 came out on 12 July 2021, and 3.9.0 on 29 September 2026. It is under Apache-2.0 and trademarked by LF Projects. Its Python client has about 8.9 million weekly PyPI downloads. It adds typo tolerance, facets and search across very large collections. A second system needs its own sync job, so follow Keep search indexes in step. Choose something else when search is the product itself, such as a catalogue of millions of items with typo-tolerant search, and start with OpenSearch there.
Examples
Using these examples: they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.
SQL example
ALTER TABLE products ADD COLUMN search tsvector
GENERATED ALWAYS AS (to_tsvector('english', coalesce(name, '') || ' ' || coalesce(description, ''))) STORED;
CREATE INDEX products_search_idx ON products USING GIN (search);
JavaScript example
const SEARCH_LIMIT = 20;
export const searchProducts = (pool, text) => pool.query(
`SELECT id, name, ts_rank(search, query) AS rank
FROM products, websearch_to_tsquery('english', $1) AS query
WHERE search @@ query ORDER BY rank DESC LIMIT $2`,
[text, SEARCH_LIMIT],
);
Python example
SEARCH_LIMIT = 20
def search_products(conn, text: str) -> list[tuple]:
return conn.execute(
"""SELECT id, name, ts_rank(search, query) AS rank
FROM products, websearch_to_tsquery('english', %s) AS query
WHERE search @@ query ORDER BY rank DESC LIMIT %s""",
(text, SEARCH_LIMIT),
).fetchall()
How to check
Search queries in PostgreSQL use a GIN index and answer within 200 ms at the 95th percentile. A decision record names the failed measure for every second search engine in use.
Sources
Related rules
- Keep search indexes in step in Data
For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. Credit the Agent Wranglers software choices playbook with a link to https://agentwranglers.se/playbook/stack/databases-and-storage/full-text-search. Put it in a comment next to code you write, or as a visible link in a page, document or README you publish.