symfony/ai-pinecone-store
Pinecone vector store integration for Symfony AI Store, enabling upsert, query, and delete of vectors plus serverless index creation through the Pinecone API.
Install Dependencies:
composer require symfony/ai symfony/ai-pinecone-store symfony/http-client
Note: If avoiding Symfony AI entirely, use symfony/http-client + Pinecone’s PHP SDK as a lightweight alternative.
Configure Pinecone Credentials:
Add to .env:
PINECONE_API_KEY=your_api_key
PINECONE_ENV=your_env_region
PINECONE_INDEX=your_index_name
Register the Store in Laravel:
Create a service provider (e.g., PineconeServiceProvider) or bind directly in AppServiceProvider:
use Symfony\AI\PineconeStore;
use Symfony\Contracts\HttpClient\HttpClientInterface;
public function register()
{
$this->app->singleton(PineconeStore::class, function ($app) {
return new PineconeStore(
$app->make(HttpClientInterface::class),
config('services.pinecone.api_key'),
config('services.pinecone.env'),
config('services.pinecone.index')
);
});
}
First Use Case: Query Vectors
use Symfony\AI\StoreInterface;
$store = app(StoreInterface::class);
$results = $store->query(
vector: $embeddingArray, // Your 1536-dim vector (e.g., from OpenAI)
limit: 5,
filter: ['category' => ['$eq' => 'electronics']] // Optional metadata filter
);
Quick Health Check (v0.14+):
Use the new count() capability to verify the index is reachable and populated before wiring up RAG flows:
$store = app(StoreInterface::class);
$total = $store->count(); // Total vectors in index
$docs = $store->count(['source' => ['$eq' => 'user_guide']]); // Filtered count
StoreInterface for AI components like Symfony\AI\Chain or Symfony\AI\Prompt.
use Symfony\AI\Chain;
use Symfony\AI\Prompt;
$chain = new Chain(
new Prompt('...'),
app(StoreInterface::class) // PineconeStore injected here
);
class PineconeRepository
{
public function __construct(private StoreInterface $store) {}
public function findSimilarProducts($embedding, int $limit): array
{
return $this->store->query($embedding, $limit, [
'product_type' => ['$in' => ['laptop', 'phone']]
]);
}
}
$vectors = [
['id' => 'doc1', 'values' => $embedding1, 'metadata' => ['source' => 'user_guide']],
['id' => 'doc2', 'values' => $embedding2, 'metadata' => ['source' => 'api_docs']],
];
$store->upsert($vectors);
use Symfony\AI\VectorDocumentInterface;
class ProductDocument implements VectorDocumentInterface
{
public function __construct(
private string $id,
private array $values,
private array $metadata,
) {}
public function getId(): string { return $this->id; }
public function getValues(): array { return $this->values; }
public function getMetadata(): array { return $this->metadata; }
}
$store->upsert([
new ProductDocument('sku-1', $embedding1, ['source' => 'catalog']),
new ProductDocument('sku-2', $embedding2, ['source' => 'catalog']),
]);
This is the cleanest way to normalize upsert payload shape and keep domain objects decoupled from Pinecone’s wire format.query() with includeMetadata: true for RAG pipelines:
$result = $store->query($queryEmbedding, 3, [], [
'includeMetadata' => true,
'includeValues' => false,
]);
// $result['matches'][0]['metadata']['source'] gives context for LLM prompts.
Combine keyword and vector search via metadata filters:
$results = $store->query(
$embedding,
10,
['price' => ['$gt' => 100]], // Filter by metadata
['includeMetadata' => true]
);
The new count() capability lets you build UI affordances and guard rails around your index without pulling vectors.
// Dashboard stats
$stats = [
'total' => $store->count(),
'electronics' => $store->count(['category' => ['$eq' => 'electronics']]),
'active_docs' => $store->count(['status' => ['$eq' => 'published']]),
];
// Pagination estimate for a filtered search UI
$matched = $store->count($filter);
$pages = (int) ceil($matched / $perPage);
Prefer count($filter) over fetching + counting client-side — it’s cheaper and keeps memory flat on large indexes.
Wrap Pinecone operations in Laravel’s try-catch or use Symfony’s HttpClient retries:
try {
$store->query($embedding, 5);
} catch (\Symfony\Contracts\HttpClient\Exception\ClientException $e) {
// Handle Pinecone API errors (e.g., rate limits)
Log::error('Pinecone query failed: ' . $e->getMessage());
}
$mockStore = Mockery::mock(StoreInterface::class);
$mockStore->shouldReceive('query')
->once()
->andReturn(['matches' => []]);
$mockStore->shouldReceive('count')
->andReturn(42); // v0.14+ — stub the new capability too
$this->app->instance(StoreInterface::class, $mockStore);
Symfony AI Overhead:
symfony/ai (~50MB), which may be unnecessary for simple Pinecone use cases.symfony/http-client directly with Pinecone’s PHP SDK for lightweight integration.Metadata Filtering Quirks:
$eq, $gt) is not documented in the package. Refer to Pinecone’s API docs.count() method (v0.14+), so a passing filter in query() should behave consistently in count().Vector Dimension Mismatch:
index.describe_index_stats() to verify dimensions. When implementing VectorDocumentInterface, validate getValues() length on construction so bad documents fail fast.Rate Limiting:
App\Exceptions\Handler:
public function render($request, Throwable $exception)
{
if ($exception instanceof \Symfony\Contracts\HttpClient\Exception\RateLimitedException) {
return response()->json(['error' => 'Rate limited'], 429);
}
return parent::render($request, $exception);
}
count() calls consume the same rate limit budget as query(). Don’t call it on every page load — cache the result.NullVector Edge Cases:
NullVector when includeValues: false. This may break Laravel’s type expectations — particularly when deserializing into VectorDocumentInterface implementations that require a non-null values array.VectorDocumentInterface on types you intend to write (upsert), or guard reads with a nullable values accessor and normalize in a repository layer:
$normalized = array_map(function ($match) {
return $match['metadata'] ?? [];
}, $result['matches']);
Interface Migration Cost (v0.14+):
VectorDocumentInterface is new; existing code that upserts plain arrays still works, but mixing array payloads and interface implementations in the same upsert call can surface inconsistent behavior across store implementations.Enable HTTP Client Logging:
$client = \Symfony\Contracts\HttpClient\HttpClient::create([
'debug' => true,
]);
Logs will appear in Laravel’s storage/logs.
Pinecone API Playground:
Test queries manually at Pinecone’s API Playground to isolate issues. Compare the returned totalVectorCount there against $store->count() to confirm the count capability is wired up correctly.
Index Stats: Verify index health with:
$stats = $store->describeIndex();
// Check 'dimension', 'status', and 'totalVectorCount'
Custom Metadata Handling:
Extend the store to transform metadata before/after Pinecone operations. If you’re using VectorDocumentInterface, you can do the same enrichment by decorating the document rather than the store:
class CustomPineconeStore extends PineconeStore
{
public function upsert(array $vectors): void
{
$normalized = array_map([$this, 'normalizeMetadata'], $vectors);
parent::upsert($normalized);
}
private function normalizeMetadata(array $vector): array
{
$vector['metadata']['processed_at'] = now()->toIso8601String();
return $vector;
}
}
Caching Layer: Cache frequent queries and counts using Laravel’s cache — counts especially benefit from a short TTL:
public function query($vector, int $limit, array $filter = [], array $options = []): array
{
$cacheKey = md5(serialize([$vector, $limit, $filter]));
return cache()->remember($cacheKey, now()->addMinutes(5), function () use ($vector, $limit, $filter, $options) {
return parent::query($vector, $limit, $filter, $options);
});
}
public function count(array $filter = []): int
{
$cacheKey = 'pinecone.count.' . md5(serialize($filter));
return cache()->remember($cacheKey, now()->addMinute(), function () use ($filter) {
return parent::count($filter);
});
}
Async Operations:
Use Laravel Queues for bulk upserts (accepts plain arrays or VectorDocumentInterface instances in v0.14+):
class UpsertPineconeVectorsJob implements ShouldQueue
{
public function handle()
{
$store = app(StoreInterface::class);
$store->upsert($this->vectors);
}
}
Environment Variables:
Ensure Pinecone credentials are in .env and loaded via Laravel’s config:
'pinecone' => [
'api_key' => env('PINECONE_API_KEY'),
'env' => env('PINECONE_ENV'),
'index' => env('PINECONE_INDEX'),
],
Avoid calling env() outside config files — config caching (used in production) will return null. Reference config('services.pinecone.*') everywhere else.
Version Pinning:
Pin symfony/ai-pinecone-store to ^0.14 if you rely on count() or VectorDocumentInterface. Earlier 0.13 installs will lack both APIs, and StoreInterface compatibility checks may fail silently at runtime rather than at install time.
Interface Compatibility:
If your codebase already declares a custom DocumentInterface, verify it doesn’t collide with VectorDocumentInterface before adopting the new type — aliasing in the use statement is often enough to keep both.
How can I help you explore Laravel packages today?