Global search is an under the radar addition in Magento 2 admin panel. The feature allows you to search for a string in various entities like products, orders, customers, configuration tabs, CMS content.

This feature is usually glossed over by most 3rd party vendors and even advanced users often do not know it is there. In this article we will explore how the global search is implemented and how you would go about adding your own functionality here.

Before we start inspecting how each type of search works lets see how the search is implemented itself. Inspecting the network requests in browser we can see that the following controller is called when searching for something - admin/index/globalSearch/.

The admin frontname is defined in Magento_Backend. The controller file we are looking for is Index/GlobalSearch.php. Inspecting the execute() function in the controller we can see the following actions:

  • First we check if current admin user is allow to use the function by validating acl.
  • Afterwards we are checking if any searchModules are defined
  • If both of the above are true we loop the searchModules, validate acl for each of the separate search configs and instantiate the search class by passing in the query and other search parameters. Take a look at the snippet below.
foreach ($this->_searchModules as $searchConfig) {
    if ($searchConfig['acl'] && !$this->_authorization->isAllowed($searchConfig['acl'])) {
        continue;
    }

    $className = $searchConfig['class'];
    if (empty($className)) {
        continue;
    }
    $searchInstance = $this->_objectManager->create($className);
    $results = $searchInstance->setStart(
        $start
    )->setLimit(
        $limit
    )->setQuery(
        $query
    )->load()->getResults();
    $items = array_merge_recursive($items, $results);
}
\Magento\Backend\Controller\Adminhtml\Index\GlobalSearch::execute

In the end a load() method is called on each model and all results are merged and passed back to user as a json response.

At this point you may be wondering where the searchModules are declared. If you inspect the code you will see that the variable was never updated. The magic happens in dependency injection declaration. The search modules are passed in as arguments for class

    <type name="Magento\Backend\Controller\Adminhtml\Index\GlobalSearch">
        <arguments>
            <argument name="searchModules" xsi:type="array">
                <item>
                    ...
                </item>
            </argument>
        </arguments>
    </type>

This allows 3rd party vendors to easily add their custom modules and hook into the native functionality without the need to rewrite anything.

In order to understand better how this works lets take a look at an example.

Products Search.

The search module is defined in native catalog search module. The module is passed in as an argument in di.xml.

    <type name="Magento\Backend\Controller\Adminhtml\Index\GlobalSearch">
        <arguments>
            <argument name="searchModules" xsi:type="array">
                <item name="products" xsi:type="array">
                    <item name="class" xsi:type="string">Magento\CatalogSearch\Model\Search\Catalog</item>
                    <item name="acl" xsi:type="string">Magento_Catalog::catalog</item>
                </item>
            </argument>
        </arguments>
    </type>

The seach model extends \Magento\Framework\DataObject which allows to return a data container with array access. The class implements the load() method - which is used in the global search implementation.  

The load implementation is fairly standard. A collection is fetched based on the set parameters.

 $collection = $this->queryFactory->get()
            ->getSearchCollection()
            ->addAttributeToSelect('name')
            ->addAttributeToSelect('description')
            ->addBackendSearchFilter($this->getQuery())
            ->setCurPage($this->getStart())
            ->setPageSize($this->getLimit())
            ->load();
\Magento\CatalogSearch\Model\Search\Catalog::load

Once the collection is loaded the data is organized in an array with certain keys.

foreach ($collection as $product) {
    $description = strip_tags($product->getDescription());
    $result[] = [
        'id' => 'product/1/' . $product->getId(),
        'type' => __('Product'),
        'name' => $product->getName(),
        'description' => $this->string->substr($description, 0, 30),
        'url' => $this->_adminhtmlData->getUrl('catalog/product/edit', ['id' => $product->getId()]),
    ];
\Magento\CatalogSearch\Model\Search\Catalog::load

The structure and key names are important because these later used to render the actual results. In case of products this returns a result with a link to exact product.

User interface

Now that we have understood how the logic and search results are defined lets take a look at how the user interface is built.

The first thing you will notice that the search is a form that returns results in the same page as suggestions below the input field. The form is defined in view/adminhtml/templates/system/search.phtml.

<form action="#" id="form-search">
    <div class="search-global-field">
        <label class="search-global-label" for="search-global"></label>
        <input
                type="text"
                class="search-global-input"
                id="search-global"
                name="query"
        <?php //phpcs:disable ?>
        data-mage-init='<?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getWidgetInitOptions()) ?>'>
        <?php //phpcs:enable ?>
        <button
                type="submit"
                class="search-global-action"
                title="<?= $block->escapeHtmlAttr(__('Search')) ?>"
        ></button>
    </div>
</form>
view/adminhtml/templates/system/search.phtml

The form is very simple - just a single input field with a label and submit button to trigger the search. The form is implemented as a widget. The widget options are defined in the block class.

public function getWidgetInitOptions()
    {
        return [
            'suggest' => [
                'dropdownWrapper' => '<div class="autocomplete-results" ></div >',
                'template' => '[data-template=search-suggest]',
                'termAjaxArgument' => 'query',
                'source' => $this->getUrl('adminhtml/index/globalSearch'),
                'filterProperty' => 'name',
                'preventClickPropagation' => false,
                'minLength' => 2,
                'submitInputOnEnter' => false,
            ]
        ];
    }
\Magento\Backend\Block\GlobalSearch

The interesting part here is that we are using a mage widget from native lib. The widget is defined in lib/web/mage/backend/suggest.js. The following options are interesting:

  • dropdownWrapper - this wraps the results in the defined tag
  • template - we are identifying the template that should be used for rendering the results. If you look at the search phtml file we can see that the template is defined in the same file <script data-template="search-suggest" type="text/x-magento-template">
  • source - this is where we define the action (controller) which will be used to fetch the results

Another interesting feature here is that there are static results sort of hardcoded in the template.

<li class="item">
    <a id="searchPreviewProducts" href="<?= $block->escapeUrl($block->getUrl('catalog/product/index/')) ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Products</a>
</li>
<li class="item">
    <a id="searchPreviewOrders" href="<?= $block->escapeUrl($block->getUrl('sales/order/index/')) ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Orders</a>
</li>

These are basically links to other search filters for specific entities, like products or orders. In cases where direct results are found this may seem useless however if you are looking for a list of orders or something of that nature this can be a great short cut to find data.

Since this is implemented as a phtml template that is calling a widget you would need to rewrite the template. The block is defined in default adminhtml scope.

<body>
    <referenceContainer name="header">
        <block class="Magento\Backend\Block\GlobalSearch" name="global.search" as="search" after="logo" aclResource="Magento_Backend::global_search"/>
    </referenceContainer>
</body>

Add custom search model

To sum up we can leave you with an idea on how to support your own entities or add additional data to the existing ones. Yotu would need to do only 2 things:

  1. Implement search model class. The class should return the data in an array with the following properties - id, type, name, description, url.
  2. Add your class as an argument in adminhtml/di.xml. The class should be passed in as searchModules argument for Magento\Backend\Controller\Adminhtml\Index\GlobalSearch.

If you would like to inspect more examples the core searchModules are a great place to start. Here are their definitions to get you started.

<type name="Magento\Backend\Controller\Adminhtml\Index\GlobalSearch">
        <arguments>
            <argument name="searchModules" xsi:type="array">
                <item name="customers" xsi:type="array">
                    <item name="class" xsi:type="string">Magento\Backend\Model\Search\Customer</item>
                    <item name="acl" xsi:type="string">Magento_Customer::customer</item>
                </item>
                <item name="sales" xsi:type="array">
                    <item name="class" xsi:type="string">Magento\Backend\Model\Search\Order</item>
                    <item name="acl" xsi:type="string">Magento_Sales::sales</item>
                </item>
                <item name="config" xsi:type="array">
                    <item name="class" xsi:type="string">Magento\Backend\Model\Search\Config</item>
                    <item name="acl" xsi:type="string">Magento_Config::config</item>
                </item>
            </argument>
        </arguments>
    </type>
etc/di.xml

Hopefully this helps you get a glimpse of how the global search is implemented and gives you an idea on how you would go about adding your own module there.