ankane/pgvector
Ruby gem for pgvector, the Postgres extension for vector similarity search. Adds vector columns, cosine/L2/inner product distance queries, and HNSW/IVFFlat index support to ActiveRecord, making it easy to build semantic search and AI-powered features on Postgres.
ankane/pgvector is a Ruby gem, but the core value is the Postgres vector extension, which is language-agnostic. In Laravel you leverage it through PostgreSQL and PDO rather than the gem itself.
Minimal setup:
pgvector/pgvector:pg16 locally, or a managed Postgres that supports the extension.DB::statement('CREATE EXTENSION IF NOT EXISTS vector');
Schema::table('documents', function (Blueprint $table) {
$table->vector('embedding', 1536)->nullable();
});
If your Laravel schema builder lacks vector(), use:
DB::statement('ALTER TABLE documents ADD COLUMN embedding vector(1536)');
pgvector/pgvector for formatting/parsing, or build a small cast.documents.embedding, then query nearest neighbours:
$vector = '[' . implode(',', $embedding) . ']';
$results = DB::table('documents')
->select('id', 'title', 'body')
->orderByRaw('embedding <=> ?::vector', [$vector])
->limit(10)
->get();
Look first at the pgvector README’s SQL examples for operators, index types, and distance functions. Those translate directly to Laravel’s query builder and raw statements.
Embedding storage and casting
Create a custom cast so models expose arrays while the database stores vector:
class VectorCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return $value ? array_map('floatval', explode(',', trim($value, '[]'))) : null;
}
public function set($model, $key, $value, $attributes)
{
return $value ? '[' . implode(',', $value) . ']' : null;
}
}
Use it on the model:
protected $casts = ['embedding' => VectorCast::class];
Generation workflow
source_id, chunk_index, and metadata alongside the vector.DB::table('documents')->upsert(...) where possible.Search patterns
Use the operator that matches your similarity metric:
| Metric | Operator | Index opclass |
|---|---|---|
| Cosine distance | <=> |
vector_cosine_ops |
| L2 distance | <-> |
vector_l2_ops |
| Inner product | <#> |
vector_ip_ops |
Typical repository method:
public function nearest(array $embedding, int $limit = 10)
{
$vector = '[' . implode(',', $embedding) . ']';
return DB::table('documents')
->whereNotNull('embedding')
->orderByRaw('embedding <=> ?::vector', [$vector])
->limit($limit)
->get();
}
Indexing
Build indexes after bulk-loading data:
DB::statement('CREATE INDEX documents_embedding_hnsw_idx
ON documents USING hnsw (embedding vector_cosine_ops)');
HNSW is usually the better default for read-heavy Laravel apps. IVFFlat is cheaper to build but needs representative data before index creation and requires tuning lists/probes.
Hybrid search
For RAG or search features, combine lexical and vector results:
to_tsvector / plainto_tsquery.<=>.Filtering
Apply tenant, status, or date filters in WHERE before ordering by distance. For large tenant-specific datasets, consider partial indexes or partitioning by tenant_id and querying within a partition.
RAG integration
Retrieve top-k chunks, pass them into your LLM prompt, and keep the retrieval layer behind a service class. This makes it easy to swap embedding models, index settings, or search strategies without touching controllers.
ankane/pgvector. Use the Postgres extension directly, or a PHP/Laravel wrapper such as pgvector/pgvector.CREATE EXTENSION vector may require superuser or platform-specific enablement. On managed Postgres, verify support before designing around it.vector(1536) column rejects other sizes. Changing embedding providers usually means a new column/table and a full re-embedding job.<=> with a vector_l2_ops index will not use the index efficiently. Keep metric choice consistent across schema, queries, and indexes.LIMIT. ORDER BY embedding <=> ... without a limit can scan and sort the entire table.SET LOCAL hnsw.ef_search.hnsw.ef_search, ivfflat.probes, and maintenance_work_mem matter. Set them with SET LOCAL inside transactions where appropriate.[0.1,0.2,...]. Never interpolate raw user input into SQL.NULL embeddings. Exclude them from searches and backfill asynchronously.How can I help you explore Laravel packages today?