Match.php 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Elasticsearch\SearchAdapter\Query\Builder;
  7. use Magento\Framework\Search\Request\Query\BoolExpression;
  8. use Magento\Framework\Search\Request\QueryInterface as RequestQueryInterface;
  9. use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface;
  10. use Magento\Framework\Search\Adapter\Preprocessor\PreprocessorInterface;
  11. /**
  12. * Builder for match query.
  13. */
  14. class Match implements QueryInterface
  15. {
  16. /**
  17. * Elasticsearch condition for case when query must not appear in the matching documents.
  18. */
  19. const QUERY_CONDITION_MUST_NOT = 'must_not';
  20. /**
  21. * @var FieldMapperInterface
  22. */
  23. private $fieldMapper;
  24. /**
  25. * @var PreprocessorInterface[]
  26. */
  27. protected $preprocessorContainer;
  28. /**
  29. * @param FieldMapperInterface $fieldMapper
  30. * @param PreprocessorInterface[] $preprocessorContainer
  31. */
  32. public function __construct(
  33. FieldMapperInterface $fieldMapper,
  34. array $preprocessorContainer
  35. ) {
  36. $this->fieldMapper = $fieldMapper;
  37. $this->preprocessorContainer = $preprocessorContainer;
  38. }
  39. /**
  40. * @inheritdoc
  41. */
  42. public function build(array $selectQuery, RequestQueryInterface $requestQuery, $conditionType)
  43. {
  44. $queryValue = $this->prepareQuery($requestQuery->getValue(), $conditionType);
  45. $queries = $this->buildQueries($requestQuery->getMatches(), $queryValue);
  46. $requestQueryBoost = $requestQuery->getBoost() ?: 1;
  47. foreach ($queries as $query) {
  48. $queryBody = $query['body'];
  49. $matchKey = isset($queryBody['match_phrase']) ? 'match_phrase' : 'match';
  50. foreach ($queryBody[$matchKey] as $field => $matchQuery) {
  51. $matchQuery['boost'] = $requestQueryBoost + $matchQuery['boost'];
  52. $queryBody[$matchKey][$field] = $matchQuery;
  53. }
  54. $selectQuery['bool'][$query['condition']][] = $queryBody;
  55. }
  56. return $selectQuery;
  57. }
  58. /**
  59. * Prepare query.
  60. *
  61. * @param string $queryValue
  62. * @param string $conditionType
  63. * @return array
  64. */
  65. protected function prepareQuery($queryValue, $conditionType)
  66. {
  67. $queryValue = $this->escape($queryValue);
  68. foreach ($this->preprocessorContainer as $preprocessor) {
  69. $queryValue = $preprocessor->process($queryValue);
  70. }
  71. $condition = $conditionType === BoolExpression::QUERY_CONDITION_NOT ?
  72. self::QUERY_CONDITION_MUST_NOT : $conditionType;
  73. return [
  74. 'condition' => $condition,
  75. 'value' => $queryValue,
  76. ];
  77. }
  78. /**
  79. * Creates valid ElasticSearch search conditions from Match queries.
  80. *
  81. * The purpose of this method is to create a structure which represents valid search query
  82. * for a full-text search.
  83. * It sets search query condition, the search query itself, and sets the search query boost.
  84. *
  85. * The search query boost is an optional in the search query and therefore it will be set to 1 by default
  86. * if none passed with a match query.
  87. *
  88. * @param array $matches
  89. * @param array $queryValue
  90. * @return array
  91. */
  92. protected function buildQueries(array $matches, array $queryValue)
  93. {
  94. $conditions = [];
  95. // Checking for quoted phrase \"phrase test\", trim escaped surrounding quotes if found
  96. $count = 0;
  97. $value = preg_replace('#^\\\\"(.*)\\\\"$#m', '$1', $queryValue['value'], -1, $count);
  98. $condition = ($count) ? 'match_phrase' : 'match';
  99. foreach ($matches as $match) {
  100. $resolvedField = $this->fieldMapper->getFieldName(
  101. $match['field'],
  102. ['type' => FieldMapperInterface::TYPE_QUERY]
  103. );
  104. $conditions[] = [
  105. 'condition' => $queryValue['condition'],
  106. 'body' => [
  107. $condition => [
  108. $resolvedField => [
  109. 'query' => $value,
  110. 'boost' => isset($match['boost']) ? $match['boost'] : 1,
  111. ],
  112. ],
  113. ],
  114. ];
  115. }
  116. return $conditions;
  117. }
  118. /**
  119. * Escape a value for special query characters such as ':', '(', ')', '*', '?', etc.
  120. *
  121. * Cut trailing plus or minus sign, and @ symbol, using of which causes InnoDB to report a syntax error.
  122. * https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html Fulltext-boolean search docs.
  123. *
  124. * @param string $value
  125. * @return string
  126. */
  127. protected function escape($value)
  128. {
  129. $value = preg_replace('/@+|[@+-]+$/', '', $value);
  130. $pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\*|\?|:|\\\)/';
  131. $replace = '\\\$1';
  132. return preg_replace($pattern, $replace, $value);
  133. }
  134. }