Files
michaelschiemer/docs/search-external-mapping-examples.md
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

7.4 KiB

External Entity Mapping für Search System

Suchfunktionalität ohne Entity-Änderungen - Komplette Integration über externe Konfiguration.

1. Basic Mapping Configuration

// In einem Service Provider oder Initializer
final class SearchMappingProvider
{
    public static function registerMappings(SearchableMappingRegistry $registry): void
    {
        // User Entity Mapping
        $userMapping = SearchableMapping::for(User::class)
            ->entityType('users')
            ->idField('id')
            ->field('name', 'name')
            ->field('email', 'email')
            ->field('bio', 'biography')
            ->field('created', 'createdAt', fn($date) => $date->format('Y-m-d'))
            ->boost('name', 2.0)
            ->boost('email', 1.5)
            ->autoIndex(true)
            ->build();
            
        $registry->register($userMapping);

        // Product Entity Mapping
        $productMapping = SearchableMapping::for(Product::class)
            ->entityType('products')
            ->field('title', 'name')
            ->field('description', 'description')
            ->field('category', 'category.name') // Nested field access
            ->field('price', 'price', fn($price) => $price->getCents() / 100) // Transform Money object
            ->field('tags', 'getTags', fn($tags) => implode(' ', $tags)) // Method call + transform
            ->boost('title', 3.0)
            ->boost('description', 1.0)
            ->build();
            
        $registry->register($productMapping);
    }
}

2. Configuration-Based Mapping

// config/search_mappings.php
return [
    User::class => [
        'entity_type' => 'users',
        'id_field' => 'id',
        'auto_index' => true,
        'fields' => [
            'name' => 'name',
            'email' => 'email', 
            'bio' => 'biography',
            'created' => [
                'field' => 'createdAt',
                'transformer' => fn($date) => $date->format('Y-m-d')
            ]
        ],
        'boosts' => [
            'name' => 2.0,
            'email' => 1.5
        ]
    ],
    
    Product::class => [
        'entity_type' => 'products',
        'fields' => [
            'title' => 'name',
            'description' => 'description',
            'category' => 'category.name', // Nested access
            'price' => [
                'field' => 'price',
                'transformer' => fn($price) => $price->toFloat()
            ]
        ],
        'boosts' => [
            'title' => 3.0
        ]
    ]
];

// Load configuration
$config = require 'config/search_mappings.php';
$registry->registerFromConfig($config);

3. Repository Integration (No Entity Changes)

final class UserRepository
{
    public function __construct(
        private EntityManager $entityManager,
        private SearchIndexingService $searchIndexing
    ) {
    }
    
    public function create(array $userData): User
    {
        $user = new User($userData);
        $this->entityManager->persist($user);
        $this->entityManager->flush();
        
        // Auto-index after creation (if enabled)
        $this->searchIndexing->indexEntity($user);
        
        return $user;
    }
    
    public function update(User $user, array $changes): void
    {
        // Apply changes to user
        foreach ($changes as $field => $value) {
            $user->{"set" . ucfirst($field)}($value);
        }
        
        $this->entityManager->flush();
        
        // Update search index
        $this->searchIndexing->updateEntity($user);
    }
    
    public function delete(User $user): void
    {
        // Remove from search first
        $this->searchIndexing->removeEntity($user);
        
        $this->entityManager->remove($user);
        $this->entityManager->flush();
    }
    
    public function reindexAll(): BulkIndexResult
    {
        return $this->searchIndexing->reindexEntityType('users', function() {
            return $this->entityManager->findAll(User::class);
        });
    }
}

4. Service Usage Without Entity Knowledge

final class ProductSearchService
{
    public function __construct(
        private SearchService $searchService,
        private SearchableMappingRegistry $mappingRegistry
    ) {
    }
    
    public function makeProductSearchable(Product $product): bool
    {
        // Check if product is configured as searchable
        if (!$this->mappingRegistry->isSearchable($product)) {
            return false;
        }
        
        // Create adapter automatically
        $adapter = $this->mappingRegistry->createAdapter($product);
        
        if (!$adapter) {
            return false;
        }
        
        // Index using the mapped fields
        return $this->searchService->index(
            $adapter->getEntityType(),
            $adapter->getId(),
            $adapter->toSearchDocument()
        );
    }
    
    public function searchProducts(string $query): SearchResult
    {
        return $this->searchService
            ->for('products')
            ->query($query)
            ->boost('title', 3.0) // Use configured boosts
            ->limit(20)
            ->search();
    }
}

5. Event-Driven Auto-Indexing

// Events werden automatisch von Entity Operations gefeuert
$eventDispatcher->dispatch(new EntityCreatedEvent($user));  // Auto-indexes
$eventDispatcher->dispatch(new EntityUpdatedEvent($product)); // Auto-updates
$eventDispatcher->dispatch(new EntityDeletedEvent($order));   // Auto-removes

6. Advanced Field Transformations

$mapping = SearchableMapping::for(Article::class)
    ->field('title', 'title')
    ->field('content', 'body')
    ->field('author', 'user.name') // Nested object access
    ->field('tags', 'tags', fn($tags) => implode(' ', array_column($tags, 'name')))
    ->field('published_date', 'publishedAt', fn($date) => $date?->format('Y-m-d'))
    ->field('word_count', 'body', fn($content) => str_word_count(strip_tags($content)))
    ->field('reading_time', 'body', fn($content) => ceil(str_word_count($content) / 200))
    ->build();

7. Conditional Indexing

final class ConditionalSearchListener
{
    #[EventListener(event: 'entity.updated')]
    public function onEntityUpdated(EntityUpdatedEvent $event): void
    {
        $entity = $event->getEntity();
        
        // Only index published articles
        if ($entity instanceof Article && $entity->isPublished()) {
            $this->indexingService->updateEntity($entity);
        } elseif ($entity instanceof Article && !$entity->isPublished()) {
            // Remove from index if unpublished
            $this->indexingService->removeEntity($entity);
        }
    }
}

Vorteile dieses Ansatzes:

Keine Entity-Änderungen erforderlich
Zentrale Konfiguration aller Mappings
Flexible Field-Transformationen
Automatische Indexierung über Events
Repository-Integration ohne Abhängigkeiten
Conditional Indexing möglich
Backward Compatible mit bestehenden Entities

Integration:

// Container Registration
$container->singleton(SearchableMappingRegistry::class);
$container->singleton(SearchIndexingService::class);

// Register mappings at startup
$registry = $container->get(SearchableMappingRegistry::class);
SearchMappingProvider::registerMappings($registry);

Dieser Ansatz ermöglicht vollständige Suchfunktionalität ohne jegliche Änderungen an bestehenden Entity-Klassen.