diff --git a/src/Discussion/Search/Gambit/FulltextGambit.php b/src/Discussion/Search/Gambit/FulltextGambit.php
index 15e6f53e0..fe82226dc 100644
--- a/src/Discussion/Search/Gambit/FulltextGambit.php
+++ b/src/Discussion/Search/Gambit/FulltextGambit.php
@@ -27,9 +27,10 @@ class FulltextGambit implements GambitInterface
throw new LogicException('This gambit can only be applied on a DiscussionSearch');
}
- // The @ character crashes fulltext searches on InnoDB tables.
- // See https://bugs.mysql.com/bug.php?id=74042
- $bit = str_replace('@', '*', $bit);
+ // Replace all non-word characters with spaces.
+ // We do this to prevent MySQL fulltext search boolean mode from taking
+ // effect: https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html
+ $bit = preg_replace('/[^\p{L}\p{N}_]+/u', ' ', $bit);
$query = $search->getQuery();
$grammar = $query->getGrammar();
diff --git a/tests/integration/api/Controller/ListDiscussionsControllerTest.php b/tests/integration/api/Controller/ListDiscussionsControllerTest.php
index ca4fe1947..75a0dd3ff 100644
--- a/tests/integration/api/Controller/ListDiscussionsControllerTest.php
+++ b/tests/integration/api/Controller/ListDiscussionsControllerTest.php
@@ -98,4 +98,52 @@ class ListDiscussionsControllerTest extends ApiControllerTestCase
// Order-independent comparison
$this->assertEquals(['2', '3'], $ids, 'IDs do not match', 0.0, 10, true);
}
+
+ /**
+ * @test
+ */
+ public function ignores_non_word_characters_when_searching()
+ {
+ $this->database()->table('posts')->insert([
+ ['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => 'not in text
'],
+ ['id' => 3, 'discussion_id' => 3, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => 'lightsail in text
'],
+ ]);
+
+ $this->database()->table('discussions')->insert([
+ ['id' => 2, 'title' => 'lightsail in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 2, 'comment_count' => 1],
+ ['id' => 3, 'title' => 'not in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 3, 'comment_count' => 1],
+ ]);
+
+ $response = $this->callWith([], [
+ 'filter' => ['q' => 'lightsail+'],
+ 'include' => 'mostRelevantPost'
+ ]);
+ $data = json_decode($response->getBody()->getContents(), true);
+ $ids = array_map(function ($row) {
+ return $row['id'];
+ }, $data['data']);
+
+ // Order-independent comparison
+ $this->assertEquals(['2', '3'], $ids, 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function search_for_special_characters_gives_empty_result()
+ {
+ $response = $this->callWith([], [
+ 'filter' => ['q' => '*'],
+ 'include' => 'mostRelevantPost'
+ ]);
+ $data = json_decode($response->getBody()->getContents(), true);
+ $this->assertEquals([], $data['data']);
+
+ $response = $this->callWith([], [
+ 'filter' => ['q' => '@'],
+ 'include' => 'mostRelevantPost'
+ ]);
+ $data = json_decode($response->getBody()->getContents(), true);
+ $this->assertEquals([], $data['data']);
+ }
}