Magento 2 – AND | OR Search Operators

Seek & ye Shall Find – or not

In Magento 1 (M1), it was possible to change the search operands from with the admin UI. Within System > Configuration > Catalog > Catalog Search was a Search Type setting with the options of Like, Fulltext and Combine (Like and Fulltext). In this simple way you had control over the way the default Magento search engine worked.

This is not the case with Magento Two (M2). By default M2 uses an ‘OR’ operand in the default search engine. This means M2 will return all products that have the term “black” or “shoe” in their title, description, etc. So a search for “black shoes” returns white shoes, black handbags, etc, etc. It will hopefully return black shoes too!

If you want to change the operand to be ‘AND’ – so that only products that have the terms “black” AND “shoe” – then you need to write your own module! This seems like over-engineering to me and hardly merchant friendly.

However, the actual module is pretty simple, only 3 files, & I’ll explain the code here.

Create your module under your company name in the Code directory. I’ve called my module Search.

Magento 2 module code structure

The first of the three files is registration.php – this does as the name suggests, registers your module in the M2 system. The code is very straightforward, bolierplate code really.

<?php
/**
 * Created by PhpStorm.
 * User: eddiemay
 * Date: 29/05/2018
 */
\Magento\Framework\Component\ComponentRegistrar::register(
	\Magento\Framework\Component\ComponentRegistrar::MODULE,
	'Fws_Search',
	__DIR__
);

The last two files are xml files that do the real work. The first is module.xml that adds your module to the M2 classpath.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Fws_Search" setup_version="1.0.1">
        <sequence>
            <module name="Magento_CatalogSearch"/>
        </sequence>
    </module>
</config>

This is again, mostly boilerplate code. The key part of this code though is the <sequence> declaration. Without it your search module will be ignored.

The final xml file is search_request.xml. This file governs the behaviour of the M2 search engine. In truth, this is an almost exact copy of the default request_search.xml that ships with M2.

The core change is the following:

<queries>
       <query xsi:type="boolQuery" name="quick_search_container" boost="1">
                <queryReference clause="must" ref="search" />

In the default implementation the queryReference clause is “should” – that Magento’s version of “OR”. Here, I’ve replaced it with “must” which translates as “AND”.

Here is the remainder of the code in search_request.xml – there are other search behaviours that you could override here in your module.

<?xml version="1.0"?>
<!--
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<requests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:noNamespaceSchemaLocation="urn:magento:framework:Search/etc/search_request.xsd">
    <request query="quick_search_container" index="catalogsearch_fulltext">
        <dimensions>
            <dimension name="scope" value="default"/>
        </dimensions>
        <queries>
            <query xsi:type="boolQuery" name="quick_search_container" boost="1">
                <queryReference clause="must" ref="search" />
                <queryReference clause="must" ref="category"/>
                <queryReference clause="must" ref="price"/>
                <queryReference clause="must" ref="visibility"/>
            </query>
            <query xsi:type="matchQuery" value="$search_term$" name="search">
                <match field="sku"/>
                <match field="*"/>
            </query>
            <query xsi:type="filteredQuery" name="category">
                <filterReference clause="must" ref="category_filter"/>
            </query>
            <query xsi:type="filteredQuery" name="price">
                <filterReference clause="must" ref="price_filter"/>
            </query>
            <query xsi:type="filteredQuery" name="visibility">
                <filterReference clause="must" ref="visibility_filter"/>
            </query>
        </queries>
        <filters>
            <filter xsi:type="termFilter" name="category_filter" field="category_ids" value="$category_ids$"/>
            <filter xsi:type="rangeFilter" name="price_filter" field="price" from="$price.from$" to="$price.to$"/>
            <filter xsi:type="termFilter" name="visibility_filter" field="visibility" value="$visibility$"/>
        </filters>
        <aggregations>
            <bucket name="price_bucket" field="price" xsi:type="dynamicBucket" method="$price_dynamic_algorithm$">
                <metrics>
                    <metric type="count"/>
                </metrics>
            </bucket>
            <bucket name="category_bucket" field="category_ids" xsi:type="termBucket">
                <metrics>
                    <metric type="count"/>
                </metrics>
            </bucket>
        </aggregations>
        <from>0</from>
        <size>10000</size>
    </request>
    <request query="advanced_search_container" index="catalogsearch_fulltext">
        <dimensions>
            <dimension name="scope" value="default"/>
        </dimensions>
        <queries>
            <query xsi:type="boolQuery" name="advanced_search_container" boost="1">
                <queryReference clause="should" ref="sku_query"/>
                <queryReference clause="should" ref="price_query"/>
                <queryReference clause="should" ref="category_query"/>
            </query>
            <query name="sku_query" xsi:type="filteredQuery">
                <filterReference clause="must" ref="sku_query_filter"/>
            </query>
            <query name="price_query" xsi:type="filteredQuery">
                <filterReference clause="must" ref="price_query_filter"/>
            </query>
            <query name="category_query" xsi:type="filteredQuery">
                <filterReference clause="must" ref="category_filter"/>
            </query>
        </queries>
        <filters>
            <filter xsi:type="wildcardFilter" name="sku_query_filter" field="sku" value="$sku$"/>
            <filter xsi:type="rangeFilter" name="price_query_filter" field="price" from="$price.from$" to="$price.to$"/>
            <filter xsi:type="termFilter" name="category_filter" field="category_ids" value="$category_ids$"/>
        </filters>
        <from>0</from>
        <size>10000</size>
    </request>
    <request query="catalog_view_container" index="catalogsearch_fulltext">
        <dimensions>
            <dimension name="scope" value="default"/>
        </dimensions>
        <queries>
            <query xsi:type="boolQuery" name="catalog_view_container" boost="1">
                <queryReference clause="must" ref="category"/>
                <queryReference clause="must" ref="price"/>
                <queryReference clause="must" ref="visibility"/>
            </query>
            <query xsi:type="filteredQuery" name="category">
                <filterReference clause="must" ref="category_filter"/>
            </query>
            <query xsi:type="filteredQuery" name="price">
                <filterReference clause="must" ref="price_filter"/>
            </query>
            <query xsi:type="filteredQuery" name="visibility">
                <filterReference clause="must" ref="visibility_filter"/>
            </query>
        </queries>
        <filters>
            <filter xsi:type="termFilter" name="category_filter" field="category_ids" value="$category_ids$"/>
            <filter xsi:type="rangeFilter" name="price_filter" field="price" from="$price.from$" to="$price.to$"/>
            <filter xsi:type="termFilter" name="visibility_filter" field="visibility" value="$visibility$"/>
        </filters>
        <aggregations>
            <bucket name="price_bucket" field="price" xsi:type="dynamicBucket" method="$price_dynamic_algorithm$">
                <metrics>
                    <metric type="count"/>
                </metrics>
            </bucket>
            <bucket name="category_bucket" field="category_ids" xsi:type="termBucket">
                <metrics>
                    <metric type="count"/>
                </metrics>
            </bucket>
        </aggregations>
        <from>0</from>
        <size>10000</size>
    </request>
</requests>

Once you’ve deployed this code, updated M2, compiled, etc, your search should now work using the AND rather than OR operands.