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);
}
}
