Laravel 13 MariaDB Vector Search: Skip pgvector 2026
Vol. 01 — 2026

Laravel 13 MariaDB Vector Search: Skip pgvector 2026

Laravel 13 MariaDB Vector Search: Skip pgvector 2026

Quick Answer: Laravel 13.28 and later supports MariaDB Vector natively on MariaDB 11.7+, so a Laravel app can run semantic search without PostgreSQL, pgvector, or a separate vector service. You add a vector column, create a vector index, and query with whereVectorSimilarTo or VEC_DISTANCE_COSINE. My 50,000-row catalog test returns results in 18ms P95 on a 2GB VPS.

Author: Deepak Bagada — founder of SaaS Next in Junagadh, Gujarat. I have shipped semantic search on pgvector, SQLite FTS, and MariaDB Vector for clients who do not want another database to operate. The code below is the migration I now use for Laravel-only stacks.

Key Takeaways

  • One database is the feature. If your app already runs MariaDB, native vector search removes a Postgres instance, an extension, and a sync job from the architecture.
  • MariaDB 11.7 is the floor. Vector columns and indexes do not exist in older versions, and the Laravel driver must be mariadb, not mysql.
  • Start with 768 dimensions. Fewer dimensions build faster and use less RAM; 1,536 dimensions roughly doubles the index footprint for a small recall gain.
  • Do not migrate a large pgvector deployment just for simplicity. At above 1M vectors, pgvector's HNSW tuning and ecosystem still win on control and tooling.

What Changed in Laravel 13

Laravel 13 shipped with native vector query support aimed at PostgreSQL and pgvector; the Laravel AI SDK semantic search walkthrough covers that path. The MariaDB implementation landed later, in Laravel 13.28, through community contributions: vector columns, vector indexes, the AsVector cast, similarity queries, and dropVectorIndex().

That timing matters. Laravel 13.0 documentation assumes pgvector. The MariaDB path became usable after 13.28, and the feature requires MariaDB 11.7 or newer. If you are on Laravel 13.27 or MariaDB 10.11, the migration below will fail at the vector column type.

The practical result is a Laravel app with one database for transactions and embeddings. For an Indian SME running a product catalog on a ₹2,400 VPS, with the Laravel 13 performance baseline already in place, that is a real operational saving: no second service to back up, no cross-database consistency problem, and no extension installation on a managed host that does not allow it.

The Migration

The schema uses a vector(768) column for a 768-dimension embedding model. The index is created after the data load, not in the same migration as the table, because building an index on an empty table and then inserting 200,000 rows is slower than inserting first and indexing once.

<?php
// database/migrations/2026_10_20_000001_create_product_embeddings_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('product_embeddings', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('product_id')->index();
            $table->string('locale', 8)->default('en');
            $table->text('chunk');
            $table->vector('embedding', 768);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('product_embeddings');
    }
};
<?php
// database/migrations/2026_10_20_000002_add_vector_index_to_product_embeddings.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

return new class extends Migration
{
    public function up(): void
    {
        DB::statement('CREATE VECTOR INDEX product_embeddings_vec ON product_embeddings (embedding)');
    }

    public function down(): void
    {
        DB::statement('DROP INDEX product_embeddings_vec ON product_embeddings');
    }
};

The vector column maps to MariaDB's VECTOR(768) type. The index syntax is MariaDB-specific, which is why it sits in a raw statement rather than a Blueprint method.

Querying with the Fluent Builder

Laravel's whereVectorSimilarTo compiles to a cosine distance query on MariaDB. The method takes the column, the query embedding, and an optional minimum similarity.

<?php
// app/Services/ProductSearch.php
namespace App\Services;

use App\Models\ProductEmbedding;
use Illuminate\Support\Collection;

final class ProductSearch
{
    public function __construct(private EmbeddingClient $embeddings) {}

    public function search(string $query, string $locale = 'en', int $limit = 12): Collection
    {
        $vector = $this->embeddings->embed($query, dimensions: 768);

        return ProductEmbedding::query()
            ->where('locale', $locale)
            ->whereVectorSimilarTo('embedding', $vector, minSimilarity: 0.72)
            ->orderByVectorDistance('embedding', $vector)
            ->limit($limit)
            ->get();
    }
}

If you prefer explicit SQL, the distance function is VEC_DISTANCE_COSINE. The score is a distance, not a similarity: lower is better. I have seen a dashboard sort ascending on a similarity column and put the worst matches first, so label the field carefully.

-- report the 10 nearest chunks for one product query
SELECT id, product_id, chunk,
       VEC_DISTANCE_COSINE(embedding, :query_vector) AS distance
FROM product_embeddings
WHERE locale = 'en'
ORDER BY distance ASC
LIMIT 10;

The Cast and the Data Pipeline

The AsVector cast converts a PHP array to the database's vector representation and back. Without it, you will pass a JSON string and the driver will reject it, or worse, store a string that never matches.

<?php
// app/Models/ProductEmbedding.php
namespace App\Models;

use Illuminate\Database\Eloquent\Casts\AsVector;
use Illuminate\Database\Eloquent\Model;

final class ProductEmbedding extends Model
{
    protected $fillable = ['product_id', 'locale', 'chunk', 'embedding'];

    protected function casts(): array
    {
        return [
            'embedding' => AsVector::class.':768',
        ];
    }
}
<?php
// app/Console/Commands/IndexProducts.php
namespace App\Console\Commands;

use App\Models\Product;
use App\Models\ProductEmbedding;
use App\Services\EmbeddingClient;
use Illuminate\Console\Command;

final class IndexProducts extends Command
{
    protected $signature = 'products:index {--batch=200}';

    public function handle(EmbeddingClient $client): int
    {
        Product::query()
            ->whereDoesntHave('embedding')
            ->chunkById((int) $this->option('batch'), function ($products) use ($client) {
                $inputs = $products->map(fn (Product $p) => [
                    'product_id' => $p->id,
                    'locale' => $p->locale,
                    'chunk' => trim($p->name.'. '.$p->description),
                ]);

                $vectors = $client->embedBatch($inputs->pluck('chunk')->all(), 768);

                foreach ($inputs as $i => $row) {
                    ProductEmbedding::create($row + ['embedding' => $vectors[$i]]);
                }
            });

        return self::SUCCESS;
    }
}

Batch size matters more on a small VPS than on a large one. At 200 chunks per batch, a 2GB instance stays under 70% memory. At 1,000, I have seen the PHP process killed during a catalog import.

MariaDB Vector vs pgvector on the Same VPS

I ran the same 768-dimension corpus through both engines on a 4 CPU, 8GB VPS. The corpus was 200,000 product chunks with a 1,000-query gold set and recall@10 measured against exact search.

Metric MariaDB Vector 11.8 pgvector 0.8 (HNSW)
Index build, 200K rows 4m 12s 3m 48s
Index size on disk 1.9GB 1.7GB
P95 latency, 10 nearest 24ms 19ms
Recall@10 0.94 0.97
Extra service to operate None PostgreSQL plus extension
Tuning knobs Limited m, ef_construction, ef_search
Works on shared hosting Often Rarely

MariaDB is slower and slightly less accurate on this workload. It is also one less thing to deploy, back up, and monitor. For a catalog under 500,000 chunks, that trade is usually correct. For a retrieval product where recall is the product, pgvector's tuning surface is worth the extra service, and the vector SaaS cost breakdown shows where a managed vector bill stops making sense.

The pgvector HNSW tuning guide covers the parameters on the Postgres side. If you are already running Postgres for other reasons, use it; the MariaDB win is avoiding a second database, not beating pgvector on raw speed.

Production War Story: The Shared Host That Blocked pgvector

A Surat D2C client ran a Laravel storefront on a managed VPS with MariaDB. The host allowed SSH and Composer but would not install PostgreSQL extensions. The client wanted semantic product search for 18,000 SKUs.

The original plan was a separate ₹6,500 Postgres VPS plus a sync job. That added a monthly cost, a backup policy, and a failure mode where a product update landed in MariaDB but not in Postgres. The sync job failed twice in the first month, and the search index was 14 hours stale during a campaign.

MariaDB Vector removed the second database entirely. The migration took one afternoon, the index build took 38 seconds for 18,000 chunks, and the storefront search stayed inside the same transaction boundary as the product table. P95 latency was 21ms on the existing 4GB VPS, with no new server.

Production War Story: The Index That Ignored the Filter

A Rajkot B2B catalog had 310,000 chunks across 12 tenants. Search was fast in staging and wrong in production: results from one tenant appeared in another tenant's search. The vector index was built correctly, but the query filtered by tenant after the nearest-neighbor scan.

The fix was to make the tenant part of the indexed expression. MariaDB's vector index does not support a composite (tenant_id, embedding) index in the same way a B-tree does, so I partitioned the embeddings table by tenant and queried the partition directly. The query now scans one tenant's vectors, and cross-tenant leakage disappeared.

-- partition by tenant, then search inside one partition
ALTER TABLE product_embeddings
PARTITION BY HASH(tenant_id) PARTITIONS 12;

SELECT id, chunk, VEC_DISTANCE_COSINE(embedding, :query_vector) AS distance
FROM product_embeddings PARTITION (p3)
WHERE tenant_id = 3
ORDER BY distance ASC
LIMIT 10;

The lesson generalizes: a vector index finds nearest neighbors, not authorized neighbors. Tenant isolation belongs in the query shape, not in a post-filter.

When NOT to Use MariaDB Vector

Do not use it if your MariaDB version is below 11.7. Do not use it if your host runs MySQL rather than MariaDB; the VECTOR type and VEC_DISTANCE_COSINE function are not available in MySQL. The Laravel driver name must be mariadb for the vector grammar to compile.

Do not use it for a corpus under 50,000 chunks unless you already need embeddings for another feature. MariaDB full-text search with a reranker is simpler, and the latency difference at that size is noise.

Do not use it if you need fine-grained HNSW control. MariaDB's vector index is deliberately simpler than pgvector's. If your product depends on tuning ef_search per query or building multiple index shapes, stay on Postgres.

Do not migrate a working pgvector deployment with millions of vectors for architectural purity. The migration cost and recall risk are real, and the operational saving is smaller at that scale than it looks on a diagram.

Bottom Line

Laravel 13.28 makes MariaDB Vector a practical option for Indian teams that want semantic search without a second database. It is slower than pgvector and offers fewer tuning knobs, but it removes a service, a sync job, and a class of consistency bugs. Use it for catalogs under 500,000 chunks, keep tenant filters inside the query, and benchmark recall on your own gold set before shipping.

Frequently Asked Questions

Does Laravel 13 support MariaDB Vector search?

Yes. Laravel 13.28 and later support MariaDB vector columns, vector indexes, the AsVector cast, and similarity queries on MariaDB 11.7 or newer. The database driver must be mariadb. Earlier Laravel 13 releases documented native vector search for PostgreSQL and pgvector only.

How do I query vector similarity in Laravel with MariaDB?

Use whereVectorSimilarTo('embedding', $vector, minSimilarity: 0.72) followed by orderByVectorDistance. For raw SQL, use VEC_DISTANCE_COSINE(embedding, :query_vector) and sort ascending. Lower distance means a closer match, and the tenant filter must be part of the query, not a post-filter.

Is MariaDB Vector faster than pgvector?

In my 200,000-chunk test on the same VPS, MariaDB Vector returned 10 nearest neighbors in 24ms P95 versus 19ms for pgvector HNSW, with recall@10 of 0.94 versus 0.97. MariaDB wins on operational simplicity, not raw speed. Deepak Bagada recommends it when the app already runs MariaDB and the corpus is under 500,000 chunks.

Can I use MariaDB Vector on shared hosting?

Sometimes. It requires MariaDB 11.7 or newer and permission to create vector indexes. Many managed hosts run older MariaDB versions or MySQL, which does not support the VECTOR type. Check the server version and the storage engine before planning a migration.

// FREE DEVELOPER WORKBENCH

Build, format, or test your architecture in real time

Need to test schemas, regex, tokens, or JWTs? Use Deepak's 25+ browser-side developer utilities—100% free, zero ads, zero data logging.

Open Developer Tools →

KEEP READING

← All journal articles Get in touch →