Skip to main content
Version: Next

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;
}
}