PostgreSQL for Laravel: JSONB, Full-Text Search and pgvector
PostgreSQL does a lot of what teams reach for a separate service to do: flexible documents, real full-text search, fuzzy matching, and vector similarity. Here is how to use each from Laravel.
A common pattern is to bolt Elasticsearch on for search and a dedicated vector database on for embeddings, then spend the rest of the project keeping three data stores in sync. For a lot of applications, PostgreSQL already covers those needs well enough that the extra services are not worth their operational cost. Here is how to use JSONB, full-text search, fuzzy matching and pgvector directly from Laravel.
JSONB: flexible documents you can still query
Use a jsonb column for data whose shape varies or evolves, cast it to an array or an ArrayObject on the model, and query it with the -> and ->> operators or Laravel's whereJsonContains. Add a GIN index so those queries use an index instead of scanning every row.
// migration
Schema::table('products', function (Blueprint $table) {
$table->jsonb('attributes')->nullable();
});
DB::statement('CREATE INDEX products_attributes_gin ON products USING gin (attributes)');
// model
protected $casts = ['attributes' => AsArrayObject::class];
// query
Product::whereJsonContains('attributes->tags', 'waterproof')
->where('attributes->weight_grams', '<', 500)
->get();Native full-text search
PostgreSQL full-text search turns text into a tsvector (normalized, stemmed tokens) and matches it against a tsquery, with ranking. In Laravel, $table->fullText() on the pgsql driver creates the tsvector GIN index for you, and whereFullText() runs the match. For control over weighting and language, add a generated tsvector column yourself.
// migration
Schema::table('articles', function (Blueprint $table) {
$table->fullText(['title', 'body']);
});
// query, ordered by relevance
Article::whereFullText(['title', 'body'], 'postgres full text search')
->orderByRaw("ts_rank(to_tsvector('english', title || ' ' || body), plainto_tsquery(?)) DESC", ['postgres full text search'])
->limit(20)
->get();Fuzzy matching with pg_trgm
Full-text search is about words and meaning; pg_trgm is about character similarity, which is what you want for typo tolerance, "did you mean", and matching against product codes or names. Enable the extension, add a GIN trigram index, and use the similarity() function or the % operator.
Vector similarity with pgvector
pgvector adds a vector column type and similarity operators, so you can store embeddings next to your relational data and run semantic search with a normal SQL query. Use an HNSW index for speed, the <=> operator for cosine distance, and keep the metadata filter in the same WHERE clause. For a moderate corpus this is enough to run RAG without a separate vector database.
CREATE EXTENSION IF NOT EXISTS vector;
ALTER TABLE documents ADD COLUMN embedding vector(1536);
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
-- retrieve the 5 closest chunks, filtered by tenant
SELECT id, content
FROM documents
WHERE tenant_id = $1
ORDER BY embedding <=> $2
LIMIT 5;Need, external service, PostgreSQL equivalent
| Need | Usual external service | PostgreSQL native |
|---|---|---|
| Flexible documents | MongoDB | jsonb + GIN index |
| Full-text search | Elasticsearch | tsvector + ts_rank |
| Typo-tolerant matching | Elasticsearch, Algolia | pg_trgm similarity |
| Vector / semantic search | Pinecone, Qdrant, Chroma | pgvector + HNSW |
| Case-insensitive text | Application logic | citext |
When PostgreSQL is not enough
Move to a dedicated engine when the numbers force it: a very large corpus where pgvector index build times and memory become a problem, search that needs advanced relevance tuning, faceting and typo handling across many languages, or a query volume that competes with your transactional load for the same database resources. Until then, one database is one thing to operate, back up and reason about.
FAQ
- Can I do full-text search without Elasticsearch?
- For many applications, yes. PostgreSQL full-text search handles stemming, ranking and multi-column matching, and pg_trgm adds typo tolerance. You outgrow it when you need advanced relevance tuning, faceting, or multi-language search at large scale, at which point a dedicated engine earns its cost.
- How do I store embeddings in Laravel?
- Enable the pgvector extension, add a vector column with the right dimension, and use a package such as pgvector-php or raw SQL to insert and query. Add an HNSW index and use the <=> operator for cosine distance. The embeddings live next to your relational data, so a filtered similarity search is one query.
- Is pgvector enough for a RAG system in production?
- For a small to moderate corpus, yes, and it removes a whole service from your stack. The limits show up with very large indexes, where build time, memory and recall tuning become real work, and when semantic search volume competes with transactional queries for the same database.
- JSONB or a normalized table?
- Normalize data you query, filter, join and constrain on. Use JSONB for data whose shape varies, is written and read as a whole, or evolves faster than you want to run migrations. A common split is normalized columns for the fields you index, JSONB for the long tail of attributes.
- How do I index a JSONB column in Laravel?
- Laravel's schema builder does not expose GIN indexes directly, so create one with DB::statement in the migration: CREATE INDEX ... USING gin (column). After that, whereJsonContains and containment queries on that column use the index instead of a full scan.
PostgreSQL is not just a relational database; it is a relational database with a search engine, a fuzzy matcher and a vector store built in. Reaching for those before adding Elasticsearch or a vector database keeps your stack small, and small stacks are easier to run.
Need help with this topic? Full Stack Development
Discover this service →