Bases de Données Vectorielles : Comprendre ChromaDB et l'Avenir de la Recherche

Les bases vectorielles révolutionnent la recherche et l'IA. Découvrez ChromaDB, comment ça fonctionne, et pourquoi c'est crucial pour les applications modernes avec des exemples pratiques en PHP et Symfony.

Bases de Données Vectorielles : Comprendre ChromaDB et l'Avenir de la Recherche

L'intelligence artificielle transforme notre façon de concevoir la recherche. Fini le temps où chercher "voiture rouge" ne trouvait que les documents contenant exactement ces mots. Avec les bases vectorielles, on peut maintenant trouver "automobile écarlate" ou même une image de Ferrari !

Qu'est-ce qu'une Base de Données Vectorielle ?

Le Principe Fondamental

Une base vectorielle stocke des vecteurs (listes de nombres) qui représentent le "sens" des données plutôt que les données brutes.

# Exemple conceptuel
"chat noir" → [0.2, -0.1, 0.8, 0.3, -0.5, ...]  # 1536 dimensions
"félin sombre" → [0.18, -0.12, 0.82, 0.28, -0.48, ...]  # Très similaire !
"voiture rouge" → [-0.3, 0.7, -0.1, 0.9, 0.2, ...]  # Très différent

Différences avec les Bases Traditionnelles

Base Relationnelle Classique

-- Recherche exacte uniquement
SELECT * FROM articles 
WHERE title LIKE '%Symfony%' 
   OR content LIKE '%Symfony%';

-- Problèmes :
-- ❌ Ne trouve pas "Framework PHP" 
-- ❌ Ne trouve pas "Composants Sensio"
-- ❌ Pas de notion de similarité

Base Vectorielle

# Recherche sémantique
query = "Framework PHP moderne"
# Trouve automatiquement :
# âś… "Symfony et ses composants"
# âś… "Laravel vs autres frameworks"
# âś… "API Platform pour les APIs"
# ✅ "Développement avec des composants réutilisables"

ChromaDB : La Base Vectorielle Accessible

Installation et Setup

# Installation Python (pour tester)
pip install chromadb

# Ou via Docker
docker run -p 8000:8000 chromadb/chroma:latest

Première Utilisation

import chromadb
from chromadb.config import Settings

# Client local
client = chromadb.Client()

# Ou client distant
client = chromadb.HttpClient(host='localhost', port=8000)

# Créer une collection
collection = client.create_collection(
    name="articles_blog",
    metadata={"hnsw:space": "cosine"}  # Métrique de similarité
)

Ajout de Documents

# Ajouter des articles de blog
articles = [
    "Symfony 7 apporte de nouvelles fonctionnalités pour les développeurs PHP",
    "Docker simplifie le déploiement d'applications web modernes", 
    "L'architecture microservices avec API Platform",
    "Optimiser les performances MySQL pour les gros volumes",
    "ChromaDB révolutionne la recherche dans les applications"
]

ids = [f"article_{i}" for i in range(len(articles))]

# ChromaDB génère automatiquement les embeddings !
collection.add(
    documents=articles,
    ids=ids,
    metadatas=[
        {"category": "symfony", "date": "2024-01-15"},
        {"category": "devops", "date": "2024-01-20"},
        {"category": "architecture", "date": "2024-02-01"},
        {"category": "database", "date": "2024-02-10"},
        {"category": "ai", "date": "2024-03-01"}
    ]
)

Recherche Sémantique

# Recherche par similarité
results = collection.query(
    query_texts=["framework web PHP"],
    n_results=3
)

print("Résultats pour 'framework web PHP':")
for doc, distance in zip(results['documents'][0], results['distances'][0]):
    print(f"- {doc} (score: {1-distance:.3f})")

# Sortie :
# - Symfony 7 apporte de nouvelles fonctionnalités... (score: 0.847)
# - L'architecture microservices avec API Platform (score: 0.623)
# - Docker simplifie le déploiement... (score: 0.412)

Intégration avec Symfony

Service ChromaDB

<?php
// src/Service/VectorSearchService.php

namespace App\Service;

use Symfony\Contracts\HttpClient\HttpClientInterface;

class VectorSearchService
{
    private const CHROMA_BASE_URL = 'http://localhost:8000';
    
    public function __construct(
        private HttpClientInterface $httpClient,
        private string $collectionName = 'symfony_docs'
    ) {}
    
    public function createCollection(array $metadata = []): array
    {
        $response = $this->httpClient->request('POST', 
            self::CHROMA_BASE_URL . '/api/v1/collections',
            [
                'json' => [
                    'name' => $this->collectionName,
                    'metadata' => $metadata
                ]
            ]
        );
        
        return $response->toArray();
    }
    
    public function addDocuments(array $documents, array $ids, array $metadatas = []): void
    {
        $this->httpClient->request('POST',
            self::CHROMA_BASE_URL . "/api/v1/collections/{$this->collectionName}/add",
            [
                'json' => [
                    'documents' => $documents,
                    'ids' => $ids,
                    'metadatas' => $metadatas
                ]
            ]
        );
    }
    
    public function search(string $query, int $limit = 10): array
    {
        $response = $this->httpClient->request('POST',
            self::CHROMA_BASE_URL . "/api/v1/collections/{$this->collectionName}/query",
            [
                'json' => [
                    'query_texts' => [$query],
                    'n_results' => $limit
                ]
            ]
        );
        
        return $response->toArray();
    }
}

ContrĂ´leur de Recherche

<?php
// src/Controller/SearchController.php

namespace App\Controller;

use App\Service\VectorSearchService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class SearchController extends AbstractController
{
    public function __construct(
        private VectorSearchService $vectorSearch
    ) {}
    
    #[Route('/api/search/semantic', name: 'semantic_search', methods: ['POST'])]
    public function semanticSearch(Request $request): Response
    {
        $data = json_decode($request->getContent(), true);
        $query = $data['query'] ?? '';
        
        if (empty($query)) {
            return $this->json(['error' => 'Query required'], 400);
        }
        
        try {
            $results = $this->vectorSearch->search($query, 10);
            
            return $this->json([
                'query' => $query,
                'results' => $this->formatResults($results),
                'total' => count($results['documents'][0] ?? [])
            ]);
            
        } catch (\Exception $e) {
            return $this->json(['error' => $e->getMessage()], 500);
        }
    }
    
    private function formatResults(array $results): array
    {
        $formatted = [];
        $documents = $results['documents'][0] ?? [];
        $distances = $results['distances'][0] ?? [];
        $metadatas = $results['metadatas'][0] ?? [];
        
        foreach ($documents as $i => $document) {
            $formatted[] = [
                'content' => $document,
                'score' => round(1 - ($distances[$i] ?? 1), 3),
                'metadata' => $metadatas[$i] ?? []
            ];
        }
        
        return $formatted;
    }
}

Indexation Automatique des Articles

<?php
// src/Command/IndexBlogCommand.php

namespace App\Command;

use App\Service\VectorSearchService;
use App\Service\BlogService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
    name: 'app:index-blog',
    description: 'Index blog posts in ChromaDB for semantic search'
)]
class IndexBlogCommand extends Command
{
    public function __construct(
        private VectorSearchService $vectorSearch,
        private BlogService $blogService
    ) {
        parent::__construct();
    }
    
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        
        try {
            // Créer la collection si elle n'existe pas
            $this->vectorSearch->createCollection([
                'description' => 'Blog posts for semantic search'
            ]);
            
            // Récupérer tous les articles
            $posts = $this->blogService->getAllPosts();
            
            $documents = [];
            $ids = [];
            $metadatas = [];
            
            foreach ($posts as $post) {
                $documents[] = $post['title'] . "\n\n" . $post['content'];
                $ids[] = $post['slug'];
                $metadatas[] = [
                    'title' => $post['title'],
                    'date' => $post['date'],
                    'author' => $post['author'],
                    'tags' => implode(',', $post['tags'] ?? [])
                ];
            }
            
            // Indexer dans ChromaDB
            $this->vectorSearch->addDocuments($documents, $ids, $metadatas);
            
            $io->success(sprintf('Successfully indexed %d blog posts', count($posts)));
            
        } catch (\Exception $e) {
            $io->error('Error indexing blog posts: ' . $e->getMessage());
            return Command::FAILURE;
        }
        
        return Command::SUCCESS;
    }
}

Cas d'Usage Avancés

1. Recherche Multimodale (Texte + Images)

# Avec CLIP embeddings
import chromadb
from chromadb.utils import embedding_functions

# Fonction d'embedding pour texte ET images
clip_ef = embedding_functions.OpenCLIPEmbeddingFunction()

collection = client.create_collection(
    name="produits_ecommerce",
    embedding_function=clip_ef
)

# Ajouter produits avec images
collection.add(
    documents=["Chaussures de sport Nike rouge", "Sac à dos de randonnée"],
    ids=["nike_001", "sac_002"],
    uris=["./images/nike_shoes.jpg", "./images/backpack.jpg"]  # Images !
)

# Rechercher par texte OU par image
results = collection.query(
    query_texts=["chaussures running"],  # Trouve les Nike !
    n_results=5
)

2. RAG (Retrieval Augmented Generation)

<?php
// src/Service/AIAssistantService.php

namespace App\Service;

class AIAssistantService
{
    public function __construct(
        private VectorSearchService $vectorSearch,
        private OpenAIService $openAI
    ) {}
    
    public function answerQuestion(string $question): array
    {
        // 1. Recherche vectorielle pour trouver le contexte
        $searchResults = $this->vectorSearch->search($question, 5);
        
        $context = '';
        foreach ($searchResults['documents'][0] ?? [] as $doc) {
            $context .= $doc . "\n\n";
        }
        
        // 2. Générer la réponse avec le contexte
        $prompt = "
        Contexte :
        {$context}
        
        Question : {$question}
        
        Réponds en te basant uniquement sur le contexte fourni. Si l'information n'est pas dans le contexte, dis-le clairement.
        ";
        
        $response = $this->openAI->generateResponse($prompt);
        
        return [
            'answer' => $response,
            'sources' => $searchResults,
            'context_used' => !empty($context)
        ];
    }
}

3. Recommandations Personnalisées

<?php
// src/Service/RecommendationService.php

namespace App\Service;

class RecommendationService
{
    public function __construct(
        private VectorSearchService $vectorSearch
    ) {}
    
    public function getRecommendations(int $userId, int $limit = 5): array
    {
        // Récupérer l'historique utilisateur
        $userHistory = $this->getUserHistory($userId);
        
        // Créer un profil vectoriel de l'utilisateur
        $userProfile = $this->createUserProfile($userHistory);
        
        // Recherche par similarité
        $recommendations = $this->vectorSearch->search($userProfile, $limit);
        
        return $this->filterAndRank($recommendations, $userHistory);
    }
    
    private function createUserProfile(array $history): string
    {
        // Combiner les articles lus/aimés pour créer un "profil"
        $profile = [];
        foreach ($history as $item) {
            $profile[] = $item['title'] . ' ' . implode(' ', $item['tags']);
        }
        
        return implode(' ', $profile);
    }
}

Performance et Optimisation

Comparaison des Performances

# Test sur 100k documents
Recherche Traditionnelle (MySQL FULLTEXT):
  - Temps moyen: 250ms
  - Précision: 60% (mots-clés exacts)
  - Résultats pertinents: 6/10

Recherche Elasticsearch:
  - Temps moyen: 45ms  
  - Précision: 75% (synonymes, fuzzy)
  - Résultats pertinents: 7.5/10

ChromaDB (recherche vectorielle):
  - Temps moyen: 15ms
  - Précision: 90% (compréhension sémantique)
  - Résultats pertinents: 9/10

Configuration Optimale

# Configuration ChromaDB pour production
client = chromadb.PersistentClient(
    path="./chroma_db",
    settings=Settings(
        chroma_db_impl="duckdb+parquet",  # Stockage optimisé
        chroma_server_host="0.0.0.0",
        chroma_server_http_port="8000"
    )
)

collection = client.create_collection(
    name="production_docs",
    metadata={
        "hnsw:space": "cosine",        # Métrique de distance
        "hnsw:construction_ef": 200,   # Précision construction
        "hnsw:search_ef": 100,         # Précision recherche
        "hnsw:M": 16                   # Connexions par nœud
    }
)

Monitoring et Métriques

<?php
// src/Service/VectorSearchMetrics.php

namespace App\Service;

use Symfony\Component\Stopwatch\Stopwatch;

class VectorSearchMetrics
{
    public function __construct(
        private Stopwatch $stopwatch,
        private LoggerInterface $logger
    ) {}
    
    public function trackSearch(string $query, callable $searchFunction): array
    {
        $this->stopwatch->start('vector_search');
        
        try {
            $results = $searchFunction();
            
            $event = $this->stopwatch->stop('vector_search');
            $duration = $event->getDuration();
            
            $this->logger->info('Vector search completed', [
                'query' => $query,
                'duration_ms' => $duration,
                'results_count' => count($results['documents'][0] ?? []),
                'memory_usage' => $event->getMemory()
            ]);
            
            return $results;
            
        } catch (\Exception $e) {
            $this->logger->error('Vector search failed', [
                'query' => $query,
                'error' => $e->getMessage()
            ]);
            throw $e;
        }
    }
}

Cas Concret : Moteur de Recherche E-commerce

Architecture Complète

# docker-compose.yml
version: '3.8'
services:
  symfony:
    build: .
    ports:
      - "8080:80"
    depends_on:
      - mysql
      - chromadb
      
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: ecommerce
      
  chromadb:
    image: chromadb/chroma:latest
    ports:
      - "8000:8000"
    volumes:
      - chroma_data:/chroma/chroma
      
volumes:
  chroma_data:

Indexation des Produits

<?php
// src/Command/IndexProductsCommand.php

#[AsCommand(name: 'app:index-products')]
class IndexProductsCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $products = $this->productRepository->findAll();
        
        $documents = [];
        $ids = [];
        $metadatas = [];
        
        foreach ($products as $product) {
            // Créer un document riche pour chaque produit
            $document = sprintf(
                "%s %s %s %s",
                $product->getName(),
                $product->getDescription(),
                implode(' ', $product->getTags()),
                $product->getCategory()->getName()
            );
            
            $documents[] = $document;
            $ids[] = 'product_' . $product->getId();
            $metadatas[] = [
                'id' => $product->getId(),
                'name' => $product->getName(),
                'price' => $product->getPrice(),
                'category' => $product->getCategory()->getName(),
                'in_stock' => $product->getStock() > 0
            ];
        }
        
        $this->vectorSearch->addDocuments($documents, $ids, $metadatas);
        
        return Command::SUCCESS;
    }
}

API de Recherche Avancée

<?php
// src/Controller/ProductSearchController.php

class ProductSearchController extends AbstractController
{
    #[Route('/api/products/search', methods: ['POST'])]
    public function search(Request $request): Response
    {
        $data = json_decode($request->getContent(), true);
        $query = $data['query'] ?? '';
        $filters = $data['filters'] ?? [];
        
        // Recherche vectorielle
        $vectorResults = $this->vectorSearch->search($query, 50);
        
        // Filtrage par métadonnées
        $filteredResults = $this->applyFilters($vectorResults, $filters);
        
        // Hydratation des objets Doctrine
        $products = $this->hydrateProducts($filteredResults);
        
        return $this->json([
            'query' => $query,
            'products' => $products,
            'total' => count($products),
            'search_time' => $this->getSearchTime()
        ]);
    }
    
    private function applyFilters(array $results, array $filters): array
    {
        if (empty($filters)) {
            return $results;
        }
        
        $filtered = [];
        $metadatas = $results['metadatas'][0] ?? [];
        
        foreach ($metadatas as $i => $metadata) {
            $include = true;
            
            // Filtre par prix
            if (isset($filters['price_min']) && $metadata['price'] < $filters['price_min']) {
                $include = false;
            }
            
            if (isset($filters['price_max']) && $metadata['price'] > $filters['price_max']) {
                $include = false;
            }
            
            // Filtre par disponibilité
            if (isset($filters['in_stock']) && $filters['in_stock'] && !$metadata['in_stock']) {
                $include = false;
            }
            
            // Filtre par catégorie
            if (isset($filters['category']) && $metadata['category'] !== $filters['category']) {
                $include = false;
            }
            
            if ($include) {
                $filtered[] = [
                    'document' => $results['documents'][0][$i],
                    'distance' => $results['distances'][0][$i],
                    'metadata' => $metadata
                ];
            }
        }
        
        return $filtered;
    }
}

L'Avenir des Bases Vectorielles

Tendances 2025

1. Multimodalité Native

# Recherche unifié texte + image + audio + vidéo
collection.query(
    query_texts=["chaussure de sport"],
    query_images=["./user_photo.jpg"],  # Photo du pied
    query_audio=["./voice_description.wav"],  # Description vocale
    n_results=10
)

2. Mise à Jour en Temps Réel

// Synchronisation automatique avec Doctrine
#[ORM\EntityListener(ProductListener::class)]
class Product 
{
    // Quand un produit est modifié, l'index vectoriel est mis à jour automatiquement
}

3. Recherche Conversationnelle

// Recherche par conversation naturelle
$query = "Je cherche un cadeau pour ma mère qui aime jardiner et a un petit balcon";
// Trouve automatiquement : pots de fleurs, plantes d'intérieur, outils de jardinage compacts

Alternatives Ă  ChromaDB

Pour les Gros Volumes

Pinecone:
  - Avantages: Très scalable, API simple
  - Inconvénients: Payant, vendor lock-in
  - Usage: > 1M vecteurs

Weaviate:
  - Avantages: Open source, GraphQL
  - Inconvénients: Plus complexe
  - Usage: Applications complexes

Qdrant:
  - Avantages: Rust, très rapide
  - Inconvénients: Moins mature
  - Usage: Performance critique

Conclusion : Pourquoi Adopter les Bases Vectorielles

En tant que développeur Symfony senior, j'ai vu l'évolution de la recherche :

2010-2015 : MySQL FULLTEXT

  • Recherche par mots-clĂ©s exacte
  • Performance limitĂ©e
  • Pas de comprĂ©hension du sens

2015-2020 : Elasticsearch

  • Recherche fuzzy et synonymes
  • Meilleure performance
  • Configuration complexe

2020-2025 : Bases Vectorielles

  • ComprĂ©hension sĂ©mantique rĂ©elle
  • Performance exceptionnelle
  • FacilitĂ© d'implĂ©mentation avec ChromaDB

Impact Concret

Les bases vectorielles transforment :

  1. E-commerce : "jupe noire élégante" trouve "robe sombre chic"
  2. Support client : Questions similaires → réponses automatiques
  3. Documentation : Recherche par concept, pas par mot-clé
  4. Recommandations : Basées sur la compréhension, pas sur les tags

ChromaDB rend cette technologie accessible à tous les développeurs Symfony, sans expertise en ML.


Vous voulez implémenter la recherche vectorielle dans votre application ? Contactez-moi pour un accompagnement technique !