Free editionVersion 0.1.0Pimcore 11 & 12

Pimcore Universal Search

Full-text frontend search for Pimcore. The bundle indexes documents, assets and data objects into OpenSearch and exposes a JSON search API, Twig functions and a document editable.

Elements are indexed automatically when editors save, publish or delete them in Pimcore. Every element type is normalized into a single record format, so one query searches pages, PDFs and products together and returns ranked, highlighted hits.

Free and Pro

Everything on this page works in the free bundle unless it carries the Pro badge. Multi-site indexing, Meilisearch, Apache Tika extraction for Office files, facets and filters, the themeable search widget and the admin configuration panel are part of Universal Search Pro, coming soon and installed on top of the free bundle. Pro sections describe the planned behavior and may change before release. Join the waiting list.

#Requirements

ComponentVersionNotes
PHP8.2+
Pimcore11.x or 12.xRequires the admin classic bundle. Pimcore 2026.x is not supported yet.
Symfony6.4 or 7.xShips with Pimcore.
OpenSearch2.xReachable over HTTP from the PHP container.
Symfony MessengeranyRecommended for async indexing. Defaults to the Doctrine transport.
pdftotextpoppler-utilsOptional. Enables PDF text extraction.

#Installation

The package is served from the 2bdev-studio Composer repository. Access is anonymous, so no token or auth.json is needed.

1. Add the repository and require the package

bash
composer config repositories.2bdev-studio composer https://2bdev-studio.repo.repman.io
composer require 2bdev-studio/pimcore-universal-search

2. Register the bundle

If Symfony Flex did not add it for you, register the bundle in config/bundles.php:

php
return [
    // ...
    PimcoreLabs\Bundle\UniversalSearchBundle\UniversalSearchBundle::class => ['all' => true],
];

3. Point it at OpenSearch

.env
OPENSEARCH_URL=http://opensearch:9200

4. Install tables and assets

bash
bin/console pimcore:bundle:install UniversalSearchBundle
bin/console assets:install
bin/console cache:clear

The installer creates the search_index_queue (retry queue) and search_index_lock (reindex mutex) tables and the search_index admin permission.

5. Declare searchable data objects

Documents and PDF/text assets are indexed out of the box. Data object classes are opt-in:

config/packages/pimcore_universal_search.yaml
pimcore_universal_search:
    objects:
        Product:
            fields: [name, description, sku]
            url_pattern: '/product/{key}'

6. Build the index and query it

bash
bin/console pimcore:search:index --sync
curl 'http://localhost/search?q=hello'

From now on, saving or deleting an element in Pimcore keeps the index up to date.

#Configuration

Everything lives under the pimcore_universal_search root key, usually in config/packages/pimcore_universal_search.yaml. YAML is the only configuration source in the free edition. A minimal setup:

yaml
pimcore_universal_search:
    enabled: true
    opensearch:
        host: '%env(OPENSEARCH_URL)%'
        index_prefix: pimcore_search
    indexing:
        async: true
    objects:
        Product:
            fields: [name, description, sku]
            url_pattern: '/product/{key}'

Global

KeyTypeDefaultDescription
enabledbooltrueMaster switch. When false, indexing listeners do nothing and search returns HTTP 503.
engine Proopensearch or meilisearchopensearchActive search backend.
force_yaml ProboolfalseIgnore database overrides and lock the admin settings to read-only.

opensearch

KeyTypeDefaultDescription
hoststring%env(OPENSEARCH_URL)%OpenSearch HTTP endpoint.
index_prefixstringpimcore_searchPhysical index name is {prefix}_0 unless sites.0.index_name is set.

#meilisearchPro

Run the same indexing pipeline, API and templates on Meilisearch instead of OpenSearch. Switch with engine: meilisearch; index names follow the same rules.

yaml
pimcore_universal_search:
    engine: meilisearch
    meilisearch:
        host: '%env(MEILISEARCH_URL)%'
        api_key: '%env(MEILISEARCH_API_KEY)%'
KeyTypeDefaultDescription
hoststring%env(MEILISEARCH_URL)%Meilisearch HTTP endpoint.
api_keystring%env(MEILISEARCH_API_KEY)%API key. Use a key with index write access.

#YAML and database configurationPro

Pro merges two configuration sources:

  1. YAML: version-controlled defaults and infrastructure settings.
  2. Database (search_index_config): runtime overrides saved from the admin panel.

The database wins, so editors can change sites, object classes and indexing options without a deploy. Set force_yaml: true to make YAML the single source of truth; the admin settings then become read-only.

KeyTypeDefaultDescription
public_routestring/searchPath of the public search endpoint.
default_per_pageint10Default page size (max 100).
log_queriesbooltrueLog search terms and result counts to the application logger.
highlight.enabledbooltrueHighlight matched terms in title and excerpt using OpenSearch highlighting.
highlight.pre_tagstring<mark>Opening tag wrapped around each match.
highlight.post_tagstring</mark>Closing tag.
facets Prolisttype, class_name, document_type, mime_typeFields to compute facet counts for. [] disables facets. See Facets and filters.

Highlighted excerpts are cut from the indexed body around the match, so they differ from the static excerpt (truncated to 300 characters at index time). When highlighting is off or no fragment comes back, highlightedTitle and highlightedExcerpt are null and you should fall back to title and excerpt.

#indexing

KeyTypeDefaultDescription
asyncbooltrueDispatch index and delete operations through Symfony Messenger.
published_onlybooltrueIndex only published elements.
exclude_non_publicbooltrueSkip elements that are not publicly accessible.
max_asset_size_mbint50Larger assets skip text extraction but are still indexed with metadata.
document_typeslistpage, linkDocument types to index.
excluded_document_typeslistfolder, snippetAlways excluded.
asset_mime_typeslistapplication/pdf, text/plainMIME types eligible for asset indexing.
max_queue_attemptsint10Retry limit for failed operations in the retry queue.
queue_retry_delay_secondsint300Minimum delay before a queued item is retried.

Asset text comes from the plain-text extractor and, for PDFs, from pdftotext when the binary is installed. Other MIME types are indexed by filename and path only.

#Apache Tika extractionPro

Pro adds an Apache Tika extractor ahead of the built-in chain, so the full text of Word, Excel and OpenDocument files becomes searchable, not just their filename. The default asset_mime_types list grows to PDF, DOCX, XLSX, ODT and plain text.

yaml
pimcore_universal_search:
    indexing:
        tika:
            enabled: true
            url: '%env(TIKA_URL)%'
            timeout: 60
KeyTypeDefaultDescription
tika.enabledbooltrueUse Apache Tika for rich document extraction.
tika.urlstring%env(default::TIKA_URL)%Tika server URL.
tika.timeoutint60HTTP timeout in seconds.

#sites (main site)

The free edition indexes the main site only. Documents inside a Pimcore Site tree are skipped; assets and objects go into the main site index. Only the 0 key is read:

yaml
pimcore_universal_search:
    sites:
        0:
            index_name: my_search    # optional custom physical index name
            excluded_paths:
                - /internal
            document_types:
                - page
KeyTypeDefaultDescription
enabledbooltrueEnable indexing for the main site.
index_namestring or nullnullCustom index name, otherwise {index_prefix}_0.
excluded_pathslist[]Element paths excluded from indexing.
document_typeslist[]When not empty, replaces the global document_types.

#Multi-sitePro

Pro indexes every Pimcore Site into its own physical index ({index_prefix}_{siteId}). An element that belongs to several sites is indexed once per site, and each site key accepts the same options as 0:

yaml
pimcore_universal_search:
    sites:
        0:
            excluded_paths: [/internal]
        2:
            enabled: true
            index_name: my_shop_search
            document_types: [page]
  • The search API accepts site_id, defaulting to the current site or 0.
  • pimcore:search:index, pimcore:search:clear and pimcore:search:status accept --site for per-site runs and counts.
  • IndexingRunContext takes siteIds to limit programmatic runs.

#objects (map keyed by class name)

Declares which Data Object classes are indexed and how their URLs are built.

yaml
pimcore_universal_search:
    objects:
        Product:
            enabled: true
            fields:
                - name
                - description
                - sku
            url_pattern: '/product/{id}'
            route_name: product_detail   # alternative to url_pattern
KeyTypeDefaultDescription
enabledbooltrueIndex objects of this class.
fieldslist[]Fields concatenated into the searchable body. The first field is the title.
url_patternstring or nullnullPlaceholder URL, supports {id} and {key}.
route_namestring or nullnullSymfony route used to generate the URL.

Objects without a resolvable URL are indexed as headless hits (headless: true): searchable, but with no link.

#How indexing works

  1. A Pimcore save, publish or delete event reaches the bundle's document, asset or object listener.
  2. The indexing service checks that the bundle is enabled and the element is eligible (published, public, not in an excluded path, allowed type or MIME type).
  3. An indexer turns the element into an IndexRecord.
  4. The dispatcher sends it through Messenger (async: true) or executes it immediately.
  5. The executor writes to OpenSearch. If OpenSearch is down, the operation goes to the retry queue.

What gets indexed per type

TypeTitleBody
DocumentPage titleMeta description, editable content (textarea, wysiwyg, input...) and H1 to H6 headings.
AssetFilenameExtracted text for text/plain and PDF (with pdftotext).
Data objectFirst configured fieldValues of the fields listed under objects.<Class>.fields, one per localized locale.

Every record carries id, siteId, locale, type, elementId, title, excerpt, body, url, headless, lastModified and customFields. Search uses a multi_match query with boosts title^3, excerpt^2, body and fuzziness: AUTO, so small typos still match.

#Async indexing and retry queue

Index messages are routed to the pimcore_universal_search Messenger transport, which defaults to doctrine://default?queue_name=pimcore_universal_search. Run a worker for it:

bash
bin/console messenger:consume pimcore_universal_search

Use another broker by overriding the transport:

config/packages/messenger.yaml
framework:
    messenger:
        transports:
            pimcore_universal_search: 'amqp://rabbitmq:5672/%2f/pimcore_universal_search'

For synchronous indexing (tests, small sites) set indexing.async: false, or pass --sync to pimcore:search:index.

Retry queue

When OpenSearch rejects or cannot receive an operation, it is stored in search_index_queue with an attempt counter, the next retry time (queue_retry_delay_seconds, 300 by default) and the last error. After max_queue_attempts (10) the item is dropped. Queued items are retried by:

Concurrent full reindexes are prevented by a lock row in search_index_lock.

#Index identity

Each record has a stable ID, so re-saving an element overwrites its entry instead of duplicating it:

format
{type}_{elementId}_{siteId}_{locale}

document_42_0_en
object_118_0_de
asset_7_0_en

The free edition always uses site ID 0, so the physical index is pimcore_search_0 by default.

#Public search API

GET /search (path set by search.public_route) returns JSON.

ParameterDescription
q or querySearch terms. A blank query returns an empty payload.
localeLocale to search. Defaults to the request or default language.
typesComma-separated or repeated: document, asset, object.
page, per_pagePagination. per_page is capped at 100.
sortlast_modified or title. Relevance when omitted.
bash
curl 'https://example.com/search?q=wireless+headphones&locale=en&types=object,document&per_page=5'
json
{
  "hits": [
    {
      "id": "document_42_0_en",
      "type": "document",
      "title": "About us",
      "excerpt": "We design products that last...",
      "url": "/about",
      "headless": false,
      "score": 1.4,
      "customFields": {},
      "highlightedTitle": null,
      "highlightedExcerpt": "...our <mark>products</mark> are built to last..."
    }
  ],
  "totalCount": 1,
  "processingTimeMs": 3.2,
  "facets": [],
  "page": 1,
  "perPage": 10
}

Sending Accept: text/html returns the same result set rendered with the results template instead of JSON. The bundled results script uses this to update the page without a reload.

Note

facets is always empty in the free edition. The key stays in the response so clients keep working unchanged after upgrading to Pro. When enabled is false the endpoint answers 503.

#Facets and filtersPro

Pro computes facet counts for the fields listed in search.facets and lets clients narrow results with filter parameters:

ParameterDescription
site_idSite scope. See Multi-site.
class_nameData object class, e.g. Product.
document_typePimcore document type, e.g. page.
mime_typeAsset MIME type, e.g. application/pdf.
headlessOnly hits with (0) or without (1) a URL.

Repeat a parameter or pass it comma-separated for multiple values: ?class_name[]=Product&class_name[]=Article or ?class_name=Product,Article.

json
"facets": {
  "type": {"document": 14, "object": 12, "asset": 3},
  "class_name": {"Product": 12}
}

The bundled results page renders the facets as filter links next to the hits, and they keep working without JavaScript.

#Twig functions

FunctionReturns
pimcore_search(terms, options = {})A SearchResult to loop over in your own markup.
pimcore_search_render(terms, options = {})Rendered HTML of the results page (hits, sorting, pagination).
pimcore_search_url(overrides)Current URL with some query parameters replaced. null or '' drops a parameter.
twig
{% set result = pimcore_search(app.request.query.get('q')) %}

<p>{{ result.totalCount }} results</p>
{% for hit in result.hits %}
    <a href="{{ hit.url }}">
        {% if hit.highlightedTitle %}{{ hit.highlightedTitle|raw }}{% else %}{{ hit.title }}{% endif %}
    </a>
{% endfor %}

<a href="{{ pimcore_search_url({page: 2}) }}">Next</a>

highlightedTitle and highlightedExcerpt are already escaped by the bundle, with only the highlight tags left as HTML, so they are safe to print with |raw.

#Results page

The quickest way to a full results page with pagination, sorting and highlighting:

twig
{{ pimcore_search_render(app.request.query.get('q')) }}
<script src="{{ asset('bundles/universalsearch/js/search-results.js') }}" defer></script>

Without JavaScript everything works: pagination links are plain links and the sort control submits a form. With search-results.js loaded, those interactions are fetched and swapped in place, then pushed to the browser history.

Overriding templates

results.html.twig is composed of _hit.html.twig and _pagination.html.twig. Override a single result card by providing your own _hit.html.twig through Twig template path priority, or replace the whole page:

twig
{{ pimcore_search_render(terms, {'template': '@App/search/my-results.html.twig'}) }}
Markup contract

search-results.js looks for the .pimcore-search-results root element and swaps that whole node. A custom template must keep that class, otherwise the script falls back to full page navigation.

#Document editable

Editors can drop a search results area into any document. Add the search_results editable to a document template; it reads the q query parameter and renders hits server side. It accepts the same template option as pimcore_search_render().

twig
{{ pimcore_editable('results', 'search_results') }}

#Search widgetPro

A dependency-free <search-widget> web component: search-as-you-type with debounced requests, keyboard navigation, highlighted results and a link to the full results page. Drop it into any layout:

twig
{{ pimcore_search_widget_assets() }}

<search-widget results-page-url="/search-results" theme="auto"></search-widget>

Attributes

AttributeDefaultDescription
api-url/searchSearch endpoint.
query-paramqQuery parameter name.
min-length3Characters before the first request.
debounce300Milliseconds to wait after typing.
per-page10Results shown in the dropdown.
placeholderSearch...Input placeholder.
typesallLimit to document, asset, object.
site-id, localecurrentSearch scope.
results-page-urlnoneWhere Enter and "See all results" lead.
themeautolight, dark or auto (follows the OS).

Defaults for every widget on a page can also be set once with window.PimcoreSearchConfig or the pimcore_search_widget_config() Twig function.

Theming

Restyle the widget with CSS custom properties, no build step required:

css
search-widget {
  --sw-bg: #ffffff;
  --sw-text: #131B24;
  --sw-muted: #4D5765;
  --sw-border: #E1E4E8;
  --sw-hover: #F1F3F6;
  --sw-accent: #0677F7;
  --sw-error: #993c1d;
}

To change the markup of a result row, add a <template id="search-result-item"> to the page and mark the slots with data-search-field (title, excerpt, url...).

Events and JavaScript API

EventDetail
search:query{ query }, fired when a debounced search starts.
search:results{ query, results }, the API response.
search:select{ hit }, the user picked a result.
javascript
document.querySelector('search-widget').addEventListener('search:select', (event) => {
  analytics.track('search_select', { id: event.detail.hit.id });
});

window.PimcoreSearch.open();
window.PimcoreSearch.search('wireless headphones');
window.PimcoreSearch.close();

#CLI commands

CommandDescription
pimcore:search:indexFull or partial reindex. Options: --type, --locale, --sync, --limit.
pimcore:search:clearDrop and recreate the index. --force skips the confirmation.
pimcore:search:statusEngine health, queue size and document counts.
pimcore:search:process-queueRetry failed index operations. Option: --limit.
bash
# reindex only data objects, without Messenger
bin/console pimcore:search:index --type=object --sync

# start from scratch
bin/console pimcore:search:clear --force
bin/console pimcore:search:index --sync

#Admin panelPro

In the free edition, Settings, Search Index (permission search_index) shows a preview of the Pro sections with a link to the waiting list; configuration lives in YAML and operations run through the CLI commands above.

With Pro the panel becomes a full control center (extra permission search_index_config to edit settings):

TabWhat it does
StatusEngine health, document counts per site and locale, last indexing run. Start a full reindex or clear an index with one click.
SitesEnable sites, set index names, excluded paths and document types.
ObjectsPick searchable classes and fields, set URL patterns or routes.
AssetsChoose MIME types, size limit and Tika extraction.
QueueSee Queue dashboard.

Changes are saved to the database and applied without a deploy, unless force_yaml is on. See YAML and database configuration.

#Queue dashboardPro

A grid of the retry queue with element, operation, attempts, next retry time and the last error. Retry items now, process the whole queue or delete entries that will never succeed, without opening a terminal.

#Testing setup

Use a dedicated index prefix and synchronous indexing in the test environment, so tests never touch the real index and see results immediately:

config/packages/test/pimcore_universal_search.yaml
pimcore_universal_search:
    opensearch:
        index_prefix: pimcore_search_test
    indexing:
        async: false

#Extension points

Every part of the pipeline can be replaced or extended with plain Symfony services. All classes live under PimcoreLabs\Bundle\UniversalSearchBundle.

WhatHow
Object indexerTag pimcore.search_index.object_indexer. Bundled one has priority -128, higher wins.
Document indexerAlias or decorate Contract\DocumentIndexerInterface.
Asset indexerAlias or decorate Contract\AssetIndexerInterface.
Text extractorTag pimcore.search_index.text_extractor, ordered by priority.
Result decoratorTag pimcore.search_index.result_decorator.
Search engineAlias Contract\SearchEngineInterface and pimcore.search_index.engine.
Frontendsearch-results:update DOM event.

#Custom object indexer

Several object indexers can coexist. The first one whose supports() returns true, by priority, builds the record. No service definition is needed if the class is in an autowired namespace.

php
namespace App\Search;

use PimcoreLabs\Bundle\UniversalSearchBundle\Contract\ObjectIndexerInterface;
use PimcoreLabs\Bundle\UniversalSearchBundle\Indexer\IndexRecord;
use Pimcore\Model\DataObject\AbstractObject;
use Pimcore\Model\DataObject\Product;
use Pimcore\Model\Site;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag('pimcore.search_index.object_indexer', ['priority' => 64])]
final class ProductIndexer implements ObjectIndexerInterface
{
    public function supports(AbstractObject $object): bool
    {
        return $object instanceof Product;
    }

    public function buildIndexRecord(
        AbstractObject $object,
        int $siteId,
        ?Site $site = null,
        string $locale = 'en',
    ): IndexRecord {
        /** @var Product $object */
        $object->setLocale($locale);

        return IndexRecord::create(
            IndexRecord::TYPE_OBJECT,
            $object->getId(),
            $siteId,
            $locale,
            (string) $object->getName(),
            implode("\n", array_filter([
                (string) $object->getDescription(),
                (string) $object->getSku(),
            ])),
            url: '/shop/' . $object->getKey(),
            customFields: ['class_name' => 'Product'],
        );
    }
}

#Document and asset indexers

Documents and assets use a single indexer service each. Implement the interface and alias it, or decorate the bundled DocumentIndexer to reuse editable extraction and change only what you need.

php
namespace App\Search;

use PimcoreLabs\Bundle\UniversalSearchBundle\Contract\DocumentIndexerInterface;
use PimcoreLabs\Bundle\UniversalSearchBundle\Indexer\IndexRecord;
use Pimcore\Model\Document;
use Pimcore\Model\Site;

final class EventDocumentIndexer implements DocumentIndexerInterface
{
    public function supports(Document $document): bool
    {
        return $document->getType() === 'page'
            && $document->getProperty('event_date') !== null;
    }

    public function buildIndexRecord(Document $document, int $siteId, ?Site $site = null): IndexRecord
    {
        return IndexRecord::create(
            IndexRecord::TYPE_DOCUMENT,
            $document->getId(),
            $siteId,
            'en',
            $document->getTitle(),
            $document->getDescription() . "\n" . $document->getProperty('event_date'),
            $document->getFullPath(),
            customFields: ['document_type' => 'event'],
        );
    }
}
config/services.yaml
services:
    App\Search\EventDocumentIndexer: ~

    PimcoreLabs\Bundle\UniversalSearchBundle\Contract\DocumentIndexerInterface:
        alias: App\Search\EventDocumentIndexer

Assets follow the same pattern with AssetIndexerInterface (supports(Asset $asset) and buildIndexRecord(Asset $asset, int $siteId, ?Site $site = null)).

#Custom text extractor

Add text extraction for formats the built-in chain (plain text, then pdftotext) does not cover. Extractors are tried by priority; the first that supports the asset and returns non-empty text wins. Throw TextExtractionException to log a warning and fall through to the next one. Remember to add the MIME type to indexing.asset_mime_types.

php
namespace App\Search;

use PimcoreLabs\Bundle\UniversalSearchBundle\Contract\DocumentTextExtractorInterface;
use Pimcore\Model\Asset;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag('pimcore.search_index.text_extractor', ['priority' => 512])]
final class MarkdownTextExtractor implements DocumentTextExtractorInterface
{
    public function supports(Asset $asset): bool
    {
        return $asset->getMimeType() === 'text/markdown';
    }

    public function extractText(Asset $asset): string
    {
        return strip_tags((string) $asset->getData());
    }
}

#Result decorators

Decorators change hits after OpenSearch returns them and before JSON or Twig output: add a badge, a thumbnail, a breadcrumb. All tagged decorators run in sequence.

php
namespace App\Search;

use PimcoreLabs\Bundle\UniversalSearchBundle\Contract\SearchResultDecoratorInterface;
use PimcoreLabs\Bundle\UniversalSearchBundle\Search\SearchHit;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag('pimcore.search_index.result_decorator')]
final class BadgeDecorator implements SearchResultDecoratorInterface
{
    public function decorate(SearchHit $hit): SearchHit
    {
        $fields = $hit->customFields;
        $fields['badge'] = $hit->type === 'object' ? 'Product' : ucfirst($hit->type);

        return new SearchHit(
            $hit->id,
            $hit->type,
            $hit->title,
            $hit->excerpt,
            $hit->url,
            $hit->headless,
            $hit->score,
            $fields,
            $hit->highlightedTitle,
            $hit->highlightedExcerpt,
        );
    }
}
Keep the highlights

highlightedTitle and highlightedExcerpt are optional trailing constructor arguments. If you rebuild a SearchHit without them they default to null and highlighting disappears. Always forward them.

#Custom search engine

Implement SearchEngineInterface to use a backend other than OpenSearch:

config/services.yaml
services:
    App\Search\AlgoliaEngine: ~

    PimcoreLabs\Bundle\UniversalSearchBundle\Contract\SearchEngineInterface:
        alias: App\Search\AlgoliaEngine

    pimcore.search_index.engine:
        alias: App\Search\AlgoliaEngine

Reuse IndexMapping for field names and IndexNameResolver for index naming. If your engine supports highlighting, ask the backend to delimit matches with HighlightSanitizer::ENGINE_PRE_TAG and ::ENGINE_POST_TAG (never with the configured output tags) and pass every raw fragment through HighlightSanitizer::sanitize($fragment, $preTag, $postTag) before putting it on a SearchHit. That is what keeps highlighted fields safe to render.

#Programmatic search and indexing

Inject SearchService in your own controllers or services:

php
use PimcoreLabs\Bundle\UniversalSearchBundle\Search\SearchQuery;
use PimcoreLabs\Bundle\UniversalSearchBundle\Service\SearchService;

public function __construct(private readonly SearchService $searchService) {}

$result = $this->searchService->search(new SearchQuery(
    terms: 'widget',
    siteId: 0,
    locale: 'en',
    types: ['document', 'object'],
    page: 1,
    perPage: 20,
));

Index elements outside the event listeners, optionally limited to some locales and forced to run synchronously with IndexingRunContext:

php
use PimcoreLabs\Bundle\UniversalSearchBundle\Indexer\IndexingRunContext;

$context = new IndexingRunContext(
    locales: ['en', 'de'],
    forceSync: true,
);

$this->documentIndexingService->indexDocument($document, $context);

#Frontend events

After each fetch and swap, search-results.js dispatches a bubbling search-results:update event with detail: { url }. Listen on document: the results container is replaced on every navigation, so a listener bound to it would be lost.

javascript
document.addEventListener('search-results:update', (event) => {
  console.log('Swapped in', event.detail.url);
});

#Troubleshooting

Saved elements do not show up in results

OpenSearch was down for a while

Operations that failed are in the retry queue. Run bin/console pimcore:search:process-queue or wait for the maintenance task. pimcore:search:status shows how many items are waiting.

PDFs are indexed without content

Install poppler-utils so pdftotext is on the PATH of the PHP process, then reindex with pimcore:search:index --type=asset --sync. Files bigger than max_asset_size_mb are indexed by name only.

Search returns 503

enabled is false for the current environment.