x-laravel / embedding-qdrant-driver
Qdrant vector database similarity driver for x-laravel/embedding.
Package info
github.com/x-laravel/embedding-qdrant-driver
pkg:composer/x-laravel/embedding-qdrant-driver
Requires
- php: ^8.3
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- x-laravel/embedding: ^1.0
Requires (Dev)
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-24 16:27:56 UTC
README
Qdrant vector database driver for x-laravel/embedding.
How It Works
- Implements
SimilarityDriver— registers as theqdrantdriver, similarity search runs entirely in Qdrant using its native ANN (Approximate Nearest Neighbor) engine - Implements
VectorStore— writes embeddings to both the SQLembeddingstable (for Eloquent relationships) and the Qdrant collection (for search) - Implements
PayloadStore— keeps payload records in the SQLembeddablestable and mirrors them onto the entity's Qdrant points, so payloadfilter:constraints run natively in Qdrant
Requirements
- PHP ^8.3
- Laravel ^12.0 | ^13.0
x-laravel/embedding ^1.0- Qdrant server (self-hosted or Qdrant Cloud)
Installation
composer require x-laravel/embedding-qdrant-driver
The QdrantEmbeddingServiceProvider is auto-discovered and registers the qdrant driver automatically.
Setup
1. Configure x-laravel/embedding
Publish the config if you haven't already:
php artisan vendor:publish --tag=embedding-config
Set the similarity driver in config/embedding.php:
'similarity' => [ 'driver' => env('EMBEDDING_SIMILARITY_DRIVER', 'qdrant'), ],
2. Configure Qdrant
Set the following environment variables:
EMBEDDING_QDRANT_URL=http://localhost:6333 EMBEDDING_QDRANT_COLLECTION=embeddings EMBEDDING_QDRANT_API_KEY= # required for Qdrant Cloud
Or publish the config to config/embedding-qdrant-driver.php for full customisation:
php artisan vendor:publish --tag=embedding-qdrant
3. Create the SQL tables and the Qdrant collection
Migrations are not loaded automatically. Publish the core migrations (embeddings and embeddables tables) and this driver's Qdrant collection migration, then run them:
php artisan vendor:publish --tag=embedding-migrations php artisan vendor:publish --tag=embedding-qdrant php artisan migrate
The embedding-qdrant tag also publishes config/embedding-qdrant-driver.php.
Note: The Qdrant collection is created with cosine similarity. The
EMBEDDING_DIMENSIONSconfig value sets the vector size — it must match your AI model's output dimension.
4. Model
Follow the standard x-laravel/embedding setup. No Qdrant-specific changes are needed on your models.
use XLaravel\Embedding\Attributes\EmbedOn; use XLaravel\Embedding\Concerns\Embeddable; use XLaravel\Embedding\Contracts\HasEmbeddings; #[EmbedOn(['title', 'body'])] class Post extends Model implements HasEmbeddings { use Embeddable; public function toEmbeddingText(string $slot = 'default'): string { return $this->title.' '.$this->body; } }
Usage
The driver is transparent — use the standard x-laravel/embedding API:
Post::similarToText('web framework', limit: 10); Post::similarTo($vector, limit: 10, threshold: 0.8); Post::rankByRelevance($posts, 'web framework'); $post->mostSimilar(limit: 5); $post->similarityTo($otherPost);
All methods set a similarity_score float attribute on each returned model. A threshold of 0.0 returns every match; a positive value is passed to Qdrant as score_threshold.
Payload filtering
Models using #[EmbedPayload] can filter similarity searches inside Qdrant:
use XLaravel\Embedding\Attributes\EmbedPayload; #[EmbedOn('name')] #[EmbedPayload(['province_id', 'category_id', 'active'])] class Venue extends Model implements HasEmbeddings { ... } Venue::similarTo($vector, limit: 300, filter: ['province_id' => 34]); // equality Venue::similarToText('kebap', filter: ['category_id' => [3, 7]]); // IN $venue->mostSimilar(limit: 5, filter: ['province_id' => 34, 'active' => true]); // AND
How the payload reaches Qdrant:
- The SQL
embeddablestable stays the source of truth.QdrantPayloadStoreextends the coreDatabasePayloadStore: after writing the row it sets the same payload under thepayloadkey of every Qdrant point of that entity (all slots), and removes that key when the row is deleted. QdrantVectorStorereads the entity's currentembeddablesrow when it upserts a point, so a point written after the payload sync carries the payload too.- The driver turns the filter into Qdrant conditions on
payload.<key>:match.valuefor equality,match.anyfor IN,is_nullfornull, all ANDed inmust. An empty array matches nothing. - Qdrant matches by JSON type, so
34never matches"34"andtruenever matches1. Records without a payload row never match a filtered search. - Hits are checked against the SQL tables before models are loaded: points whose
embeddingsrow is gone, or (for a filtered search) whoseembeddablesrow is gone, are dropped.
Filter keys must match ^[A-Za-z_][A-Za-z0-9_]*$. For large collections, create Qdrant payload indexes on embeddable_type, slot and the payload.<key> fields you filter on.
Note: Qdrant points are not deleted when their
embeddingsrows are: deleting a model,embedding:vector:clearandembedding:vector:cleanremove SQL rows only, andembedding:payload:clear/embedding:payload:cleanbypass thePayloadStore. Search results stay correct because hits are checked against SQL, but the stale points remain in the collection and count againstlimit. Runembedding:payload:sync --forceafter clearing payloads if you want Qdrant back in step.
Testing
# Build first (once per PHP version) DOCKER_BUILDKIT=0 docker compose --profile php83 build # Run tests docker compose --profile php83 up docker compose --profile php84 up docker compose --profile php85 up
License
This package is open-sourced software licensed under the MIT license.