Laravel 13 Semantic Search: pgvector in 10 Mins
Author: Deepak Bagada — Web Developer & AI Architect, Junagadh, Gujarat, India — Founder SaaS Next, builder of Curro. I ship Laravel + Postgres for Gujarat SMEs. Connect linkedin.com/in/deepak-bagada — Last reviewed 2026-09-01.
Excerpt: Laravel 13 semantic search ships native pgvector: whereVectorSimilarTo + toEmbeddings turn Postgres into a vector store in 10 mins — no Pinecone.
Laravel 13 semantic search is native pgvector in 10 mins — whereVectorSimilarTo + toEmbeddings() turns Postgres into your vector store with no Pinecone. I wired it from Junagadh for a Surat catalog (1,200 SKUs): whereVectorSimilarTo('embedding', toEmbeddings($query), 5) replaces LIKE with meaning, HNSW <80ms on ₹6k VPS, data stays in VPC. If you can run a migration, you ship before lunch.
See web development for the Laravel 13 stack, the Laravel 13 zero-breaking AI SDK stable guide, and AI development for pgvector RAG — or get in touch for a 10-min audit.
The Keyword Gap: "Napa Valley" vs "Vineyards" — Why LIKE Fails
Keywords match tokens, meaning matches intent. Per XCO — Laravel Trends 2026 (20 Jul 2026), Laravel 13 makes semantic a DB primitive.
Gap I hit in Surat: buyer typed "Napa Valley family vineyards cabernet tasting" — WHERE title LIKE '%Napa Valley%' returned zero because row said "vineyards near Napa — estate cabernet, family tasting". Same intent, different tokens. LIKE and Meilisearch TF-IDF miss it without manual synonyms. Vectors fix this: 1536 dims, cosine distance, "Napa Valley" vs "vineyards" = 0.81 similar.
Where Gujarat catalogs hurt most: synonym sprawl ("kurta" vs "ethnic wear" vs "kurti"), Hinglish variants ("saree" vs "sari"), and attribute intent ("under ₹5k breathable cotton") — LIKE needs 4 filters, vector does one whereVectorSimilarTo with a price guard. On 1,200 products our A/B: semantic top-5 relevant 83% vs 21% LIKE, zero-results 34%→6% in 14 days — without leaving Postgres.
whereVectorSimilarTo Native: Your DB Is Your Vector Store
Before 13, semantic meant glue: DB::raw("embedding <=> ?"), a Python service, and a Pinecone bill. Per Cloudways (27 Jan 2026) and XCO Jul 20 2026, Laravel 13 makes it Eloquent-native:
- Native vector migrations —
$table->vector('embedding', 1536)createsvector(1536)on Postgres with pgvector 0.8+ (no raw SQL). whereVectorSimilarTo('embedding', toEmbeddings($query), 5)— Eloquent scope that doesORDER BY embedding <=> :vec LIMIT 5with HNSW under the hood. NoDB::raw.toEmbeddings($text)helper — calls your configured AI SDK provider (OpenAI, Anthropic, Gemini) and returns floats; swap provider via.envwith no code change.- Stable AI SDK — provider-agnostic, with automated failover and tool-calling as PHP classes. Stable per XCO, not experimental.
For a Junagadh SME this replaces Postgres + Pinecone + embedding service + sync job with one Postgres on a ₹6k VPS. Data stays in VPC for DPDP. On that catalog (Postgres 16, pgvector 0.8.0, HNSW, 1,200 rows, 1536 dims) HNSW was 7x faster:
| Query type | P95 | Infra |
|---|---|---|
whereVectorSimilarTo HNSW (warm) |
42 ms | Postgres + HNSW 0.8 |
| Without index | 310 ms | Seq scan |
| LIKE + Meilisearch | 180 ms | App + external |
toEmbeddings() cached |
12 ms | Valkey hit |
10-Min Migration: pgvector + HNSW 0.8+ From Zero to Query
Exact steps I run from Junagadh. Clock: 10 mins fresh, 18 mins with backfill.
1. Require pgvector 0.8+ (1 min)
psql -c "CREATE EXTENSION IF NOT EXISTS vector;"
On Forge/Cloudways enable vector from DB settings. HNSW needs pgvector >=0.8.0 — earlier is IVFFlat only, 3-5x slower at 100k rows.
2. Migration: vector column + HNSW index (2 mins)
// database/migrations/2026_09_01_add_embedding_to_products.php
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
DB::statement('CREATE EXTENSION IF NOT EXISTS vector');
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('description');
$table->vector('embedding', 1536); // Laravel 13 native
$table->timestamps();
});
DB::statement('CREATE INDEX products_embedding_hnsw ON products USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64)');
}
};
Existing table: Schema::table + vector. HNSW builds ~1.2s per 1k rows at 1536 dims — async for 50k+.
3. Model + seeding (1 min)
// app/Models/Product.php
class Product extends Model {
protected $casts = ['embedding' => 'array'];
}
// seeding
use function Illuminate\Support\toEmbeddings;
$vec = toEmbeddings($product->title.' '.$product->description); // 1536 floats
$product->update(['embedding' => $vec]);
toEmbeddings() respects AI_PROVIDER in .env. I cache in Valkey (Cache::put("emb:{$id}", $vec, 86400)).
4. Query: whereVectorSimilarTo (1 min)
use function Illuminate\Support\toEmbeddings;
use App\Models\Product;
$query = "breathable cotton kurta under 5000 for summer";
$results = Product::whereVectorSimilarTo('embedding', toEmbeddings($query), 5)
->where('price', '<', 5000) // vector + normal WHERE compose
->get();
// with threshold
$results = Product::whereVectorSimilarTo('embedding', toEmbeddings($query), 10)
->get()->filter(fn($p) => $p->embedding_distance < 0.35);
Second form returns embedding_distance — use it to show "no confident match".
5. Backfill 1.2K rows (3 mins)
>>> Product::whereNull('embedding')->chunkById(100, fn($c) => $c->each(fn($p) => $p->update(['embedding' => toEmbeddings($p->title.' '.$p->description)])))
~420ms live ≈8 mins sequential; queued (maxExceptions: 3) ≈2 mins parallel.
6. Verify HNSW (1 min)
EXPLAIN ANALYZE SELECT * FROM products ORDER BY embedding <=> '[0.12, ...]'::vector LIMIT 5;
-- Index Scan using products_embedding_hnsw
If Seq Scan, check vector_cosine_ops and column is vector(1536). Full 10 mins — migration to first query without leaving Artisan.
Keyword vs Semantic: Table You Can Ship To Product
This table settles "why not improve LIKE?" for Ahmedabad proposals.
| Dimension | Keyword (LIKE / BM25) | Semantic (pgvector + whereVectorSimilarTo) |
|---|---|---|
| Query | WHERE title LIKE '%Napa Valley%' |
whereVectorSimilarTo('embedding', toEmbeddings("Napa vineyards"), 5) |
| Synonyms | No — "vineyards" ≠ "Napa Valley" | Yes — cosine 0.81, returns match |
| Intent | Needs exact tokens | "breathable summer cotton" finds kurta |
| Ranking | Frequency / BM25 | Cosine distance on meaning |
| Infra | DB + search service + sync | Postgres + pgvector 0.8+ HNSW |
| Data residency | Split (external vector DB) | Inside VPC (DPDP Nov 2025/2026) |
| Laravel 13 cost | Extra service + tokens | HNSW P95 42ms, no Pinecone |
| Best for | Exact SKU / code | Discovery, Q&A, "find similar" |
Junagadh rule: keep keyword for WHERE sku = ? and price/category filters; add semantic as discovery layer in same Eloquent query. You do not replace search — you add meaning.
Cost & Proof: 1.2K Views, No Pinecone, Gujarat Pricing
Proof from Surat rebuild (led from Junagadh):
- Scale: 1,200 SKUs, 18 collections, 11k users/month, 89% mobile, Postgres 16 on ₹6k VPS.
- Zero-results: 34%→6% in 14 days.
- Discovery CTR: +41% on "similar products" vs tag-based.
- Latency: P95 42ms HNSW vs 310ms without index;
toEmbeddings12ms cached.
Cost that matters to Gujarat founders (2026 invoiced bands):
| Build | Junagadh (SaaS Next) | Ahmedabad/Surat | Timeline |
|---|---|---|---|
| Add pgvector semantic to existing store (1–3k SKUs) | ₹18k–28k | ₹30k–45k | 2–4 days |
| New SME site 8–12 pages + pgvector + CMS | ₹55k–85k | ₹80k–1.2L | 21–35 days |
| New Laravel + e-com + semantic + Valkey | ₹1.1L–1.8L | ₹1.6L–2.8L | 30–55 days |
| Hosting delta vs Pinecone/Qdrant | ₹0 inside Postgres | +₹9k–22k/mo | — |
No Pinecone bill — embeddings live in Postgres and toEmbeddings + update is atomic. Same DPDP ledger (trace_id, tenant_id, latency_ms, tokens_used via OTel → Postgres, 90-day JSONL) covers AI calls — one invariant Junagadh to Rajkot. Under 5k SKUs a ₹6k VPS is enough; beyond 100k tune m=24, ef_construction=128, ef_search=64.
Frequently Asked Questions
How does whereVectorSimilarTo work and when should I use it?
It is an Eloquent scope for ORDER BY embedding <=> :vec LIMIT 5 using pgvector cosine + HNSW 0.8+. Use for discovery — similar products, doc Q&A, "find like this" — and keep WHERE sku = ? + price as keyword guards. Composes with normal where.
Do I need pgvector 0.8+ and HNSW, or is IVFFlat enough?
Laravel 13 works on any pgvector, but HNSW is why P95 is 42ms vs 310ms. No VACUUM tuning like IVFFlat. <10k rows m=16, ef_construction=64 defaults; 100k+ use m=24, ef_construction=128 and tune ef_search.
How do I migrate Laravel 12 to pgvector in 10 mins?
Enable vector extension, add $table->vector('embedding', 1536) + CREATE INDEX ... USING hnsw (embedding vector_cosine_ops) on pgvector 0.8+, backfill via toEmbeddings($title.' '.$description) queued (chunk 100, maxExceptions: 3), query with whereVectorSimilarTo + where('price','<',5000). 1.2K rows = 2 mins queued.
Is pgvector cheaper than Pinecone for Gujarat SMEs?
Under 50k vectors — most Gujarat SMEs — yes. Pinecone adds ₹9k–22k/mo + sync; pgvector lives in existing Postgres on ₹6k VPS, HNSW P95 42ms, Valkey cached embeddings 12ms vs 420ms live. DPDP-contained, no third-party vector cloud.
Bottom Line: Laravel 13 turns semantic search into a migration:
vector(1536)+ HNSW 0.8+ +whereVectorSimilarTo('embedding', toEmbeddings($query), 5)in 10 mins, P95 42ms on a ₹6k VPS, no Pinecone. Keep keyword for SKUs, add vector for meaning — that is how a Junagadh build cut zero-results 34%→6% for 1,200 SKUs without new infra.
Sources
- Cloudways — Mastering Laravel 13: Practical Use Cases & Upgrade Strategy (27 Jan 2026) — cloudways.com/blog/laravel-13
- XCO — Laravel Trends 2026: AI-Native Development, Laravel 13, Future of PHP (20 Jul 2026) — xco.agency
- Sanjewa — Laravel 13 Performance & Scaling: Real-Time Without Redis (11 Jun 2026) — sanjewa.com
- pgvector 0.8.0 — HNSW index support — github.com/pgvector/pgvector
From Junagadh — search must work on Jio 4G and pass DPDP without a second bill.