symfony/ai-neo4j-store
Neo4j Store integration for Symfony AI Store, enabling use of Neo4j as a vector store with support for vector indexes. Includes links to Neo4j documentation and Symfony AI resources for contributing and reporting issues.
Install Dependencies
composer require symfony/ai-neo4j-store neo4j/neo4j-php-client
Configure Neo4j Connection
Add to .env:
NEO4J_URI=bolt://localhost:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=your_password
Set Up Neo4j Schema Run this Cypher query in Neo4j Browser:
CREATE SEMANTIC INDEX `embedding_index`
FOR (n:Document)
OPTIONS {
`indexConfig`: {
`vector.dimensions`: 768,
`vector.similarity_function`: 'cosine'
}
}
Bind the Store in Laravel
In AppServiceProvider.php:
public function register()
{
$this->app->singleton(\Symfony\Component\AI\VectorStoreInterface::class, function ($app) {
return new \Symfony\Component\AI\Store\Neo4jStore(
new \Neo4j\ClientBuilder()
->withUri(env('NEO4J_URI'))
->withBasicAuth(env('NEO4J_USERNAME'), env('NEO4J_PASSWORD'))
->build()
);
});
}
First Use Case: Basic Similarity Search
use Symfony\Component\AI\VectorStoreInterface;
$store = app(VectorStoreInterface::class);
// Add embeddings
$store->add([0.1, 0.2, 0.3, ...]); // Your 768-dim vector
// Search
$results = $store->similaritySearch([0.11, 0.21, 0.31, ...], 5);
\Symfony\Component\AI\VectorStoreInterface defines the contract this package implements.// 1. Query Neo4j for graph-aware context
$graphResults = $neo4jClient->run(
'MATCH (d:Document)-[:RELATED_TO]->(c:Concept)
WHERE d.category = $category
RETURN d.embedding AS embedding, d.id AS id',
['category' => 'quantum_computing']
);
// 2. Use Symfony AI to search embeddings
$store = app(VectorStoreInterface::class);
$results = $store->similaritySearch($queryEmbedding, 10);
// 3. Merge results (e.g., prioritize graph-connected items)
$merged = collect($results)->merge($graphResults)->unique('id')->values();
Leverage Neo4j’s Cypher filtering:
// Custom query with filters
$results = $store->similaritySearch(
$queryEmbedding,
5,
['filters' => ['author' => 'Einstein', 'year' => ['>' => 1900]]]
);
// Under the hood, this generates:
/*
MATCH (d:Document)
WHERE vectorSimilarity(d.embedding, $query) > 0.8
AND d.author = 'Einstein'
AND d.year > 1900
RETURN d
ORDER BY vectorSimilarity(d.embedding, $query) DESC
LIMIT 5
*/
// Batch add embeddings (e.g., from a Laravel collection)
$embeddings = Model::chunk(100, function ($items) {
$vectors = $items->map(fn ($item) => $item->embedding)->toArray();
$store->add($vectors);
});
// Batch remove by IDs
$store->remove(['id1', 'id2', 'id3']);
Create a Laravel-friendly facade to hide Symfony dependencies:
namespace App\Services;
use Symfony\Component\AI\VectorStoreInterface;
class Neo4jVectorStoreService
{
public function __construct(private VectorStoreInterface $store) {}
public function findSimilarDocuments(array $embedding, int $limit = 5, array $filters = []): array
{
return $this->store->similaritySearch($embedding, $limit, $filters);
}
public function addDocument(array $embedding, array $metadata = []): void
{
$this->store->add($embedding, $metadata);
}
}
Use Laravel events to sync embeddings:
// In a model observer
ModelObserved::created(function ($model) {
$store = app(Neo4jVectorStoreService::class);
$store->addDocument($model->embedding, [
'id' => $model->id,
'type' => 'document',
'metadata' => $model->metadata
]);
});
CALL db.indexes()
YIELD name, type, labelsOrTypes, properties, options
WHERE type = 'SEMANTIC'
RETURN name, options
UNWIND for bulk operations:
UNWIND $embeddings AS embedding
CREATE (:Document {embedding: embedding})
Cache frequent queries in Redis:
public function cachedSimilaritySearch(array $embedding, int $limit = 5): array
{
$cacheKey = 'vector_search_' . md5(serialize($embedding));
return cache()->remember($cacheKey, now()->addHours(1), function () use ($embedding, $limit) {
return $this->store->similaritySearch($embedding, $limit);
});
}
:Content instead of :Document, :Article) and use properties for type discrimination.// Bad: Hardcoded label
CREATE (:Document {embedding: $vector})
// Good: Flexible schema
CREATE (n:Content {type: 'document', embedding: $vector})
public function add(array $embedding, array $metadata = []): void
{
if (count($embedding) !== 768) {
throw new \InvalidArgumentException('Embedding must have 768 dimensions');
}
$this->store->add($embedding, $metadata);
}
$client = (new \Neo4j\ClientBuilder())
->withUri(env('NEO4J_URI'))
->withConnectionTimeout(60) // Increase timeout
->build();
$results = $store->similaritySearch($embedding, 5, [
'rawCypher' => '
MATCH (d:Document)-[:TAGGED_WITH]->(t:Tag {name: $tag})
WHERE vectorSimilarity(d.embedding, $query) > 0.8
RETURN d
ORDER BY vectorSimilarity(d.embedding, $query) DESC
LIMIT 5
',
'params' => ['tag' => 'quantum', 'query' => $embedding]
]);
// Good: Server-side similarity
CALL db.index.vectorQueryNodes('embedding_index', $query, 5)
// Bad: Client-side (avoid)
MATCH (
How can I help you explore Laravel packages today?