Deploy PostgreSQL + Hybrid Search
Embeddings, BM25 and 30+ language search — replace your search cluster.
PostgresSearch
Just deployed
/var/lib/postgresql/data
Deploy and Host PostgreSQL + Hybrid Search on Railway
PostgreSQL 18 with BM25 ranking, vector search and analyzers for 30+ languages — including Chinese, Japanese, Korean and Thai — built on Railway's own Postgres image, so you keep TLS and pgBackRest point-in-time recovery.
It exists to answer one question: do you need a second service just to search your own data? BM25 relevance, vector similarity and ordinary SQL joins all run in one query here, which is impossible across a Postgres/Elasticsearch split without a sync pipeline to keep them agreeing.
About Hosting PostgreSQL + Hybrid Search
Deploying is one click and there is nothing to configure afterwards. Every extension is
created on first boot — vector, vchord_bm25, icu_ext, unaccent, pg_trgm,
btree_gin, fuzzystrmatch — along with a multilingual analyzer and a small SQL API for
creating and querying search indexes. A stock Postgres leaves you to work out which
CREATE EXTENSION statements to run; this one has run them before you connect.
Storage is a Railway volume mounted at /var/lib/postgresql/data, so your data survives
redeploys. Because the image is built on Railway's postgres-ssl, the TLS certificate and
pgBackRest backup tooling come with it rather than being traded away.
Railway's own database dashboard comes with it too, and this is worth knowing before you reach for a client. The console gives you Data for browsing tables and running queries, Stats, and Config for connection details, connection pooling and an Extensions view. That last one earns its place here more than it would on a stock Postgres: the extensions are the product, so being able to confirm all eight are installed from the console — without connecting to anything — is how you know the deploy did what it claims.
Idle memory is about 7 MB, so leaving it running between projects costs almost nothing.
Why Deploy PostgreSQL + Hybrid Search?
One database instead of a database plus a search cluster. Elasticsearch's JVM floor is roughly 1 GB before it indexes anything. This is 7 MB idle and 88 MB after a 20,000 document workload, and it removes the sync pipeline entirely — your search index cannot drift from your data when it is your data.
You get Railway's database dashboard, not just a connection string. Deploy it and the console gives you Data for browsing tables and running queries, Stats, and Config for connection details, connection pooling and an Extensions view. A custom Postgres image usually trades that away — you get a container and a URL, and every look at your own data goes through a client you had to set up. This one keeps the dashboard, verified on a live deploy, so you can go from deploy to first search result without leaving the browser.
It speaks your users' languages. Postgres' own text search cannot segment scripts that do not put spaces between words; it returns a whole Chinese or Thai sentence as one unsearchable token. This routes each script to a tokenizer that handles it — ICU word segmentation for Chinese, Thai, Khmer, Lao and Burmese, bigrams for Japanese and Korean, stemming and stopwords for the 30 spaced languages — within a single document. It is verified against a corpus of 13 locales and 44,000 documents of real prose in six languages, and the engine underneath it is upstream-tested against English only, so that verification is the guarantee.
Search that behaves like a search engine. Typo tolerance, highlighting that works in CJK, per-column boosting, faceting, phrase and proximity search, and hybrid BM25 + vector ranking — all as SQL you can read, with no service to operate.
Honest numbers. On 44,196 documents of real prose in six languages: 20–39 ms for a top-10 search, 250–450 documents per second to ingest. Synthetic corpora make both look far better — they disagree with real text by more than 10× on ingest — so the figures quoted here are the real ones. The measurements, the corpora and the figures that turned out wrong are all published.
Common Use Cases
- Retrieval for RAG and AI applications — chunk documents, store embeddings beside BM25 vectors, and fuse both rankings in one query instead of running a vector database next to your Postgres.
- In-app search for a multilingual product — search boxes, help centres, message history and catalogues where users type Chinese, Japanese, Korean or Thai and expect it to work.
- Replacing a small Elasticsearch or Meilisearch deployment whose only job is to index data that already lives in Postgres.
Dependencies for PostgreSQL + Hybrid Search
None. Everything the database needs is inside the image, and no other service is required.
Deployment Dependencies
- VectorChord-bm25 — the BM25 index, dual-licensed AGPLv3 / Elastic License v2
- icu_ext — ICU word segmentation
- postgres-ssl — Railway's PostgreSQL base image, providing TLS and pgBackRest
- pgvector — vector similarity
Example Usage Script
You do not need a client for this. Open the service's Data tab in the Railway console
and paste the script into the query box — no psql, no connection string, no local setup.
It confirms everything works and shows you the shape of a search. If you would rather
connect from your own machine, the template sets DATABASE_URL for that.
-- 1. A table to search. `embedding` is ordinary pgvector.
CREATE TABLE items (
id bigserial PRIMARY KEY,
body text,
embedding vector(3)
);
INSERT INTO items (body, embedding) VALUES
('sourdough bread starter', '[1,2,3]'),
('a guide to rye bread', '[4,5,6]'),
('酸種麵包的做法', '[2,2,2]');
-- 2. One call adds the BM25 column, its trigger and its index,
-- and backfills the rows already there.
SELECT public.bm25_create_index('items', 'body');
-- 3. Keyword search, ranked by BM25.
SELECT i.body, s.score
FROM public.bm25_search('items', 'bread', 10, 'body') s
JOIN items i USING (id);
-- 4. The same search in Chinese. No configuration, no second index.
SELECT i.body
FROM public.bm25_search('items', '麵包', 10, 'body') s
JOIN items i USING (id);
-- 5. Vector similarity, exactly as in any pgvector database.
SELECT body, l2_distance(embedding, '[3,1,2]') AS distance
FROM items ORDER BY distance LIMIT 5;
The last query uses l2_distance rather than pgvector's distance operator, because
Railway's template pages HTML-escape angle brackets — pasted from this page, the operator
form would arrive mangled and fail. Both compute the same distance. The operator form is
the one an HNSW or IVFFlat index can accelerate, so switch to it once you are past this
first query; pgvector's README has the operator table.
bm25_search returns only documents that share a term with the query, and the BM25 index
name never appears in your code. From there:
- pass
truefor thefuzzyargument to get typo tolerance bm25_search_multito weight a title above a bodybm25_chunkto split long documents before indexing thembm25_headlineto highlight matches, including in Chinese and Thai
Implementation Details
Storage lives on a Railway volume at /var/lib/postgresql/data, and the image keeps
PGDATA inside it automatically, so a redeploy never loses data.
Licensing. This template's own files are MIT, but the image ships vchord_bm25, which
is dual-licensed AGPLv3 / Elastic License v2. Neither is permissive. Running the image
as your own database triggers no obligation; modifying vchord_bm25 itself and exposing
the modified version over a network does.
Template Content
PostgresSearch
ghcr.io/yuting1214/postgres-search:0.4.0