Laravel AI
Semantic search on PostgreSQL with Laravel 13
Laravel 13 can do semantic search with a vector column, one query method and the AI SDK, with no separate search service. The work that decides quality is how you split your content and scope your queries.
By itmarkerz Technologies. Updated 30 October 2026.
First, check you need it
Semantic search matches meaning: a search for “best wineries in Napa Valley” can find “Top Vineyards to Visit”, to use the example in Laravel’s search documentation. If your users search for part numbers, names or exact phrases, a full-text index will serve them better. Laravel’s whereFullText works on MariaDB, MySQL and PostgreSQL, and a reranking step can improve the order of its results without storing any vectors.
The database and the column
You need the laravel/ai package, which means Laravel 12 or 13 on PHP 8.3 or later, plus an embedding provider. For the database, this guide uses PostgreSQL with the pgvector extension. Laravel also supports MariaDB 11.7 or later and MongoDB through its own package.
Schema::ensureVectorExtensionExists();
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id')->index();
$table->string('title');
$table->text('content');
$table->vector('embedding', dimensions: 1536)->index();
$table->timestamps();
});The dimension count must match your embedding model: 1536 is the documented figure for OpenAI’s text-embedding-3-small. Calling index() creates an HNSW index for faster similarity searches. On the model, cast the column with AsVector. On managed PostgreSQL, check your host allows the extension before you plan around it. The migration role may need extra permission to create it.
Split, then embed
The docs’ examples embed whole strings, which works for short records. A long manual or policy gives you a single vector that is an average of everything in it, so it matches nothing well. Split long content into sections, keep each section’s title and a link back to its source, and store one row per section. Headings make better boundaries than fixed character counts, where your documents have them.
use Laravel\Ai\Embeddings;
$response = Embeddings::for($sections->pluck('content')->all())
->generate();
// $response->embeddings is in the same order as the inputBatching takes one API call rather than one per section. Run this in a queued job, since a back catalogue takes time, and providers rate-limit. Then deal with updates: when content changes, its vector is out of date. Dispatch a job from a model event and store a hash of the text so an edit that changes nothing doesn’t cost anything.
Querying, with the scope first
$documents = Document::query()
->where('team_id', $user->team_id)
->whereVectorSimilarTo('embedding', $request->input('query'), minSimilarity: 0.4)
->limit(10)
->get();The where on team_id is the line that matters most in a multi-tenant application. It stops one customer’s query from retrieving another’s documents, and an agent’s instructions can’t replace it. If you put this search behind an agent’s SimilaritySearch tool, use the closure form so the same constraint applies.
Passing a string makes Laravel embed the query through your configured provider, which adds an API call and a little latency to each search. whereVectorSimilarTo uses cosine similarity, drops anything under the threshold and sorts by relevance. The whereVectorDistanceLessThan, selectVectorDistance and orderByVectorDistance methods give finer control.
Filtered vector search has a known wrinkle: the approximate index finds near neighbours first, and a strict filter may then discard most of them, so you get fewer results than you asked for. With heavily partitioned tenants, read the pgvector documentation on filtering and test with your smallest tenant, not just your largest.
Judge it before you ship it
Write down twenty or thirty real queries with the documents that should come back. Run them after each change to the splitting, the model or the threshold, and compare. Without this, quality is a matter of opinion, and tuning a threshold is guesswork. Also decide what happens when nothing clears it: an honest “no result” is better than showing the closest poor match, especially if a model will then answer from it.
Cost has two parts, the one-off embedding of your content and an embedding for each query. Our readiness checklist has an estimator and the other checks to run before this reaches users.
Questions
Do we need a dedicated vector database?
Not to start. Laravel's vector methods work on PostgreSQL with pgvector, so vectors can sit beside the rows they describe and share their transactions, backups and permissions. Consider a dedicated service once you have measured a limit you can't solve in your database.
What happens if we change embedding model?
Vectors from different models aren't comparable, and the dimension count may differ. Plan to re-embed everything into a new column, switch reads to it, then drop the old one. Record the model name with your data so you can tell what produced each vector.
What minSimilarity should we use?
There isn't a universal number. Laravel's documentation uses 0.4 in one example and the SimilaritySearch tool example uses 0.7. Test against real queries where you know which documents should come back, then set it from what you see.
More on Laravel AI
- Adding AI to an existing Laravel app: version, data and cost checksA readiness checklist and a cost estimator, before anyone writes an agent.
- Laravel AI SDK or Prism?What each package requires, and how to choose for an existing application.
- Exposing your Laravel app over MCPWhat to expose first, how to authenticate it and how to test it.
Want search that answers from your own data?
Tell us what people search for and what they get today. We will tell you whether vectors are the right tool or whether full-text would do.
We reply within one business day (IST).