Extending the Search Index
Adding Custom Fields via Events
The index update process stores system fields and supported data object/asset field types by default. Extend the index with custom attributes using the following events.
UpdateIndexDataEvent
Store additional fields in the search index. Use the event matching your element type:
Pimcore\Bundle\GenericDataIndexBundle\Event\Asset\UpdateIndexDataEvent(assets)Pimcore\Bundle\GenericDataIndexBundle\Event\DataObject\UpdateIndexDataEvent(concrete data objects)Pimcore\Bundle\GenericDataIndexBundle\Event\DataObject\UpdateFolderIndexDataEvent(data object folders)Pimcore\Bundle\GenericDataIndexBundle\Event\Document\UpdateIndexDataEvent(documents)
An indexed document in the search index has this structure:
{
"system_fields": {
"id": 145,
"creationDate": "2019-05-24T15:42:20+0200",
"modificationDate": "2019-08-23T15:15:54+0200",
"type": "image",
"key": "abandoned-automobile-automotive-1082654.jpg"
},
"standard_fields": [ ... ],
"custom_fields": [ ]
}
The three sections are:
- system_fields - Base fields common to all elements (id, creationDate, fullPath, etc.)
- standard_fields - Data object fields or asset metadata supported out of the box
- custom_fields - Custom data added via
UpdateIndexDataEvent. Added fields are automatically included in full text search (depending on mapping).
ExtractMappingEvent
Define the search engine mapping for custom fields (see Elasticsearch mapping types or OpenSearch field types). Use the event matching your element type:
Pimcore\Bundle\GenericDataIndexBundle\Event\Asset\ExtractMappingEvent(assets)Pimcore\Bundle\GenericDataIndexBundle\Event\DataObject\ExtractMappingEvent(concrete data objects)Pimcore\Bundle\GenericDataIndexBundle\Event\DataObject\ExtractFolderMappingEvent(data object folders)Pimcore\Bundle\GenericDataIndexBundle\Event\Document\ExtractMappingEvent(documents)
Example 1: Asset File Size Category
This event subscriber categorizes assets by file size into small (< 300 KB),
medium (300 KB - 3 MB), and big (> 3 MB):
<?php
namespace App\EventListener;
use Pimcore\Bundle\GenericDataIndexBundle\Event\Asset\ExtractMappingEvent;
use Pimcore\Bundle\GenericDataIndexBundle\Event\Asset\UpdateIndexDataEvent;
use Pimcore\Model\Asset\Folder;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class FileSizeIndexSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
UpdateIndexDataEvent::class => 'onUpdateIndexData',
ExtractMappingEvent::class => 'onExtractMapping',
];
}
public function onUpdateIndexData(UpdateIndexDataEvent $event): void
{
$asset = $event->getElement();
if ($asset instanceof Folder) {
return;
}
$customFields = $event->getCustomFields();
$fileSize = $asset->getFileSize();
$customFields['fileSizeSelection'] = match (true) {
$fileSize < 300_000 => 'small',
$fileSize <= 3_000_000 => 'medium',
default => 'big',
};
$event->setCustomFields($customFields);
}
public function onExtractMapping(ExtractMappingEvent $event): void
{
$customFieldsMapping = $event->getCustomFieldsMapping();
// 'keyword' works well for select and multi-select filters
$customFieldsMapping['fileSizeSelection'] = [
'type' => 'keyword'
];
$event->setCustomFieldsMapping($customFieldsMapping);
}
}
# config/services.yaml
services:
_defaults:
autowire: true
App\EventListener\FileSizeIndexSubscriber:
tags:
- { name: kernel.event_subscriber }
Example 2: Data Object Variant Count
This event subscriber adds a numberOfVariants field to Car data objects,
counting direct children (variants) of each car:
<?php
namespace App\EventListener;
use Pimcore\Bundle\GenericDataIndexBundle\Event\DataObject\ExtractMappingEvent;
use Pimcore\Bundle\GenericDataIndexBundle\Event\DataObject\UpdateIndexDataEvent;
use Pimcore\Model\DataObject\Car;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class CarVariantCountSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
UpdateIndexDataEvent::class => 'onUpdateIndexData',
ExtractMappingEvent::class => 'onExtractMapping',
];
}
public function onUpdateIndexData(UpdateIndexDataEvent $event): void
{
$car = $event->getElement();
if (!$car instanceof Car) {
return;
}
$customFields = $event->getCustomFields();
$customFields['numberOfVariants'] = count($car->getChildren() ?? []);
$event->setCustomFields($customFields);
}
public function onExtractMapping(ExtractMappingEvent $event): void
{
if ($event->getClassDefinition()->getId() !== 'CAR') {
return;
}
$customFieldsMapping = $event->getCustomFieldsMapping();
$customFieldsMapping['numberOfVariants'] = [
'type' => 'integer'
];
$event->setCustomFieldsMapping($customFieldsMapping);
}
}
Rebuild Index After Changes
After registering an event subscriber, rebuild the search index:
bin/console generic-data-index:update:index -r
Search Body Processors
A search body processor transforms the fully serialized search body (the array produced by
toArray()) inside search() and getCount(), receiving the body in the same shape on both
paths — useful for constructs that only exist once the search is fully composed (e.g., relocating
composed bool filters into a kNN clause for engine-side pre-filtering).
Boundaries to be aware of: processor output is not the final request payload — the search path
appends track_total_hits afterward, and the count path strips keys _count does not accept
(_source, sort, from, size, aggs) afterward. Because the same output is sent to both
endpoints, it must stay valid for both — in practice, transform the query subtree only. Raw
client calls that bypass the adapter search (e.g. countByAttributeValue()) do not run processors.
Implement SearchBodyProcessorInterface; the implementation is tagged automatically
(pimcore.generic_data_index.search_body_processor). Processors must be side-effect-free and must
return the (transformed or unchanged) body — never null.
use Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\DefaultSearch\Search\Processor\SearchBodyProcessorInterface;
final readonly class MySearchBodyProcessor implements SearchBodyProcessorInterface
{
public function process(array $body, string $indexName): array
{
// inspect/transform $body['query'] here
return $body;
}
}