---
title: "Deploy PostgreSQL + Hybrid Search"
description: "Embeddings, BM25 and 30+ language search — replace your search cluster."
category: "Storage"
url: https://railway.com/deploy/postgresql-hybrid-search
---

# Deploy PostgreSQL + Hybrid Search

Embeddings, BM25 and 30+ language search — replace your search cluster.

**[Deploy PostgreSQL + Hybrid Search on Railway](https://railway.com/template/postgresql-hybrid-search)**

- **Creator:** Mark Chen's Projects
- **Category:** Storage
- **Total deploys:** 2

## Template content

### PostgresSearch https://devicons.railway.app/i/postgresql.svg

- **Image:** ghcr.io/yuting1214/postgres-search:0.4.0

## Documentation

# 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](https://github.com/tensorchord/VectorChord-bm25) — the BM25 index,
  dual-licensed AGPLv3 / Elastic License v2
- [icu_ext](https://github.com/dverite/icu_ext) — ICU word segmentation
- [postgres-ssl](https://github.com/railwayapp-templates/postgres-ssl) — Railway's
  PostgreSQL base image, providing TLS and pgBackRest
- [pgvector](https://github.com/pgvector/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.

```sql
-- 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 `true` for the `fuzzy` argument to get typo tolerance
- `bm25_search_multi` to weight a title above a body
- `bm25_chunk` to split long documents before indexing them
- `bm25_headline` to 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.


## Similar templates

- [Garage S3 Storage](https://railway.com/deploy/garage-s3-storage) — Ultra-light S3 server: fast, open-source, plug-and-play.
- [Redis](https://railway.com/deploy/redis-1) — Self Host Latest Redis with Railway
- [EasyImg](https://railway.com/deploy/easyimg) — Simple self-hostable Nuxt.js personal image hosting system.

Open this page in a browser: https://railway.com/deploy/postgresql-hybrid-search
