From ca306bf3f1b348c2f2f36b7188727b594dd2a51b Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Thu, 2 Oct 2014 13:35:47 +0200
Subject: [PATCH] Issue #1842226 by jhodgdon | spartlow: Fixed Search OR
 statements don't work if same keyword is used.

---
 core/modules/search/src/SearchQuery.php       | 44 ++++++++++++-------
 .../search/src/Tests/SearchMatchTest.php      | 26 ++++++-----
 2 files changed, 45 insertions(+), 25 deletions(-)

diff --git a/core/modules/search/src/SearchQuery.php b/core/modules/search/src/SearchQuery.php
index 4211bd9c7e72..b3e2cac5eab0 100644
--- a/core/modules/search/src/SearchQuery.php
+++ b/core/modules/search/src/SearchQuery.php
@@ -235,7 +235,7 @@ protected function parseSearchExpression() {
     }
 
     // Classify tokens.
-    $or = FALSE;
+    $in_or = FALSE;
     $limit_combinations = \Drupal::config('search.settings')->get('and_or_limit');
     // The first search expression does not count as AND.
     $and_count = -1;
@@ -248,13 +248,14 @@ protected function parseSearchExpression() {
         break;
       }
 
-      $phrase = FALSE;
       // Strip off phrase quotes.
+      $phrase = FALSE;
       if ($match[2]{0} == '"') {
         $match[2] = substr($match[2], 1, -1);
         $phrase = TRUE;
         $this->simple = FALSE;
       }
+
       // Simplify keyword according to indexing rules and external
       // preprocessors. Use same process as during search indexing, so it
       // will match search index.
@@ -275,7 +276,7 @@ protected function parseSearchExpression() {
           $last = array($last);
         }
         $this->keys['positive'][] = $last;
-        $or = TRUE;
+        $in_or = TRUE;
         $or_count++;
         continue;
       }
@@ -290,7 +291,7 @@ protected function parseSearchExpression() {
           // Lower-case "or" instead of "OR" is a warning condition.
           $this->status |= SearchQuery::LOWER_CASE_OR;
         }
-        if ($or) {
+        if ($in_or) {
           // Add to last element (which is an array).
           $this->keys['positive'][count($this->keys['positive']) - 1] = array_merge($this->keys['positive'][count($this->keys['positive']) - 1], $words);
         }
@@ -299,33 +300,38 @@ protected function parseSearchExpression() {
           $and_count++;
         }
       }
-      $or = FALSE;
+      $in_or = FALSE;
     }
 
     // Convert keywords into SQL statements.
-    $simple_and = FALSE;
-    $simple_or = FALSE;
+    $has_and = FALSE;
+    $has_or = FALSE;
     // Positive matches.
     foreach ($this->keys['positive'] as $key) {
       // Group of ORed terms.
       if (is_array($key) && count($key)) {
-        $simple_or = TRUE;
-        $any = FALSE;
+        // If we had already found one OR, this is another one AND-ed with the
+        // first, meaning it is not a simple query.
+        if ($has_or) {
+          $this->simple = FALSE;
+        }
+        $has_or = TRUE;
+        $has_new_scores = FALSE;
         $queryor = db_or();
         foreach ($key as $or) {
           list($num_new_scores) = $this->parseWord($or);
-          $any |= $num_new_scores;
+          $has_new_scores |= $num_new_scores;
           $queryor->condition('d.data', "% $or %", 'LIKE');
         }
         if (count($queryor)) {
           $this->conditions->condition($queryor);
           // A group of OR keywords only needs to match once.
-          $this->matches += ($any > 0);
+          $this->matches += ($has_new_scores > 0);
         }
       }
       // Single ANDed term.
       else {
-        $simple_and = TRUE;
+        $has_and = TRUE;
         list($num_new_scores, $num_valid_words) = $this->parseWord($key);
         $this->conditions->condition('d.data', "% $key %", 'LIKE');
         if (!$num_valid_words) {
@@ -335,9 +341,10 @@ protected function parseSearchExpression() {
         $this->matches += $num_new_scores;
       }
     }
-    if ($simple_and && $simple_or) {
+    if ($has_and && $has_or) {
       $this->simple = FALSE;
     }
+
     // Negative matches.
     foreach ($this->keys['negative'] as $key) {
       $this->conditions->condition('d.data', "% $key %", 'NOT LIKE');
@@ -409,8 +416,15 @@ public function prepareAndNormalize() {
     $this
       ->condition('i.type', $this->type)
       ->groupBy('i.type')
-      ->groupBy('i.sid')
-      ->having('COUNT(*) >= :matches', array(':matches' => $this->matches));
+      ->groupBy('i.sid');
+
+    // If the query is simple, we should have calculated the number of
+    // matching words we need to find, so impose that criterion. For non-
+    // simple queries, this condition could lead to incorrectly deciding not
+    // to continue with the full query.
+    if ($this->simple) {
+      $this->having('COUNT(*) >= :matches', array(':matches' => $this->matches));
+    }
 
     // Clone the query object to calculate normalization.
     $normalize_query = clone $this->query;
diff --git a/core/modules/search/src/Tests/SearchMatchTest.php b/core/modules/search/src/Tests/SearchMatchTest.php
index 66ec5b137879..929f355c431c 100644
--- a/core/modules/search/src/Tests/SearchMatchTest.php
+++ b/core/modules/search/src/Tests/SearchMatchTest.php
@@ -88,15 +88,13 @@ function getText2($n) {
    * Run predefine queries looking for indexed terms.
    */
   function _testQueries() {
-    /*
-      Note: OR queries that include short words in OR groups are only accepted
-      if the ORed terms are ANDed with at least one long word in the rest of the query.
-
-      e.g. enim dolore OR ut = enim (dolore OR ut) = (enim dolor) OR (enim ut) -> good
-      e.g. dolore OR ut = (dolore) OR (ut) -> bad
-
-      This is a design limitation to avoid full table scans.
-    */
+    // Note: OR queries that include short words in OR groups are only accepted
+    // if the ORed terms are ANDed with at least one long word in the rest of
+    // the query. Examples:
+    //   enim dolore OR ut = enim (dolore OR ut) = (enim dolor) OR (enim ut)
+    // is good, and
+    //   dolore OR ut = (dolore) OR (ut)
+    // is bad. This is a design limitation to avoid full table scans.
     $queries = array(
       // Simple AND queries.
       'ipsum' => array(1),
@@ -109,7 +107,7 @@ function _testQueries() {
       'ut minim' => array(5),
       'xx minim' => array(),
       'enim veniam am minim ut' => array(5),
-      // Simple OR queries.
+      // Simple OR and AND/OR queries.
       'dolore OR ipsum' => array(1, 2, 7),
       'dolore OR xxxxx' => array(2, 7),
       'dolore OR ipsum OR enim' => array(1, 2, 4, 5, 6, 7),
@@ -119,6 +117,14 @@ function _testQueries() {
       'minim dolore OR ipsum OR enim' => array(5, 6, 7),
       'dolore xx OR yy' => array(),
       'xxxxx dolore OR ipsum' => array(),
+      // Sequence of OR queries.
+      'minim' => array(5, 6, 7),
+      'minim OR xxxx' => array(5, 6, 7),
+      'minim OR xxxx OR minim' => array(5, 6, 7),
+      'minim OR xxxx minim' => array(5, 6, 7),
+      'minim OR xxxx minim OR yyyy' => array(5, 6, 7),
+      'minim OR xxxx minim OR cillum' => array(6, 7, 5),
+      'minim OR xxxx minim OR xxxx' => array(5, 6, 7),
       // Negative queries.
       'dolore -sit' => array(7),
       'dolore -eu' => array(2),
-- 
GitLab