Vol. 01 — 2026

Laravel 13 Semantic Search: pgvector in 10 Mins

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) creates vector(1536) on Postgres with pgvector 0.8+ (no raw SQL).
  • whereVectorSimilarTo('embedding', toEmbeddings($query), 5) — Eloquent scope that does ORDER BY embedding <=> :vec LIMIT 5 with HNSW under the hood. No DB::raw.
  • toEmbeddings($text) helper — calls your configured AI SDK provider (OpenAI, Anthropic, Gemini) and returns floats; swap provider via .env with 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; toEmbeddings 12ms 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.

← All journal articles Get in touch →