Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Fulltext search in the Magento backend to use elasticsearch #38634

Open
wants to merge 5 commits into
base: 2.4-develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,20 @@ private function analyzeRows(Select $sql)

return max(array_column($results, 'rows'));
}

/**
* When the filter is set after the backendCollection was performed, we can set the order. At this time, array_reverse is the only way for me to return good results
*
* @param $productId
* @param $exclude
*
* @return ProductCollection
*/
public function addIdFilter($productId, $exclude = false): ProductCollection
{
if (is_array($productId)) {
$this->getSelect()->order("find_in_set(e.entity_id,'".implode(',', array_reverse($productId))."')");
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The array_reverse is likely not needed but it is the only way at this time for the product result to be correctly sorted

}
return parent::addIdFilter($productId, $exclude);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<?php
declare(strict_types=1);

namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext;

use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer;
use Magento\Catalog\Model\Indexer\Product\Flat\State;
use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver;
use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler;
use Magento\Catalog\Model\Product\OptionFactory;
use Magento\Catalog\Model\ResourceModel\Category;
use Magento\Catalog\Model\ResourceModel\Helper;
use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory;
use Magento\Catalog\Model\ResourceModel\Product\Gallery;
use Magento\Catalog\Model\ResourceModel\Url;
use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory;
use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface;
use Magento\CatalogUrlRewrite\Model\Storage\DbStorage;
use Magento\Customer\Api\GroupManagementInterface;
use Magento\Customer\Model\Session;
use Magento\Eav\Model\Config;
use Magento\Framework\Api\FilterBuilder;
use Magento\Framework\Api\Search\FilterGroupBuilder;
use Magento\Framework\Api\Search\SearchCriteriaBuilder;
use Magento\Framework\Api\Search\SearchResultInterface;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SortOrderBuilder;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Data\Collection\Db\FetchStrategyInterface;
use Magento\Framework\Data\Collection\EntityFactory;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Event\ManagerInterface;
use Magento\Framework\Indexer\DimensionFactory;
use Magento\Framework\Module\Manager;
use Magento\Framework\Search\Search;
use Magento\Framework\Stdlib\DateTime;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
use Magento\Framework\Validator\UniversalFactory;
use Magento\Store\Model\StoreManagerInterface;
use Psr\Log\LoggerInterface;

class BackendCollection extends \Magento\Catalog\Model\ResourceModel\Product\Collection
{
/**
* @var FilterBuilder
*/
private FilterBuilder $filterBuilder;
/**
* @var SortOrderBuilder
*/
private SortOrderBuilder $sortOrderBuilder;
/**
* @var SearchCriteriaBuilder
*/
private SearchCriteriaBuilder $searchCriteriaBuilder;
/**
* @var SearchResultApplierFactory
*/
private SearchResultApplierFactory $searchResultApplierFactory;

/**
* @param EntityFactory $entityFactory
* @param LoggerInterface $logger
* @param FetchStrategyInterface $fetchStrategy
* @param ManagerInterface $eventManager
* @param Config $eavConfig
* @param ResourceConnection $resource
* @param \Magento\Eav\Model\EntityFactory $eavEntityFactory
* @param Helper $resourceHelper
* @param UniversalFactory $universalFactory
* @param StoreManagerInterface $storeManager
* @param Manager $moduleManager
* @param State $catalogProductFlatState
* @param ScopeConfigInterface $scopeConfig
* @param OptionFactory $productOptionFactory
* @param Url $catalogUrl
* @param TimezoneInterface $localeDate
* @param Session $customerSession
* @param DateTime $dateTime
* @param GroupManagementInterface $groupManagement
* @param FilterBuilder $filterBuilder
* @param SortOrderBuilder $sortOrderBuilder
* @param FilterGroupBuilder $filterGroupBuilder
* @param SearchCriteriaBuilder $searchCriteriaBuilder
* @param SearchResultApplierFactory $searchResultApplierFactory
* @param Search $search
* @param AdapterInterface|null $connection
* @param ProductLimitationFactory|null $productLimitationFactory
* @param MetadataPool|null $metadataPool
* @param TableMaintainer|null $tableMaintainer
* @param PriceTableResolver|null $priceTableResolver
* @param DimensionFactory|null $dimensionFactory
* @param Category|null $categoryResourceModel
* @param DbStorage|null $urlFinder
* @param GalleryReadHandler|null $productGalleryReadHandler
* @param Gallery|null $mediaGalleryResource
*/
public function __construct(
\Magento\Framework\Data\Collection\EntityFactory $entityFactory,
\Psr\Log\LoggerInterface $logger,
\Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy,
\Magento\Framework\Event\ManagerInterface $eventManager,
\Magento\Eav\Model\Config $eavConfig,
\Magento\Framework\App\ResourceConnection $resource,
\Magento\Eav\Model\EntityFactory $eavEntityFactory,
\Magento\Catalog\Model\ResourceModel\Helper $resourceHelper,
\Magento\Framework\Validator\UniversalFactory $universalFactory,
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Framework\Module\Manager $moduleManager,
\Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState,
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
\Magento\Catalog\Model\Product\OptionFactory $productOptionFactory,
\Magento\Catalog\Model\ResourceModel\Url $catalogUrl,
\Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
\Magento\Customer\Model\Session $customerSession,
\Magento\Framework\Stdlib\DateTime $dateTime,
GroupManagementInterface $groupManagement,
FilterBuilder $filterBuilder,
SortOrderBuilder $sortOrderBuilder,
FilterGroupBuilder $filterGroupBuilder,
SearchCriteriaBuilder $searchCriteriaBuilder,
SearchResultApplierFactory $searchResultApplierFactory,
private readonly Search $search,
\Magento\Framework\DB\Adapter\AdapterInterface $connection = null,
ProductLimitationFactory $productLimitationFactory = null,
MetadataPool $metadataPool = null,
TableMaintainer $tableMaintainer = null,
PriceTableResolver $priceTableResolver = null,
DimensionFactory $dimensionFactory = null,
Category $categoryResourceModel = null,
DbStorage $urlFinder = null,
GalleryReadHandler $productGalleryReadHandler = null,
\Magento\Catalog\Model\ResourceModel\Product\Gallery $mediaGalleryResource = null
) {
parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $eavConfig, $resource, $eavEntityFactory, $resourceHelper, $universalFactory, $storeManager, $moduleManager, $catalogProductFlatState, $scopeConfig, $productOptionFactory, $catalogUrl, $localeDate, $customerSession, $dateTime, $groupManagement, $connection, $productLimitationFactory, $metadataPool, $tableMaintainer, $priceTableResolver, $dimensionFactory, $categoryResourceModel, $urlFinder, $productGalleryReadHandler, $mediaGalleryResource);
$this->filterBuilder = $filterBuilder;
$this->sortOrderBuilder = $sortOrderBuilder;
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
$this->searchResultApplierFactory = $searchResultApplierFactory;
}

/**
* Add a filter onto the collection
*
* @param string $attribute
* @param mixed $condition
*
* @return void
*/
public function addFieldToFilter($attribute, $condition = null): void
{
$this->filterBuilder->setField($attribute);
$this->filterBuilder->setValue($condition);
$this->searchCriteriaBuilder->addFilter($this->filterBuilder->create());
}

/**
* Setup a search term filter and submit the search to ElasticSearch
*
* @param string $fulltext
*
* @return void
*/
public function addSearchFilter(string $fulltext): void
{
$this->addFieldToFilter('search_term', $fulltext);

$searchCriteria = $this->searchCriteriaBuilder->create();
$searchCriteria->setRequestName('admin_search_container');

$sort = $this->sortOrderBuilder->setField('relevance')->setDescendingDirection()->create();
$searchCriteria->setSortOrders([$sort]);
$result = $this->search->search($searchCriteria);
$this->getSearchResultApplier($result, $searchCriteria)->apply();
}

/**
* Assign the result from ElasticSearch to the collection
*
* @param SearchResultInterface $searchResult
* @param SearchCriteriaInterface $searchCriteria
*
* @return SearchResultApplierInterface
*/
private function getSearchResultApplier(
SearchResultInterface $searchResult,
SearchCriteriaInterface $searchCriteria
): SearchResultApplierInterface {
$sort = current($searchCriteria->getSortOrders());

return $this->searchResultApplierFactory->create(
[
'collection' => $this,
'searchResult' => $searchResult,
/** This variable sets by serOrder method, but doesn't have a getter method. */
'orders' => [$sort->getField(), $sort->getDirection()],
'size' => $searchCriteria->getPageSize(),
'currentPage' => $searchCriteria->getPageSize(),
]
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
*/
namespace Magento\CatalogSearch\Ui\DataProvider\Product;

use Magento\CatalogSearch\Model\ResourceModel\Search\Collection as SearchCollection;
use Magento\CatalogSearch\Model\ResourceModel\Fulltext\BackendCollection;
use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory as ConfigurableCollectionFactory;
use Magento\Framework\Data\Collection;
use Magento\Ui\DataProvider\AddFilterToCollectionInterface;

Expand All @@ -14,19 +15,18 @@
*/
class AddFulltextFilterToCollection implements AddFilterToCollectionInterface
{
/**
* Search Collection
*
* @var SearchCollection
*/
private $searchCollection;
private BackendCollection $backendCollection;
private ConfigurableCollectionFactory $linkCollectionFactory;

/**
* @param SearchCollection $searchCollection
* @param BackendCollection $backendCollection
*/
public function __construct(SearchCollection $searchCollection)
{
$this->searchCollection = $searchCollection;
public function __construct(
BackendCollection $backendCollection,
ConfigurableCollectionFactory $linkCollectionFactory
) {
$this->backendCollection = $backendCollection;
$this->linkCollectionFactory = $linkCollectionFactory;
}

/**
Expand All @@ -38,13 +38,30 @@ public function addFilter(Collection $collection, $field, $condition = null)
{
/** @var $collection \Magento\Catalog\Model\ResourceModel\Product\Collection */
if (isset($condition['fulltext']) && (string)$condition['fulltext'] !== '') {
$this->searchCollection->addBackendSearchFilter($condition['fulltext']);
$productIds = $this->searchCollection->load()->getAllIds();
$this->backendCollection->addSearchFilter((string) $condition['fulltext']);
$productIds = $this->backendCollection->load()->getAllIds();
$simpleProductIds = [];
if (empty($productIds)) {
//add dummy id to prevent returning full unfiltered collection
$productIds = -1;
} else {
$simpleProductIds = $this->getSimpleProductIds($this->backendCollection->getItems());
}
$collection->addIdFilter($productIds);
$collection->addIdFilter(array_merge($simpleProductIds, $productIds));
}
}

/**
* @param array $products
* @return array
*/
private function getSimpleProductIds(array $products): array
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context with this additional query is that elasticsearch does not store the simple products data from the configurable. This solution is not perfect but it does mean the backend sees the simple products as well as the parents

{
/** @var \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection $collection */
$collection = $this->linkCollectionFactory->create();
$collection->setProductListFilter($products);
$productIds = $collection->load()->getAllIds();

return $productIds;
}
}
20 changes: 20 additions & 0 deletions app/code/Magento/CatalogSearch/etc/search_request.xml
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,24 @@
<from>0</from>
<size>10000</size>
</request>
<request query="admin_search_container" index="catalogsearch_fulltext">
<dimensions>
<dimension name="scope" value="default"/>
</dimensions>
<queries>
<query xsi:type="boolQuery" name="admin_search_container" boost="1">
<queryReference clause="should" ref="search" />
<queryReference clause="should" ref="partial_search" />
</query>
<query xsi:type="matchQuery" value="$search_term$" name="search">
<match field="name" matchCondition="match"/>
</query>
<query xsi:type="matchQuery" value="$search_term$" name="partial_search">
<match field="name" matchCondition="match"/>
<match field="sku" matchCondition="match_phrase_prefix"/>
</query>
</queries>
<from>0</from>
<size>10000</size>
</request>
</requests>
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ public function setProductFilter($product)
return $this;
}

/**
* Set the collection with a list of products as filter so that we can return all the linked product for a list of products
*
* @param array $products
* @return $this
*/
public function setProductListFilter(array $products)
{
$this->products = $products;
return $this;
}

/**
* Add parent ids to `in` filter before load.
*
Expand Down