[转]PostgreSQL的FTI(TSearch)与中文全文索引的实践
发布日期:2021-09-30 20:46:22 浏览次数:8 分类:技术文章

本文共 7385 字,大约阅读时间需要 24 分钟。

安装 postgresql

你可以像平常一样编译和安装 postgresql,使用 tsearch2 进行中文的全文索引的时候,真正的区别发生在初始化数据库的时候。

初始化数据库

在linux里面使用tsearch2,首先要把数据库初始化成支持中文的 locale,比如我用 zh_CN.utf8:

initdb --locale=zh_CN.utf8 --encoding=utf8 ...

在一般用途的postgresql的使用时,一般会建议使用 C 做为初始化 locale,这样PG将会使用自身内部的比较函数对各种字符(尤其是中文字符)进行排序,这么做是合适的,因为大量OS的locale实现存在一些问题。对于tsearch2,因为它使用的是locale来进行基础的字串分析的工作,因此,如果错误使用locale,那么很有可能得到的是空字串结果,因为多字节的字符会被当做非法字符过滤掉。

我正在评估现代 OS/libc 的 locale 的实现是否已经成熟到可以接受的程度。如果是,则我们可以安全地使用 之,如果否,可能我们必须接合 slony 等工具来使用tsearch2。

安装 tsearch2

安装 tsearch2很简单,首先:

cd $PGSRC/contrib/tsearch2

然后:

gmake allgmake install
配置tsearch2

我们需要生成自己的字典和自己的对应locale 的配置选项,所以,要给 tsearch2的表里插入一些数据,简单起见,我先用一个最基本的做演示,将来丰富了中文独立的字典之后,将继续补充:

---- first you need to use zh_CN.utf8 as locale to initialize your database-- initdb -D $YOUR_PG_DATA_DIR --locale zh_CN.utf8 --encoding utf8--insert into pg_ts_cfg (ts_name, prs_name, locale) values ('utf8_chinese', 'default', 'zh_CN.utf8');insert into pg_ts_dict (dict_name, dict_init, dict_initoption, dict_lexize, dict_comment)  values ('chinese_stem_utf8', 'snb_ru_init_utf8(internal)', 'contrib/chinese.stop.utf8', 'snb_lexize(internal,internal,integer)', 'Chinese Stemmer, Laser. UTF8 Encoding');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'lword','{en_stem}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'nlword', '{chinese_stem_utf8}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'word', '{chinese_stem_utf8}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'email', '{simple}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'url', '{simple}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'host', '{simple}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'sfloat', '{simple}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'version', '{simple}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'part_hword', '{simple}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'nlpart_hword', '{chinese_stem_utf8}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'lpart_hword', '{en_stem}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'hword', '{chinese_stem_utf8}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'lhword', '{en_stem}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'nlhword', '{chinese_stem_utf8}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'uri', '{simple}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'file', '{simple}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'float', '{simple}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'int', '{simple}');insert into pg_ts_cfgmap(ts_name,tok_alias, dict_name) values('utf8_chinese', 'uint', '{simple}');
基础的分字程序

下面是 Carrie (感谢 Carrie!:D )写的一个基础的分字程序,在很大程度上可以满足相当多应用的需要:

---- a basic Chinese word segment function-- author: Carrie--create or replace function CarrieCharSeg( input text ) returns text as $$declare        retVal text;        i int;        j int;begin        i:= char_length(input);        j:= 0;        retVal:= '';        LOOP                retVal:= retVal || substring(input from j for 1) || ' ';                j:= j+1;        EXIT WHEN j=i+1;        END LOOP;        return retVal;end;$$language plpgsql;

下面是一个重载的分词程序,区分了单词和汉字,对单词单独进行分词:

---- a basic Chinese word segment function-- author: Carrie--create or replace function CarrieCharSeg(input text,int) returns text as $Q$declare        query text:= '';        retVal text:= '';        thisVal text:= '';        lastVal text:= '';        i integer:= 0;        j integer:= 0;begin        query:= lower(regexp_replace(input,'[[:punct:]]',' ','g'));        --raise notice '123: %',query;        i:= char_length(query);        LOOP                thisVal:= substring(query from j for 1);                IF ((thisVal <> ' ')AND(thisVal <> ' ')) THEN                        IF (((lastVal >= 'a') AND (lastVal <= 'z'))                            OR((lastVal >= 'A')AND(lastVal <= 'Z'))) AND                            (((thisVal >= 'a') AND (thisVal <= 'z')) OR                            ((thisVal >= 'A')AND(thisVal <= 'Z'))) THEN                                        retVal:= retVal || thisVal;                        ELSE                                retVal:= retVal || ' ' || thisVal;                        END IF;                END IF;                lastVal:= thisVal;                j:= j+1;                EXIT WHEN j > i;        END LOOP;        RETURN trim(retVal);end$Q$language plpgsql;
使用 tsearch2

OK,所有东西都安装完毕,我们需要试验一下了,下面是一个基本过程:

例子1:在原有的表上增加全文索引字段
准备全文索引的表

假设我们准备使用全文索引的数据库名字叫 BBS,首先要在这个数据库上安装 tsearch2:

psql -d BBS -f tsearch2.sql

在编译完成tsearch2之后,tsearch2.sql 在 $PGSRC/contrib/tsearch2 里面。

假设我们有这样一个表,里面保存着BBS的帖子:

create table BbsPost( id serial, topic varchar(100), content text, posterId integer, postTime timestamp);
增加一个全文索引字段

现在我们希望我们能够对 topic 字段和 content 字段进行全文索引,那么我们需要添加一个字段给表

BbsPost

psql BBSalter table BbsPost add column idxFti tsvector;

然后,我们要更新这个字段,使之包含合理的分词字段:

update BbsPost set idxFti = to_tsvector(coalesce(carriecharseg(topic, 1), '') || ' ' || coalesce(carriecharseg(content, 1), ''));
创建全文索引

然后,我们需要在这个字段上创建特殊的索引:

create index bbspost_idxfti_idx on BbsPost using gist(idxFti);
清理数据库

之后,我们再清理一下数据库:

vacuum full analyze analyze;

这样,全文索引就准备完成了!

例子二:通过扩展表使用全文索引

首先,仍然对BBS数据库进行处理,这次我们不直接在原有全文字段表上增加字段,而是另外建立一个表:

create table bbsPost_fti (id integer, idxfti tsvector);

我们创建两个字段的表,用于对

bbsPost

表进行全文索引。然后,我们给

bbsPost_fti

加一个外键:

alter table bbsPost_fti add foreign key (id) references bbsPost(id);

这样保证

bbsPost_fti

的 id 一定是

bbsPost

中存在的。然后,我们可以向

bbsPost_fti

里面插入数据:

insert into bbsPost_fti (id, idxfti) select id, to_tsvector(coalesce(carriecharseg(topic, 1), '') || ' ' || coalesce(carriecharseg(content, 1), '')) from bbsPost order by id;

这样,初始化所有数据到

bbsPost_fti

中。然后创建索引:

create index idxfti_idx on bbsPost_fti using gist(idxfti);create index bbsPost_fti_id_idx on bbsPost_fti (id);

我们也可以用倒排索引(GIN)进行全文索引,这样检索速度比 GIST 更快:

create index idxfti_idx on bbsPost_fti using gin(idxfti);

在我的试验中,GIN比GIST快了将近5倍,唯一的缺点是更新速度明显变慢。这个需要用户自己去平衡了。

最后,也是清理数据库:

vacuum full analyze;

然后我们也可以开始使用了!

使用全文索引

我们上面用的是分字程序,那么我们可以这样使用SQL查询来进行数据检索:

select * from BbsPost where idxFti @@ '中&文';

这样,就把包含“中”和“文”字的帖子都找了出来。@@ 操作符还支持与或非的逻辑:

select * from BbsPost where idxFti @@ '(中&文)|(英&文)';

用圆括弧:() 和 & 和 | 和! 可以组合查询条件,对数据表进行查询。目前,我们只拥有分字的程序,将来在完成中文分词程序之后,我们可以实现更简单的搜索,类似:

select * from BbsPost where idxFti @@ '(中文)|(英文)&(中国)';

这样的查询,大家可以用 explain analyze 看看,这样的查询的效率和 like 查询的效率差距有多大?;)

中文屏蔽词的添加

在语言里面有很多没有用的屏蔽词,比如英文里面的 "the",那么中文也有许多这样的词,比如“的”字,在这些词上建立索引,没必要也浪费,因此,我们可以用一种叫做 stop word (屏蔽词)的机制,给关闭这些词。具体做法是:

  • 找到$PGHOME/share/contrib/
  • echo "的" | iconv -f gbk -t utf8 > chinese.stop.utf8 (如果你的终端是UTF8的,则可以忽略 iconv 语句。)
  • 更多的 chinese stop word 可以用 echo "屏蔽词" | iconv -f gbk -t utf8 >> chinese.stop.utf8 实现

至此,使用 tsearch2 进行中文全文索引的最基本的要点已经在此列出,更多相关的信息,我会在有进一步的试验结果之后再详细展开。

【参考】

另外一个中文PostgreSQL分词组件参考:

TSearch官方网站和文档:

PostgreSQL官方TSearch文档参考:

Using tsearch with PostgreSQL:

本文来源:

转载地址:https://blog.csdn.net/iteye_11848/article/details/81647696 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:《走出软件作坊》接受采访,SD2.0大会专门安排课程,还有抽奖赠书
下一篇:[WCF的Binding模型]之三:信道监听器(Channel Listener)

发表评论

最新留言

逛到本站,mark一下
[***.202.152.39]2024年03月10日 03时14分00秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章

IP/tzgm.php,LianjiaSpider/在售数量.ipynb at master · BerSerK/LianjiaSpider · GitHub 2019-04-21
linux移动文件的脚本,使用Linux脚本移动文件 2019-04-21
linux查看系统所有变量,Linux系统各指标命令 2019-04-21
linux打印机守护程序,linux下怎么编写守护程序呢? 2019-04-21
嵌入式linux 设置时间,time_clock控件应用,使用命令date -s 12:00:00手动设置时间后,时间就停住不走了,我在Ubuntu和嵌入式Linux平台都测试到了... 2019-04-21
linux 8086下编译,Ubuntu18.04/Linux下安装DosBox进行8086汇编 2019-04-21
linux监控windows,zabbix监控之linux及windows客户端安装配置 2019-04-21
linux中怎么卸载tree,Liunx系统命令中tree命令详解 2019-04-21
linux 网络音箱 混音6,Linux音频编程(三)混音器介绍 2019-04-21
node与mysql开源_node与mysql的相互使用————node+mysql 2019-04-21
python合并列表重新排序_python – 将两个已排序的列表合并为一个更大的排序列表... 2019-04-21
vbs用mysql语句查询数据库_vbs脚本实现window环境下的mysql数据库的备份及删除早期备份... 2019-04-21
mysql连接nginx_nginx四层负载均衡连接mysql 2019-04-21
mysql截取栏目字符_substring从指定字符串开始截取(图) 2019-04-21
python 函数参数前面两个星号_Python中参数前面一个星号两个星号(*参数,**参数)起什么作用呢?... 2019-04-21
python类属性初始化_Python类定义、属性、初始化和析构 2019-04-21
mysql构建url给scrapy_Python Scrapy从mysq填充起始url 2019-04-21
owdcloud mysql_MySQL在Ubuntu远程配置 2019-04-21
python基础装饰器_Python基础 装饰器及练习 2019-04-21
python导出csv不带引号的句子_不带双引号写入CSV文件 2019-04-21