2018/04/12

MySQL 全文檢索

近期還挺常遇到客戶想要他的搜尋 bar 像 Google 那麼強大,可以多關鍵字查找網站內容,但要達到那樣的要求在中文是很難的,中文有語意以及斷字問題,英文每個字都會分開寫,中文是全部連在一起,所以要達成這樣的功能必須使用中文斷詞的套件,再餵給 solr 或 elasticsearch 那種全文檢索引擎,但客戶的 content 不算龐大,安裝 solr 或 es 並且調校要花不少的成本,幸好 MySQL 在 5.6.4 之後已經支援全文檢索了,以下示範使用方式

斷詞套件

首先是要選擇斷詞套件,目前網路最熱門的應該就屬於 jieba 了,這個套件有 PHP 的版本,使用 composer 安裝好以後就可以寫程式碼了

資料表

不過要使用功能之前當然要設定我們的 MySQL,先建立表單

CREATE TABLE `cuts` (
	`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	`content` TEXT NULL COLLATE 'utf8mb4_unicode_ci',
	`full_text` TEXT NULL COLLATE 'utf8mb4_unicode_ci',
	PRIMARY KEY (`id`),
	FULLTEXT INDEX `full_text` (`full_text`)
)
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB;

content 部份是要存原來的資料,而 full_text 的欄位是要存斷詞之後的結果

斷詞

<?php

ini_set('memory_limit', '1024M');

include 'vendor/autoload.php';

use Fukuball\Jieba\Jieba;
use Fukuball\Jieba\Finalseg;

Jieba::init();
Finalseg::init();

$content = '我工作於掌中乾坤,預計今年去看 NBA,去看劉寶傑打球';
$cuts = Jieba::cut($content);

印出來的結果為

Array
(
    [0] => 我
    [1] => 工作
    [2] => 於
    [3] => 掌中
    [4] => 乾坤
    [5] => ,
    [6] => 預計
    [7] => 今年
    [8] => 去
    [9] => 看
    [10] => NBA
    [11] => ,
    [12] => 去
    [13] => 看
    [14] => 劉寶傑
    [15] => 打球
)

看起來斷的挺精準的,不過我的公司叫掌中乾坤,如果打這四個字會搜尋不到,我們可以用自己定義的詞庫

<?php

ini_set('memory_limit', '1024M');

include 'vendor/autoload.php';

use Fukuball\Jieba\Jieba;
use Fukuball\Jieba\Finalseg;

Jieba::init();
Jieba::loadUserDict('user.txt');
Finalseg::init();

$content = '我工作於掌中乾坤,預計今年去看 NBA,去看劉寶傑打球';
$cuts = Jieba::cut($content);
print_r($cuts);

會印出

Array
(
    [0] => 我
    [1] => 工作
    [2] => 於
    [3] => 掌中乾坤
    [4] => ,
    [5] => 預計
    [6] => 今年
    [7] => 去
    [8] => 看
    [9] => NBA
    [10] => ,
    [11] => 去
    [12] => 看
    [13] => 劉寶傑
    [14] => 打球
)

有一好沒兩好,乾坤這個詞洗掉了,可以修改斷詞為全模式

$cuts = Jieba::cut($content, true);

會印出

Array
(
    [0] => 我
    [1] => 工作
    [2] => 於
    [3] => 掌中乾坤
    [4] => 乾坤
    [5] => ,
    [6] => 預
    [7] => 今年
    [8] => 去
    [9] => 看
    [10] => N
    [11] => B
    [12] => A
    [13] => ,
    [14] => 去
    [15] => 看
    [16] => 寶
    [17] => 傑
    [18] => 打球
)

看起來反而對精準度有所影響,看自己怎麼取捨,接著我們把這些內容轉成 base64 存到資料庫,因為 MySQL 的全文檢索不認中文…

<?php

ini_set('memory_limit', '1024M');

include 'vendor/autoload.php';

use Fukuball\Jieba\Jieba;
use Fukuball\Jieba\Finalseg;

Jieba::init();
Jieba::loadUserDict('user.txt');
Finalseg::init();

$content = '我工作於掌中乾坤,預計今年去看 NBA,去看劉寶傑打球';
$cuts = Jieba::cut($content);
$cutStrings = implode(' ', array_map('base64_encode', $cuts));

try {
    $db = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'root', 123456);
    $sql = "INSERT INTO `cuts`(content, full_text) VALUES(:content, :full_text)";
    $sth = $db->prepare($sql);
    $sth->bindParam(':content', $content, \PDO::PARAM_STR);
    $sth->bindParam(':full_text', $cutStrings, \PDO::PARAM_STR);
    $sth->execute();
} catch (Exception $e) {
    var_dump($e->getMessage());
}

搜尋的模擬程式碼如下:

<?php

$searchKey = '找 掌中乾坤';
$keys = explode(' ', $searchKey);
$trim = function($item) {
    return $item !== '';
};
$transkey = implode(' ', array_map('base64_encode', array_filter($keys, $trim)));

try {
    $db = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'root', 123456);
    $sql = "SELECT * FROM `cuts` WHERE MATCH(full_text) AGAINST(:full_text IN BOOLEAN MODE)";
    $sth = $db->prepare($sql);
    $sth->bindParam(':full_text', $transkey, \PDO::PARAM_STR);
    $sth->execute();
    $rows = $sth->fetchAll(\PDO::FETCH_ASSOC);

    var_dump($rows);
} catch (Exception $e) {
    var_dump($e->getMessage());
}

這樣就可以找到正確的結果了,不過 jieba 套件在 PHP 執行上算慢的,同樣的環境 PHP 7 要跑 2 秒,Python 版本是 0.2 秒…怕 PHP 存檔有延遲的人可以送 queue 後處理,或者是用排程跑 python 去搞定這件事情,也有大神寫了 extension 去呼叫 cpp 的版本,或者兩秒可以忍受的話就牙一咬吧 XDDDD

沒有留言: