最近,我在开发桌面搜索软件,其中桌面搜索最核心的部分就是全文检索。之前已经完成了一个初始版本。全文检索是使用的中科院计算所郭博士的Firtex,后来有位老师说Firtex最近没人在维护,建议使用CLucene,于是我老板就提议再开发另一个版本——CLucene版的桌面搜索。
CLucene是C++版的Lucene,提供全文检索的功能。在网上找了部分资料,主要都是如何在CLucene中加入中文分词。于是我根据网上的资料和自己在开发过程中对CLucene的理解,开始向CLucene加入中文分词。听说ICTCLAS的中文分词很出名,就拿来试试。
CLucene使用的是0.9.21版,同时通过svn更新到最新的文件,下载地址是
http://sourceforge.net/projects/clucene/。
ICTCLAS中文分词使用是ICTCLAS3.0,这是共享版,但不开源,可以增加用户自定义词典,下载地址
http://www.ictclas.org/Download.html。
CLucene只支持Unicode的编码,所以对需要对输入的字符串都要进行转换。在Windows下可以使用api WideCharToMultiByte和MultiByteToWideChar。在Linux下,可以使用linux下有著名的iconv。
我的开发平台是windows,vs2005。在CLucene中加入中文分词,分三步完成。
1、把项目设置为Use Unicode Character Set。因为使用ANSI时,汉字存在与其他语言编码重叠的问题,不能准确判断是否为汉字。一般下载的CLucene默认就是设置好的。
2、 \src\CLucene\util\Misc.cpp中有个Misc::_cpycharToWide函数,这个函数是CLucene中用来将char字符串转换为wchar_t字符串的,但原代码中使用的是不考虑编码的转换,把汉字从ANSI编码转到UCS2编码会失效,所以需要使用MultiByteToWideChar进行转换。同样的在同一个文件中,有这样一个函数Misc::_cpywideToChar,是将wchar_t转换成char,同理使用WideCharToMultiByte进行转换。
3、也是最麻烦的一步。如果了解Lucene结构的开发者,应该知道Lucene进行索引前需要对文本进行分词,就是需要使用Analyzer类进行分词。在CLucene中有一个StandardAnalyzer类,它本意是可以处理亚洲语言(包括中日韩),但不完善。可以通过改写 \src\CLucene\analysis\standard\StandardTokenizer.cpp文件来达到目的。因此,在这一步有二种方法,一是上面所说的,修改StandardTokenizer,让它支持中文分词。另一个就是自己写一个ChineseTokenizer和ChineseAnalyzer,在索引的时候调用它就好了,同样查询的时候也要ChineseAnalyzer进行分析。
第一种方法:在StandardTokenizer中有一个StandardTokenizer::next()方法,用来获取下一个token。Token可以简单看成是索引文件中的一个项。在原代码中,先判断是否为ALPHA,再判断是否为_CJK,而在中文windows系统上,将一个UCS2编码的汉字(正好一个wchar_t)作为参数传给 iswalpha函数时,返回值是true。所以任何CJK字符都会被处理成ALPHA。因此,项目修改为使用Unicode编码后,ReadCJK函数 不会被调用到。所以需要将原代码中的if(_CJK)的判断分支放到if(ALPHA)前面,这样遇到CJK字符时就会调用 StandardTokenizer::ReadCJK。ReadCJK函数用于读取一个CJK的token。这个函数不能处理中文分词,它遇到CJK字符时一直向后读取,直到遇到非CJK字符或文件结束才跳出,把读取到的整个字符串作为索引中的一个项。所以需要重写这个函数。我个人认为这个方法需要修改原代码地方太多,而且代码重用率低,所以我选择了第二种方法。
第二种方法:就是自己重新写一个ChineseAnalyzer和ChineseTokenizer。这样对原代码修改少,改错也方便,最重要可以将它应用其他需要的程序中。其实这两个类也并不难写,只要根据StandardTokenizer和StandardAnalyzer的结构,模仿一下就行了。ChineseTokenizer是继承Tokenizer类的,必须实现Tokenizer类的next()虚成员函数。这个成员函数就是要输入下一个分词的字符串。所以在这里将ICTCLAS加入,每次将一个分词输出就好。因此ICTCLAS只支持char类型分词,所以还要将Unicode转换成char,分词使用ParagraphProcess函数。关键代码如下:
str=STRDUP_TtoA(ioBuffer); //将输入字符串转换成char
pResult=STRDUP_AtoT(ICTCLAS_ParagraphProcess(str,0)); //ICTCLAS中文分词函数调用
_CLDELETE_ARRAY(str);// 回收内存
pResult保存了分词后的结构,每个词是空格分隔,所以每次调用next()时,都从pResult里取下一个词,直接取完,则从Reader里读入下一段,继续分词,直接Reader已经到达尾部。代码如下
TCHAR word[LUCENE_MAX_WORD_LEN]={0}; //保存单个词的临时空间
while(pResult[end] !=_T(' ') && end <nCount) //查找空间,找到单词的结束位置
{
word[end-start]=pResult[end];
end++;
}
word[end-start]=0;
while(pResult[end]==_T(' ') && end<nCount) end++; //查找下一个词的开始位置
start=end;
token->setText(word); //输出到token
这个完成之后,ChineseTokenizer也就完成了。不过在这过程中,发现ICTCLAS分词速度并没想象中的快。不知道是不是因为这个是共享版的问题。
ChineseTokenizer完成后,接着就实现ChineseAnalyzer类。这个更加简单,只要完全模仿StandardAnalyzer就好了。ChineseAnalyzer是继承于Analyzer的,同样必须实现TokenStream* tokenStream(const TCHAR* fieldName, CL_NS(util)::Reader* reader)就可以了。这个只要生成一个ChineseTokenizer,返回就行了。代码如下:
TokenStream* ChineseAnalyzer::tokenStream(const TCHAR* fieldName, Reader* reader) {
TokenStream* ret= _CLNEW ChineseTokenizer(reader); //中文分词ChineseTokenizer
ret = _CLNEW LowerCaseFilter(ret,true); //英文大小变换,统一成小写
ret = _CLNEW StopFilter(ret,true, &stopSet); //停用词过滤,如“的”,“这”,“an”
return ret;
}
关于停用词的过滤,可以修改\src\CLucene\analysis\Analyzers.cpp里关于StopAnalyzer的定义。我个人加入了中文的停用词表
static const TCHAR* CHINESE_STOP_WORDS[];
在ChineseAnalyzer的构造函数里,将停用词表加入到映射中,先在ChineseAnalyzer里定义这个成员
CL_NS(util)::CLSetList<const TCHAR*> stopSet;用于保存停用词表,再加构造函数里加入下面这句:
StopFilter::fillStopTable( &stopSet,CL_NS(analysis)::StopAnalyzer::CHINESE_STOP_WORDS);
这样就完成了ChineseAnalyzer类。
接着就可以在索引里使用这个ChineseAnalyzer。
索引时需要生成ChineseAnalyzer的一个实例,将它传给IndexWriter。
查询时也是一样的道理,只不过是将它传给QueryParser。
这样就完成了CLucene加入ICTCLAS中文分词,但是发现ICTCLAS的分词速度不行,我只使用65K的文件就需要至少10秒的时间才完成。我正在寻找能替换ICTCLAS的中文分词。
CLucene加入ICTCLAS中文分词
最新推荐文章于 2018-10-15 00:21:37 发布