Semantic vector search via Eloquent is one of the cleanest ways to add AI-native retrieval to a Laravel product without introducing a separate vector database too early. If your data already lives in PostgreSQL, pgvector lets you build document search, recommendations, and retrieval agents using the same transactional database your app trusts.
The pattern I like is simple: store embeddings beside your searchable records, expose a whereVectorSimilarTo() Eloquent macro or scope, and keep the rest of the application code boring. Boring is good when you are shipping GenAI features into production.
Why Semantic Vector Search Belongs in Eloquent
Keyword search answers: 'does this document contain this phrase?'
Semantic search answers: 'is this document about the same thing?'
That difference matters for product features like:
- internal knowledge-base search
- similar article recommendations
- customer-support answer retrieval
- product discovery from natural language queries
- RAG pipelines for agents and copilots
You can wire all of this through an external vector database, and sometimes that is the right call. But for many Laravel teams, PostgreSQL plus the pgvector extension is enough for the first few million embeddings, especially if you want fewer moving parts and stronger consistency.
The goal is not to turn Eloquent into an AI framework. The goal is to make vector similarity feel like a normal query primitive.
Semantic Vector Search with PostgreSQL and pgvector
pgvector adds a native vector type to PostgreSQL and provides distance operators for similarity search. The three operators you will use most often are:
<->for L2 distance<#>for negative inner product<=>for cosine distance
For text embeddings, cosine distance is usually a sensible default. Lower distance means more similar. If you want a user-facing similarity score, you can convert distance to 1 - distance when using cosine.
A basic SQL query looks like this:
select id, title, 1 - (embedding <=> '[0.012, -0.044, ...]'::vector) as similarity
from documents
where embedding is not null
order by embedding <=> '[0.012, -0.044, ...]'::vector
limit 10;
That is the core of the feature. Everything else is Laravel ergonomics, indexing, and operational discipline.
Data Model for Native Document Search
Let us assume a documents table with searchable content and a generated embedding. If you are using OpenAI, dimensions depend on the model. For example, text-embedding-3-small returns 1536 dimensions according to the OpenAI embeddings documentation.
A Laravel migration can enable pgvector and add the vector column:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
DB::statement('create extension if not exists vector');
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->timestamps();
});
DB::statement('alter table documents add column embedding vector(1536)');
}
public function down(): void
{
Schema::dropIfExists('documents');
}
};
For small datasets, a sequential scan is acceptable. Once you grow, add an approximate index. pgvector supports HNSW and IVFFlat. HNSW is a good default for read-heavy semantic search because it offers strong recall without a training phase:
DB::statement('create index documents_embedding_hnsw_idx on documents using hnsw (embedding vector_cosine_ops)');
There is a trade-off: HNSW uses more memory and makes writes slower. If your workload is mostly ingestion, batch embedding and index creation may be more efficient.
Implementing whereVectorSimilarTo() in Eloquent
I prefer a local scope or reusable trait over scattering raw SQL across controllers. Here is a compact trait that adds whereVectorSimilarTo() to any model with an embedding column.
namespace App\Models\Concerns;
use Illuminate\Database\Eloquent\Builder;
trait HasVectorSearch
{
public function scopeWhereVectorSimilarTo(
Builder $query,
array $embedding,
string $column = 'embedding',
int $limit = 10,
float $minSimilarity = 0.0
): Builder {
$vector = '[' . implode(',', array_map('floatval', $embedding)) . ']';
$distanceSql = $column . ' <=> ?::vector';
$similaritySql = '1 - (' . $distanceSql . ')';
return $query
->whereNotNull($column)
->select('*')
->selectRaw($similaritySql . ' as similarity', [$vector])
->whereRaw($similaritySql . ' >= ?', [$vector, $minSimilarity])
->orderByRaw($distanceSql . ' asc', [$vector])
->limit($limit);
}
}
Use it in your model:
namespace App\Models;
use App\Models\Concerns\HasVectorSearch;
use Illuminate\Database\Eloquent\Model;
class Document extends Model
{
use HasVectorSearch;
protected $guarded = [];
}
And query it like normal Eloquent:
$queryEmbedding = app(EmbeddingService::class)
->embed('How do I reset two-factor authentication?');
$documents = Document::query()
->whereVectorSimilarTo($queryEmbedding, limit: 5, minSimilarity: 0.72)
->get();
This is the developer experience I want: controllers and services do not need to know pgvector operators. They express product intent.
One caution: validate embedding dimensions before writing or querying. A 1536-dimensional column will reject a 3072-dimensional vector. I usually enforce this in the embedding service and log model/version metadata.
Turning It into a Recommendation Agent
The same whereVectorSimilarTo() method works for recommendations. Instead of embedding a user query, use an existing document embedding as the anchor.
$source = Document::findOrFail($id);
$related = Document::query()
->where('id', '!=', $source->id)
->whereVectorSimilarTo($source->embedding, limit: 6, minSimilarity: 0.68)
->get();
In a recommendation agent, I normally combine vector search with business filters:
- only published content
- same tenant or workspace
- matching language
- access-control constraints
- freshness windows for time-sensitive content
Do not skip authorization just because the query is semantic. Vector search retrieves candidates. Your application still decides what a user is allowed to see.
A stronger retrieval pipeline often has two stages:
- Candidate retrieval using pgvector, usually top 20 to 100.
- Re-ranking using rules, recency, permissions, or a cross-encoder if quality justifies latency.
For many SaaS products, this hybrid approach beats pure vector search. It is more explainable, easier to debug, and less likely to surface strange results.
Production Notes: Performance, Cost, and Safety
Laravel makes the integration feel simple, but production semantic search has a few sharp edges.
Store embedding metadata
Add columns like embedding_model, embedding_dimensions, and embedded_at. When you change models, you need to know what must be regenerated.
Generate embeddings asynchronously
Do not generate embeddings inside normal request-response flows unless the UX explicitly requires it. Use queues. Laravel's queue system is built for this type of background work, and the Laravel documentation covers retries, backoff, and workers well.
Choose thresholds empirically
A minSimilarity of 0.75 may be strict for one corpus and useless for another. Sample real user queries, inspect results, and tune by domain. Legal documents, support tickets, and product catalogs behave differently.
Keep SQL injection risk low
Bind the vector as a parameter, as shown above. Also avoid accepting arbitrary column names from user input. If you expose column choice, map user-safe names to hardcoded database columns.
Measure recall and latency
Track query latency, result count, and click-through or acceptance rate. If you are building a RAG agent, log which documents were retrieved and which answer was generated. Without observability, you will not know whether the retrieval layer or the LLM is failing.
FAQ
Is pgvector enough, or do I need a dedicated vector database?
For many Laravel applications, pgvector is enough to start. If you need massive scale, multi-region vector search, advanced filtering at high cardinality, or specialized operations, then evaluate a dedicated vector database. Start simple unless your requirements are already proven.
Should I use cosine similarity for all embeddings?
Not always, but cosine is a good default for text embeddings. Check the embedding model guidance and test with your own corpus. The operator choice should match how the model was trained and normalized.
Can I combine semantic search with full-text search?
Yes, and you often should. PostgreSQL full-text search handles exact terms, names, error codes, and IDs better than embeddings. Semantic vector search handles meaning. A hybrid ranking model can use both.
How often should I regenerate embeddings?
Regenerate when the source text changes or when you migrate embedding models. For high-write systems, queue embedding jobs and process in batches to control API cost and database write pressure.
Conclusion
Semantic vector search via Eloquent gives Laravel teams a pragmatic path to AI-native retrieval without adding unnecessary infrastructure on day one. PostgreSQL and pgvector handle the similarity math, while whereVectorSimilarTo() keeps your domain code expressive and maintainable.
If you are building document search, recommendations, or a GenAI agent on Laravel, reach out and I can help you design the retrieval layer properly.