{"id":11467,"date":"2026-05-03T07:39:25","date_gmt":"2026-05-03T07:39:25","guid":{"rendered":"https:\/\/programmingfields.com\/?p=11467"},"modified":"2026-05-03T07:59:44","modified_gmt":"2026-05-03T07:59:44","slug":"laravel-13-semantic-search-pgvector","status":"publish","type":"post","link":"https:\/\/programmingfields.com\/laravel-13-semantic-search-pgvector\/","title":{"rendered":"Laravel 13 Semantic Search: Build AI-Powered Search with pgvector"},"content":{"rendered":"\n<p>Standard search works by matching keywords. So, when a user searches for the <strong>best wineries in Napa Valley<\/strong>, traditional search won&#8217;t find an article titled <strong>Top Vineyards to Visit<\/strong> \u2014 even though they clearly mean the same thing. That\u2019s exactly where the keyword gap appears. However, this is also where Laravel semantic search truly shines. Instead of relying on exact keyword matches, Laravel 13 introduces a smarter approach. In fact, it closes that gap by understanding the meaning behind the search, not just the words themselves.<\/p>\n\n\n\n<p>Even better, Laravel 13 now ships with native vector search support directly inside the query builder. So, there\u2019s no need for Pinecone, and more importantly, no need for a separate Python service. All you need is PostgreSQL, the pgvector extension, and just a few lines of clean Laravel code.<\/p>\n\n\n\n<p>Now, let\u2019s build it step by step.<\/p>\n\n\n\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_82_2 counter-hierarchy ez-toc-counter ez-toc-light-blue ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Table of Contents<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/programmingfields.com\/laravel-13-semantic-search-pgvector\/#What_You_Need\" >What You Need<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/programmingfields.com\/laravel-13-semantic-search-pgvector\/#Step_1_%E2%80%94_Set_Up_the_Vector_Column_in_Your_Migration\" >Step 1 \u2014 Set Up the Vector Column in Your Migration<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/programmingfields.com\/laravel-13-semantic-search-pgvector\/#Step_2_%E2%80%94_Cast_the_Vector_Column_on_Your_Model\" >Step 2 \u2014 Cast the Vector Column on Your Model<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/programmingfields.com\/laravel-13-semantic-search-pgvector\/#Step_3_%E2%80%94_Generate_and_Store_Embeddings\" >Step 3 \u2014 Generate and Store Embeddings<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/programmingfields.com\/laravel-13-semantic-search-pgvector\/#Step_4_%E2%80%94_Run_a_Semantic_Search\" >Step 4 \u2014 Run a Semantic Search<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/programmingfields.com\/laravel-13-semantic-search-pgvector\/#Bonus_%E2%80%94_Combine_Full-Text_Search_with_AI_Reranking\" >Bonus \u2014 Combine Full-Text Search with AI Reranking<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/programmingfields.com\/laravel-13-semantic-search-pgvector\/#Real-World_Use_Cases\" >Real-World Use Cases<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/programmingfields.com\/laravel-13-semantic-search-pgvector\/#Final_Thoughts\" >Final Thoughts<\/a><\/li><\/ul><\/nav><\/div>\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"What_You_Need\"><\/span><strong>What You Need<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Before you start, make sure you have:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Laravel 13<\/strong> installed<\/li>\n\n\n\n<li><strong>PostgreSQL<\/strong> as your database driver<\/li>\n\n\n\n<li><strong>Laravel AI SDK<\/strong> \u2014 <code>composer require laravel\/ai<\/code><\/li>\n\n\n\n<li>An API key from an embedding provider (OpenAI, Anthropic, etc.)<\/li>\n<\/ul>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>Note:<\/strong> <strong>Laravel pgvector<\/strong> support only works with <strong>PostgreSQL<\/strong>. It is not available on <strong>MySQL<\/strong> or <strong>SQLite<\/strong> in Laravel 13.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Step_1_%E2%80%94_Set_Up_the_Vector_Column_in_Your_Migration\"><\/span><strong>Step 1 \u2014 Set Up the Vector Column in Your Migration<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>First, enable the pgvector extension and add a vector column. Now, to add a column, firstly, you will need a migration. Hence, we will create a new migration for articles.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">php artisan make:model Article -m<\/code><\/pre>\n\n\n\n<p>The above command will create a model and a migration. Now, with the model and migration in place, let&#8217;s go ahead and define the schema in the migration.<\/p>\n\n\n\n<p>Laravel 13 makes this very simple:<\/p>\n\n\n\n<pre title=\"database\/migrations\/xxxx_create_articles_table.php\" class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">use Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nSchema::ensureVectorExtensionExists();\n\nSchema::create('articles', function (Blueprint $table) {\n    $table->id();\n    $table->string('title');\n    $table->text('body');\n    $table->vector('embedding', dimensions: 1536)->index(); \/\/ 1536 for OpenAI\n    $table->timestamps();\n});<\/code><\/pre>\n\n\n\n<p>The <code>Schema::ensureVectorExtensionExists()<\/code> call enables the pgvector extension on your PostgreSQL database. The <code>->index()<\/code> chain automatically creates an HNSW index. That index dramatically speeds up <strong>Laravel vector search<\/strong> on large datasets.<\/p>\n\n\n\n<p>Respectively, you will have to update the fillable property in the model.<\/p>\n\n\n\n<pre title=\"app\/Models\/Article.php\" class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">&lt;?php\n\nnamespace App\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\n\nclass Article extends Model\n{\n    protected fillable = ['title', 'body', 'embedding'];\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Step_2_%E2%80%94_Cast_the_Vector_Column_on_Your_Model\"><\/span><strong>Step 2 \u2014 Cast the Vector Column on Your Model<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Next, cast the <code>embedding<\/code> column to an array on your Eloquent model. This lets Laravel automatically convert between PHP arrays and the database&#8217;s vector format:<\/p>\n\n\n\n<pre title=\"app\/Models\/Article.php\" class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">&lt;?php\n\nnamespace App\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\n\nclass Article extends Model\n{\n    protected function casts(): array\n    {\n        return [\n            'embedding' => 'array',\n        ];\n    }\n}<\/code><\/pre>\n\n\n\n<p>That&#8217;s all the model configuration needed for <strong>Laravel semantic search<\/strong> to work.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Step_3_%E2%80%94_Generate_and_Store_Embeddings\"><\/span><strong>Step 3 \u2014 Generate and Store Embeddings<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>In order to generate and store an embedding, let&#8217;s create a controller first.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">php artisan make:controller ArticleController<\/code><\/pre>\n\n\n\n<p>When you create or update an article, generate an embedding for its content and store it. Use the <code>toEmbeddings()<\/code> method available on Laravel&#8217;s <code>Stringable<\/code> class:<\/p>\n\n\n\n<pre title=\"app\/Http\/Controllers\/ArticleController.php\" class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">use Illuminate\\Support\\Str;\nuse App\\Models\\Article;\n\n$article = Article::create([\n    'title' => 'Top Vineyards to Visit in California',\n    'body'  => 'California wine country has world-class vineyards...',\n]);\n\n\/\/ Generate embedding from the article body\n$embedding = Str::of($article->body)->toEmbeddings();\n\n\/\/ Store it\n$article->update(['embedding' => $embedding]);<\/code><\/pre>\n\n\n\n<p>For bulk processing \u2014 like seeding your entire articles table. Firstly, create a seeder using the below artisan command.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">php artisan make:seeder ArticleEmbeddingSeeder<\/code><\/pre>\n\n\n\n<p>Now, after having the seeder, use the <code>Embeddings<\/code> class. It is far more efficient because it sends a single API request instead of one per item:<\/p>\n\n\n\n<pre title=\"database\/seeders\/ArticleEmbeddingSeeder.php\" class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">use Laravel\\Ai\\Embeddings;\nuse App\\Models\\Article;\n\n$articles = Article::whereNull('embedding')->get();\n\n$response = Embeddings::for($articles->pluck('body')->all())->generate();\n\n$articles->each(function ($article, $index) use ($response) {\n    $article->update(['embedding' => $response->embeddings[$index]]);\n});<\/code><\/pre>\n\n\n\n<p>This approach makes it very practical to set up <strong>Laravel pgvector<\/strong> on an existing dataset.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Step_4_%E2%80%94_Run_a_Semantic_Search\"><\/span><strong>Step 4 \u2014 Run a Semantic Search<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Now comes the exciting part. Once embeddings are stored, <strong>Laravel semantic search<\/strong> is just one query away.<\/p>\n\n\n\n<p>So, let&#8217;s create another controller where we will use this vector search.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">php artisan make:controller SearchController<\/code><\/pre>\n\n\n\n<p>Now, in this controller, let&#8217;s use this method Use <code>whereVectorSimilarTo()<\/code> to search by meaning:<\/p>\n\n\n\n<pre title=\"app\/Http\/Controllers\/SearchController.php\" class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">use App\\Models\\Article;\n\n$results = Article::whereVectorSimilarTo('embedding', 'best wineries in Napa Valley')\n    ->limit(10)\n    ->get();<\/code><\/pre>\n\n\n\n<p>That&#8217;s it. Under the hood, Laravel converts the search string into an embedding using the AI SDK. Then it performs a cosine similarity search against stored vectors using <strong>whereVectorSimilarTo Laravel<\/strong>. Finally, it returns the most semantically relevant results \u2014 even if they share zero exact words with the query.<\/p>\n\n\n\n<p>You can also set a minimum similarity threshold. This filters out weak matches:<\/p>\n\n\n\n<pre title=\"app\/Http\/Controllers\/SearchController.php\" class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">$results = Article::whereVectorSimilarTo(\n    column: 'embedding',\n    value: 'best wineries in Napa Valley',\n    minSimilarity: 0.7  \/\/ 0.0 to 1.0 \u2014 higher means stricter match\n)\n->limit(10)\n->get();<\/code><\/pre>\n\n\n\n<p>A threshold of <code>0.7<\/code> works well for most <strong>Laravel vector search<\/strong> use cases. Adjust it based on your content and how strict you want the matching to be.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Bonus_%E2%80%94_Combine_Full-Text_Search_with_AI_Reranking\"><\/span><strong>Bonus \u2014 Combine Full-Text Search with AI Reranking<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p><strong>Laravel semantic search<\/strong> and full-text search work great together. Use full-text search for speed. Then use AI reranking for semantic relevance.<\/p>\n\n\n\n<pre title=\"app\/Http\/Controllers\/SearchController.php\" class=\"wp-block-code\"><code class=\"\">$articles = Article::whereFullText('body', $request->query)\n    ->limit(50)\n    ->get()\n    ->rerank('body', $request->query, limit: 10);<\/code><\/pre>\n\n\n\n<p>Here is what happens. First, <strong>PostgreSQL<\/strong> full-text search quickly retrieves 50 candidates. Then the AI reranker \u2014 powered by Cohere or Jina \u2014 scores each one by semantic relevance. Finally, it returns only the top 10 most meaningful results.<\/p>\n\n\n\n<p>This gives you both speed and accuracy. It is the best approach for production <strong>Laravel semantic search<\/strong> features.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Real-World_Use_Cases\"><\/span><strong>Real-World Use Cases<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p><strong>Laravel vector search<\/strong> is not just for blog articles. Here are some common use cases developers are building right now:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Product search<\/strong> \u2014 customers find products by describing them in natural language instead of guessing exact keywords<\/li>\n\n\n\n<li><strong>Documentation search<\/strong> \u2014 users find relevant docs even when they use different terminology<\/li>\n\n\n\n<li><strong>Support ticket matching<\/strong> \u2014 automatically find similar past tickets when a new one is submitted<\/li>\n\n\n\n<li><strong>Content recommendations<\/strong> \u2014 suggest related articles based on meaning, not just tags<\/li>\n<\/ul>\n\n\n\n<p>Each of these was previously complex to build. With <strong>whereVectorSimilarTo Laravel<\/strong>, it now takes under 30 minutes to set up.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Final_Thoughts\"><\/span><strong>Final Thoughts<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p><strong>Laravel semantic search<\/strong> is one of the most impactful features in Laravel 13. It transforms how users interact with your application&#8217;s data. Moreover, the implementation is surprisingly simple. A migration, a model cast, an embedding step, and one query method. That&#8217;s the entire <strong>Laravel vector search<\/strong> setup from scratch. Additionally, because it runs directly on PostgreSQL with <strong>Laravel pgvector<\/strong>, you do not need any external services. Everything stays in your existing stack.<\/p>\n\n\n\n<p>If you are building anything with a search feature in 2026, this is the upgrade worth making.<\/p>\n\n\n\n<p>Laravel 13 semantic search, Laravel Vector Search, whereVectorSimilarTo, Laravel pgvector, Laravel AI search, Laravel embeddings, Laravel 13 new features, vector search Laravel 2026<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Standard search works by matching keywords. So, when a user searches for the best wineries in Napa Valley, traditional search won&#8217;t find an article titled Top Vineyards to Visit \u2014 even though they clearly mean the same thing. That\u2019s exactly where the keyword gap appears. However, this is also where Laravel semantic search truly shines. [&hellip;]<\/p>\n","protected":false},"author":5,"featured_media":11468,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_genesis_hide_title":false,"_genesis_hide_breadcrumbs":false,"_genesis_hide_singular_image":false,"_genesis_hide_footer_widgets":false,"_genesis_custom_body_class":"","_genesis_custom_post_class":"","_genesis_layout":"","jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[2564],"tags":[3112,3113,3119,3136,3152,3153,3150],"yst_prominent_words":[],"class_list":{"0":"post-11467","1":"post","2":"type-post","3":"status-publish","4":"format-standard","5":"has-post-thumbnail","7":"category-laravel","8":"tag-laravel-13","9":"tag-laravel-13-features","10":"tag-laravel-13-semantic-search","11":"tag-laravel-ai","12":"tag-laravel-ai-search","13":"tag-semantic-search","14":"tag-wherevectorsimilarto","15":"entry"},"jetpack_publicize_connections":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/programmingfields.com\/wp-content\/uploads\/2026\/05\/Laravel-pgvector.png?fit=1672%2C941&ssl=1","jetpack_likes_enabled":true,"jetpack_sharing_enabled":true,"jetpack-related-posts":[],"_links":{"self":[{"href":"https:\/\/programmingfields.com\/wp-json\/wp\/v2\/posts\/11467","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/programmingfields.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/programmingfields.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/programmingfields.com\/wp-json\/wp\/v2\/users\/5"}],"replies":[{"embeddable":true,"href":"https:\/\/programmingfields.com\/wp-json\/wp\/v2\/comments?post=11467"}],"version-history":[{"count":2,"href":"https:\/\/programmingfields.com\/wp-json\/wp\/v2\/posts\/11467\/revisions"}],"predecessor-version":[{"id":11472,"href":"https:\/\/programmingfields.com\/wp-json\/wp\/v2\/posts\/11467\/revisions\/11472"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/programmingfields.com\/wp-json\/wp\/v2\/media\/11468"}],"wp:attachment":[{"href":"https:\/\/programmingfields.com\/wp-json\/wp\/v2\/media?parent=11467"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/programmingfields.com\/wp-json\/wp\/v2\/categories?post=11467"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/programmingfields.com\/wp-json\/wp\/v2\/tags?post=11467"},{"taxonomy":"yst_prominent_words","embeddable":true,"href":"https:\/\/programmingfields.com\/wp-json\/wp\/v2\/yst_prominent_words?post=11467"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}