elasticsearch_search_api-1.0.x-dev/modules/elasticsearch_search_api_example/src/Search/ExampleElasticSearchParamsBuilder.php
modules/elasticsearch_search_api_example/src/Search/ExampleElasticSearchParamsBuilder.php
<?php namespace Drupal\elasticsearch_search_api_example\Search; use Drupal\elasticsearch_search_api\Search\ElasticSearchParamsBuilder; use Drupal\elasticsearch_search_api\Search\Facet\Control\CompositeFacetControlInterface; use Drupal\elasticsearch_search_api\Search\Facet\Control\FacetControlInterface; use Drupal\elasticsearch_search_api\Search\Facet\FacetCollection; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\elasticsearch_search_api\Search\FacetedKeywordSearchAction; use Drupal\elasticsearch_search_api\Search\FacetedSearchActionInterface; use Drupal\elasticsearch_search_api\Search\IndexFactoryAdapter; use Drupal\elasticsearch_search_api\Utility\UtilityHelper; use Drupal\search_api\Entity\Index; use Drupal\search_api\IndexInterface; use Drupal\search_api\Item\FieldInterface; /** * Builds parameters to pass with an ElasticSearch search request. */ class ExampleElasticSearchParamsBuilder extends ElasticSearchParamsBuilder { /** * The default boost used for ngram fields. Can be overridden. */ const NGRAM_FIELD_DEFAULT_BOOST = 0.1; /** * Set up exceptions for ngram fields that need a specific boost. * * Format: * [ * 'field_paragraph_title' => 0.5, * ] */ const NGRAM_FIELD_BOOST_EXCEPTIONS = []; /** * Index id. * * @var \Drupal\search_api\Entity\Index */ protected $index; /** * The lang code of the current language. * * @var string */ protected $langcode; /** * Index factory adapter. * * @var \Drupal\elasticsearch_search_api\Search\IndexFactoryAdapter */ protected $indexFactoryAdapter; /** * ElasticSearchParamsBuilder constructor. */ public function __construct(IndexInterface $index, LanguageManagerInterface $languageManager, IndexFactoryAdapter $indexFactoryAdapter) { $this->index = $index; $this->langcode = $languageManager->getCurrentLanguage()->getId(); $this->indexFactoryAdapter = $indexFactoryAdapter; } /** * {@inheritdoc} */ public function build(FacetedSearchActionInterface $searchAction): array { if ($searchAction instanceof FacetedKeywordSearchAction && $keyword = $searchAction->getKeyword()) { $fragment_size = 300; $params = [ 'body' => [ '_source' => FALSE, 'from' => $searchAction->getFrom(), 'size' => $searchAction->getSize(), 'query' => $this->buildMultiMatchQueries($keyword), 'highlight' => [ 'number_of_fragments' => 1, 'fragment_size' => is_numeric($fragment_size) ? $fragment_size : 300, 'fields' => [ "*" => [ "require_field_match" => FALSE, ], ], 'pre_tags' => '<strong>', 'post_tags' => '</strong>', ], ], ]; } else { $params = [ 'body' => [ '_source' => FALSE, 'from' => $searchAction->getFrom(), 'size' => $searchAction->getSize(), ], ]; } $chosenFacetValues = $searchAction->getChosenFacetValues(); if (count($searchAction->getAvailableFacets())) { $params['body']['aggs'] = $this->buildAggregations($searchAction, $chosenFacetValues); } $post_filter = $this->buildFacetFilters($chosenFacetValues); $params['body']['post_filter'] = [ 'bool' => [ 'must' => $post_filter, ], ]; $index = $this->getIndexName($this->index); $params['index'] = $index; return $params; } /** * Get standard filters for the search query. * * Returns a list of standard filters, such as language, published state, * and content type. * * @return array * Standard filters. */ protected function getStandardFilters() { return [ [ 'term' => [ 'langcode' => $this->langcode, ], ], [ 'term' => [ 'status' => 1, ], ], ]; } /** * Builds a filter for a given set of facet values. * * @param \Drupal\elasticsearch_search_api\Search\Facet\FacetCollection $facet_values * Facet values. * * @return array * Array to be used as an ElasticSearch filter. */ protected function buildFacetFilters(FacetCollection $facet_values): array { $post_filter = []; /** @var \Drupal\elasticsearch_search_api\Search\Facet\FacetValueInterface[] $selected_values */ foreach ($facet_values as $facet => $selected_values) { $facetControlService = \Drupal::service('elasticsearch_search_api.facet_control.' . $facet); if ($facetControlService instanceof CompositeFacetControlInterface) { $facet_post_filter = $facetControlService->buildFacetFilter($selected_values); } elseif ($facetControlService instanceof FacetControlInterface) { $facet_post_filter = []; foreach ($selected_values as $selected_value) { $facet_post_filter[] = [ 'term' => [ $facetControlService->getFieldName() => $selected_value->value(), ], ]; } } if (count($facet_post_filter) > 1) { $post_filter[] = [ 'bool' => [ 'should' => $facet_post_filter, ], ]; } elseif (count($facet_post_filter) === 1) { $post_filter[] = reset($facet_post_filter); } } return $post_filter; } /** * Build aggregations for an elastic query. * * @param \Drupal\elasticsearch_search_api\Search\FacetedSearchActionInterface $searchAction * The search action to get available facets from. * @param \Drupal\elasticsearch_search_api\Search\Facet\FacetCollection $chosenFacetValues * Chosen facet values. * * @return array * List of aggregations. */ protected function buildAggregations(FacetedSearchActionInterface $searchAction, FacetCollection $chosenFacetValues) { $aggregations = []; foreach ($searchAction->getAvailableFacets() as $facet) { /** @var \Drupal\elasticsearch_search_api\Search\Facet\Control\FacetControlInterface $facetControlService */ $facetControlService = \Drupal::service('elasticsearch_search_api.facet_control.' . $facet); if (!$facetControlService->addToAggregations()) { continue; } $aggregation_facet_values = $chosenFacetValues->without($facet); // Use a sub-aggregation & apply the the filter of all other facets on it. if (!$aggregation_facet_values->isEmpty()) { $agg_filter = $this->buildFacetFilters($aggregation_facet_values); $aggregations[$facet] = [ 'filter' => ['bool' => ['must' => $agg_filter]], 'aggs' => [ 'filtered' => [ 'terms' => [ 'field' => $facetControlService->getFieldName(), 'size' => 999, ], ], ], ]; } else { $aggregations[$facet] = [ 'terms' => [ 'field' => $facetControlService->getFieldName(), 'size' => 999, ], ]; } } return $aggregations; } /** * Dynamically generate a list of fields to add to the query. * * This list will include a basic field query * and optionally include an ngram query. * Every list item will contain boosting. * * @return array * An array of fields. */ protected function buildMultimatchFields($include_ngram = TRUE) { $multi_match_fields = []; $configured_fields = $this->index->getFulltextFields(); foreach ($configured_fields as $field_name) { $field = $this->index->getField($field_name); $multi_match_fields[] = $this->buildBasicFieldMatch($field); if ($include_ngram) { $multi_match_fields[] = $this->buildNgramFieldMatch($field); } } return $multi_match_fields; } /** * Build a part of the multimatch query. * * Format: field_paragraph_title^5 * * @param \Drupal\search_api\Item\FieldInterface $field * A configured search api field. * * @return string * Field identifier string with boosting value. */ protected function buildBasicFieldMatch(FieldInterface $field) { return "{$field->getFieldIdentifier()}^{$field->getBoost()}"; } /** * Build a part of the multimatch query. * * Format: field_paragraph_title.ngram^5 * * By default, the constant NGRAM_FIELD_DEFAULT_BOOST value * will be used to boost ngram fields. This can be overridden * using the NGRAM_FIELD_BOOST_EXCEPTIONS constant, to provide * field specific ngram boosting. * * @param \Drupal\search_api\Item\FieldInterface $field * A configured search api field. * * @return string * Field name as a string concatenated with ngram boosting value. */ protected function buildNgramFieldMatch(FieldInterface $field) { $field_name = $field->getFieldIdentifier(); $ngram_boost = static::NGRAM_FIELD_DEFAULT_BOOST; if (array_key_exists($field_name, static::NGRAM_FIELD_BOOST_EXCEPTIONS)) { $ngram_boost = static::NGRAM_FIELD_BOOST_EXCEPTIONS[$field_name]; } return "{$field_name}.ngram^{$ngram_boost}"; } /** * Build the multi match queries. * * @return array * The multi match query in array format. */ protected function buildMultiMatchQueries($keyword) { $quoted_phrase = UtilityHelper::extractQuotedString($keyword); $words_in_keyword = str_word_count($keyword); $query = []; if (!empty($keyword)) { $query['function_score']['query']['bool']['should'][] = [ 'bool' => [ 'must' => [ 'multi_match' => [ 'query' => $keyword, 'fields' => $this->buildMultimatchFields(), 'type' => 'most_fields', ], ], ], ]; if ($words_in_keyword > 1) { $query['function_score']['functions'][] = [ 'filter' => [ 'multi_match' => [ 'query' => $keyword, 'fields' => $this->buildMultimatchFields(FALSE), 'minimum_should_match' => $words_in_keyword, 'type' => 'cross_fields', ], ], 'weight' => 100, ]; } } foreach ($quoted_phrase as $phrase) { $query['function_score']['query']['bool']['should'][] = [ 'bool' => [ 'must' => [ 'multi_match' => [ 'query' => $phrase, 'fields' => $this->buildMultimatchFields(), 'type' => 'phrase', ], ], ], ]; } $query['function_score']['boost'] = 1; $query['function_score']['boost_mode'] = "multiply"; return $query; } /** * Get the name of the index. * * @param \Drupal\search_api\Entity\Index $index * The index. * * @return string * The index name. */ protected function getIndexName(Index $index) { return $this->indexFactoryAdapter->getIndexName($index); } }