eloquage / laravel-vector-search
Laravel vector search engine with embeddings, pgvector drivers, and optional Scout integration.
Fund package maintenance!
Requires
- php: ^8.4
- illuminate/bus: ^11.0||^12.0||^13.0
- illuminate/contracts: ^11.0||^12.0||^13.0
- illuminate/database: ^11.0||^12.0||^13.0
- illuminate/http: ^11.0||^12.0||^13.0
- illuminate/support: ^11.0||^12.0||^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- laravel/scout: ^11.0 || ^12.0 || ^13.0
- meilisearch/meilisearch-php: ^1.0
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^11.0.0||^10.0.0||^9.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- spatie/laravel-ray: ^1.35
Suggests
- eloquage/filament-vector-search: Filament v5 admin plugin with search dashboard.
- laravel/scout: Required to use the SearchableVector trait and VectorSearchEngine (^11.0).
- meilisearch/meilisearch-php: Required when using the Meilisearch vector driver (^1.0).
Provides
None
Conflicts
None
Replaces
None
README
Semantic vector search for Laravel: chunk documents, generate embeddings, store vectors, and query by meaning. Ships with PostgreSQL/pgvector and Meilisearch drivers, pluggable embedding providers, queued indexing, and optional Laravel Scout integration.
The optional eloquage/filament-vector-search package adds a Filament v5 admin UI for indexes, documents, and search.
Architecture
flowchart LR
subgraph indexing [IndexingPipeline]
VI[VectorIndex]
VD[VectorDocument]
PVD[ProcessVectorDocument]
EVC[EmbedVectorChunks]
VI --> VD --> PVD --> EVC
end
subgraph providers [Providers]
EP[EmbeddingProviderManager]
VDM[VectorDriverManager]
end
EVC --> EP
EVC --> VDM
subgraph search [Search]
VSI[VectorSearchIndex]
VSI --> EP
VSI --> VDM
end
Loading
Indexing pipeline (queued by default):
ProcessVectorDocument— split content into chunks using the index'schunking_strategy,chunk_size, andchunk_overlapEmbedVectorChunks— batch-embed chunk text and persist vectors via the index driver (dispatchedafterCommit)
Search embeds the query, then delegates similarity search to the index's vector driver. When the driver is Meilisearch, keywordSearch() provides full-text search with optional metadata filters.
Requirements
| Requirement | Notes |
|---|---|
| PHP | 8.4+ |
| Laravel | 11, 12, or 13 |
| PostgreSQL + pgvector | Default driver (postgres_vector); migrations call Schema::ensureVectorExtensionExists() on Laravel 13+ |
| Queue worker | Required for production indexing via indexDocument() or SearchableVector |
| Meilisearch | Optional; required for meilisearch driver and keywordSearch() |
| Laravel Scout | Optional; required for SearchableVector trait |
Installation
composer require eloquage/laravel-vector-search
Publish and run migrations:
php artisan vendor:publish --provider="Eloquage\LaravelVectorSearch\LaravelVectorSearchServiceProvider"
php artisan migrate
Publish config (optional):
php artisan vendor:publish --provider="Eloquage\LaravelVectorSearch\LaravelVectorSearchServiceProvider"
Configure .env for your driver and embedding provider:
pgvector + OpenAI
VECTOR_SEARCH_DRIVER=postgres_vector VECTOR_SEARCH_EMBEDDING_PROVIDER=openai OPENAI_API_KEY=sk-... OPENAI_EMBEDDING_MODEL=text-embedding-3-small
pgvector + Ollama (local)
VECTOR_SEARCH_DRIVER=postgres_vector VECTOR_SEARCH_EMBEDDING_PROVIDER=ollama OLLAMA_HOST=http://localhost:11434 OLLAMA_EMBEDDING_MODEL=nomic-embed-text
Meilisearch
VECTOR_SEARCH_DRIVER=meilisearch MEILISEARCH_HOST=http://localhost:7700 MEILISEARCH_KEY=masterKey
Pinecone
VECTOR_SEARCH_DRIVER=pinecone PINECONE_API_KEY=pc-... PINECONE_INDEX_HOST=https://your-index-xxxx.svc.region.pinecone.io
Cohere embeddings
VECTOR_SEARCH_EMBEDDING_PROVIDER=cohere COHERE_API_KEY=... COHERE_EMBEDDING_MODEL=embed-english-v3.0
Start a queue worker before indexing documents in production:
php artisan queue:work
For pgvector, optionally publish and run the HNSW index migration stub (add_vector_chunks_embedding_hnsw_index) for faster similarity queries.
Configuration
| Config key | Env variable | Default | Description |
|---|---|---|---|
default_driver |
VECTOR_SEARCH_DRIVER |
postgres_vector |
Default vector storage driver |
default_embedding_provider |
VECTOR_SEARCH_EMBEDDING_PROVIDER |
openai |
Default embedding provider |
default_index |
VECTOR_SEARCH_DEFAULT_INDEX |
default |
Fallback index name |
allow_null_embedding_provider |
VECTOR_SEARCH_ALLOW_NULL_EMBEDDING |
false |
Allow null provider outside local/testing |
strict_boot_validation |
VECTOR_SEARCH_STRICT_BOOT_VALIDATION |
false |
Throw (not warn) on boot validation failures |
embedding_batch_size |
VECTOR_SEARCH_EMBEDDING_BATCH_SIZE |
100 |
Max texts per embedding API request |
embedding_timeout |
VECTOR_SEARCH_EMBEDDING_TIMEOUT |
60 |
HTTP timeout (seconds) for embedding providers |
index_defaults.dimensions |
VECTOR_SEARCH_INDEX_DIMENSIONS |
1536 |
Default vector column size for migrations |
scout.default_index |
VECTOR_SEARCH_SCOUT_INDEX |
— | Index used by Scout engine |
embedding_providers.openai.* |
OPENAI_API_KEY, OPENAI_EMBEDDING_MODEL |
— | OpenAI embedding settings |
embedding_providers.ollama.* |
OLLAMA_HOST, OLLAMA_EMBEDDING_MODEL, OLLAMA_EMBEDDING_CONCURRENCY |
— | Ollama embedding settings |
embedding_providers.cohere.* |
COHERE_API_KEY, COHERE_EMBEDDING_MODEL |
— | Cohere embedding settings |
drivers.pgvector.connection |
VECTOR_DB_CONNECTION |
— | DB connection for pgvector |
drivers.meilisearch.* |
MEILISEARCH_HOST, MEILISEARCH_KEY, MEILISEARCH_BATCH_SIZE |
— | Meilisearch connection settings |
drivers.pinecone.* |
PINECONE_API_KEY, PINECONE_INDEX_HOST |
— | Pinecone data-plane host + API key |
splitters |
— | [] |
Custom TextSplitterInterface classes keyed by strategy value |
testing.embedding_dimensions |
— | 8 |
Vector size for null provider in tests |
The config key for the pgvector connection is drivers.pgvector, but the driver enum value is postgres_vector. Both postgres_vector and pgvector resolve to the same driver in VectorDriverManager.
Built-in text splitters: recursive_character, token_limit, sentence.
Vector drivers
Each VectorIndex record stores its own driver. The application default is config('vector-search.default_driver').
| Driver | Enum value | Status | Keyword search |
|---|---|---|---|
| PostgreSQL/pgvector | postgres_vector |
Implemented | No |
| Meilisearch | meilisearch |
Implemented | Yes (keywordSearch) |
| Pinecone | pinecone |
Implemented | No |
Per-index driver selection is set when creating the index:
use Eloquage\LaravelVectorSearch\Enums\ChunkingStrategy; use Eloquage\LaravelVectorSearch\Enums\EmbeddingProvider; use Eloquage\LaravelVectorSearch\Enums\IndexDriver; use Eloquage\LaravelVectorSearch\Models\VectorIndex; VectorIndex::query()->create([ 'name' => 'document-archive', 'driver' => IndexDriver::POSTGRES_VECTOR, 'embedding_provider' => EmbeddingProvider::OPENAI, 'embedding_model' => 'text-embedding-3-small', 'dimensions' => 1536, 'chunking_strategy' => ChunkingStrategy::RECURSIVE_CHARACTER, 'chunk_size' => 1000, 'chunk_overlap' => 200, ]);
Embedding providers
| Provider | Value | Status |
|---|---|---|
| OpenAI | openai |
Implemented |
| Ollama | ollama |
Implemented |
| Cohere | cohere |
Implemented |
| Null (testing) | null |
Implemented — deterministic fake vectors |
Each index stores embedding_provider, embedding_model, and optional dimensions. At search time, VectorSearchIndex uses embedQuery() for query vectors (Cohere uses search_query input type).
Index names
Choose an index name in config/vector-search.php. An application can reuse that name in its own integrations.
Events and archive integration
| Event | When |
|---|---|
VectorDocumentIndexed |
EmbedVectorChunks completes successfully |
VectorDocumentFailed |
Chunking or embedding fails |
Host applications can listen to these events to update related records or trigger downstream workflows.
Indexing: queued vs sync
Production (queued) — default for indexDocument() and the SearchableVector trait:
use Eloquage\LaravelVectorSearch\Facades\LaravelVectorSearch; use Eloquage\LaravelVectorSearch\Models\VectorDocument; LaravelVectorSearch::indexDocument($document); // dispatches ProcessVectorDocument → EmbedVectorChunks
Both jobs implement ShouldBeUnique with exponential backoff ([10, 60, 300] seconds) and a 300-second timeout.
Tests and scripts (sync) — process immediately without a worker:
use Eloquage\LaravelVectorSearch\Jobs\ProcessVectorDocument; ProcessVectorDocument::dispatchSync($document);
EmbedVectorChunks is dispatched automatically after chunking commits.
Quick start
1. Create an index
use Eloquage\LaravelVectorSearch\Enums\ChunkingStrategy; use Eloquage\LaravelVectorSearch\Enums\EmbeddingProvider; use Eloquage\LaravelVectorSearch\Enums\IndexDriver; use Eloquage\LaravelVectorSearch\Models\VectorIndex; VectorIndex::query()->create([ 'name' => 'docs', 'driver' => IndexDriver::POSTGRES_VECTOR, 'embedding_provider' => EmbeddingProvider::OPENAI, 'embedding_model' => 'text-embedding-3-small', 'dimensions' => 1536, 'chunking_strategy' => ChunkingStrategy::RECURSIVE_CHARACTER, 'chunk_size' => 1000, 'chunk_overlap' => 200, ]);
2. Add a document and index it
use Eloquage\LaravelVectorSearch\Enums\DocumentStatus; use Eloquage\LaravelVectorSearch\Facades\LaravelVectorSearch; use Eloquage\LaravelVectorSearch\Models\VectorDocument; $document = VectorDocument::query()->create([ 'vector_index_id' => $index->id, 'title' => 'Deployment guide', 'content' => 'Run migrations and start a queue worker before indexing.', 'metadata' => ['collection' => 'guides'], 'status' => DocumentStatus::DRAFT, ]); LaravelVectorSearch::indexDocument($document);
3. Search
$chunks = LaravelVectorSearch::forIndex('docs')->search('queue worker', limit: 5); foreach ($chunks as $chunk) { echo $chunk->content; }
Pass a VectorIndex model instead of a name if you already have the record:
LaravelVectorSearch::forIndex($index)->search($query);
Meilisearch keyword search
When the index driver is meilisearch, use keywordSearch for full-text queries:
$chunks = LaravelVectorSearch::forIndex($index) ->keywordSearch('deployment', limit: 20, filters: ['collection' => 'guides']);
End-to-end example
This example creates a named index, stores documents with collection metadata, and runs semantic and optional Meilisearch keyword searches.
use Eloquage\LaravelVectorSearch\Enums\ChunkingStrategy; use Eloquage\LaravelVectorSearch\Enums\DocumentStatus; use Eloquage\LaravelVectorSearch\Enums\EmbeddingProvider; use Eloquage\LaravelVectorSearch\Enums\IndexDriver; use Eloquage\LaravelVectorSearch\Facades\LaravelVectorSearch; use Eloquage\LaravelVectorSearch\Jobs\ProcessVectorDocument; use Eloquage\LaravelVectorSearch\Models\VectorDocument; use Eloquage\LaravelVectorSearch\Models\VectorIndex; // 1. Ensure the index exists $index = VectorIndex::query()->firstOrCreate( ['name' => 'document-archive'], [ 'driver' => IndexDriver::POSTGRES_VECTOR, 'embedding_provider' => EmbeddingProvider::OPENAI, 'embedding_model' => 'text-embedding-3-small', 'dimensions' => 1536, 'chunking_strategy' => ChunkingStrategy::RECURSIVE_CHARACTER, 'chunk_size' => 1000, 'chunk_overlap' => 200, ], ); // 2. Create and index a document $document = VectorDocument::query()->create([ 'vector_index_id' => $index->id, 'title' => 'Witness statement', 'content' => 'The defendant was observed leaving the premises at 22:15.', 'metadata' => [ 'collection' => '479-26-0T9MTS', 'canonical_basename' => 'witness-statement-001', ], 'status' => DocumentStatus::DRAFT, ]); ProcessVectorDocument::dispatchSync($document); // use indexDocument() + queue worker in production // 3. Semantic search $chunks = LaravelVectorSearch::forIndex('document-archive')->search('witness statement', limit: 10); $results = $chunks ->filter(fn ($chunk) => ($chunk->vectorDocument?->metadata['collection'] ?? null) === '479-26-0T9MTS') ->map(fn ($chunk) => [ 'content' => $chunk->content, 'canonical_basename' => $chunk->vectorDocument?->metadata['canonical_basename'] ?? null, ]) ->values(); // 4. Keyword search (Meilisearch driver only) if ($index->driver === IndexDriver::MEILISEARCH) { $keywordHits = LaravelVectorSearch::forIndex($index) ->keywordSearch('witness', limit: 20, filters: ['collection' => '479-26-0T9MTS']); }
Use your configured embedding provider and metadata filters when indexing and querying documents.
Laravel Scout integration
Install Scout and set the driver:
composer require laravel/scout
SCOUT_DRIVER=vector-search VECTOR_SEARCH_SCOUT_INDEX=docs
Add the trait to any Eloquent model:
use Eloquage\LaravelVectorSearch\Concerns\SearchableVector; use Laravel\Scout\Searchable; class Article extends Model { use Searchable, SearchableVector; // Optional overrides: // vectorSearchIndexName(), vectorSearchTitle(), vectorSearchContent(), vectorSearchMetadata() }
On save/delete, the trait syncs a VectorDocument and queues ProcessVectorDocument. Scout queries resolve chunk hits back to the source model via VectorSearchEngine, which auto-registers when Scout is installed.
When using both Searchable and SearchableVector, duplicate dispatches are deduplicated via ShouldBeUnique on ProcessVectorDocument and EmbedVectorChunks.
Custom drivers and embedding providers
Register extensions in AppServiceProvider::register():
use Eloquage\LaravelVectorSearch\LaravelVectorSearch; use Eloquage\LaravelVectorSearch\Managers\EmbeddingProviderManager; use Eloquage\LaravelVectorSearch\Managers\VectorDriverManager; LaravelVectorSearch::configure(function (VectorDriverManager $drivers, EmbeddingProviderManager $embeddings): void { $drivers->extend('custom', fn () => new CustomVectorDriver); $embeddings->extend('custom', fn () => new CustomEmbeddingProvider); });
Custom text splitters can be registered in config/vector-search.php under splitters and referenced by ChunkingStrategy value on the index.
Optional integrations
Filament admin (eloquage/filament-vector-search)
composer require eloquage/filament-vector-search
Registers a dashboard, index/document resources, search playground, and pipeline widgets. See the Filament plugin README.
Testing
composer test
composer analyse
composer format
Use EmbeddingProvider::NULL_PROVIDER and ProcessVectorDocument::dispatchSync() in tests to avoid external API calls. The vector-search.testing.embedding_dimensions config key controls null-provider vector size (default: 8).
Changelog
See CHANGELOG.
Security
Report vulnerabilities via the security policy.
License
MIT. See LICENSE.
