Skip to content

Commit

Permalink
Implement Full-Text Searches for MySQL
Browse files Browse the repository at this point in the history
  • Loading branch information
driesvints committed Dec 21, 2021
1 parent f3e5cb3 commit 4e5ee00
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 0 deletions.
59 changes: 59 additions & 0 deletions src/Illuminate/Database/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -1821,6 +1821,65 @@ protected function addDynamic($segment, $connector, $parameters, $index)
$this->where(Str::snake($segment), '=', $parameters[$index], $bool);
}

/**
* Add a "match against" clause to the query.
*
* @param string|string[] $column
* @param string|string[] $values
* @param string $boolean
* @param bool $expanded
* @param string $mode
* @return $this
*/
public function matchAgainst($columns, $values, $boolean = 'and', bool $expanded = false, string $mode = '')
{
$type = 'MatchAgainst';

// Next, if the value is Arrayable we need to cast it to its raw array form so we
// have the underlying array value instead of an Arrayable object which is not
// able to be added as a binding, etc. We will then add to the wheres array.
if ($values instanceof Arrayable) {
$values = $values->toArray();
}

$columns = (array) $columns;
$value = collect($this->cleanBindings((array) $values))->implode(',');

$this->wheres[] = compact('type', 'columns', 'value', 'expanded', 'mode', 'boolean');

$this->addBinding($value);

return $this;
}

/**
* Add a "match against in boolean mode" clause to the query.
*
* @param string|string[] $column
* @param string|string[] $values
* @param string $boolean
* @param bool $expanded
* @return $this
*/
public function matchAgainstBoolean($columns, $values, $boolean = 'and', bool $expanded = false)
{
return $this->matchAgainst($columns, $values, $boolean, $expanded, 'boolean');
}

/**
* Add a "match against with expanded query" clause to the query.
*
* @param string|string[] $column
* @param string|string[] $values
* @param string $boolean
* @param string $mode
* @return $this
*/
public function matchAgainstExpanded($columns, $values, $boolean = 'and', string $mode = '')
{
return $this->matchAgainst($columns, $values, $boolean, true, $mode);
}

/**
* Add a "group by" clause to the query.
*
Expand Down
12 changes: 12 additions & 0 deletions src/Illuminate/Database/Query/Grammars/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,18 @@ protected function compileJsonLength($column, $operator, $value)
throw new RuntimeException('This database engine does not support JSON length operations.');
}

/**
* Compile a "where match against" clause.
*
* @param \Illuminate\Database\Query\Builder $query
* @param array $match
* @return string
*/
public function whereMatchAgainst(Builder $query, $match)
{
throw new RuntimeException('This database engine does not support MATCH AGAINST operations.');
}

/**
* Compile the "group by" portions of the query.
*
Expand Down
20 changes: 20 additions & 0 deletions src/Illuminate/Database/Query/Grammars/MySqlGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ protected function whereNotNull(Builder $query, $where)
return parent::whereNotNull($query, $where);
}

/**
* Compile a "where match against" clause.
*
* @param \Illuminate\Database\Query\Builder $query
* @param array $match
* @return string
*/
public function whereMatchAgainst(Builder $query, $match)
{
$columns = $this->columnize($match['columns']);

$value = $this->parameter($match['value']);

$mode = $match['mode'] === 'boolean' ? 'in boolean mode' : 'in natural language mode';

$expand = $match['expanded'] && $match['mode'] !== 'boolean' ? ' with query expansion' : '';

return "match ({$columns}) against (".$value." {$mode}{$expand})";
}

/**
* Compile an insert ignore statement into SQL.
*
Expand Down
38 changes: 38 additions & 0 deletions tests/Database/DatabaseQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,44 @@ public function testArrayWhereColumn()
$this->assertEquals([], $builder->getBindings());
}

public function testMatchAgainst()
{
$builder = $this->getMySqlBuilderWithProcessor();
$builder->select('*')->from('users')->matchAgainst('body', 'Hello World');
$this->assertSame('select * from `users` where match (`body`) against (? in natural language mode)', $builder->toSql());
$this->assertEquals(['Hello World'], $builder->getBindings());

$builder = $this->getMySqlBuilderWithProcessor();
$builder->select('*')->from('users')->matchAgainst('body', 'Hello World', 'and', true);
$this->assertSame('select * from `users` where match (`body`) against (? in natural language mode with query expansion)', $builder->toSql());
$this->assertEquals(['Hello World'], $builder->getBindings());

$builder = $this->getMySqlBuilderWithProcessor();
$builder->select('*')->from('users')->matchAgainstExpanded('body', 'Hello World');
$this->assertSame('select * from `users` where match (`body`) against (? in natural language mode with query expansion)', $builder->toSql());
$this->assertEquals(['Hello World'], $builder->getBindings());

$builder = $this->getMySqlBuilderWithProcessor();
$builder->select('*')->from('users')->matchAgainst('body', '+Hello -World', 'and', false, 'boolean');
$this->assertSame('select * from `users` where match (`body`) against (? in boolean mode)', $builder->toSql());
$this->assertEquals(['+Hello -World'], $builder->getBindings());

$builder = $this->getMySqlBuilderWithProcessor();
$builder->select('*')->from('users')->matchAgainstBoolean('body', '+Hello -World');
$this->assertSame('select * from `users` where match (`body`) against (? in boolean mode)', $builder->toSql());
$this->assertEquals(['+Hello -World'], $builder->getBindings());

$builder = $this->getMySqlBuilderWithProcessor();
$builder->select('*')->from('users')->matchAgainst('body', '+Hello -World', 'and', true, 'boolean');
$this->assertSame('select * from `users` where match (`body`) against (? in boolean mode)', $builder->toSql());
$this->assertEquals(['+Hello -World'], $builder->getBindings());

$builder = $this->getMySqlBuilderWithProcessor();
$builder->select('*')->from('users')->matchAgainst(['body', 'title'], ['Car', 'Plane']);
$this->assertSame('select * from `users` where match (`body`, `title`) against (? in natural language mode)', $builder->toSql());
$this->assertEquals(['Car,Plane'], $builder->getBindings());
}

public function testUnions()
{
$builder = $this->getBuilder();
Expand Down
69 changes: 69 additions & 0 deletions tests/Integration/Database/MySql/MatchAgainstTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace Illuminate\Tests\Integration\Database\MySql;

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

/**
* @requires extension pdo_mysql
* @requires OS Linux|Darwin
*/
class MatchAgainstTest extends MySqlTestCase
{
protected function defineDatabaseMigrationsAfterDatabaseRefreshed()
{
Schema::create('articles', function (Blueprint $table) {
$table->id('id');
$table->string('title', 200);
$table->text('body');
$table->fulltext(['title', 'body']);
});
}

protected function destroyDatabaseMigrations()
{
Schema::drop('articles');
}

protected function setUp(): void
{
parent::setUp();

DB::table('articles')->insert([
['title' => 'MySQL Tutorial', 'body' => 'DBMS stands for DataBase ...'],
['title' => 'How To Use MySQL Well', 'body' => 'After you went through a ...'],
['title' => 'Optimizing MySQL', 'body' => 'In this tutorial, we show ...'],
['title' => '1001 MySQL Tricks', 'body' => '1. Never run mysqld as root. 2. ...'],
['title' => 'MySQL vs. YourSQL', 'body' => 'In the following database comparison ...'],
['title' => 'MySQL Security', 'body' => 'When configured properly, MySQL ...'],
]);
}

/** @link https://dev.mysql.com/doc/refman/8.0/en/fulltext-natural-language.html */
public function testMatchAgainst()
{
$articles = DB::table('articles')->matchAgainst(['title', 'body'], 'database')->get();

$this->assertCount(2, $articles);
$this->assertSame('MySQL Tutorial', $articles[0]->title);
$this->assertSame('MySQL vs. YourSQL', $articles[1]->title);
}

/** @link https://dev.mysql.com/doc/refman/8.0/en/fulltext-boolean.html */
public function testMatchAgainstBoolean()
{
$articles = DB::table('articles')->matchAgainstBoolean(['title', 'body'], '+MySQL -YourSQL')->get();

$this->assertCount(5, $articles);
}

/** @link https://dev.mysql.com/doc/refman/8.0/en/fulltext-query-expansion.html */
public function testMatchAgainstExpanded()
{
$articles = DB::table('articles')->matchAgainstExpanded(['title', 'body'], 'database')->get();

$this->assertCount(6, $articles);
}
}

0 comments on commit 4e5ee00

Please sign in to comment.