3 namespace Drupal\search\Plugin\views\filter;
5 use Drupal\Core\Database\Query\Condition;
6 use Drupal\Core\Form\FormStateInterface;
7 use Drupal\views\Plugin\views\filter\FilterPluginBase;
8 use Drupal\views\Plugin\views\display\DisplayPluginBase;
9 use Drupal\views\ViewExecutable;
10 use Drupal\views\Views;
13 * Filter handler for search keywords.
15 * @ingroup views_filter_handlers
17 * @ViewsFilter("search_keywords")
19 class Search extends FilterPluginBase {
22 * This filter is always considered multiple-valued.
26 protected $alwaysMultiple = TRUE;
29 * A search query to use for parsing search keywords.
31 * @var \Drupal\search\ViewsSearchQuery
33 protected $searchQuery = NULL;
36 * TRUE if the search query has been parsed.
40 protected $parsed = FALSE;
43 * The search type name (value of {search_index}.type in the database).
47 protected $searchType;
52 public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
53 parent::init($view, $display, $options);
55 $this->searchType = $this->definition['search_type'];
61 protected function defineOptions() {
62 $options = parent::defineOptions();
64 $options['operator']['default'] = 'optional';
72 protected function operatorForm(&$form, FormStateInterface $form_state) {
75 '#title' => $this->t('On empty input'),
76 '#default_value' => $this->operator,
78 'optional' => $this->t('Show All'),
79 'required' => $this->t('Show None'),
87 protected function valueForm(&$form, FormStateInterface $form_state) {
89 '#type' => 'textfield',
91 '#default_value' => $this->value,
92 '#attributes' => ['title' => $this->t('Search keywords')],
93 '#title' => !$form_state->get('exposed') ? $this->t('Keywords') : '',
100 public function validateExposed(&$form, FormStateInterface $form_state) {
101 if (!isset($this->options['expose']['identifier'])) {
105 $key = $this->options['expose']['identifier'];
106 if (!$form_state->isValueEmpty($key)) {
107 $this->queryParseSearchExpression($form_state->getValue($key));
108 if (count($this->searchQuery->words()) == 0) {
109 $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.'));
115 * Sets up and parses the search query.
117 * @param string $input
118 * The search keywords entered by the user.
120 protected function queryParseSearchExpression($input) {
121 if (!isset($this->searchQuery)) {
122 $this->parsed = TRUE;
123 $this->searchQuery = db_select('search_index', 'i', ['target' => 'replica'])->extend('Drupal\search\ViewsSearchQuery');
124 $this->searchQuery->searchExpression($input, $this->searchType);
125 $this->searchQuery->publicParseSearchExpression();
132 public function query() {
133 // Since attachment views don't validate the exposed input, parse the search
134 // expression if required.
135 if (!$this->parsed) {
136 $this->queryParseSearchExpression($this->value);
139 if (!isset($this->searchQuery)) {
143 $words = $this->searchQuery->words();
149 if ($this->operator == 'required') {
150 $this->query->addWhere($this->options['group'], 'FALSE');
154 $search_index = $this->ensureMyTable();
156 $search_condition = new Condition('AND');
158 // Create a new join to relate the 'search_total' table to our current
159 // 'search_index' table.
161 'table' => 'search_total',
163 'left_table' => $search_index,
164 'left_field' => 'word',
166 $join = Views::pluginManager('join')->createInstance('standard', $definition);
167 $search_total = $this->query->addRelationship('search_total', $join, $search_index);
169 // Add the search score field to the query.
170 $this->search_score = $this->query->addField('', "$search_index.score * $search_total.count", 'score', ['function' => 'sum']);
172 // Add the conditions set up by the search query to the views query.
173 $search_condition->condition("$search_index.type", $this->searchType);
174 $search_dataset = $this->query->addTable('node_search_dataset');
175 $conditions = $this->searchQuery->conditions();
176 $condition_conditions =& $conditions->conditions();
177 foreach ($condition_conditions as $key => &$condition) {
178 // Make sure we just look at real conditions.
179 if (is_numeric($key)) {
180 // Replace the conditions with the table alias of views.
181 $this->searchQuery->conditionReplaceString('d.', "$search_dataset.", $condition);
184 $search_conditions =& $search_condition->conditions();
185 $search_conditions = array_merge($search_conditions, $condition_conditions);
187 // Add the keyword conditions, as is done in
188 // SearchQuery::prepareAndNormalize(), but simplified because we are
189 // only concerned with relevance ranking so we do not need to normalize.
190 $or = new Condition('OR');
191 foreach ($words as $word) {
192 $or->condition("$search_index.word", $word);
194 $search_condition->condition($or);
196 $this->query->addWhere($this->options['group'], $search_condition);
198 // Add the GROUP BY and HAVING expressions to the query.
199 $this->query->addGroupBy("$search_index.sid");
200 $matches = $this->searchQuery->matches();
201 $placeholder = $this->placeholder();
202 $this->query->addHavingExpression($this->options['group'], "COUNT(*) >= $placeholder", [$placeholder => $matches]);
204 // Set to NULL to prevent PDO exception when views object is cached.
205 $this->searchQuery = NULL;