3 namespace Drupal\search\Plugin\views\filter;
5 use Drupal\Core\Form\FormStateInterface;
6 use Drupal\views\Plugin\views\filter\FilterPluginBase;
7 use Drupal\views\Plugin\views\display\DisplayPluginBase;
8 use Drupal\views\ViewExecutable;
9 use Drupal\views\Views;
12 * Filter handler for search keywords.
14 * @ingroup views_filter_handlers
16 * @ViewsFilter("search_keywords")
18 class Search extends FilterPluginBase {
21 * This filter is always considered multiple-valued.
25 protected $alwaysMultiple = TRUE;
28 * A search query to use for parsing search keywords.
30 * @var \Drupal\search\ViewsSearchQuery
32 protected $searchQuery = NULL;
35 * TRUE if the search query has been parsed.
37 protected $parsed = FALSE;
40 * The search type name (value of {search_index}.type in the database).
44 protected $searchType;
49 public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
50 parent::init($view, $display, $options);
52 $this->searchType = $this->definition['search_type'];
58 protected function defineOptions() {
59 $options = parent::defineOptions();
61 $options['operator']['default'] = 'optional';
69 protected function operatorForm(&$form, FormStateInterface $form_state) {
72 '#title' => $this->t('On empty input'),
73 '#default_value' => $this->operator,
75 'optional' => $this->t('Show All'),
76 'required' => $this->t('Show None'),
84 protected function valueForm(&$form, FormStateInterface $form_state) {
86 '#type' => 'textfield',
88 '#default_value' => $this->value,
89 '#attributes' => ['title' => $this->t('Search keywords')],
90 '#title' => !$form_state->get('exposed') ? $this->t('Keywords') : '',
97 public function validateExposed(&$form, FormStateInterface $form_state) {
98 if (!isset($this->options['expose']['identifier'])) {
102 $key = $this->options['expose']['identifier'];
103 if (!$form_state->isValueEmpty($key)) {
104 $this->queryParseSearchExpression($form_state->getValue($key));
105 if (count($this->searchQuery->words()) == 0) {
106 $form_state->setErrorByName($key, $this->formatPlural(\Drupal::config('search.settings')->get('index.minimum_word_size'), 'You must include at least one keyword to match in the content, and punctuation is ignored.', 'You must include at least one keyword to match in the content. Keywords must be at least @count characters, and punctuation is ignored.'));
112 * Sets up and parses the search query.
114 * @param string $input
115 * The search keywords entered by the user.
117 protected function queryParseSearchExpression($input) {
118 if (!isset($this->searchQuery)) {
119 $this->parsed = TRUE;
120 $this->searchQuery = db_select('search_index', 'i', ['target' => 'replica'])->extend('Drupal\search\ViewsSearchQuery');
121 $this->searchQuery->searchExpression($input, $this->searchType);
122 $this->searchQuery->publicParseSearchExpression();
129 public function query() {
130 // Since attachment views don't validate the exposed input, parse the search
131 // expression if required.
132 if (!$this->parsed) {
133 $this->queryParseSearchExpression($this->value);
136 if (!isset($this->searchQuery)) {
140 $words = $this->searchQuery->words();
146 if ($this->operator == 'required') {
147 $this->query->addWhere($this->options['group'], 'FALSE');
151 $search_index = $this->ensureMyTable();
153 $search_condition = db_and();
155 // Create a new join to relate the 'search_total' table to our current
156 // 'search_index' table.
158 'table' => 'search_total',
160 'left_table' => $search_index,
161 'left_field' => 'word',
163 $join = Views::pluginManager('join')->createInstance('standard', $definition);
164 $search_total = $this->query->addRelationship('search_total', $join, $search_index);
166 // Add the search score field to the query.
167 $this->search_score = $this->query->addField('', "$search_index.score * $search_total.count", 'score', ['function' => 'sum']);
169 // Add the conditions set up by the search query to the views query.
170 $search_condition->condition("$search_index.type", $this->searchType);
171 $search_dataset = $this->query->addTable('node_search_dataset');
172 $conditions = $this->searchQuery->conditions();
173 $condition_conditions =& $conditions->conditions();
174 foreach ($condition_conditions as $key => &$condition) {
175 // Make sure we just look at real conditions.
176 if (is_numeric($key)) {
177 // Replace the conditions with the table alias of views.
178 $this->searchQuery->conditionReplaceString('d.', "$search_dataset.", $condition);
181 $search_conditions =& $search_condition->conditions();
182 $search_conditions = array_merge($search_conditions, $condition_conditions);
184 // Add the keyword conditions, as is done in
185 // SearchQuery::prepareAndNormalize(), but simplified because we are
186 // only concerned with relevance ranking so we do not need to normalize.
188 foreach ($words as $word) {
189 $or->condition("$search_index.word", $word);
191 $search_condition->condition($or);
193 $this->query->addWhere($this->options['group'], $search_condition);
195 // Add the GROUP BY and HAVING expressions to the query.
196 $this->query->addGroupBy("$search_index.sid");
197 $matches = $this->searchQuery->matches();
198 $placeholder = $this->placeholder();
199 $this->query->addHavingExpression($this->options['group'], "COUNT(*) >= $placeholder", [$placeholder => $matches]);
201 // Set to NULL to prevent PDO exception when views object is cached.
202 $this->searchQuery = NULL;