邓世龙的自留地

兼济天下则达 独善其身则穷

分类目录归档: 搜索引擎

那些浮躁的年轻人


四个月前,遇到了Sphinx-for-Chinese的分词问题,也就是分词的粒度问题。在使用Sphinx-for-Chinese时,结果中有秦始皇兵马俑博物馆,搜索兵马俑时搜不到它,搜索兵马俑博物馆时才能搜到它。这只因为词典里有兵马俑博物馆这个词,而Sphinx-for-Chinese使用的是mmseg分词算法,所以使用最优分词策略时,将秦始皇兵马俑博物馆分成秦始皇+兵马俑博物馆。当时只是将词典中类似兵马俑博物馆这些词删除,也解决了这一类问题,但显然还是存在问题的。最近产品那边又提出一些问题,如搜索西海,海岸时搜不到西海岸,搜索肯尼亚时,搜索不到肯尼亚山。于是这个分词问题又提上了日程。

对于这些问题,再次使用去除词典中的词显得不太合适。于是只好采用别的办法,考虑使用近义词的办法,可是还是不合适,因为如果将西海和西海岸设置为同义词,那么搜索西海岸时,也能搜索关于西海的东西,这显然不是我想要的。看来只有改源码了,可是看到Sphinx-for-Chinese那些代码,没有信心做到。于是只好求助网络,可是这么专业的问题,在网页上是找不到的。于是求助Sphinx-for-Chinese,也就在这时,我才发现许多时候还是只能靠自己,许多时候程序员是很浮躁的。

在群里问了一些人,有个人说全切分,可是问他如何做,他就不理你了;有人说思路有很多,近义词就是其中一种,可是和他说了近义词的弊端后,他也不说话了;有人说在配置文件里加个选项即可,想来使用Sphinx-for-Chinese也有些时间,怎么就不知道有一个支持细粒度分词的选项呢?了解了之后,才知道CoreSeek才有这个选项,再细问后,他就说自己很忙,问其他人去。想想自己有问题都是看文档,除非遇到解决不了的问题,才会求助于他人。也知道如果有一些例子,会起到很大的帮助,所以才会写了几篇关于Sphinx-for-Chinese的文章。可是却被这帮人当成不看文档的人,真是有些无辜。最后只好求助于Sphinx-for-Chinese的开发人员,从他口中得知,要想解决这个问题,唯一的办法就是修改源码,这和自己预计的是一样的。

或许自己也变得浮躁了,明知道只有改源码,还是去麻烦其他人,企图有更简便的办法。记得当初遇到SCWS时就不是这样,那时遇到分词问题,愣是使用printf输出找到修改的办法。可是现在,遇到问题时想的却是有没哟更简便的办法。幸好组长没有变浮躁,在他的参与下,很快找到了问题的症结,并给出了一个最初的原型。正是这个原型,点燃了星星之火,带领组长和我走向解决问题的道路,虽然目前看来,还不是最优的解决方案,但至少解决了这个问题。

自己也应该沉下心来,不要再那么浮躁了。

追踪query too complex not enough stack错误


很早之前,在使用Sphinx搭建搜索服务时,遇到这个问题,到Sphinx for Chinese的群里请教,没有得到满意的答案,于是将sql_query_info 这个选项注释掉,就没有报错了。今天正好有时间,于是着手找到这个问题的症结,也算是为Sphinx做点贡献。

打开源代码,才发现用的是匈牙利命名法,看得不爽。也许因为没有Lucene那么出名,只有两个人在维护这个项目,代码里到处充斥这Fix Me,还好结构还算精良,要不然真不知道从和看起。本来想用GDB调试的,还不太熟练,于是就只好用最原始的printf输出。经过缩小范围,找到了一些蛛丝马迹, 在search.cpp中 的第331附近,主要的查询工作就在这里完成的,跳转过去之后

 if ( !pIndex->MultiQuery ( &tQuery, pResult, 1, &pTop, NULL ) )

锁定了到下面这个函数 在sphinx.cpp中 17301 if ( !sphCheckQueryHeight ( tParsed.m_pRoot, pResult->m_sError ) ) 继续跳转,到了下面这行 在sphinx.cpp中 16404 int64_t iQueryStack = sphGetStackUsed() + iHeight*SPH_EXTNODE_STACK_SIZE;输出之后,发现问题出在sphGetStackUsed这个函数里 在sphinxstd.cpp 中 1218行 int64_t sphGetStackUsed()

继续跳转,sphinxstd.cpp 中 1221行

BYTE cStack;
BYTE * pStackTop = (BYTE*)sphMyStack();

线程栈的使用大小就是上面两个值的差,继续查找 在sphinxstd.cpp return sphThreadGet ( g_tMyThreadStack );

这里用到了线程私有数据,看到私有数据的设置还是很正常,所以依然不知道哪里出了问题。于是索性将int64_t iQueryStack = sphGetStackUsed() + iHeight*SPH_EXTNODE_STACK_SIZE;这行改成int64_t iQueryStack = iHeight*SPH_EXTNODE_STACK_SIZE;这样sql_query_info就可以使用了,也不会再报query too complex not enough stack错误。

可是这个自己查询得到的中文显示出来都是乱码,我认为是没有设置SET NAMES utf8的原因,但又无法在sql_query_info这里添加这句。虽然在sql_query_pre = SET NAMES utf8已经设置了,但是因为不是同一个查询连接,所以无效。

所以最终我得到解决这个错误的结论,那就是注释掉sql_query_info这个选项。最坑人的是,官方的示例中是开启这个选项的。

Sphinx使用一元分词


之前说过用Sphinx给同事搭建搜索服务,可是他提了一个要求,也就是文本中有牛皮癣这个词,搜牛皮时也要能搜到牛皮癣,这个要求在经过分词后是不可以完成的。于是只好去寻求一元分词和二元分词的办法。 在http://lutaf.com/157.htm这里看到,“ sphinx只要把min_word_len设置为1,并配置charset_table,默认就是单字切分 ”于是试着配置,结果不行。于是只好看文档,在文档中找到,默认情况下,Sphinx已经支持一元分词。 只需设置 charset_type = utf-8 , ngram_len = 1, ngram_chars = U+3000..U+2FA1F 这样,再次搜牛皮时,就可以搜到牛皮癣了。

用Sphinx提供的klist处理文章更新和删除


在前面的介绍中,都没有处理更新和删除问题,这里有必要说说。在关于sphinx引擎的一些想法中说过公司所用的引擎中,处理更新和删除的办法是在索引中增加一个属性来标志这条记录是否失效,每次做增量时,就要去主索引和增量索引中更改相应id的属性值,这确实可以解决问题。不过并不是一个很好的解决办法,Sphinx的作者也说过这种方法既麻烦又容易出错。既然有更新和删除这个需求,必然会提供解决的办法,这个办法就是kilst。所谓的klist,就是kill list,按照字面理解,就是删除列表。我们只需要在增量索引中保存一个id列表,搜索时,如果在主索引中搜到相关文档,而文档的id存在于增量索引的id列表中,则这个文档将被丢弃。

这里有一个需要注意的是,当文章被删除时,仅仅通过增量抓取,在增量索引中并不能知道主索引中哪一个文档被删除了,所以这就必须在表中文档被删除时,能够记录下被删除的id,这就需要用到触发器,也需要建立一个辅助表来保存这些id。辅助表的建立如下:

create table sphinxklist(
        id integer not null,
        ts timestamp not null
);

触发器的建立如下:

DELIMITER //
CREATE TRIGGER sphinx_kill
AFTER DELETE ON wp_posts
FOR EACH ROW
BEGIN
        INSERT INTO sphinxklist VALUES (OLD.ID, NOW());
END
//

有了这些准备工作后,我们就可以使用klist了,事实上在之前的配置文件的基础上,只需要修改一点点内容就好了。首先修改主索引

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_query_post_index = DELETE FROM sphinxklist WHERE ts < (SELECT main_maxts FROM sphinx_helper WHERE appid='blog_search');
        sql_attr_timestamp = post_modified
        sql_field_string = post_title
}

可以看到,相对于之前的配置,这里只添加了一行 sql_query_post_index = DELETE FROM sphinxklist WHERE ts < (SELECT main_maxts FROM sphinx_helper WHERE appid='blog_search'); 添加这行是为了防止之前运行引擎时留下的id再次被使用。 之后修改临时索引:

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 &gt;= (SELECT main_maxts FROM sphinx_helper WHERE appid='blog_search')\
                AND post_modified &lt; @maxtsdelta;
        sql_query_killlist = SELECT ID FROM wp_posts WHERE post_modified &gt;= (SELECT main_maxts FROM sphinx_helper WHERE \
                appid='blog_search') AND post_modified &lt; @maxtsdelta UNION SELECT id FROM sphinxklist;
        sql_query_post_index = UPDATE sphinx_helper SET delta_maxts=delta_tmp_maxts WHERE appid='blog_search';
}

也只是添加了一行,也就是将这次抓取的id与sphinxlist中的id合并。 之后还需要修改Shell脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash
baseDir=/home/long/sphinxforchinese/blog_search
conf=$baseDir/etc/blog_search.conf
binDir=$baseDir/bin
cd $binDir
while [ true ]
do
        #./indexer -c $conf --merge-klists --rotate --merge delta deltaTemp
        ./indexer -c $conf  --merge-klists --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

这个脚本相对于原来的只增加了--merge-klists这个参数,这个参数的意义是,将delta_temp合并到delta时,并不会删除delta的klist,而是将delta_temp的klist和delta的klist合并,这正是我们想要的。经过这样的变化,一个可以处理更新和删除的main+delta索引就建好了。

感谢Sphinx团队,感谢Sphinx-for-chinese团队,给我们提供了一个这么好用的开源引擎。

用Sphinx建立main+delta索引(下篇)


上篇中,我们介绍了一种建立主索引和增量索引的方法,这种方法有一种不足之处就是会改变主索引,因为每次增量索引都会与主索引合并成新的主索引。为此,我们可以想出另一种解决的办法,每次只改变增量索引,这就需要另外再建立一个临时索引。

这里只需要改变少量地方,一个是增量索引,另外还需新增一个临时索引,具体配置如下:

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 &gt;= (SELECT main_maxts FROM sphinx_helper WHERE appid='blog_search')\
                AND post_modified &lt; @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

事实上,改变的内容还是很少的。经过这样的改变,我们就无需再改变主索引了。第一次建立主索引后,就一直保持不变,变化的是增量索引。

用Sphinx建立main+delta索引(上篇)


虽然只建立主索引就可以满足许多应用,但当数据非常多时,每次都重建索引是一件非常耗时的事情,而且每次重建都会浪费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 &lt; (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 &gt;= (SELECT main_maxts FROM sphinx_helper WHERE appid='blog_search')\
                AND post_modified &lt; @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建立只有主索引的引擎


因为一直都对Wordpress自带的搜索功能略有微词,可是又不想去改它,想想自己的博客一天都没有一个人会访问,更不用说这个搜索功能了。因为现在学习使用Sphinx-for-chinese,拿博客的数据来练练手。

先从最简单的情况开始,以后再一步一步的完善功能,这样才符合学习的线路,从易到难,而不是一开始就给你一个很完善的模型,然后改改路径就好了。最简单的情况就是只有一个主索引,然后隔一段时间重建索引。得益于Sphinx的高效,建索引的速度非常快,在文档中说达到了10M/s, 按照一篇文章为4KB计算,一秒钟可以给250篇文章建索引了,对于博客来说,已经足够了。对于其它的应用,当数据不多时,只有一个主索引也是可以的。

这里只使用了wp_posts表中的数据,只是用了ID, post_title, post_content, post_modified四个字段,所以非常的简单,直接上配置文件

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 = \
                SELECT ID, post_title, post_content, UNIX_TIMESTAMP(post_modified) AS post_modified FROM wp_posts WHERE \
                        post_status='publish' AND post_modified &lt; NOW();
        sql_attr_timestamp = post_modified
        sql_field_string = post_title

}
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
}
indexer {
        mem_limit = 32M
}

searchd {
        listen = 9300
        log = /home/long/sphinxforchinese/blog_search/var/log/searchd.log
        query_log = /home/long/sphinxforchinese/var/log/query.log
        read_timeout = 5
        max_children = 30
        pid_file = /home/long/sphinxforchinese/var/log/searchd.pid
        max_matches = 1000
        seamless_rotate = 1
        preopen_indexes = 1
        unlink_old = 1
        workers = threads
        binlog_path = /home/long/sphinxforchinese/var/data
}

相关配置选项的意义可以查看示例,写的非常的详细。这里没有对post_content进行定义,因为只想对这个字段建索引,并不想保存它的原始内容,所以这里使用了默认行为,也就是只建索引。

建好索引,搜索跑步的相关文章,得到如下结果

  1. document=41, weight=2661, post_title=跑步一周年, post_modified=Sun Apr  7 10:11:56 2013
  2. document=286, weight=2660, post_title=跑步两周年, post_modified=Fri Jan  4 12:49:47 2013
  3. document=537, weight=1642, post_title=写在广州马拉松之前, post_modified=Sat Nov  9 00:00:45 2013
  4. document=39, weight=1632, post_title=看棒球英豪漫画, post_modified=Sun Apr  7 09:57:34 2013
  5. document=2, weight=1626, post_title=关于我, post_modified=Fri Jun 14 19:49:08 2013
  6. document=565, weight=1626, post_title=2013广州马拉松纪实, post_modified=Sun Nov 24 22:10:57 2013
  7. document=43, weight=1617, post_title=三个月来的小结, post_modified=Sun Apr  7 10:10:22 2013
  8. document=56, weight=1617, post_title=价值博客们, post_modified=Sun Apr  7 09:52:51 2013
  9. document=205, weight=1617, post_title=2012扬州马拉松纪实, post_modified=Tue Apr  2 11:29:04 2013
  10. document=5, weight=1602, post_title=2011年的阅读, post_modified=Tue May 29 11:19:49 2012
  11. document=305, weight=1602, post_title=羽毛球心结, post_modified=Mon Apr  8 08:33:37 2013
  12. document=40, weight=1574, post_title=通关manufactoria, post_modified=Sun Apr  7 10:01:06 2013
  13. document=233, weight=1574, post_title=当了一回胃扩张, post_modified=Fri Jul 20 15:46:35 2012

搜索结果还行吧。

搭建Sphinx-for-chinese引擎时遇到的问题


关于sphinx引擎的一些想法说过用Sphinx给同事搭引擎,可是那是建立在之前的配置文件之上,我只要依葫芦画瓢,改一改路径以及查询语句就搞定了,实质上没学到什么东西。在我看来,要想真正了解它,还是得重新造轮子,从头到尾自己搭一遍,在这个过程中出现了许多奇怪的错误,在这里记录一下。

1.checking for clock_gettime in -lrt... 这是我遇到的第一个问题,事实证明,这根本不是问题。到Sphinx-for-chinese下载了编译包,开始编译,之后就卡在了这里。刚开始以为是缺少librt,然而我在lib中找到了这个链接库。将编译包放在其它机器上编译,又是可以通过的,百思不得其解。只好到Sphinx-fro-chinese的QQ群里发问,黑猫给出解答是要将librt所在路径加入到etc/ld.so.conf,并运行ldconfig命令。按照他的办法,结果运行ldconfig命令时卡住了,于是可以断定是机器的问题。

2.ERROR: cannot find MySQL include files.

这个问题比较好解决,就是缺少MySQL的库文件。因为虚拟机装的是Ubuntu,只要运行以下命令就好了。

sudo apt-get install libmysql++-dev libmysqlclient15-dev checkinstall

如果是其它系统,相信也是类似的方法。如果已经有库文件了,则只需要将路径加入到/etc/ld.so.conf中,并执行ldconfig命令

3.index 'test1': search error: query too complex, not enough stack (thread_stack=1217498K or higher required).

这也是一个很奇怪的错误。我是按照文档中给出的例子建好索引,之后用命令行工具,也就是search要搜索的,结果就出现了这个错误。在网上搜索这个错误,没找到有用的信息,于是又求助于Sphinx-for-chinese群,群里的人说是因为命令行存在问题,用客户端搜就没问题。于是用客户端搜果然没问题,可是我还是无法释怀,因为之前公司的引擎中,用命令行是没有问题的。于是对照着公司用的引擎中的配置文件,发现配置文件中没有这一行,在自己的配置文件中注释掉这行后,果然没问题了。

所以对于这个错误的解决办法就是,将sql_query_info = SELECT * FROM documents WHERE id=$id这行注释掉.

这个确实太坑人了,连官方的配置文件都会出错,得浪费多少人的时间。

4.ERROR: index 'main': No fields in schema - will not index.

光运行例子是不行的,还是得自己写一些东西,于是将自己的博客文章来搜索。用了Wordpress中wp_posts表中的数据,我只用的四个字段ID,post_title,post_content,post_modified,将post_title,post_content定义成sql_attr_string,sql_attr_timestamp,结果就出现了这个错误。在网上找了,发现在官方bug报告中有提到这个问题

http://sphinxsearch.com/bugs/view.php?id=1632

管理员说,引擎中需要一个全文索引字段,否则就没有东西需要索引了,这样它就不会建索引。管理员建议定义为sql_field_string,这样就会对这个字段既索引又保存内容。对于我的配置,我并不想保存post_content这个字段,所以不想将它定义为sql_field_string,那怎样才能让它只被所以呢?看过文档之后,才知道默认情况下,是会被索引。这也是为什么,在上面的帖子中,将sql_attr_string = text注释掉就可以建索引了。所以我只能说管理员也没有真正理解这个错误的原因,看来不能迷信权威啊。

5.FATAL: there must be 2 indexes to merge specified

这个是在测试Klist的时,出现的。文档中说,当合并两个索引时,使用--merge-klists就可以将两个索引的klist合并,于是我在合并时加上了这个参数。具体如下:

./indexer -c $conf --rotate --merge --merge-klists delta deltaTemp

运行时就出现这个错误,我纳闷了,明明官方文档中说加入这个参数是没问题的。到网上找资料,有人是用--merge-killlists这个参数,试过之后,同样报这个错误。无奈之际,将--merge-klist参数放到--rotate前面,

./indexer -c $conf --merge-klists --rotate --merge delta deltaTemp

奇迹出现了,这次没有报错。我只能说,这真是个坑。

《Introduction to Search with Sphinx》写的还是非常不错的,毕竟是Sphinx的作者,表达能力和写作能力自然非同凡响,关于Sphinx的知识,许多都来自本书。等有时间了,可以将引擎的搭建过程写一写,应该可以帮助一些人。这次搭建过程,我学到了许多,虽然用的是开源的引擎,但真要从头到尾搭建一个引擎,并提供可靠的服务,并不是那么容易的,还是得多实践才行。

关于Sphinx引擎的一些想法


最近一个星期都在看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的源码很不舒服,因为可恶的匈牙利命名。