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.
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
| Component | Version | Notes |
|---|---|---|
| PHP | 8.2+ | |
| Pimcore | 11.x or 12.x | Requires the admin classic bundle. Pimcore 2026.x is not supported yet. |
| Symfony | 6.4 or 7.x | Ships with Pimcore. |
| OpenSearch | 2.x | Reachable over HTTP from the PHP container. |
| Symfony Messenger | any | Recommended for async indexing. Defaults to the Doctrine transport. |
pdftotext | poppler-utils | Optional. 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
composer config repositories.2bdev-studio composer https://2bdev-studio.repo.repman.io
composer require 2bdev-studio/pimcore-universal-search2. Register the bundle
If Symfony Flex did not add it for you, register the bundle in config/bundles.php:
return [
// ...
PimcoreLabs\Bundle\UniversalSearchBundle\UniversalSearchBundle::class => ['all' => true],
];3. Point it at OpenSearch
OPENSEARCH_URL=http://opensearch:92004. Install tables and assets
bin/console pimcore:bundle:install UniversalSearchBundle
bin/console assets:install
bin/console cache:clearThe 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:
pimcore_universal_search:
objects:
Product:
fields: [name, description, sku]
url_pattern: '/product/{key}'6. Build the index and query it
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:
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
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Master switch. When false, indexing listeners do nothing and search returns HTTP 503. |
engine Pro | opensearch or meilisearch | opensearch | Active search backend. |
force_yaml Pro | bool | false | Ignore database overrides and lock the admin settings to read-only. |
opensearch
| Key | Type | Default | Description |
|---|---|---|---|
host | string | %env(OPENSEARCH_URL)% | OpenSearch HTTP endpoint. |
index_prefix | string | pimcore_search | Physical 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.
pimcore_universal_search:
engine: meilisearch
meilisearch:
host: '%env(MEILISEARCH_URL)%'
api_key: '%env(MEILISEARCH_API_KEY)%'| Key | Type | Default | Description |
|---|---|---|---|
host | string | %env(MEILISEARCH_URL)% | Meilisearch HTTP endpoint. |
api_key | string | %env(MEILISEARCH_API_KEY)% | API key. Use a key with index write access. |
#YAML and database configurationPro
Pro merges two configuration sources:
- YAML: version-controlled defaults and infrastructure settings.
- 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.
#search
| Key | Type | Default | Description |
|---|---|---|---|
public_route | string | /search | Path of the public search endpoint. |
default_per_page | int | 10 | Default page size (max 100). |
log_queries | bool | true | Log search terms and result counts to the application logger. |
highlight.enabled | bool | true | Highlight matched terms in title and excerpt using OpenSearch highlighting. |
highlight.pre_tag | string | <mark> | Opening tag wrapped around each match. |
highlight.post_tag | string | </mark> | Closing tag. |
facets Pro | list | type, class_name, document_type, mime_type | Fields 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
| Key | Type | Default | Description |
|---|---|---|---|
async | bool | true | Dispatch index and delete operations through Symfony Messenger. |
published_only | bool | true | Index only published elements. |
exclude_non_public | bool | true | Skip elements that are not publicly accessible. |
max_asset_size_mb | int | 50 | Larger assets skip text extraction but are still indexed with metadata. |
document_types | list | page, link | Document types to index. |
excluded_document_types | list | folder, snippet | Always excluded. |
asset_mime_types | list | application/pdf, text/plain | MIME types eligible for asset indexing. |
max_queue_attempts | int | 10 | Retry limit for failed operations in the retry queue. |
queue_retry_delay_seconds | int | 300 | Minimum 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.
pimcore_universal_search:
indexing:
tika:
enabled: true
url: '%env(TIKA_URL)%'
timeout: 60| Key | Type | Default | Description |
|---|---|---|---|
tika.enabled | bool | true | Use Apache Tika for rich document extraction. |
tika.url | string | %env(default::TIKA_URL)% | Tika server URL. |
tika.timeout | int | 60 | HTTP 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:
pimcore_universal_search:
sites:
0:
index_name: my_search # optional custom physical index name
excluded_paths:
- /internal
document_types:
- page| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable indexing for the main site. |
index_name | string or null | null | Custom index name, otherwise {index_prefix}_0. |
excluded_paths | list | [] | Element paths excluded from indexing. |
document_types | list | [] | 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:
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 or0. pimcore:search:index,pimcore:search:clearandpimcore:search:statusaccept--sitefor per-site runs and counts.IndexingRunContexttakessiteIdsto limit programmatic runs.
#objects (map keyed by class name)
Declares which Data Object classes are indexed and how their URLs are built.
pimcore_universal_search:
objects:
Product:
enabled: true
fields:
- name
- description
- sku
url_pattern: '/product/{id}'
route_name: product_detail # alternative to url_pattern| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Index objects of this class. |
fields | list | [] | Fields concatenated into the searchable body. The first field is the title. |
url_pattern | string or null | null | Placeholder URL, supports {id} and {key}. |
route_name | string or null | null | Symfony 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
- A Pimcore save, publish or delete event reaches the bundle's document, asset or object listener.
- 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).
- An indexer turns the element into an
IndexRecord. - The dispatcher sends it through Messenger (
async: true) or executes it immediately. - The executor writes to OpenSearch. If OpenSearch is down, the operation goes to the retry queue.
What gets indexed per type
| Type | Title | Body |
|---|---|---|
| Document | Page title | Meta description, editable content (textarea, wysiwyg, input...) and H1 to H6 headings. |
| Asset | Filename | Extracted text for text/plain and PDF (with pdftotext). |
| Data object | First configured field | Values 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:
bin/console messenger:consume pimcore_universal_searchUse another broker by overriding the transport:
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:
- the
pimcore:search:process-queuecommand; - the Pimcore maintenance task
search_index_queue, so a runningpimcore:maintenancecron is enough.
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:
{type}_{elementId}_{siteId}_{locale}
document_42_0_en
object_118_0_de
asset_7_0_enThe 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.
| Parameter | Description |
|---|---|
q or query | Search terms. A blank query returns an empty payload. |
locale | Locale to search. Defaults to the request or default language. |
types | Comma-separated or repeated: document, asset, object. |
page, per_page | Pagination. per_page is capped at 100. |
sort | last_modified or title. Relevance when omitted. |
curl 'https://example.com/search?q=wireless+headphones&locale=en&types=object,document&per_page=5'{
"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.
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:
| Parameter | Description |
|---|---|
site_id | Site scope. See Multi-site. |
class_name | Data object class, e.g. Product. |
document_type | Pimcore document type, e.g. page. |
mime_type | Asset MIME type, e.g. application/pdf. |
headless | Only 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.
"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
| Function | Returns |
|---|---|
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. |
{% 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:
{{ 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:
{{ pimcore_search_render(terms, {'template': '@App/search/my-results.html.twig'}) }}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().
{{ 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:
{{ pimcore_search_widget_assets() }}
<search-widget results-page-url="/search-results" theme="auto"></search-widget>Attributes
| Attribute | Default | Description |
|---|---|---|
api-url | /search | Search endpoint. |
query-param | q | Query parameter name. |
min-length | 3 | Characters before the first request. |
debounce | 300 | Milliseconds to wait after typing. |
per-page | 10 | Results shown in the dropdown. |
placeholder | Search... | Input placeholder. |
types | all | Limit to document, asset, object. |
site-id, locale | current | Search scope. |
results-page-url | none | Where Enter and "See all results" lead. |
theme | auto | light, 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:
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
| Event | Detail |
|---|---|
search:query | { query }, fired when a debounced search starts. |
search:results | { query, results }, the API response. |
search:select | { hit }, the user picked a result. |
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
| Command | Description |
|---|---|
pimcore:search:index | Full or partial reindex. Options: --type, --locale, --sync, --limit. |
pimcore:search:clear | Drop and recreate the index. --force skips the confirmation. |
pimcore:search:status | Engine health, queue size and document counts. |
pimcore:search:process-queue | Retry failed index operations. Option: --limit. |
# 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):
| Tab | What it does |
|---|---|
| Status | Engine health, document counts per site and locale, last indexing run. Start a full reindex or clear an index with one click. |
| Sites | Enable sites, set index names, excluded paths and document types. |
| Objects | Pick searchable classes and fields, set URL patterns or routes. |
| Assets | Choose MIME types, size limit and Tika extraction. |
| Queue | See 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:
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.
| What | How |
|---|---|
| Object indexer | Tag pimcore.search_index.object_indexer. Bundled one has priority -128, higher wins. |
| Document indexer | Alias or decorate Contract\DocumentIndexerInterface. |
| Asset indexer | Alias or decorate Contract\AssetIndexerInterface. |
| Text extractor | Tag pimcore.search_index.text_extractor, ordered by priority. |
| Result decorator | Tag pimcore.search_index.result_decorator. |
| Search engine | Alias Contract\SearchEngineInterface and pimcore.search_index.engine. |
| Frontend | search-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.
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.
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'],
);
}
}services:
App\Search\EventDocumentIndexer: ~
PimcoreLabs\Bundle\UniversalSearchBundle\Contract\DocumentIndexerInterface:
alias: App\Search\EventDocumentIndexerAssets 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.
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.
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,
);
}
}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:
index(IndexRecord $record): voiddelete(string $id): voidsearch(SearchQuery $query): SearchResultclear(string $siteId): voidgetIndexStats(int $siteId): array
services:
App\Search\AlgoliaEngine: ~
PimcoreLabs\Bundle\UniversalSearchBundle\Contract\SearchEngineInterface:
alias: App\Search\AlgoliaEngine
pimcore.search_index.engine:
alias: App\Search\AlgoliaEngineReuse 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:
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:
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.
document.addEventListener('search-results:update', (event) => {
console.log('Swapped in', event.detail.url);
});#Troubleshooting
Saved elements do not show up in results
- With
async: truea Messenger worker must consumepimcore_universal_search. Check withbin/console messenger:stats. - Unpublished elements are skipped while
published_onlyis on. - Documents inside a Pimcore Site tree are not indexed in the free edition.
- Data object classes must be listed under
objects, with at least one field.
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.