A vector database stores embeddings: lists of numbers, produced by a language model, that represent the meaning of a piece of text or an image. Two texts with similar meanings have embeddings that are close to each other, even if they share no words. Searching by closeness finds documents that answer a question rather than documents that repeat its words. This is the basis of semantic search, of recommendations, and of retrieval-augmented generation (RAG), in which an AI model answers from documents found in this way.
The Vector driver stores embeddings and searches them from a Kipchak API. It is free and MIT-licensed, and was listed on the public roadmap.
One interface, three stores
Several vector databases are in common use, and the right choice depends on what an organisation already runs. The driver supports three:
- Qdrant, a database built for vector search.
- PostgreSQL with the pgvector extension, through a connection of Kipchak’s Doctrine driver, so that vectors can be stored beside the data they describe.
- OpenSearch with its k-NN plugin, for organisations that already run OpenSearch for full-text search.
Application code is the same for all three. Each store measures similarity on its own scale, so the driver converts every score to the same scale: a cosine similarity is a cosine similarity whichever store returned it, and a threshold set for one store keeps the same matches in another. Filters on the data stored with each vector behave identically too. Moving from one store to another is therefore a change of configuration.
To make that true, the driver’s tests run a single shared suite against all three stores, and a Kipchak API under FrankenPHP’s worker mode returned identical results from each.
Storing and searching
$vectors = Vector::get();
$vectors->ensureCollection('articles', 1536);
$vectors->upsert('articles', [
new Point('article-17', $embedding, ['tenant' => 'acme', 'published' => 1761696000]),
]);
$matches = $vectors->search('articles', $queryEmbedding, limit: 5,
filter: Filter::and(
Filter::equals('tenant', 'acme'),
Filter::range('published', gte: strtotime('-30 days')),
),
threshold: 0.75,
);
Filters support equality, lists of values, numeric ranges, and their combination with and, or and not, including on nested fields. A filter on the tenant, as above, keeps each customer’s documents separate in a shared collection.
Embedding text and answering from it
With an embedding model configured, the driver embeds text itself, through Kipchak’s OpenAI driver. That driver also reaches servers that offer the same interface, such as Azure OpenAI, Ollama and vLLM, so the model can run within the organisation’s own infrastructure:
$vectors->upsertTexts('handbook', [
new Document('leave-1', 'Staff accrue 25 days of annual leave a year.', ['section' => 'leave']),
]);
$context = $vectors->searchText('handbook', 'How much holiday do I get?', limit: 3);
Each text is stored with its vector, so the matches contain what is needed to build a prompt. Passed to Kipchak’s Anthropic or OpenAI driver, they let the API answer a question from the organisation’s own documents. In testing with a model running locally on Ollama, this produced identical results in all three stores.
Behaviour in each store
Each store has limits that the driver handles so that application code does not have to:
- Qdrant accepts only integers and UUIDs as identifiers. The driver maps other identifiers to UUIDs and returns the original identifier.
- pgvector cannot index vectors longer than 2,000 dimensions. Longer embeddings, such as the 3,072 dimensions of
text-embedding-3-large, are stored and searched at half precision, which can be indexed. Filtered searches use pgvector’s iterative index scans, so a selective filter still returns the number of results requested. - OpenSearch creates an index with the wrong structure if data is written to an index that does not exist. The driver checks that the collection exists before the first write in each worker. Filters are applied during the search rather than after it, so they do not reduce the number of results.
FrankenPHP worker mode
Qdrant and OpenSearch are reached through Kipchak’s HTTP driver and hold nothing open between requests. pgvector uses the Doctrine driver’s database connection. When PostgreSQL is restarted, an operation is repeated once on a new connection, unless the application has its own transaction open on that connection. In worker-mode testing, the API served 2,700 concurrent requests and continued to answer after each of the three stores was restarted.
Installation
The Vector driver is free and MIT-licensed:
composer require kipchak/driver-vector
pgvector also requires the Doctrine driver and PHP’s pdo_pgsql extension, and embedding text requires the OpenAI driver. The configuration reference for each store is in the Vector driver documentation.