View source
<?php
namespace Drupal\elasticsearch_connector\ElasticSearch\Parameters\Builder;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\elasticsearch_connector\ElasticSearch\Parameters\Factory\FilterFactory;
use Drupal\elasticsearch_connector\ElasticSearch\Parameters\Factory\IndexFactory;
use Drupal\search_api\ParseMode\ParseModeInterface;
use Drupal\search_api\Query\Condition;
use Drupal\search_api\Query\ConditionGroupInterface;
use Drupal\search_api\Query\QueryInterface;
use Elasticsearch\Common\Exceptions\ElasticsearchException;
use MakinaCorpus\Lucene\Query;
use MakinaCorpus\Lucene\TermCollectionQuery;
use MakinaCorpus\Lucene\TermQuery;
use Drupal\elasticsearch_connector\Event\PrepareSearchQueryEvent;
use Drupal\elasticsearch_connector\Event\BuildSearchParamsEvent;
use Drupal\search_api\Utility\Utility as SearchApiUtility;
use Drupal\Core\Messenger\MessengerTrait;
class SearchBuilder {
use StringTranslationTrait;
use MessengerTrait;
protected $index;
protected $query;
protected $body;
public function __construct(QueryInterface $query) {
$this->query = $query;
$this->index = $query
->getIndex();
$this->body = [];
}
public function build() {
$indexFactory = \Drupal::service('elasticsearch_connector.index_factory');
$params = $indexFactory
->index($this->index, TRUE);
$query_options = $this
->getSearchQueryOptions();
$this->body['from'] = $query_options['query_offset'];
$this->body['size'] = $query_options['query_limit'];
if (!empty($query_options['sort'])) {
$this->body['sort'] = $query_options['sort'];
}
if (!empty($query_options['query_search_string']) && !empty($query_options['query_search_filter'])) {
$this->body['query']['bool']['must'] = $query_options['query_search_string'];
$this->body['query']['bool']['filter'] = $query_options['query_search_filter'];
}
elseif (!empty($query_options['query_search_string'])) {
if (empty($this->body['query'])) {
$this->body['query'] = [];
}
$this->body['query'] += $query_options['query_search_string'];
}
elseif (!empty($query_options['query_search_filter'])) {
$this->body['query'] = $query_options['query_search_filter'];
}
if (empty($fields)) {
unset($this->body['fields']);
}
if (empty($this->body['post_filter'])) {
unset($this->body['post_filter']);
}
if (empty($query_body)) {
$query_body['match_all'] = [];
}
$exclude_source_fields = $this->query
->getOption('elasticsearch_connector_exclude_source_fields', []);
if (!empty($exclude_source_fields)) {
$this->body['_source'] = [
'excludes' => $exclude_source_fields,
];
}
$this
->setMoreLikeThisQuery($query_options);
$params['body'] = $this->body;
$this->query
->setOption('ElasticParams', $params);
$indexFactory = \Drupal::service('elasticsearch_connector.index_factory');
$indexName = $indexFactory
->getIndexName($this->index);
$dispatcher = \Drupal::service('event_dispatcher');
$buildSearchParamsEvent = new BuildSearchParamsEvent($params, $indexName);
$event = $dispatcher
->dispatch(BuildSearchParamsEvent::BUILD_QUERY, $buildSearchParamsEvent);
$params = $event
->getElasticSearchParams();
return $params;
}
protected function getSearchQueryOptions() {
$query_options = $this->query
->getOptions();
$parse_mode = $this->query
->getParseMode();
$index_fields = $this->index
->getFields();
if (empty($index_fields['search_api_datasource'])) {
$index_fields['search_api_datasource'] = \Drupal::getContainer()
->get('search_api.fields_helper')
->createField($this->index, 'search_api_datasource', [
'type' => 'string',
]);
}
$query_offset = empty($query_options['offset']) ? 0 : $query_options['offset'];
$query_limit = empty($query_options['limit']) ? 10 : $query_options['limit'];
$query_search_string = NULL;
$query_search_filter = NULL;
$keys = $this->query
->getKeys();
if (!empty($keys)) {
if (is_string($keys)) {
$keys = [
$keys,
];
}
$query_full_text_fields = $this->query
->getFulltextFields();
if ($query_full_text_fields) {
$query_full_text_fields = array_intersect($this->index
->getFulltextFields(), $query_full_text_fields);
}
else {
$query_full_text_fields = $this->index
->getFulltextFields();
}
$query_fields = [];
foreach ($query_full_text_fields as $full_text_field_name) {
$full_text_field = $index_fields[$full_text_field_name];
$query_fields[] = $full_text_field
->getFieldIdentifier() . '^' . $full_text_field
->getBoost();
}
$lucene = $this
->flattenKeys($keys, $parse_mode, $this->index
->getServerInstance()
->getBackend()
->getFuzziness());
$search_string = $lucene
->__toString();
if (!empty($search_string)) {
$query_search_string = [
'query_string' => [],
];
$query_search_string['query_string']['query'] = $search_string;
$query_search_string['query_string']['fields'] = $query_fields;
}
}
$sort = NULL;
try {
$sort = $this
->getSortSearchQuery();
} catch (ElasticsearchException $e) {
watchdog_exception('Elasticsearch Search API', $e);
$this
->messenger()
->addError($e
->getMessage());
}
$languages = $this->query
->getLanguages();
if ($languages !== NULL) {
$this->query
->getConditionGroup()
->addCondition('_language', $languages, 'IN');
}
try {
$parsed_query_filters = $this
->getQueryFilters($this->query
->getConditionGroup(), $index_fields);
if (!empty($parsed_query_filters)) {
$query_search_filter = $parsed_query_filters;
}
} catch (ElasticsearchException $e) {
watchdog_exception('Elasticsearch Search API', $e, Html::escape($e
->getMessage()));
$this
->messenger()
->addError(Html::escape($e
->getMessage()));
}
$mlt = [];
if (isset($query_options['search_api_mlt'])) {
$mlt = $query_options['search_api_mlt'];
}
$elasticSearchQuery = [
'query_offset' => $query_offset,
'query_limit' => $query_limit,
'query_search_string' => $query_search_string,
'query_search_filter' => $query_search_filter,
'sort' => $sort,
'mlt' => $mlt,
];
$indexFactory = \Drupal::service('elasticsearch_connector.index_factory');
$indexName = $indexFactory
->getIndexName($this->index);
$dispatcher = \Drupal::service('event_dispatcher');
$prepareSearchQueryEvent = new PrepareSearchQueryEvent($elasticSearchQuery, $indexName);
$event = $dispatcher
->dispatch(PrepareSearchQueryEvent::PREPARE_QUERY, $prepareSearchQueryEvent);
$elasticSearchQuery = $event
->getElasticSearchQuery();
return $elasticSearchQuery;
}
protected function flattenKeys(array $keys, ParseModeInterface $parse_mode = NULL, $fuzzy = TRUE) {
$conjunction = isset($keys['#conjunction']) ? $keys['#conjunction'] : 'AND';
$negation = !empty($keys['#negation']);
$query = (new TermCollectionQuery())
->setOperator($conjunction);
if ($negation) {
$query
->setExclusion(Query::OP_PROHIBIT);
}
$keys = array_filter($keys, function ($key) {
return $key[0] !== '#';
}, ARRAY_FILTER_USE_KEY);
foreach ($keys as $key) {
$element = NULL;
if (is_array($key)) {
$element = $this
->luceneFlattenKeys($key, $parse_mode);
}
elseif (is_string($key)) {
$element = (new TermQuery())
->setValue($key);
if ($fuzzy) {
$element
->setFuzzyness($fuzzy);
}
}
if (isset($element)) {
$query
->add($element);
}
}
return $query;
}
protected function getSortSearchQuery() {
$index_fields = $this->index
->getFields();
$sort = [];
$query_full_text_fields = $this->index
->getFulltextFields();
foreach ($this->query
->getSorts() as $field_id => $direction) {
$direction = mb_strtolower($direction);
if ($field_id === 'search_api_relevance') {
$sort['_score'] = $direction;
}
elseif ($field_id === 'search_api_id') {
$sort['id'] = $direction;
}
elseif (isset($index_fields[$field_id])) {
if (in_array($field_id, $query_full_text_fields)) {
$sort[$field_id . '.keyword'] = $direction;
}
else {
$sort[$field_id] = $direction;
}
}
else {
throw new \Exception(t('Incorrect sorting!'));
}
}
return $sort;
}
protected function getQueryFilters(ConditionGroupInterface $condition_group, array $index_fields) {
$filters = [];
$backend_fields = [
'_language' => TRUE,
];
if (!empty($condition_group)) {
$conjunction = $condition_group
->getConjunction();
foreach ($condition_group
->getConditions() as $condition) {
$filter = NULL;
if ($condition instanceof Condition) {
if (!$condition
->getField() || !$condition
->getValue() || !$condition
->getOperator()) {
}
$field_id = $condition
->getField();
if (!isset($index_fields[$field_id]) && !isset($backend_fields[$field_id])) {
throw new \Exception(t(':field_id Undefined field ! Incorrect filter criteria is using for searching!', [
':field_id' => $field_id,
]));
}
if (!$condition
->getOperator()) {
throw new \Exception(t('Empty filter operator for :field_id field! Incorrect filter criteria is using for searching!', [
':field_id' => $field_id,
]));
}
if (isset($index_fields[$field_id])) {
$field = $index_fields[$field_id];
switch ($field
->getType()) {
case 'boolean':
$condition
->setValue((bool) $condition
->getValue());
break;
}
}
$filter = FilterFactory::filterFromCondition($condition);
if (!empty($filter)) {
$filters[] = $filter;
}
}
elseif ($condition instanceof ConditionGroupInterface) {
$nested_filters = $this
->getQueryFilters($condition, $index_fields);
if (!empty($nested_filters)) {
$filters[] = $nested_filters;
}
}
}
$filters = $this
->setFiltersConjunction($filters, $conjunction);
}
return $filters;
}
protected function setFiltersConjunction(array &$filters, $conjunction) {
if ($conjunction === 'OR') {
$filters = [
'should' => $filters,
];
}
elseif ($conjunction === 'AND') {
$filters = [
'must' => $filters,
];
}
else {
throw new \Exception(t('Undefined conjunction :conjunction! Available values are :avail_conjunction! Incorrect filter criteria is using for searching!', [
':conjunction!' => $conjunction,
':avail_conjunction' => $conjunction,
]));
}
return [
'bool' => $filters,
];
}
protected function setMoreLikeThisQuery(array $query_options) {
if (!empty($query_options['mlt'])) {
$mlt_query['more_like_this'] = [];
if (isset($query_options['mlt']['id'])) {
$query_options['mlt']['ids'] = is_array($query_options['mlt']['id']) ? $query_options['mlt']['id'] : [
$query_options['mlt']['id'],
];
unset($query_options['mlt']['id']);
}
$language_ids = $this->query
->getLanguages();
if (empty($language_ids)) {
$language_ids[] = \Drupal::languageManager()
->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)
->getId();
$language_ids[] = LanguageInterface::LANGCODE_NOT_SPECIFIED;
$this->query
->setLanguages($language_ids);
}
foreach ($query_options['mlt']['ids'] as $id) {
foreach ($this->index
->getDatasources() as $datasource) {
if ($entity_type_id = $datasource
->getEntityTypeId()) {
$entity = \Drupal::entityTypeManager()
->getStorage($entity_type_id)
->load($id);
if ($entity instanceof ContentEntityInterface) {
$translated = FALSE;
if ($entity
->isTranslatable()) {
foreach ($language_ids as $language_id) {
if ($entity
->hasTranslation($language_id)) {
$ids[] = SearchApiUtility::createCombinedId($datasource
->getPluginId(), $datasource
->getItemId($entity
->getTranslation($language_id)
->getTypedData()));
$translated = TRUE;
}
}
}
if (!$translated) {
$ids[] = SearchApiUtility::createCombinedId($datasource
->getPluginId(), $datasource
->getItemId($entity
->getTypedData()));
}
}
else {
$ids[] = $id;
}
}
}
}
if (!empty($ids)) {
$mlt_query['more_like_this']['like'] = [];
foreach ($ids as $id) {
$mlt_query['more_like_this']['like'][] = [
'_index' => IndexFactory::getIndexName($this->index),
'_type' => '_doc',
'_id' => $id,
];
}
}
if (isset($query_options['mlt']['like'])) {
$mlt_query['more_like_this']['like'] = $query_options['mlt']['like'];
}
if (isset($query_options['mlt']['unlike'])) {
$mlt_query['more_like_this']['unlike'] = $query_options['mlt']['unlike'];
}
$mlt_query['more_like_this']['fields'] = array_values($query_options['mlt']['fields']);
$mlt_query['more_like_this']['max_query_terms'] = 1;
$mlt_query['more_like_this']['min_doc_freq'] = 1;
$mlt_query['more_like_this']['min_term_freq'] = 1;
$this->body['query']['bool']['must'][] = $mlt_query;
}
}
}