兼济天下则达 独善其身则穷
在上篇中,我们介绍了一种建立主索引和增量索引的方法,这种方法有一种不足之处就是会改变主索引,因为每次增量索引都会与主索引合并成新的主索引。为此,我们可以想出另一种解决的办法,每次只改变增量索引,这就需要另外再建立一个临时索引。
这里只需要改变少量地方,一个是增量索引,另外还需新增一个临时索引,具体配置如下:
source srcdelta : srcmain{ sql_query_pre = SET NAMES utf8 sql_query_pre = SET SESSION query_cache_type=OFF sql_query = SELECT ID, post_title, post_content, UNIX_TIMESTAMP(post_modified) AS post_modified FROM wp_posts WHERE \ post_status='publish' limit 0; sql_query_post_index = } source srcdelta_temp : srcmain { sql_query_pre = SET NAMES utf8 sql_query_pre = SET SESSION query_cache_type=OFF sql_query_pre = SET @maxtsdelta:=NOW(); sql_query_pre = UPDATE sphinx_helper SET delta_tmp_maxts=@maxtsdelta WHERE appid='blog_search'; sql_query = SELECT ID, post_title, post_content, UNIX_TIMESTAMP(post_modified) AS post_modified FROM wp_posts WHERE \ post_status='publish' AND post_modified >= (SELECT main_maxts FROM sphinx_helper WHERE appid='blog_search')\ AND post_modified < @maxtsdelta; sql_query_post_index = UPDATE sphinx_helper SET delta_maxts=delta_tmp_maxts WHERE appid='blog_search'; } index delta_temp : main{ source = srcdelta_temp path = /home/long/sphinxforchinese/blog_search/var/data/delta_temp }
实际上,我们是先建立一个空的增量索引,之后临时索引中的数据慢慢合并到增量索引中。在这里,增量索引很像上篇中的主索引,而临时索引则像上篇中的增量索引。 此时我们需要修改dist_blog_search,即增加临时索引
index dist_blog_search { type = distributed local = main local = delta local = delta_temp agent_connect_timeout = 1000 agent_query_timeout = 3000 }
此后还需改变Shell脚本的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #!/bin/bash baseDir=/home/long/sphinxforchinese/blog_search conf=$baseDir/etc/main_delta_temp.conf binDir=$baseDir/bin cd $binDir while [ true ] do ./indexer -c $conf --rotate --merge delta delta_temp if [ "$?" -eq "0" ]; then cat $baseDir/script/post_merge.sql | mysql -u root --password=123456 blog ./indexer -c $conf --rotate delta_temp fi sleep 60 done |
事实上,改变的内容还是很少的。经过这样的改变,我们就无需再改变主索引了。第一次建立主索引后,就一直保持不变,变化的是增量索引。
虽然只建立主索引就可以满足许多应用,但当数据非常多时,每次都重建索引是一件非常耗时的事情,而且每次重建都会浪费CPU,这也是极为不好的。考虑这样一种情况,在数据库中一共有1千万个文档,而每天只新增一万个文档,如果每次都要重建索引,则第一次重建时,是1001万个文档,第二次时是1002万个文档,这都非常耗时的。如果建好主索引后,只对这些新增的一万个数据建一个增量索引,之后把它合并到主索引中,所需的时间将缩短。所以建立main+delta索引是一个不错的选择。
这里依然以之前的博客搜索为例。为了便于做增量,我们需要记录每次抓取的时间,而为了持久保存这个时间,我们需要在数据中建立一个辅助表,建表语句如下
create table sphinx_helper( appid varchar(300) not null primary key, main_maxts datetime, main_tmp_maxts datetime, delta_maxts datetime, delta_tmp_maxts datetime ); insert into sphinx_helper (appid) values ('blog_search');
在wp_posts表中, post_modified这个时间字段是随着每次文章的更新而自动变化的,所以可以使用它来做增量。主要思路就是用一个值来保存上次增量索引的时间,当需要再做增量索引时,则只需索引从这个保存的时间到现在这段时间里的数据。在sphinx_helper中,这个值用main_maxts来标示。对于主索引,写成配置文件如下,
source base{ type = mysql sql_host = localhost sql_user = root sql_pass = 123456 sql_db = blog sql_port = 3306 } source srcmain : base{ sql_query_pre = SET NAMES utf8 sql_query_pre = SET SESSION query_cache_type=OFF sql_query_pre = UPDATE sphinx_helper SET main_tmp_maxts=NOW() WHERE appid='blog_search'; sql_query = \ SELECT ID, post_title, post_content, UNIX_TIMESTAMP(post_modified) AS post_modified FROM wp_posts WHERE \ post_status='publish' AND post_modified < (SELECT main_tmp_maxts FROM sphinx_helper WHERE appid='blog_search'); sql_query_post_index = UPDATE sphinx_helper SET main_maxts=main_tmp_maxts WHERE appid='blog_search'; sql_attr_timestamp = post_modified sql_field_string = post_title }
以上就是主索引的配置,之所以需要将NOW()得到的时间保存到数据库中,之后在sql_query_post_index中取出来用,是因为sql_query_post_index和sql_query不是用一个数据连接。而之所以在sql_query_post_index里才更新main_maxts,是为了保证只有在索引成功建立后才更新这个值。而对于增量索引的配置,则如下:
source srcdelta : srcmain { sql_query_pre = SET NAMES utf8 sql_query_pre = SET SESSION query_cache_type=OFF sql_query_pre = SET @maxtsdelta:=NOW(); sql_query_pre = UPDATE sphinx_helper SET delta_tmp_maxts=@maxtsdelta WHERE appid='blog_search'; sql_query = SELECT ID, post_title, post_content, UNIX_TIMESTAMP(post_modified) AS post_modified FROM wp_posts WHERE \ post_status='publish' AND post_modified >= (SELECT main_maxts FROM sphinx_helper WHERE appid='blog_search')\ AND post_modified < @maxtsdelta; sql_query_post_index = UPDATE sphinx_helper SET delta_maxts=delta_tmp_maxts WHERE appid='blog_search'; }
在sql_query中可以看到,每次增量索引的数据都是在[max_maxts, NOW()]之间,而只在sql_query_post_index中更新delta_maxts也是基于上述理由。剩下的配置如下:
index main { source = srcmain path = /home/long/sphinxforchinese/blog_search/var/data/main docinfo = extern charset_type = utf-8 chinese_dictionary = /home/long/sphinxforchinese/blog_search/etc/xdict } index delta : main { source = srcdelta path = /home/long/sphinxforchinese/blog_search/var/data/delta } index dist_blog_search { type = distributed local = main local = delta agent_connect_timeout = 1000 agent_query_timeout = 3000 }
这里我们多了一个dist_blog_search,它是结合main和delta的搜索结果,在客户端中搜索时,我们使用dist_blog_search这个索引的结果。剩下的配置与只有主索引时相同,这里就不累述了。
写好配置文件后,还需要有一个步骤。因为我们的策略是每隔一段时间将增量索引与主索引合并,当合并之后,我们需要更新main_maxts这个值。如果我们是每个60秒做一次增量索引,这需要写一个shell脚本,脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #!/bin/bash baseDir=/home/long/sphinxforchinese/blog_search conf=$baseDir/etc/main_delta.conf binDir=$baseDir/bin cd $binDir while [ true ] do ./indexer -c $conf --rotate --merge main delta if [ "$?" -eq "0" ]; then cat $baseDir/script/post_merge.sql | mysql -u root --password=123456 blog ./indexer -c $conf --rotate delta fi sleep 60 done |
先执行./indexer -c $conf --rotate --merge main delta
,这句是将主索引和增量索引合并,当合并成功时,则需要到数据库中修改main_maxts这个值,这个句子在post_merge.sql中,post_merge.sql的内容如下:
UPDATE sphinx_helper SET main_maxts=delta_maxts\ WHERE appid='blog_search';
之后进行增量抓取 ./indexer -c $conf --rotate delta
, 这里说说--rotate这个选项,这个选项非常有用。在主索引和增量索引合并时,indexer程序会将这两个索引合并成一个索引,当合并成功后,程序会发送一个SIGHUP信号给searchd,之后searchd就好去加载这个新的索引。
到这里,一个main+delta的索引就完成了。
最近一个星期都在看Sphinx搜索引擎的文档,并和组里的一个同事合作为公司的企业空间搜索建立索引,提供搜索服务,所以对于Sphinx有了一些了解,顺便几下来,以后用到了可以再看看。
先八卦一下,Sphinx首先是俄罗斯人Andrew Aksyonoff开发的全文搜索引擎,开源之后有其他人参与进来,功能更加强大了。俄罗斯人还是真是厉害,之前是Nginx,现在是Sphinx。可是Sphinx不支持中文,所以要下载Sphinxforchinese才可以用。
Sphinx的数据源主要来自数据库,如Mysql,这也是最常见的方式。以下主要写给公司配置引擎时的一些体会。
1.一般使用都是一个主索引和增量索引,主索引建立后一直不变,变化的是增量索引,搜索的结果为合并主索引和增量索引的搜索结果。每隔一段时间就到数据源中抓取数据,保存在一个tmp索引中,然后这个tmp索引和增量索引合并,当然也可以隔一段时间就将增量索引和主索引合并,但这个时间间隔最好长一些。
2.建索引需要的数据分布在许多个表上,所以要先写爬虫将这些表的数据从数据库中抓取出来,存到另一个表中。之后在Sphin的配置文件中,数据就可以来自这个新建的表。这个新建的表最好有一个自动变化的时间字段,也就是每次在这个表中插入数据或更新数据,这个时间自动都会变化,这个字段将用于增量索引。另外还需要建一个表来保存上一次抓取的时间,从这个时间往后,抓取新的数据。
3.默认情况下,从数据源中选出的数据都是建索引的。而默认情况下,对于建索引的数据,Sphinx将不会保存原始数据,如果需要Sphinx既建索引,又要保存数据,在配置文件中,将这个字段写为sql_field_string。对于时间类型,在sql_query中select数据时,就要用函数unix_timestamp将它转为整形的时间戳,在配置文件中,要将这个字段写为sql_attr_timestamp,这样在客户端中调用api转化时间时才会准确。
4.sql_query_post和sql_query_post_index是有区别的。前者是当Sphinx从数据库中得到数据后,立刻就会运行,而后者只有当索引真正成功建立后才会运行,这个区别还是很重要的。对于真正严格的程序,不应该在sql_query_pre和sql_query_post中更新增量时间,而应该在sql_query_post_index中更新增量时间。还有一个区别是sql_query_post和sql_query_post_index是存在与两个不同的tcp连接中,因为Sphinx从数据库中得到数据后去建索引,将会花费很长时间,所以它会将数据库连接关闭,等到索引建好之后,再去连接数据库,所以sql_query_post_index会在另一个连接中
5.对于可以使用id来做增量索引的数据,需要将这次增量的最大id保存到数据库中。一个很诱人的做法是,将这次增量的最大id保存在一个值中,然后在sql_query_post_index中将这个值保存到数据库中,这其实是不对的。因为上一条中说过,sql_query_post_index会在另一个连接中,所以之前连接中的值在这个连接中失效了。一个做法是将增量的最大id保存到数据库中一个tmp字段中,等到索引建成功后,在sql_query_post_index中,将这个最大id从数据库中读出,写到用于做增量索引的字段中。
6.事实上,比较难的一点是在于数据有更新的情况下,如何处理。当数据有更新时,在主索引中原来的数据将会失效,但是搜索时还是会搜到它。一个解决的办法是将原来的数据标示为删除,这就需要一个标示字段了。这个办法是我组长想出来的,在sql_query中就给它添加一个字段,标示未删除的。每次增量索引结束后,就通过每条记录的id(在Sphinx中,会给每条记录一个id),将主索引中相应的记录标示为删除。在搜索时,只需要搜索出标示为未删除的就可以了。对于官方文档中,我还没有看到如何解决这个问题的介绍。
7.如果能写一个程序来自动生成配置文件,那就再好不过了。上次我是手动输的,既容易出错,有耗眼力和精力。
整个过程最重要的还是将分布在多个表中的数据合并为一个表以及处理更新这两步上,如果能解决这两个问题,一个可用的全文搜索就完成了。暂时先写到这里,等以后有了新的体会再补充。
看来是我错了,文档中有说到数据更新这个问题,是用Klist,具体可以看文档。看Sphinx的源码很不舒服,因为可恶的匈牙利命名。