关于亚洲语言的的切分词问题(Word Segment)
对于中文来说,全文索引首先还要解决一个语言分析的问题,对于英文来说,语句中单词之间是天然通过空格分开的,但亚洲语言的中日韩文语句中的字是一个字挨一个,所有,首先要把语句中按“词”进行索引的话,这个词如何切分出来就是一个很大的问题。
首先,肯定不能用单个字符作(si-gram)为索引单元,否则查“上海”时,不能让含有“海上”也匹配。
但一句话:“北京天坛”,计算机如何按照中文的语言习惯进行切分呢?
“北京 天坛” 还是“北 京 天坛”?让计算机能够按照语言习惯进行切分,往往需要机器有一个比较丰富的词库才能够比较准确的识别出语句中的单词。
另外一个解决的办法是采用自动切分算法:将单词按照2元语法(bigram)方式切分出来,比如:
"北京天坛" ==> "北京 京天 天坛"。
这样,在查询的时候,无论是查询"北京" 还是查询"天坛",将查询词组按同样的规则进行切分:"北京","天安安门",多个关键词之间按与"and"的关系组合,同样能够正确地映射到相应的索引中。这种方式对于其他亚洲语言:韩文,日文都是通用的。
基于自动切分的最大优点是没有词表维护成本,实现简单,缺点是索引效率低,但对于中小型应用来说,基于2元语法的切分还是够用的。基于2元切分后的索引一般大小和源文件差不多,而对于英文,索引文件一般只有原文件的30%-40%不同,
自动切分 | 词表切分 | |
实现 | 实现非常简单 | 实现复杂 |
查询 | 增加了查询分析的复杂程度, | 适于实现比较复杂的查询语法规则 |
存储效率 | 索引冗余大,索引几乎和原文一样大 | 索引效率高,为原文大小的30%左右 |
维护成本 | 无词表维护成本 | 词表维护成本非常高:中日韩等语言需要分别维护。 |
适用领域 | 嵌入式系统:运行环境资源有限 | 对查询和存储效率要求高的专业搜索引擎 |
目前比较大的搜索引擎的语言分析算法一般是基于以上2个机制的结合。关于中文的语言分析算法,大家可以在Google查关键词"wordsegment search"能找到更多相关的资料。
下载:http://jakarta.apache.org/lucene/
注意:Lucene中的一些比较复杂的词法分析是用JavaCC生成的(JavaCC:JavaCompilerCompiler,纯Java的词法分析生成器),所以如果从源代码编译或需要修改其中的QueryParser、定制自己的词法分析器,还需要从https://javacc.dev.java.net/下载javacc。
lucene的组成结构:对于外部应用来说索引模块(index)和检索模块(search)是主要的外部应用入口
org.apache.Lucene.search/ | 搜索入口 |
org.apache.Lucene.index/ | 索引入口 |
org.apache.Lucene.analysis/ | 语言分析器 |
org.apache.Lucene.queryParser/ | 查询分析器 |
org.apache.Lucene.document/ | 存储结构 |
org.apache.Lucene.store/ | 底层IO/存储结构 |
org.apache.Lucene.util/ | 一些公用的数据结构 |
简单的例子演示一下Lucene的使用方法:
索引过程:从命令行读取文件名(多个),将文件分路径(path字段)和内容(body字段)2个字段进行存储,并对内容进行全文索引:索引的单位是Document对象,每个Document对象包含多个字段Field对象,针对不同的字段属性和数据输出的需求,对字段还可以选择不同的索引/存储字段规则,列表如下:
方法 | 切词 | 索引 | 存储 | 用途 |
Field.Text(String name, String value) | Yes | Yes | Yes | 切分词索引并存储,比如:标题,内容字段 |
Field.Text(String name, Reader value) | Yes | Yes | No | 切分词索引不存储,比如:META信息, |
Field.Keyword(String name, String value) | No | Yes | Yes | 不切分索引并存储,比如:日期字段 |
Field.UnIndexed(String name, String value) | No | No | Yes | 不索引,只存储,比如:文件路径 |
Field.UnStored(String name, String value) | Yes | Yes | No | 只全文索引,不存储 |
public class IndexFiles {
//使用方法:: IndexFiles [索引输出目录] [索引的文件列表] ...
public static void main(String[] args) throws Exception {
String indexPath = args[0];
IndexWriter writer;
//用指定的语言分析器构造一个新的写索引器(第3个参数表示是否为追加索引)
writer = new IndexWriter(indexPath, new SimpleAnalyzer(), false);
for (int i=1; i<args.length; i++) {
System.out.println("Indexing file " + args[i]);
InputStream is = new FileInputStream(args[i]);
//构造包含2个字段Field的Document对象
//一个是路径path字段,不索引,只存储
//一个是内容body字段,进行全文索引,并存储
Document doc = new Document();
doc.add(Field.UnIndexed("path", args[i]));
doc.add(Field.Text("body", (Reader) new InputStreamReader(is)));
//将文档写入索引
writer.addDocument(doc);
is.close();
};
//关闭写索引器
writer.close();
}
}
索引过程中可以看到:
· 语言分析器提供了抽象的接口,因此语言分析(Analyser)是可以定制的,虽然lucene缺省提供了2个比较通用的分析器SimpleAnalyser和StandardAnalyser,这2个分析器缺省都不支持中文,所以要加入对中文语言的切分规则,需要修改这2个分析器。
· Lucene并没有规定数据源的格式,而只提供了一个通用的结构(Document对象)来接受索引的输入,因此输入的数据源可以是:数据库,WORD文档,PDF文档,HTML文档……只要能够设计相应的解析转换器将数据源构造成成Docuement对象即可进行索引。
· 对于大批量的数据索引,还可以通过调整IndexerWrite的文件合并频率属性(mergeFactor)来提高批量索引的效率。
检索过程和结果显示:
搜索结果返回的是Hits对象,可以通过它再访问Document==>Field中的内容。
假设根据body字段进行全文检索,可以将查询结果的path字段和相应查询的匹配度(score)打印出来,
public class Search {
public static void main(String[] args) throws Exception {
String indexPath = args[0], queryString = args[1];
//指向索引目录的搜索器
Searcher searcher = new IndexSearcher(indexPath);
//查询解析器:使用和索引同样的语言分析器
Query query = QueryParser.parse(queryString, "body",
new SimpleAnalyzer());
//搜索结果使用Hits存储
Hits hits = searcher.search(query);
//通过hits可以访问到相应字段的数据和查询的匹配度
for (int i=0; i<hits.length(); i++) {
System.out.println(hits.doc(i).get("path") + "; Score: " +
hits.score(i));
};
}
}
在整个检索过程中,语言分析器,查询分析器,甚至搜索器(Searcher)都是提供了抽象的接口,可以根据需要进行定制。