laravel(7.0)下tntsearch(2.0)和jieba-php使用

於彬
2023-12-01

使用的"teamtnt/laravel-scout-tntsearch-driver": "^8.1"版本。

如需要,请先查看完此篇文章再修改代码。


按照参考文章TNTSearch - PHP 实现的全文索引引擎,已经完成了英文分词的搜索。中文就需要jieba-php,但苦于tntsearch的驱动已经跟进laravel7,但jieba并没有跟进,致使我参考TNTSearch 轻量级全文索引+中文分词一直报错,报vendor\fukuball\jieba-php\src\class\Jieba.php:265的index "HMM" 不存在。

查看信息栈发现,TNTIndexer.php调用jieba的tokenize(),传的参数对不上。

//vendor\teamtnt\tntsearch\src\Indexer\TNTIndexer.php:447
 
   public function breakIntoTokens($text)
    {
        if ($this->decodeHTMLEntities) {
            $text = html_entity_decode($text);
        }
        return $this->tokenizer->tokenize($text, $this->stopWords);
    }
//vendor\fukuball\jieba-php\src\class\Jieba.php:263

  public static function tokenize($sentence, $options = array("HMM" => true))
    {
        $seg_list = self::cut($sentence, false, array("HMM" => $options["HMM"]));
        $tokenize_list = [];
        $start = 0;
        $end = 0;
        foreach ($seg_list as $seg) {
            $end = $start+mb_strlen($seg, 'UTF-8');
            $tokenize = [
                'word' => $seg,
                'start' => $start,
                'end' => $end
            ];
            $start = $end;
            $tokenize_list[] = $tokenize;
        }
        return $tokenize_list;
    }

jiebe-php的tokenize()第二参数需要的是HMM算法是否使用,而TNTIndexer调用传入的第二参数是stopwords,空数组也是个正经变量,致使默认参数的HMM这个index没了,jieba就报错。

TNTSearch 轻量级全文索引 + 中文分词 里的是新建了 TokenizerHandler.php,但config/app.php里的配置可以依旧TeamTNT\Scout\TNTSearchScoutServiceProvider::class,并不需要改。新建的ScoutServiceProvider.php对于我来说是多余的,新建的 TokenizerHandler 继承了 TokenizerInterface,实际相当于 vendor\teamtnt\tntsearch\src\Support\Tokenizer.php

<?php
namespace TeamTNT\TNTSearch\Support;

class Tokenizer implements TokenizerInterface
{
    public function tokenize($text, $stopwords = [])
    {
        $text  = mb_strtolower($text);
        $split = preg_split("/[^\p{L}\p{N}]+/u", $text, -1, PREG_SPLIT_NO_EMPTY);
        return array_diff($split, $stopwords);
    }
}
//vendor\teamtnt\tntsearch\src\Support\Tokenizer.php

而我们只需要更换tntsearch里的tokenizer成TokenizerHandler就能使用。

config\scout.php的tntsearch里tokenizer按新的tntsearch换成了只需填写class就可以,而不是数组。

//config\scout.php
  
  'tntsearch' => [
        'storage'  => storage_path(),
        'fuzziness' => env('TNTSEARCH_FUZZINESS', false),
        'fuzzy' => [
            'prefix_length' => 2,
            'max_expansions' => 50,
            'distance' => 2
        ],
        'tokenizer' => App\Handlers\TokenizerHandler::class,
        'asYouType' => false,
        'searchBoolean' => env('TNTSEARCH_BOOLEAN', false),
    ],

而 TokenizerHandler.php 我也做了改动,把默认的tokenizer抄了过来。

因为jieba-php切英文时vendor\teamtnt\tntsearch\src\Indexer\TNTIndexer.php:468的saveWordlist()报错。

但我把jieba和自带tokenizer切出来的split数组对比,同英文文章数据结构一样,就长度不同,我觉得很正常啊,但程序不这样认为。有解决方案的朋友请告知我。

最后TokenizerHandler.php内容如下

<?php
namespace App\Handlers;
use Fukuball\Jieba\Jieba;
use Fukuball\Jieba\Finalseg;
use TeamTNT\TNTSearch\Support\TokenizerInterface;

class TokenizerHandler implements TokenizerInterface
{
    public function __construct(array $options = ['dict' => 'small'])
    {
        Jieba::init($options);
        Finalseg::init($options);
    }

    public function tokenize($text, $stopwords = [])
    {
        if(is_numeric($text)){
            return [];
        }
        if (preg_match("/[\x{4e00}-\x{9fff}]+/u", $text)) {
            //含中文
            return $this->getTokens($text, $stopwords);
        } else {
            return array_diff(preg_split("/[^\p{L}\p{N}]+/u", $text, -1, PREG_SPLIT_NO_EMPTY),$stopwords);
        }
    }

    public function getTokens($text, $stopwords = [])
    {
        if(empty($stopwords)){
            $stopwords=$this->getStopWords();
        }
        $text = $this->replaceWords($text,$stopwords);
        $split = Jieba::cutForSearch($text);
        return $split;
    }

    public function getStopWords()
    {
        $stopwords = [];
        $fp = fopen(base_path('vendor/fukuball/jieba-php/src/dict/stop_words.txt'), 'rb');
        if ($fp) {
            while (!feof($fp)){
                array_push($stopwords, str_replace("\n", "", fgets($fp)));         
            }
            fclose($fp);     
        }
        return array_merge($stopwords,[',','。','&nbsp','br']);
    }

    public function replaceWords($doc, $stopwords = [], $replace = ' ')
    {     
        return strtr($doc,array_combine($stopwords, array_fill(0, count($stopwords), $replace))); 
    }

}

自带的tokenizer可以切很多语言,我测试的除英语,韩,泰,希腊语都可以分,所以保留下来很有必要。而日语含有中文字所以用jieba切,但没切成功。

这样改动使得php artisan tntsearch:import App\***会此上的saveWordlist错误,使用scout没报错,项目里正常使用无报错,原本项目就是用scout管理索引,我就没修复这个tntsearch:import问题。如有解决访问的请告知我,谢谢。

php artisan scout:import "App\****"

总结

自己这两星期才实践型基于laravel写个东西,tntsearch也是刚接触两三天就用的,所以有很多不理解,刚开没理解tokenizer,jieba加进去也头疼了一天。

按我现在的理解,tokenizer原意为分词器,而jieba就是分词器,傻傻我还以为jieba只是配合tntsearch的组件而,实际jieba也可以单独使用,从这个我才看到HMM算法。之后关键字过滤可以使用,虽说不是智能,但也比自己一篇文本对比关键词强。

而tntsearch是为生成、搜索索引而用。而最开始我想使用elasticsearch,但它需要另起服务,还是java的,就感觉很费性能,而tntsearch使用sqlite存储索引,TNTIndexer对应的那些***.index文件实际是sqlite数据库文件,可以用sqlite方式参看,可以查看wordlist表,得知分词结果。而我项目本身用的就是sqlite,更不用另起服务,更节省资源。

 类似资料: