Skip to content

[4.x]: Unnecessary UpdateSearchIndex jobs #13917

@MangoMarcus

Description

@MangoMarcus

Description

We have a multisite and noticed that an excessive amount of UpdateSearchIndex (ie. "Updating search index") jobs are being queued.

After some investigation I've realised that it's triggering these jobs regardless of if any searchable fields or attributes have been updated.

Please let me know if I've misunderstood anything here

Steps to reproduce

  1. Save an existing entry without changing any fields or attributes, and it queues a new UpdateSearchIndex job even though no searchable fields have been updated
  2. Save an existing entry and only change non-searchable fields, and it queues a new UpdateSearchIndex job even though no searchable fields have been updated
  3. Save an existing entry and only change a translatable field (searchable or otherwise), and it propagates to other sites and queues a new UpdateSearchIndex for job even though no searchable fields have been updated for those sites

Expected behavior

Only queue an UpdateSearchIndex job if the element has updated searchable fields or searchable attributes

Actual behavior

Unnecessary UpdateSearchIndex jobs are queued and clogs up the queue

Suggested solution

Perhaps add an additional check here by filtering $element->getDirtyFields() and $element->getDirtyAttributes() to $searchableDirtyFields and $searchableDirtyAttributes, and skip the indexing if they're both empty. You'd then pass $searchableDirtyFields to new UpdateSearchIndex() instead of $element->getDirtyFields(). This doesn't account for the multisite aspect though in step 3.

Workaround (WIP)

I started a solution for this using EVENT_BEFORE_UPDATE_SEARCH_INDEX but hit a wall

use Craft;
use craft\events\ElementEvent;
use craft\helpers\ElementHelper;
use craft\services\Elements;
use yii\base\Event;

// Skip indexing if none of the fields or attributes are searchable
Event::on(
    Elements::class,
    Elements::EVENT_BEFORE_UPDATE_SEARCH_INDEX,
    function (ElementEvent $event) {
        $el = $event->element;
        $root = ElementHelper::rootElement($el);

        $fieldLayout = $el->getFieldLayout();
        if (!$fieldLayout) {
            return;
        }

        // Check for searchable dirty fields
        $dirtyFields = $el->getDirtyFields();
        $searchableDirtyFields = array_filter(
            $dirtyFields,
            function (string $fieldHandle) use ($fieldLayout): bool  {
                $field = $fieldLayout->getFieldByHandle($fieldHandle);
                return !$field || $field->searchable;
            },
        );
        if (count($searchableDirtyFields)) {
            return; // Allow the job to be queued even though some of the fields might not be searchable
        }

        // Check for searchable dirty attributes (eg. title, slug)
        $dirtyAttributes = $root->getDirtyAttributes();
        // Get the searchable attributes using the same logic as \craft\services\search::indexElementAttributes()
        $searchableAttributes = array_flip($root::searchableAttributes());
        $searchableAttributes['slug'] = true;
        if ($root::hasTitles()) {
            $searchableAttributes['title'] = true;
        }
        $searchableAttributes = array_keys($searchableAttributes);
        $searchableDirtyAttributes = array_intersect($dirtyAttributes, $searchableAttributes);
        if (count($searchableDirtyAttributes)) {
            return; // Allow the job to be queued even though some of the attributes might not be searchable
        }

        // Skip indexing if none of the fields or attributes are searchable
        Craft::warning('Skipping indexing of non-searchable fields or attributes', __METHOD__);
        $event->isValid = false;
    }
);

This mostly does the job but it doesn't work as expected when changing an attribute like the title or slug, in this case $dirtyAttributes is still empty and the job is incorrectly prevented.

This doesn't address unnecessary propagation to other sites but it does prevent those site entries from being queued, as they have no dirty searchable fields / attributes.

There might be other things I haven't considered too, any suggestions are welcome

Craft CMS version

4.5.10

PHP version

8.1

Operating system and version

No response

Database type and version

No response

Image driver and version

No response

Installed plugins and versions

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions