Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] Implement Full-Text Search for MySQL & PostgreSQL #40129

Merged
merged 12 commits into from
Jan 6, 2022
21 changes: 21 additions & 0 deletions src/Illuminate/Database/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -1821,6 +1821,27 @@ protected function addDynamic($segment, $connector, $parameters, $index)
$this->where(Str::snake($segment), '=', $parameters[$index], $bool);
}

/**
* Add a "where fulltext" clause to the query.
*
* @param string|string[] $column
driesvints marked this conversation as resolved.
Show resolved Hide resolved
* @param string $value
* @param string $boolean
* @return $this
*/
public function whereFulltext($columns, $value, array $options = [], $boolean = 'and')
{
$type = 'Fulltext';

$columns = (array) $columns;

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

$this->addBinding($value);

return $this;
driesvints marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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 fulltext" clause.
*
* @param \Illuminate\Database\Query\Builder $query
* @param array $where
* @return string
*/
public function whereFulltext(Builder $query, $where)
{
throw new RuntimeException('This database engine does not support fulltext search operations.');
driesvints marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Compile the "group by" portions of the query.
*
Expand Down
24 changes: 24 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,30 @@ protected function whereNotNull(Builder $query, $where)
return parent::whereNotNull($query, $where);
}

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

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

$mode = ($where['options']['mode'] ?? []) === 'boolean'
? ' in boolean mode'
: ' in natural language mode';

$expanded = ($where['options']['expanded'] ?? []) && ($where['options']['mode'] ?? []) !== 'boolean'
? ' with query expansion'
: '';

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

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

public function testWhereFulltext()
driesvints marked this conversation as resolved.
Show resolved Hide resolved
{
$builder = $this->getMySqlBuilderWithProcessor();
$builder->select('*')->from('users')->whereFulltext('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')->whereFulltext('body', 'Hello World', ['expanded' => 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')->whereFulltext('body', '+Hello -World', ['mode' => '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')->whereFulltext('body', '+Hello -World', ['mode' => 'boolean', 'expanded' => true]);
$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')->whereFulltext(['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/FulltextTest.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 FulltextTest 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 testWhereFulltext()
{
$articles = DB::table('articles')->whereFulltext(['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 testWhereFulltextWithBooleanMode()
{
$articles = DB::table('articles')->whereFulltext(['title', 'body'], '+MySQL -YourSQL', ['mode' => 'boolean'])->get();

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

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

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