/**
* Retrieve result page url and set "secure" param to avoid confirm
* message when we submit form from secure page to unsecure
*
* @param string $query
* @return string
*/
public function getResultUrl($query = null)
{
return $this->_getUrl(
'catalogsearch/result',
['_query' => [QueryFactory::QUERY_VAR_NAME => $query], '_secure' => $this->_request->isSecure()]
);
}
public function saveIncrementalPopularity(QueryModel $query)
{
$adapter = $this->getConnection();
$table = $this->getMainTable();
$saveData = [
'store_id' => $query->getStoreId(),
'query_text' => $query->getQueryText(),
'popularity' => 1,
];
$updateData = [
'popularity' => new \Zend_Db_Expr('`popularity` + 1'),
];
$adapter->insertOnDuplicate($table, $saveData, $updateData);
}
/**
* Check query of a warnings
*
* @param mixed $store
* @return $this
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function checkNotes($store = null)
{
if ($this->isQueryTooLong($this->getQueryText(), $this->getMaxQueryLength())) {
$this->addNoteMessage(
__(
'Your search query can\'t be longer than %1, so we shortened your query.',
$this->getMaxQueryLength()
)
);
}
return $this;
}
<block class="Magento\CatalogSearch\Block\Result" name="search.result" template="result.phtml" cacheable="false">
<block class="Magento\CatalogSearch\Block\SearchResult\ListProduct" name="search_result_list" template="product/list.phtml" cacheable="false">
<arguments>
<!-- If argument's position depends on image size changeable in VDE:
positions:list-secondary,grid-secondary,list-actions,grid-actions,list-primary,grid-primary
-->
<argument name="positioned" xsi:type="string">positions:list-secondary</argument>
</arguments>
<block class="Magento\Catalog\Block\Product\ProductList\Toolbar" name="product_list_toolbar" template="product/list/toolbar.phtml" cacheable="false">
<block class="Magento\Theme\Block\Html\Pager" name="product_list_toolbar_pager" cacheable="false"/>
</block>
<action method="setToolbarBlockName">
<argument name="name" xsi:type="string">product_list_toolbar</argument>
</action>
<block class="Magento\Framework\View\Element\RendererList" name="category.product.type.details.renderers" as="details.renderers">
<block class="Magento\Framework\View\Element\Template" as="default"/>
</block>
</block>
<action method="setListOrders"/>
<action method="setListModes"/>
<action method="setListCollection"/>
<block class="Magento\CatalogSearch\Block\SearchResult\ListProduct" name="search_result_list" template="product/list.phtml" cacheable="false">
<arguments>
<!-- If argument's position depends on image size changeable in VDE:
positions:list-secondary,grid-secondary,list-actions,grid-actions,list-primary,grid-primary
-->
<argument name="positioned" xsi:type="string">positions:list-secondary</argument>
</arguments>
<block class="Magento\Catalog\Block\Product\ProductList\Toolbar" name="product_list_toolbar" template="product/list/toolbar.phtml" cacheable="false">
<block class="Magento\Theme\Block\Html\Pager" name="product_list_toolbar_pager" cacheable="false"/>
</block>
<action method="setToolbarBlockName">
<argument name="name" xsi:type="string">product_list_toolbar</argument>
</action>
<block class="Magento\Framework\View\Element\RendererList" name="category.product.type.details.renderers" as="details.renderers">
<block class="Magento\Framework\View\Element\Template" as="default"/>
</block>
</block>
/**
* Before prepare product collection handler
*
* @param \Magento\Catalog\Model\Layer $subject
* @param \Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection $collection
*
* @return void
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function beforePrepareProductCollection(
\Magento\Catalog\Model\Layer $subject,
\Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection $collection
) {
if ($this->_isEnabledShowOutOfStock()) {
return;
}
$this->stockHelper->addIsInStockFilterToCollection($collection);
}
/**
* Initialize product collection
*
* @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection
* @return \Magento\Catalog\Model\Layer
*/
public function prepareProductCollection($collection)
{
$this->collectionFilter->filter($collection, $this->getCurrentCategory());
return $this;
}
/**
* Add search filter criteria to search collection
*
* @param \Magento\Catalog\Model\Layer\Search\CollectionFilter $subject
* @param \Closure $proceed
* @param \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection $collection
* @param Category $category
* @return void
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function aroundFilter(
\Magento\Catalog\Model\Layer\Search\CollectionFilter $subject,
\Closure $proceed,
$collection,
Category $category
) {
$proceed($collection, $category);
public function filter(
$collection,
\Magento\Catalog\Model\Category $category
) {
$collection
->addAttributeToSelect($this->catalogConfig->getProductAttributes())
->setStore($this->storeManager->getStore())
->addMinimalPrice()
->addFinalPrice()
->addTaxPercents()
->addStoreFilter()
->addUrlRewrite()
->setVisibility($this->productVisibility->getVisibleInSearchIds());
}
foreach ($searchCriteria->getFilterGroups() as $filterGroup) {
foreach ($filterGroup->getFilters() as $filter) {
$this->addFieldToFilter($filter->getField(), $filter->getValue());
}
}
$this->requestBuilder->setFrom($searchCriteria->getCurrentPage() * $searchCriteria->getPageSize());
$this->requestBuilder->setSize($searchCriteria->getPageSize());
$request = $this->requestBuilder->create();
/**
* Bind data to request data
*
* @param array $requestData
* @param array $bindData
* @return array
*/
public function bind(array $requestData, array $bindData)
{
$data = $this->processLimits($requestData, $bindData);
$data['dimensions'] = $this->processDimensions($requestData['dimensions'], $bindData['dimensions']);
$data['queries'] = $this->processData($requestData['queries'], $bindData['placeholder']);
$data['filters'] = $this->processData($requestData['filters'], $bindData['placeholder']);
$data['aggregations'] = $this->processData($requestData['aggregations'], $bindData['placeholder']);
return $data;
/**
* Convert array to Request instance
*
* @param array $data
* @return RequestInterface
*/
private function convert($data)
{
/** @var Mapper $mapper */
$mapper = $this->objectManager->create(
'Magento\Framework\Search\Request\Mapper',
[
'objectManager' => $this->objectManager,
'rootQueryName' => $data['query'],
'queries' => $data['queries'],
'aggregations' => $data['aggregations'],
'filters' => $data['filters']
]
);
return $this->objectManager->create(
'Magento\Framework\Search\Request',
[
'name' => $data['query'],
'indexName' => $data['index'],
'from' => $data['from'],
'size' => $data['size'],
'query' => $mapper->getRootQuery(),
'dimensions' => $this->buildDimensions(isset($data['dimensions']) ? $data['dimensions'] : []),
'buckets' => $mapper->getBuckets()
]
);
}
/**
* @param array $dimensionsData
* @return array
*/
private function buildDimensions(array $dimensionsData)
{
$dimensions = [];
foreach ($dimensionsData as $dimensionData) {
$dimensions[$dimensionData['name']] = $this->objectManager->create(
'Magento\Framework\Search\Request\Dimension',
$dimensionData
);
}
return $dimensions;
}
public function create(array $data = [])
{
$currentAdapter = $this->scopeConfig->getValue($this->path, $this->scope);
if (!isset($this->adapterPool[$currentAdapter])) {
throw new \LogicException(
'There is no such adapter: ' . $currentAdapter
);
}
$adapterClass = $this->adapterPool[$currentAdapter];
$adapter = $this->objectManager->create($adapterClass, $data);
if (!($adapter instanceof \Magento\Framework\Search\AdapterInterface)) {
throw new \InvalidArgumentException(
'Adapter must implement \Magento\Framework\Search\AdapterInterface'
);
}
return $adapter;
}
/**
* Build adapter dependent query
*
* @param RequestInterface $request
* @throws \LogicException
* @return Select
*/
public function buildQuery(RequestInterface $request)
{
if (!array_key_exists($request->getIndex(), $this->indexProviders)) {
throw new \LogicException('Index provider not configured');
}
$select = $this->resource->getConnection()->select()
->from(
['search_index' => $searchIndexTable],
['entity_id' => 'entity_id']
)
->joinLeft(
['cea' => $this->resource->getTableName('catalog_eav_attribute')],
'search_index.attribute_id = cea.attribute_id',
[]
);
The current result of the $select->assemble()
:
SELECT
`search_index`.`entity_id`
FROM
`catalogsearch_fulltext_scope1` AS `search_index`
LEFT JOIN
`catalog_eav_attribute` AS `cea`
ON
search_index.attribute_id = cea.attribute_id
;
public function addTables(Select $select, RequestInterface $request)
{
$mappedTables = [];
$filters = $this->getFilters($request->getQuery());
foreach ($filters as $filter) {
list($alias, $table, $mapOn, $mappedFields) = $this->getMappingData($filter);
if (!array_key_exists($alias, $mappedTables)) {
$select->joinLeft(
[$alias => $table],
$mapOn,
$mappedFields
);
$mappedTables[$alias] = $table;
}
}
return $select;
}
$isShowOutOfStock = $this->config->isSetFlag(
'cataloginventory/options/show_out_of_stock',
ScopeInterface::SCOPE_STORE
);
if ($isShowOutOfStock === false) {
$select->joinLeft(
['stock_index' => $this->resource->getTableName('cataloginventory_stock_status')],
'search_index.entity_id = stock_index.product_id'
. $this->resource->getConnection()->quoteInto(
' AND stock_index.website_id = ?',
$this->storeManager->getWebsite()->getId()
),
[]
);
$select->where('stock_index.stock_status = ?', 1);
}
return $select;
The current result of the $select->assemble()
:
SELECT
`search_index`.`entity_id`
FROM
`catalogsearch_fulltext_scope1` AS `search_index`
LEFT JOIN `catalog_eav_attribute` AS `cea`
ON search_index.attribute_id = cea.attribute_id
LEFT JOIN `cataloginventory_stock_status` AS `stock_index`
ON
search_index.entity_id = stock_index.product_id
AND
stock_index.website_id = '1'
WHERE (stock_index.stock_status = 1)
/** @var ScoreBuilder $scoreBuilder */
$scoreBuilder = $this->scoreBuilderFactory->create();
$select = $this->processQuery(
$scoreBuilder,
$request->getQuery(),
$select,
BoolQuery::QUERY_CONDITION_MUST,
$queryContainer
);
/**
* Process query
*
* @param ScoreBuilder $scoreBuilder
* @param RequestQueryInterface $query
* @param Select $select
* @param string $conditionType
* @param QueryContainer $queryContainer
* @return Select
* @throws \InvalidArgumentException
*/
protected function processQuery(
ScoreBuilder $scoreBuilder,
RequestQueryInterface $query,
Select $select,
$conditionType,
QueryContainer $queryContainer
) {
The select has not been changed in my case.
$select = $this->addDerivedQueries(
$request,
$queryContainer,
$scoreBuilder,
$select,
$indexBuilder
);
/**
* @param RequestInterface $request
* @param QueryContainer $queryContainer
* @param ScoreBuilder $scoreBuilder
* @param Select $select
* @param IndexBuilderInterface $indexBuilder
* @return Select
* @throws \Zend_Db_Exception
*/
private function addDerivedQueries(
RequestInterface $request,
QueryContainer $queryContainer,
ScoreBuilder $scoreBuilder,
Select $select,
IndexBuilderInterface $indexBuilder
) {
$matchQueries = $queryContainer->getMatchQueries();
if (!$matchQueries) {
$select->columns($scoreBuilder->build());
$select = $this->createAroundSelect($select, $scoreBuilder);
} else {
$matchContainer = array_shift($matchQueries);
$this->matchBuilder->build(
$scoreBuilder,
$select,
$matchContainer->getRequest(),
$matchContainer->getConditionType()
);
/**
* {@inheritdoc}
*/
public function build(
ScoreBuilder $scoreBuilder,
Select $select,
RequestQueryInterface $query,
$conditionType
) {
/** @var $query \Magento\Framework\Search\Request\Query\Match */
$queryValue = $this->prepareQuery($query->getValue(), $conditionType);
/**
* Returns an array of arrays consisting of the synonyms found for each word in the input phrase
*
* For phrase: "Elizabeth is the English queen" correct output is an array of arrays containing synonyms for each
* word in the phrase:
*
* [
* 0 => [ 0 => "elizabeth" ],
* 1 => [ 0 => "is" ],
* 2 => [ 0 => "the" ],
* 3 => [ 0 => "british", 1 => "english" ],
* 4 => [ 0 => "queen", 1 => "monarch" ]
* ]
* @param string $phrase
* @return array
*/
public function getSynonymsForPhrase($phrase)
{
/**
* A helper function to query by phrase and get results
*
* @param string $phrase
* @return array
*/
private function queryByPhrase($phrase)
{
$matchQuery = $this->fullTextSelect->getMatchQuery(
['synonyms' => 'synonyms'],
$phrase,
Fulltext::FULLTEXT_MODE_BOOLEAN
);
/**
* Method for FULLTEXT search in Mysql, will generated MATCH ($columns) AGAINST ('$expression' $mode)
*
* @param string|string[] $columns Columns which add to MATCH ()
* @param string $expression Expression which add to AGAINST ()
* @param string $mode
* @return string
*/
public function getMatchQuery($columns, $expression, $mode = self::FULLTEXT_MODE_NATURAL)
{
if (is_array($columns)) {
$columns = implode(', ', $columns);
}
$expression = $this->connection->quote($expression);
$condition = self::MATCH . " ({$columns}) " . self::AGAINST . " ({$expression} {$mode})";
return $condition;
}
$query->assemble()
:
SELECT `search_synonyms`.*
FROM `search_synonyms`
WHERE (MATCH (synonyms) AGAINST ('alligator' IN BOOLEAN MODE));
$synonyms = [];
foreach ($rows as $row) {
$synonyms [] = $row['synonyms'];
}
// Go through every returned record looking for presence of the actual phrase. If there were no matching
// records found in DB then create a new entry for it in the returned array
$words = explode(' ', $phrase);
foreach ($words as $w) {
$position = $this->findInArray($w, $synonyms);
if ($position !== false) {
$synGroups[] = explode(',', $synonyms[$position]);
} else {
// No synonyms were found. Return the original word in this position
$synGroups[] = [$w];
}
}
return $synGroups;
}
$stringPrefix = '';
if ($conditionType === BoolExpression::QUERY_CONDITION_MUST) {
$stringPrefix = '+';
} elseif ($conditionType === BoolExpression::QUERY_CONDITION_NOT) {
$stringPrefix = '-';
}
$queryValues = explode(' ', $queryValue);
foreach ($queryValues as $queryKey => $queryValue) {
if (empty($queryValue)) {
unset($queryValues[$queryKey]);
} else {
$stringSuffix = self::MINIMAL_CHARACTER_LENGTH > strlen($queryValue) ? '' : '*';
$queryValues[$queryKey] = $stringPrefix . $queryValue . $stringSuffix;
}
}
$queryValue = implode(' ', $queryValues);
return $queryValue;
public function resolve(array $fields)
{
$resolvedFields = [];
foreach ($fields as $field) {
if ('*' === $field) {
$resolvedFields = [
$this->fieldFactory->create(
[
'attributeId' => null,
'column' => 'data_index',
'type' => FieldInterface::TYPE_FULLTEXT
]
)
];
break;
}
$attribute = $this->attributeCollection->getItemByColumnValue('attribute_code', $field);
$attributeId = $attribute ? $attribute->getId() : 0;
$resolvedFields[$field] = $this->fieldFactory->create(
[
'attributeId' => $attributeId,
'column' => 'data_index',
'type' => FieldInterface::TYPE_FULLTEXT
]
);
}
return $resolvedFields;
}
/**
* Add Condition for score calculation
*
* @param string $score
* @param bool $useWeights
* @return void
*/
public function addCondition($score, $useWeights = true)
{
$this->addPlus();
$condition = "{$score}";
if ($useWeights) {
$condition = "LEAST(($condition), 1000000) * POW(2, " . self::WEIGHT_FIELD . ')';
}
$this->scoreCondition .= $condition;
}
if ($fieldIds) {
$matchQuery = sprintf('(%s AND search_index.attribute_id IN (%s))', $matchQuery, implode(',', $fieldIds));
}
$select->where($matchQuery);
return $select;
The current result of the $select->assemble()
:
SELECT `search_index`.`entity_id`
FROM
`catalogsearch_fulltext_scope1` AS `search_index`
LEFT JOIN `catalog_eav_attribute` AS `cea`
ON search_index.attribute_id = cea.attribute_id
LEFT JOIN `cataloginventory_stock_status` AS `stock_index`
ON
search_index.entity_id = stock_index.product_id
AND stock_index.website_id = '1'
WHERE
(stock_index.stock_status = 1)
AND
(MATCH (data_index) AGAINST ('alligator*' IN BOOLEAN MODE))
/**
* Specifies the columns used in the FROM clause.
*
* The parameter can be a single string or Zend_Db_Expr object,
* or else an array of strings or Zend_Db_Expr objects.
*
* @param array|string|Zend_Db_Expr $cols The columns to select from this table.
* @param string $correlationName Correlation name of target table. OPTIONAL
* @return Zend_Db_Select This Zend_Db_Select object.
*/
public function columns($cols = '*', $correlationName = null)
/**
* @param Select $select
* @param ScoreBuilder $scoreBuilder
* @param string $scorePattern
* @return Select
*/
private function createAroundSelect(Select $select, ScoreBuilder $scoreBuilder)
{
$parentSelect = $this->getConnection()->select();
$parentSelect->from(
['main_select' => $select],
[
$this->entityMetadata->getEntityId() => 'entity_id',
'relevance' => sprintf('MAX(%s)', $scoreBuilder->getScoreAlias())
]
)->group($this->entityMetadata->getEntityId());
return $parentSelect;
}
$parentSelect->assemble()
SELECT
`main_select`.`entity_id`
, MAX(score) AS `relevance`
FROM (
SELECT
`search_index`.`entity_id`
, (
(0)
+ LEAST((MATCH (data_index) AGAINST ('alligator*' IN BOOLEAN MODE)), 1000000)
* POW(2, search_weight)
) AS `score`
FROM
`catalogsearch_fulltext_scope1` AS `search_index`
LEFT JOIN
`catalog_eav_attribute` AS `cea`
ON search_index.attribute_id = cea.attribute_id
LEFT JOIN `cataloginventory_stock_status` AS `stock_index`
ON
search_index.entity_id = stock_index.product_id
AND
stock_index.website_id = '1'
WHERE
(stock_index.stock_status = 1)
AND
(MATCH (data_index) AGAINST ('alligator*' IN BOOLEAN MODE))
) AS `main_select`
GROUP BY `entity_id`
The select has not been changed in my case because of the $matchQueries
absence.
/**
* @return Table
* @throws \Zend_Db_Exception
*/
private function createTemporaryTable()
{
$connection = $this->getConnection();
$tableName = $this->resource->getTableName(str_replace('.', '_', uniqid(self::TEMPORARY_TABLE_PREFIX, true)));
$table = $connection->newTable($tableName);
$connection->dropTemporaryTable($table->getName());
$table->addColumn(
self::FIELD_ENTITY_ID,
Table::TYPE_INTEGER,
10,
['unsigned' => true, 'nullable' => false, 'primary' => true],
'Entity ID'
);
$table->addColumn(
self::FIELD_SCORE,
Table::TYPE_DECIMAL,
[32, 16],
['unsigned' => true, 'nullable' => false],
'Score'
);
$table->setOption('type', 'memory');
$connection->createTemporaryTable($table);
return $table;
}
/**
* Get insert from Select object query
*
* @param Select $select
* @param string $table insert into table
* @param array $fields
* @param int|false $mode
* @return string
*/
public function insertFromSelect(Select $select, $table, array $fields = [], $mode = false)
The method returns:
INSERT INTO `search_tmp_5701d40e461c60_04941413`
SELECT
`main_select`.`entity_id`
, MAX(score) AS `relevance`
FROM (
SELECT
`search_index`.`entity_id`
, (
(0)
+ LEAST((MATCH (data_index) AGAINST ('alligator*' IN BOOLEAN MODE)), 1000000)
* POW(2, search_weight)
) AS `score`
FROM
`catalogsearch_fulltext_scope1` AS `search_index`
LEFT JOIN
`catalog_eav_attribute` AS `cea`
ON search_index.attribute_id = cea.attribute_id
LEFT JOIN `cataloginventory_stock_status` AS `stock_index`
ON
search_index.entity_id = stock_index.product_id
AND
stock_index.website_id = '1'
WHERE
(stock_index.stock_status = 1)
AND
(MATCH (data_index) AGAINST ('alligator*' IN BOOLEAN MODE))
) AS `main_select`
GROUP BY `entity_id`
ORDER BY `relevance` DESC
LIMIT 10000
The query result: