Commit 44f5057e authored by StryKaizer's avatar StryKaizer Committed by borisson_

Issue #2911515 by StryKaizer: Move facets core search into a separate module

parent 6cbf1565
......@@ -85,3 +85,14 @@ function facets_update_8002() {
->execute();
}
}
/**
* WARNING: Facets core search support has been moved into a separate project.
* If you are using this feature, you need do download the "facets_core_search" module on drupal.org."
*/
function facets_update_8003() {
\Drupal::database()->delete('key_value')
->condition('collection', 'system.schema')
->condition('name', 'core_search_facets')
->execute();
}
<?php
/**
* @file
* Hooks provided by the core_search_facets module.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Adds field types as possible options for facets.
*
* @param array $allowed_field_types
* The field types.
*
* @return array
* Array that contains the field types.
*/
function hook_facets_core_allowed_field_types(array $allowed_field_types) {
$allowed_field_types[] = 'float';
return $allowed_field_types;
}
/**
* @} End of "addtogroup hooks".
*/
name: 'Core Search Facets'
type: module
description: 'Facets integration for Core.'
core: 8.x
package: Search
dependencies:
- facets:facets
- drupal:search
<?php
/**
* @file
* Contains core_search_facets.module.
*/
use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\facets\FacetSourceInterface;
/**
* Implements hook_query_TAG_alter().
*
* Tag search_$type with $type node_search.
*/
function core_search_facets_query_search_node_search_alter(AlterableInterface $query) {
// Obtain the Facet Source id for the current search.
$request = \Drupal::requestStack()->getMasterRequest();
$search_page = $request->attributes->get('entity');
/** @var \Drupal\facets\FacetManager\DefaultFacetManager $facet_manager */
$facet_manager = \Drupal::service('facets.manager');
$facetsource_id = 'core_node_search:' . $search_page->id();
// Add the active filters.
$facet_manager->alterQuery($query, $facetsource_id);
}
/**
* Implements hook_search_plugin_alter().
*
* Alter search plugin definitions.
*/
function core_search_facets_search_plugin_alter(array &$definitions) {
// Replace the Search Plugin class to alter the search form adding the
// possibility to show or hide the Content Type and Language advanced filters.
if (isset($definitions['node_search'])) {
$definitions['node_search']['class'] = 'Drupal\core_search_facets\Plugin\Search\NodeSearchFacets';
}
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Edits the facet_source_edit_form, so we can add a checkbox to show or hide
* some advanced filters for the core search.
*/
function core_search_facets_form_facet_source_edit_form_alter(&$form, FormStateInterface $form_state) {
$request = \Drupal::requestStack()->getMasterRequest();
$facet_source_id = str_replace(":", "__", $request->attributes->get('facets_facet_source'));
if (strpos($facet_source_id, 'core_node_search') !== FALSE) {
$form['advanced_filters'] = [
'#type' => 'checkbox',
'#title' => t('Show Advanced Filters.'),
'#default_value' => \Drupal::config("facets.facet_source.{$facet_source_id}")->get('third_party_settings.core_search_facets.advanced_filters'),
];
$form['#entity_builders'][] = 'core_search_facets_facet_source_form_form_builder';
}
}
/**
* Entity builder for the facet source form edit form with third party options.
*
* @see core_search_facets_form_facet_source_edit_form_alter()
*/
function core_search_facets_facet_source_form_form_builder($entity_type, FacetSourceInterface $facet_source, &$form, FormStateInterface $form_state) {
$facet_source->setThirdPartySetting('core_search_facets', 'advanced_filters', $form_state->getValue('advanced_filters'));
}
/**
* Implements hook_facets_core_allowed_field_types().
*/
function core_search_facets_facets_core_allowed_field_types(array $allowed_field_types) {
$allowed_field_types[] = 'taxonomy_term';
$allowed_field_types[] = 'integer';
return $allowed_field_types;
}
<?php
namespace Drupal\core_search_facets;
use Drupal\Core\Database\Query\Condition;
use Drupal\search\SearchQuery;
/**
* Extension of the SearchQuery class.
*/
class FacetsQuery extends SearchQuery {
/**
* Stores joined tables.
*
* @var array
*/
protected $joinedTables = [];
/**
* Adds the facet join, but only does so once.
*
* @param array $query_info
* An associative array of query information.
* @param string $table_alias
* The alias of the table being joined.
*/
public function addFacetJoin(array $query_info, $table_alias) {
if (isset($query_info['joins'][$table_alias])) {
if (!isset($this->joinedTables[$table_alias])) {
$this->joinedTables[$table_alias] = TRUE;
$join_info = $query_info['joins'][$table_alias];
$this->join($join_info['table'], $join_info['alias'], $join_info['condition']);
}
}
}
/**
* Adds the facet field, ensures the alias is "value".
*
* @param array $query_info
* An associative array of query information.
*
* @return FacetsQuery
* An instance of this class.
*/
public function addFacetField(array $query_info) {
foreach ($query_info['fields'] as $field_info) {
$this->addField($field_info['table_alias'], $field_info['field'], 'value');
}
return $this;
}
/**
* Executes a facet query.
*/
public function execute() {
$this->parseSearchExpression();
// Adds OR conditions.
if (!empty($this->words)) {
$or = new Condition('OR');
foreach ($this->words as $word) {
$or->condition('i.word', $word);
}
$this->condition($or);
}
// Build query for keyword normalization.
$this->join('search_total', 't', 'i.word = t.word');
$this
->condition('i.type', $this->type)
->groupBy('i.langcode')
->groupBy('value')
->groupBy('i.type')
->groupBy('i.sid')
->having('COUNT(*) >= :matches', [':matches' => $this->matches]);
// Add conditions to query.
$this->join('search_dataset', 'd', 'i.sid = d.sid AND i.type = d.type AND i.langcode = d.langcode');
if (count($this->conditions)) {
$this->condition($this->conditions);
}
// Add tag and useful metadata.
$this
->addTag('search_' . $this->type)
->addMetaData('normalize', $this->normalize);
// Adds subquery to group the results in the r table.
$subquery = \Drupal::database()->select($this->query, 'r')
->fields('r', ['value'])
->groupBy('r.value');
// Adds COUNT() expression to get facet counts.
$subquery->addExpression('COUNT(r.value)', 'count');
$subquery->orderBy('count', 'DESC');
// Executes the subquery.
return $subquery->execute();
}
/**
* Returns the search expression.
*
* @return string
* The search expression.
*/
public function getSearchExpression() {
return $this->searchExpression;
}
}
<?php
namespace Drupal\core_search_facets\Plugin;
use Drupal\facets\FacetInterface;
/**
* Additional interface for core facet sources.
*
* A facet source is used to abstract the data source where facets can be added
* to. A good example of this is a Search API view. There are other possible
* facet data sources, these all implement the FacetSourcePluginInterface.
*/
interface CoreSearchFacetSourceInterface {
/**
* Sets the facet query object.
*
* @return \Drupal\core_search_facets\FacetsQuery
* The facet query object.
*/
public function getFacetQueryExtender();
/**
* Returns the query info for this facet field.
*
* @param \Drupal\facets\FacetInterface $facet
* The facet definition as returned by facets_facet_load().
*
* @return array
* An associative array containing:
* - fields: An array of field information, each of which are associative
* arrays containing:
* - table_alias: The table alias the field belongs to.
* - field: The name of the field containing the facet data.
* - joins: An array of join info, each of which are associative arrays
* containing:
* - table: The table being joined.
* - alias: The alias of the table being joined.
* - condition: The condition that joins the table.
*/
public function getQueryInfo(FacetInterface $facet);
}
<?php
namespace Drupal\core_search_facets\Plugin\Search;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\node\Plugin\Search\NodeSearch;
/**
* Handles searching for node entities using the Search module index.
*/
class NodeSearchFacets extends NodeSearch {
/**
* {@inheritdoc}
*/
public function searchFormAlter(array &$form, FormStateInterface $form_state) {
$parameters = $this->getParameters();
$keys = $this->getKeywords();
$used_advanced = !empty($parameters[self::ADVANCED_FORM]);
if ($used_advanced) {
$f = isset($parameters['f']) ? (array) $parameters['f'] : [];
$defaults = $this->parseAdvancedDefaults($f, $keys);
}
else {
$defaults = ['keys' => $keys];
}
$form['basic']['keys']['#default_value'] = $defaults['keys'];
// Add advanced search keyword-related boxes.
$form['advanced'] = [
'#type' => 'details',
'#title' => t('Advanced search'),
'#attributes' => ['class' => ['search-advanced']],
'#access' => $this->account && $this->account->hasPermission('use advanced search'),
'#open' => $used_advanced,
];
$form['advanced']['keywords-fieldset'] = [
'#type' => 'fieldset',
'#title' => t('Keywords'),
];
$form['advanced']['keywords'] = [
'#prefix' => '<div class="criterion">',
'#suffix' => '</div>',
];
$form['advanced']['keywords-fieldset']['keywords']['or'] = [
'#type' => 'textfield',
'#title' => t('Containing any of the words'),
'#size' => 30,
'#maxlength' => 255,
'#default_value' => isset($defaults['or']) ? $defaults['or'] : '',
];
$form['advanced']['keywords-fieldset']['keywords']['phrase'] = [
'#type' => 'textfield',
'#title' => t('Containing the phrase'),
'#size' => 30,
'#maxlength' => 255,
'#default_value' => isset($defaults['phrase']) ? $defaults['phrase'] : '',
];
$form['advanced']['keywords-fieldset']['keywords']['negative'] = [
'#type' => 'textfield',
'#title' => t('Containing none of the words'),
'#size' => 30,
'#maxlength' => 255,
'#default_value' => isset($defaults['negative']) ? $defaults['negative'] : '',
];
$form['advanced']['submit'] = [
'#type' => 'submit',
'#value' => t('Advanced search'),
'#prefix' => '<div class="action">',
'#suffix' => '</div>',
'#weight' => 100,
];
if (\Drupal::config("facets.facet_source.core_node_search__{$this->searchPageId}")->get('third_party_settings.core_search_facets.advanced_filters')) {
// Add node types.
$types = array_map(['\Drupal\Component\Utility\Html', 'escape'], node_type_get_names());
$form['advanced']['types-fieldset'] = [
'#type' => 'fieldset',
'#title' => t('Types'),
];
$form['advanced']['types-fieldset']['type'] = [
'#type' => 'checkboxes',
'#title' => t('Only of the type(s)'),
'#prefix' => '<div class="criterion">',
'#suffix' => '</div>',
'#options' => $types,
'#default_value' => isset($defaults['type']) ? $defaults['type'] : [],
];
$form['advanced']['submit'] = [
'#type' => 'submit',
'#value' => t('Advanced search'),
'#prefix' => '<div class="action">',
'#suffix' => '</div>',
'#weight' => 100,
];
// Add languages.
$language_options = [];
$language_list = $this->languageManager->getLanguages(LanguageInterface::STATE_ALL);
foreach ($language_list as $langcode => $language) {
// Make locked languages appear special in the list.
$language_options[$langcode] = $language->isLocked() ? t('- @name -', ['@name' => $language->getName()]) : $language->getName();
}
if (count($language_options) > 1) {
$form['advanced']['lang-fieldset'] = [
'#type' => 'fieldset',
'#title' => t('Languages'),
];
$form['advanced']['lang-fieldset']['language'] = [
'#type' => 'checkboxes',
'#title' => t('Languages'),
'#prefix' => '<div class="criterion">',
'#suffix' => '</div>',
'#options' => $language_options,
'#default_value' => isset($defaults['language']) ? $defaults['language'] : [],
];
}
}
}
}
<?php
namespace Drupal\core_search_facets\Plugin\facets\facet_source;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\facets\FacetSource\FacetSourceDeriverBase;
use Drupal\search\SearchPluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Derives a facet source plugin definition for every Search API view.
*
* @see \Drupal\facets\Plugin\facets\facet_source\SearchApiViews
*/
class CoreNodeSearchFacetSourceDeriver extends FacetSourceDeriverBase {
/**
* The plugin manager for core search plugins.
*
* @var \Drupal\search\SearchPluginManager
*/
protected $searchManager;
/**
* Creates an instance of the deriver.
*
* @param string $base_plugin_id
* The plugin ID.
* @param \Drupal\search\SearchPluginManager $search_manager
* The plugin manager for core search plugins.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity manager.
*/
public function __construct($base_plugin_id, SearchPluginManager $search_manager, EntityTypeManagerInterface $entity_type_manager) {
$this->searchManager = $search_manager;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$base_plugin_id,
$container->get('plugin.manager.search'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$base_plugin_id = $base_plugin_definition['id'];
if (!isset($this->derivatives[$base_plugin_id])) {
$plugin_derivatives = [];
$pages = $this->entityTypeManager->getStorage('search_page')->loadMultiple();
foreach ($pages as $machine_name => $page) {
/* @var \Drupal\search\Entity\SearchPage $page * */
if ($page->get('plugin') == 'node_search') {
// Detect if the plugin has "faceted" definition.
$plugin_derivatives[$machine_name] = [
'id' => $base_plugin_id . PluginBase::DERIVATIVE_SEPARATOR . $machine_name,
'label' => $this->t('Core Search Page: %page_name', ['%page_name' => $page->get('label')]),
'description' => $this->t('Provides a facet source.'),
'display_id' => $machine_name,
] + $base_plugin_definition;
}
$this->derivatives[$base_plugin_id] = $plugin_derivatives;
}
}
return $this->derivatives[$base_plugin_id];
}
}
<?php
namespace Drupal\core_search_facets\Plugin\facets\query_type;
use Drupal\facets\QueryType\QueryTypePluginBase;
use Drupal\facets\Result\Result;
use Drupal\facets\Result\ResultInterface;
/**
* A date query type for core search.
*
* @FacetsQueryType(
* id = "core_node_search_date",
* label = @Translation("Date"),
* )
*/
class CoreNodeSearchDate extends QueryTypePluginBase {
/**
* The backend's native query object.
*
* @var \Drupal\search_api\Query\QueryInterface
*/
protected $query;
/**
* {@inheritdoc}
*/
public function execute() {
/** @var \Drupal\facets\Utility\FacetsDateHandler $date_handler */
$date_handler = \Drupal::getContainer()->get('facets.utility.date_handler');
/** @var \Drupal\core_search_facets\Plugin\CoreSearchFacetSourceInterface $facet_source */
$facet_source = $this->facet->getFacetSource();
// Gets the last active date, bails if there isn't one.
$active_items = $this->facet->getActiveItems();
if (!$active_item = end($active_items)) {
return;
}
// Gets facet query and this facet's query info.
/** @var \Drupal\core_search_facets\FacetsQuery $facet_query */
$facet_query = $facet_source->getFacetQueryExtender();
$query_info = $facet_source->getQueryInfo($this->facet);
$tables_joined = [];
$active_item = $date_handler->extractActiveItems($active_item);
foreach ($query_info['fields'] as $field_info) {
// Adds join to the facet query.
$facet_query->addFacetJoin($query_info, $field_info['table_alias']);
// Adds join to search query, makes sure it is only added once.
if (isset($query_info['joins'][$field_info['table_alias']])) {
if (!isset($tables_joined[$field_info['table_alias']])) {
$tables_joined[$field_info['table_alias']] = TRUE;
$join_info = $query_info['joins'][$field_info['table_alias']];
$this->query->join($join_info['table'], $join_info['alias'], $join_info['condition']);
}
}
// Adds field conditions to the facet and search query.
$field = $field_info['table_alias'] . '.' . $field_info['field'];
$this->query->condition($field, $active_item['start']['timestamp'], '>=');
$this->query->condition($field, $active_item['end']['timestamp'], '<');
$facet_query->condition($field, $active_item['start']['timestamp'], '>=');
$facet_query->condition($field, $active_item['end']['timestamp'], '<');
}
}
/**
* {@inheritdoc}
*/
public function build() {
$parent_facet_results = [];
/** @var \Drupal\facets\Utility\FacetsDateHandler $date_handler */
$date_handler = \Drupal::getContainer()->get('facets.utility.date_handler');
// Gets base facet query, adds facet field and filters.
/* @var \Drupal\core_search_facets\Plugin\CoreSearchFacetSourceInterface $facet_source */
$facet_source = $this->facet->getFacetSource();
$query_info = $facet_source->getQueryInfo($this->facet);
/** @var \Drupal\core_search_facets\FacetsQuery $facet_query */
$facet_query = $facet_source->getFacetQueryExtender();
$facet_query->addFacetField($query_info);
foreach ($query_info['joins'] as $table_alias => $join_info) {
$facet_query->addFacetJoin($query_info, $table_alias);
}
if ($facet_query->getSearchExpression()) {
// Executes query, iterates over results.
$result = $facet_query->execute();
foreach ($result as $record) {
$raw_values[$record->value] = $record->count;
}
ksort($raw_values);
// Gets active facets, starts building hierarchy.
$parent = NULL;
$gap = NULL;
$last_parent = NULL;
foreach ($this->facet->getActiveItems() as $value => $item) {
if ($active_item = $date_handler->extractActiveItems($item)) {
$date_gap = $date_handler->getDateGap($active_item['start']['iso'], $active_item['end']['iso']);
$gap = $date_handler->getNextDateGap($date_gap, $date_handler::FACETS_DATE_MINUTE);
$last_parent = '[' . $active_item['start']['iso'] . ' TO ' . $active_item['end']['iso'] . ']';
$result = new Result($this->facet, $last_parent, $date_handler->formatTimestamp($active_item['start']['timestamp'], $date_gap), NULL);
$result->setActiveState(TRUE);
// Sets the children for the current parent..
if ($parent) {
$children = array_merge($parent->getChildren(), [$result]);
$parent->setChildren($children);
}
else {
$parent = $parent_facet_results[] = $result;
}
}
}
// Mind the gap! Calculates gap from min and max timestamps.
$timestamps = array_keys($raw_values);
if (is_null($parent)) {
if (count($raw_values) > 1) {
$gap = $date_handler->getTimestampGap(min($timestamps), max($timestamps));
}
else {
$gap = $date_handler::FACETS_DATE_HOUR;
}
}
// Converts all timestamps to dates in ISO 8601 format.
$dates = [];
foreach ($timestamps as $timestamp) {
$dates[$timestamp] = $date_handler->isoDate($timestamp, $gap);