SCWS分词(二)自定义字典及分词器
经过上篇文章的学习,相信大家对分词的概念已经有了更深入的了解了吧。我们也知道了,SCWS 是 XS 中的一个重要组成部分,但它也是可以单独拿出来使用的。而对于分词器来说,不管是 SCWS 还是现在流行的 IK、Jieba ,其实概念方面都是差不多的。比如说它们都需要字典来做为分词的依据,也会有停用词库这一类的附加字典。今天,我们主要来学习的就是 SCWS 字典相关的一些配置。此外,还有自定义分词器的实现。
自定义字典
上回已经说过,SCWS 有提供一个非常小的,但词汇量非常大的字典。不过现在的网络社会,各种新鲜词汇层出不穷,总会有超出默认字典的新词出现。同时,还有一种情况就是一些专业领域的专业词汇,比如医学或者工程上面的,也不会在通用的字典库中。像这类的词项,我们就可以通过自定义字典库来添加。
XS 的字典库分为两种,一种是全局的,一种是针对某个项目的。对于全局字典库,我们可以在 XS 的安装目录中的 etc 目录下找到。
#/usr/local/xunsearch/etc
[root@localhostetc]#ll
total14832
-rw-r--r--.1501games14315504May232022dict.utf8.xdb
-rw-r--r--.1rootroot447May232022dict_user.txt
-rw-r--r--.1rootroot843730Nov1120:55py.xdb
-rw-r--r--.1rootroot3729May232022rules.ini
-rw-r--r--.1rootroot4424May232022rules.utf8.ini
-rw-r--r--.1rootroot4383May232022rules_cht.utf8.ini
-rw-r--r--.1rootroot272May232022stopwords.txt
默认情况下,dict.uft8.xdb 就是 XS 自带的那个字典库。而 dict_user.txt 则是我们可以自己定义的字典库。py.xdb 是拼音库、rules开头的文件名全都是规则文件,也就是我们各种词要如何组合,词性变化与评分配置等。最后还有一个 stopwords.txt 停用词库。好了,咱们先用之前的数据来测试吧,就是最早那篇文章里的数据。
>phpvendor/hightman/xunsearch/util/Indexer.php--source=csv--clean./config/demo3_56.101.ini
1,关于xunsearch的DEMO项目测试,项目测试是一个很有意思的行为!,1314336158
2,测试第二篇,这里是第二篇文章的内容,1314336160
3,项目测试第三篇,俗话说,无三不成礼,所以就有了第三篇,1314336168
>php./vendor/hightman/xunsearch/util/Quest.php./config/demo3_56.101.ini''
在3条数据中,大约有3条包含,第1-3条,用时:0.0014秒。
1.关于xunsearch的DEMO项目测试#1#[100%,0.00]
项目测试是一个很有意思的行为!
Chrono:1314336158Author:
2.测试第二篇#2#[100%,0.00]
这里是第二篇文章的内容
Chrono:1314336160Author:
3.项目测试第三篇#3#[100%,0.00]
俗话说,无三不成礼,所以就有了第三篇
Chrono:1314336168Author:
我们先来测试一个不正常的词,比如说“无三”。很明显,它不是一个传统意义上的正常的单词,但就像很多专业词汇一样,我们假设“无三”就是一个专业词汇。目前直接搜索“无三”肯定是没有数据的。
>php./vendor/hightman/xunsearch/util/Quest.php./config/demo3_56.101.ini--show-query'无三'
--------------------
解析后的QUERY语句:Query(无三@1)
--------------------
在3条数据中,大约有0条包含无三,第0-0条,用时:0.0009秒。
好了,那么咱们就打开 dict_user.txt 进行配置。
//vim/usr/local/xunsearch/etc/dict_user.txt
#Customdictionaryforscws(UTF-8encoding)
#每行一条记录,以#开头的号表示注释忽略
#每行最多包含4个字段,依次代表"词条""TF""IDF""词性"
#字段之间用空格或制表符分开,特殊词性"!"用于表示删除该词
#参见scws自定义词典帮助:
#http://bbs.xunsearch.com/showthread.php?tid=1303
#$Id$
#
#WORDTFIDFATTR
#------------------------------------------------------
无三
可以看到文件上方的注释已经很清晰了,四个字段,分别是词条、TF词频、IDF逆文档频率和词性,用逗号分隔,并按行划分。后面三个字段属性其实是可以不用写的,它会有默认值。
我们直接添加一个“无三”,后面的不用填,然后重新索引添加数据。再次查询,就可以看到“无三”可以被搜索到了。
>php./vendor/hightman/xunsearch/util/Quest.php./config/demo3_56.101.ini--show-query'无三'
--------------------
解析后的QUERY语句:Query(无三@1)
--------------------
在3条数据中,大约有1条包含无三,第1-1条,用时:0.0020秒。
1.项目测试第三篇#3#[100%,0.62]
俗话说,无三不成礼,所以就有了第三篇
Chrono:1314336168Author:
后面的三个参数,我也不知道怎么填,查了一些资料也没讲得特别明白的。所以咱们保持默认就好,或者说你明确的知道这个新词的词性,那就把 TF 和 IDF 都设置成 1 ,然后指定词性就好了。大部分专业词汇其实都是名词居多的,直接设置一个 n 就行了。
项目字典
接下来我们来自定义一个项目字典。这种字典就是针对某一个具体项目的,比如说针对我们的 demo 项目,那么就直接找到安装目录的 data 目录,然后找到 demo 文件夹,在这个文件夹中创建一个 dict_user.txt 文件。关于项目数据目录的问题,我们之前在学习索引管理时就说过,后面学习 Xapian 的时候也会再说一下。
好了,直接在项目目录下面的 dict_user.txt 文件中添加新词吧,词条规则和全局文件是一样的。
//vim/usr/local/xunsearch/data/demo/dict_user.txt
无三不
对于项目字典,我们在在代码中也可以直接操作,比如使用 XSIndex 对象的 getCustomDict() 方法就可以获取到自定义字典的内容,使用 setCustomDict() 就可以设置当前项目的自定义字典信息。
print_r($xs->index->getCustomDict());//无三不
$dict=index->setCustomDict($dict));
print_r($xs->index->getCustomDict());
//无三不
//无三不成00n
在上面的代码中,我们通过 PHP 来设置了自定义字典,大家可以再回到项目目录下,看看 dict_user.txt 文件是不是也已经被重写成了新的内容,多了“无三不成”这样一个单词。这种功能有个什么好处呢?那就是我们的字典也可以通过在 MySQL 或其它数据库中进行存储,然后直接在 PHP 代码中操作字典,是不是非常方便。
当然,txt 格式的明文字典效率其实不高的,而且如果单词数量比较多,占用的空间也会比较大。SCWS 在命令行还提供了一个 scws-gen-dict 工具。和上篇文章中我们命令行操作 scws 的工具是放在一起的。这个工具可以将 txt 文件转换为 xdb 文件,也就是 SCWS 的默认词典文件一样的压缩格式。如果确实有非常大量的专业词汇,建议还是转换一下哦。这里我就不演示了,SCWS 还是比较智能的,普通的 txt 文件其实大部分情况下还是能满足需求的。
接下来咱们测试一下。
php./vendor/hightman/xunsearch/util/Quest.php./config/demo3_56.101.ini--show-query'无三不成'
--------------------
解析后的QUERY语句:Query((无三不成@1SYNONYM(无三不@78AND不成@79)))
--------------------
在1条数据中,大约有1条包含无三不成,第1-1条,用时:0.0018秒。
1.项目测试第三篇#3#[100%,0.15]
俗话说,无三不成礼,所以就有了第三篇
Chrono:1314336168Author:
“无三不成”这个新词没问题。那么“无三不”呢?
>php./vendor/hightman/xunsearch/util/Quest.php./config/demo3_56.101.ini--show-query'无三不'
--------------------
解析后的QUERY语句:Query((无三不@1SYNONYM(无三@78AND三不@79)))
--------------------
在1条数据中,大约有0条包含无三不,第0-0条,用时:0.0008秒。
嗯?咋查不到数据了?明明添加到字典里了啊!使用 SDK 的分词工具来看也是正常分词的,分词结果中有“无三不成”、“无三不”这两个单词。
$tokenizer=newXSTokenizerScws;//直接创建实例
$words=$tokenizer->getResult("俗话说,无三不成礼,所以就有了第三篇");
foreach($wordsas$w){
echo$w['word']."/".$w['attr']."/".$w['off'],"";
}
//话说/n/0俗话/n/0话说/n/3,/un/9无三不成/n/12无三不/@/12不成/d/18礼/n/24,/un/27所以/c/30就/d/36就有/v/36有了/v/39了第/m/42第三/m/45三篇/q/48
好吧,自己玩出来的火,就得自己灭。原理我也没搞明白,但是从上面 getQuery() 的分析中,我们可以通过两种方式来搜索到。
-
使用 fuzzy 模糊查询
在 getQuery() 返回的数据中,我们看到了“无三不”以及它的同义二元词“无三”和“三不”。在同义词中,是 AND 连接,也就是必须要同时出现“无三”和“三不”(其实应该也不需要这样,比如搜索“俗话说”,是AND同义词,但是可以搜索到)。但咱们换成 setFuzzy() 效果,全部变成 OR ,就可以通过全局字典中的“无三”查到了。
>php./vendor/hightman/xunsearch/util/Quest.php./config/demo3_56.101.ini--show-query'无三不'--fuzzy
--------------------
解析后的QUERY语句:Query((无三不@1SYNONYM(无三@78OR三不@79)))
--------------------
在1条数据中,大约有1条包含无三不,第1-1条,用时:0.0018秒。
1.项目测试第三篇#3#[100%,0.15]
俗话说,无三不成礼,所以就有了第三篇
Chrono:1314336168Author:
好吧,承认我是猜的,上面的分析不是权威分析哦。希望有懂得大佬能在评论区指点一下。
-
删除全局字典中的那个“无三”,再重新索引数据。
很奇怪,我们直接删全局字典中的那个“无三”,重新索引添加数据之后,使用“无三不”就可以搜索到数据了。这个真的不知道原因,也不瞎猜了,反正就告诉大家,这样可以搜到。ES 类似的能力我没有测,将来或者说小伙伴有兴趣的可以使用 ES 的 IK 分词器测一下,并在评论区说下啥效果哦,就当是一个小作业啦!
停用词库
XS 的停用词库这一块,即使在官方文档上也没有详细的说明,全网也找不到什么有用的资料,真的独一份哦。
停用词的意思就是这个词不用了,不参与分词。或者说分词器如果看到这个词了,直接略过不管它。如果你学过 ES 中的 IK ,一定会知道这个东西。而且 XS 中也是使用 stopwords 这个文件名来定义停用词库的。
前面在 XS 安装目录的 etc 中目录中,大家就已经看到有一个 stopwords.txt 文件了吧,直接打开它,里面已经有一堆默认内容了,我们再添加一个。
#vimetc/stopwords.txt
………………
俗话说
添加完之后,来测试一下吧。
>php./vendor/hightman/xunsearch/util/Quest.php./config/demo3_56.101.ini--show-query'俗话说'
--------------------
解析后的QUERY语句:Query((俗话说@1SYNONYM(俗话@78AND话说@79)))
-------------服务器托管网-------
在1条数据中,大约有1条包含俗话说,第1-1条,用时:0.0020秒。
1.whereiswhichwho#3#[100%,0.23]
俗话说,无三不成礼,所以就有了第三篇
Chrono:Author:
还是能查到?跟你说,不管你是重建索引,还是重启服务,都没用,这个停用词库感觉就跟没配一样,完全不起作用。这是为啥呢?因为默认情况下,XS 就根本没启用停用词库的功能。所以在官方文档上,你会看到有同学报怨说这个功能没用。但其实,咱们只要开启加载这个功能及停用词库就好啦。
查看我们启动服务器的 xs-ctl.sh 脚本。就是安装目录下面的 bin 目录中的那个脚本文件,我的虚拟机上是位于 /usr/local/xunsearch/bin/xs-ctl.sh 。给最后启动服务的两行代码中,加上 -s etc/stopwords.txt
就可以了。
#vimbin/xs-ctl.sh
………………
#run
case"$cmd"in
start|stop|faststop|fastrestart|restart|reload)
#index
iftest"$server"!="search";then
#bin/xs-indexd$opt_index$opt_pub-k$cmd#这里是之前的
bin/xs-indexd$opt_index$opt_pub-setc/stopwords.txt-k$cmd
fi
#search
iftest"$server"!="index";then
#bin/xs-searchd$opt_search$opt_pub-k$cmd#这里是之前的
bin/xs-searchd$opt_search$opt_pub-setc/stopwords.txt-k$cmd
fi
;;
*)
echo"Unknowncommand:$cmd"
show_usage
;;
esac
这个参数就表明加载指定的停用词库,并启用停用词功能。然后再来测试。
>php./vendor/hightman/xunsearch/util/Quest.php./config/demo3_56.101.ini--show-query'俗话说'
--------------------
解析后的QUERY语句:Query()
--------------------
在3条数据中,大约有3条包含俗话说,第1-3条,用时:0.0015秒。
1.关于xunsearch的DEMO项目测试#1#[100%,0.00]
项目测试是一个很有意思的行为!
Chrono:1314336158Author:
2.测试第二篇#2#[100%,0.00]
这里是第二篇文章的内容
Chrono:1314336160Author:
3.项目测试第三篇#3#[100%,0.00]
俗话说,无三不成礼,所以就有了第三篇
Chrono:1314336168Author:
注意看,我们搜索“俗话说”,getQuery() 返回的结果是“alldocument”,就和空查询条件一样。下面的搜索结果也是全部结果都出来了。也就是说,“俗话说”这个停用词已经生效了,SCWS 看到它根本就不管,直接略过。由于没有别的词了,我们的查询结果就和完全没有搜索条件一样,直接返回全部数据了。
我是怎么找到这个功能的?额,在 Github 的 XS 源码中搜 stopword ,结果就找到在 import.cc 、indexed.c 和 searchd.c 中有相关的内容。再仔细看下源码,它们在 -s
这个参数的注释中都写明了要通过这个参数加载停用词库。接着就来看 xs-ctl.sh 脚本,发现在启动索引和搜索服务时,压根就没传这两个参数。剩下的,就不用我多说了吧。
自定义分词器
接下来,我们来试试自定义分词器的功能。这一块是针对 SDK 来说的。在索引配置文件中,我们之前说过有默认的 scws、full、split、none、xlen、xstep 这几种分词类型。其实它们的源码都在 vendor/hightman/xunsearch/lib/XSTokenizer.class.php 这个文件里面。除了 scws 是请求服务端进行分词的,其它几种其实都是 PHP 代码的算法实现,比如说 split ,最终就是一个 explode() 。具体源码大家可以自己去看一下。
这些分词器,其实都是实现了一个 XSTokenizer 接口。而这个接口中,只有一个方法,那就是 getTokens() 方法。那么,如果要实现我们自己自定义的分词器,其实只要和那些自带的分词器一样,实现这个接口方法就可以了嘛。
我们在 vendor/hightman/xunsearch/lib 目录下新建一个 XSTokenizerJieba.class.php 文件,我想使用 Jieba 分词来实现一个分词器。Jiaba-PHP(结巴分词PHP版) https://github.com/fukuball/jieba-php 的文档和说明大家可以在这个 Github 链接上看一下。
#vendor/hightman/xunsearch/lib/XSTokenizerJieba.class.php
require_onceXS_LIB_ROOT.'/../../../autoload.php';
classXSTokenizerJiebaimplementsXSTokenizer
{
publicfunction__construct($arg=null)
{
}
publicfunctiongetTokens($value,XSDocument$doc=null)
{
//composerrequirefukuball/jieba-php:dev-master
ini_set("memory_limit","-1");
FukuballJiebaJieba::init();
FukuballJiebaFinalseg::init();
returnFukuballJiebaJieba::cut($value);;
}
}
为什么是在 vendor/hightman/xunsearch/lib 目录呢?能不能像 ini 文件一样放到一个我们指定的目录中呢?抱歉,我看了源码,不行。
//vendor/hightman/xunsearch/lib/XSFieldScheme.class.php
publicfunctiongetCustomTokenizer()
{
if(isset(self::$_tokenizers[$this->tokenizer])){
returnself::$_tokenizers[$this->tokenizer];
}else{
if(($pos1=strpos($this->tokenizer,'('))!==false
&&($pos2=strrpos($this->tokenizer,')',$pos1+1))){
$name='XSTokenizer'.ucfirst(trim(substr($this->tokenizer,0,$pos1)));
$arg=substr($this->tokenizer,$pos1+1,$pos2-$pos1-1);
}else{
$name='XSTokenizer'.ucfirst($this->tokenizer);
$arg=null;
}
if(!class_exists($name)){
$file=$name.'.class.php';
if(file_exists($file)){
require_once$file;
}elseif(file_exists(XS_LIB_ROOT.DIRECTORY_SEPARATOR.$file)){
require_onceXS_LIB_ROOT.DIRECTORY_SEPARATOR.$file;
}
if(!class_exists($name)){
thrownewXSException('Undefinedcustomtokenizer`'.$this->tokenizer.''forfield`'.$this->name.''');
}
}
$obj=$arg===null?new$name:new$name($arg);
if(!$objinstanceofXSTokenizer){
thrownewXSException($name.'forfield`'.$this->name.''dosenotimplementtheinterface:XSTokenizer');
}
self::$_tokenizers[$this->tokenizer]=$obj;
return$obj;
}
}
为指定字段加载分词器的源码就是上面这段,你可以看到,它固定了分词器的名称。然后在下面 require 时,判断了名称如果在当前目录存在,就加载,如果不存在,则接上 XS_LIB_ROOT 目录去加载。可问题是,$name
在前面是通过前缀的方式来固定的,这就相当于给了我们两个选择。
一是自定义分词器要放在运行当前代码的目录路径下,也就是直接 require_once "XSTokenizerJ服务器托管网iaba.php"
这一行的效果。比如我们在 source 目录下运行 php 代码,则这个自定义文件就要放在 source 目录下。
二是放在 vendor/hightman/xunsearch/lib 目录下,也就是第二个拼接上 XL_LIB_ROOT 的效果,require_once XS_LIB_ROOT."XSTokenizerJiaba.php"
。
反正怎么都不是太爽。如果确实需要用到,还是使用第一种吧,毕竟 composer 里面的东西,能不动不就要动了。
好了,不多吐槽了,这一块说不定将来也会有变动呢。我们还是先来测功能。将配置文件中的相关字段的分词器换成我们自定义的。
# config/demo3_56.101.ini
……………………
[subject]
type = title
#tokenizer=scws(15)
tokenizer=jieba
[message]
type = body
#tokenizer=scws(15)
tokenizer=jieba
……………………
重新索引数据后,进行测试。
>php./vendor/hightman/xunsearch/util/Quest.php./config/demo3_56.101.ini--show-query'俗话说'
--------------------
解析后的QUERY语句:Query((俗话说@1SYNONYM(俗话@78AND话说@79)))
--------------------
在2条数据中,大约有1条包含俗话说,第1-1条,用时:0.0027秒。
1.项目测试第三篇#3#[100%,0.39]
俗话说,无三不成礼,所以就有了第三篇
Chrono:1314336168Author:
“俗话说”要是查不到,可以取消前面的停用词功能哦。我们可以直接使用分词器的 getTokens() 来进行直接的分词测试,并且看看当前默认的分词器用得是哪个。
var_dump($xs->getFieldTitle()->getCustomTokenizer());
//object(XSTokenizerJieba)#9(0){}
var_dump((newXSTokenizerJieba())->getTokens('俗话说,无三不成礼,所以就有了第三篇'));
//array(11){
//[0]=>
//string(9)"俗话说"
//[1]=>
//string(3)","
//[2]=>
//string(6)"无三"
//[3]=>
//string(6)"不成"
//[4]=>
//string(3)"礼"
//[5]=>
//string(3)","
//[6]=>
//string(6)"所以"
//[7]=>
//string(3)"就"
//[8]=>
//string(3)"有"
//[9]=>
//string(3)"了"
//[10]=>
//string(9)"第三篇"
//}
var_dump((newXSTokenizerScws)->getResult('俗话说,无三不成礼,所以就有了第三篇'));
//array(16){
//[0]=>
//array(3){
//["off"]=>
//int(0)
//["attr"]=>
//string(4)"n"
//["word"]=>
//string(9)"俗话说"
//}
//[1]=>
//array(3){
//["off"]=>
//int(0)
//["attr"]=>
//string(4)"n"
//["word"]=>
//string(6)"俗话"
//}
//[2]=>
//array(3){
//["off"]=>
//int(3)
//["attr"]=>
//string(4)"n"
//["word"]=>
//string(6)"话说"
//}
//………………………………
//………………………………
看到两个分词器的效果不同了吧。当然,Jiaba 我们没有做任何配置,而 SCWS 默认是 3 也就是最短词和二元一起进行分词的,所以“话说”也会被分出来。
解决单字 Like 问题
大家还记得吧?早前的文章中,我们就说过一个问题,那就是搜索“项”这种默认不会分词的单字,也就是不会建立倒排索引的单字,是无法查询出数据的。那么在学习了分词的原理之后,特别是字典以及上期讲的复合分词等级的内容之后,大家是不是有想法,也能猜到如何来解决这个问题了吧。使用字典,可能比较麻烦,你需要将很多单字加到字典,而且我测试有的情况下还没效果。那么使用复合分词等级呢?咱们就直接设置分词复合等级为 15 。也就是配置文件中的 title 和 body 字段,都将它们的 tokenizer
设置成 scws(15)
。
接下来重新索引添加数据,然后查询试试。
>php./vendor/hightman/xunsearch/util/Quest.php./config/demo3_56.101.ini--show-query'项'
--------------------
解析后的QUERY语句:Query(项@1)
--------------------
在2条数据中,大约有2条包含项,第1-2条,用时:0.0012秒。
1.关于xunsearch的DEMO项目测试#1#[100%,0.16]
项目测试是一个很有意思的行为!
Chrono:1314336158Author:
2.项目测试第三篇#3#[99%,0.16]
俗话说,无三不成礼,所以就有了第三篇
Chrono:1314336168Author:
可以通过单个“项”字查询到数据了吧。复合分词等级的知识上次已经说过,设置成 15 其实就是各种词项、二元、重要单字、全部单字都进行拆分,拆分出来的内容非常多。其实这么做的结果大家也能想到,就是它会带来一个非常重大的问题:倒排索引库会变得非常大,而且如果 body 字段是那种文献类型的超长文章或超大文章,索引缓冲也会出问题导致索引失败。即使是在 ES 中,也没法这么玩的。
又回到当初的那个问题了。搜索引擎+分词器(倒排索引),不是 Like !!
总结
自定义字典有点意思吧?更重要的是,咱们还有全网唯一一份的 XS 中停用词的使用方法哦。这不来个赞真的对不起这篇文章啊。另外,通过自定义分词器,咱们也能看出来,XS 完全也是可以使用别的分词工具的嘛。不管是你是想用第三方的 Jieba 分词还是别的什么非常特殊的行业切分格式,都可以通过代码自己来实现。最后,关于分词的内容,在 ES 中现在最流行的是 IK 分词器,小伙伴们在学习 ES 的时候,如果学到了 IK 的部分,其实有这两篇 SCWS 的基础垫底,掌握起来还是会比较轻松的。毕竟什么词性、分词级别、字典,甚至字典名称有很多都是相同的。
分词部分的学习结束了,我们的搜索引擎整体课程也就接近尾声了。后面的两篇文章是扩展部分,也是很有意思的哦,我们会看一下 Xapian 官方文档中我发现的一些有用的内容。最后还会介绍一套非常有意思的完全 PHP 实现的搜索引擎方案哦。
测试代码:
https://github.com/zhangyue0503/dev-blog/blob/master/xunsearch/source/17.php
参考文档:
http://www.xunsearch.com/doc/php/guide/index.dict
http://www.xunsearch.com/doc/php/guide/ini.tokenizer
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
网关: 一:apisix doc:https://apisix.apache.org/zh/docs/apisix/getting-started/README/ github:https://github.com/apache/apisix 二:Ko…