分析器
# 30.分析器
接下来我们讲一个我们略过的重要的对象:分析器
# 什么是分析器
作用:即分析文档,去除停用词和标点符号等。默认使用的是标准分析器 StandardAnalyzer。
我们之前构造索引的时候,使用的是默认配置 IndexWriterConfig:
IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig());
查看 IndexWriterConfig类
的源码,构造方法是这样的:默认使用 StandardAnalyzer
public IndexWriterConfig() {
this(new StandardAnalyzer());
}
2
3
标准分析器,对英文的原始文档是没有问题的,怎么看有没问题呢?首先我们得会查看分析器的分析效果:
- 首先 StandardAnalyzer 继承了 Analyzer,抽象类。所有分析器的类都继承了它
- 使用 Analyzer 对象的 tokenStream 方法返回一个 Tokenstream 对象。该对象中包含了最终分词结果
# 使用分析器
所以我们想查看分析效果,直接使用并查看即可。实现步骤:
- 创建一个 Analyzer 对象,这里我们使用 StandardAnalyzer 对象
- 使用分析器对象的 tokenStream 方法获得一个 TokenStream 对象
- 向 TokenStream 对象中设置一个引用,相当于是一个指针。TokenStream 对象包含了所有关键词,类似链表一样排列着,我们用一个指针指向关键词,访问指针就相当于访问关键词
- 调用 TokenStream 对象的 rest 方法,用处是将指针指向第一个数据(如果不调用会抛异常)
- 使用 while 循环遍历 TokenStream 对象
- 关闭 TokenStream
代码如下:这里我们直接指定一段文本给分析器分析。
@Test
public void testTokenSteam() throws Exception {
// 1. 创建一个Analyzer对象,这里我们使用StandardAnalyzer对象
Analyzer analyzer = new StandardAnalyzer();
// 2. 使用分析器对象的tokenStream方法获得一个TokenStream对象
TokenStream tokenStream = analyzer.tokenStream("", "The Spring Framework provides a comprehensive programming and configuration model.");
// 3. 向TokenStream对象中设置一个引用,相当于是一个指针。TokenStream对象包含了所有关键词,类似链表一样排列着,我们用一个指针指向关键词,访问指针就相当于访问关键词
CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
// 4. 调用TokenStream对象的rest方法,用处是将指针指向第一个数据(如果不调用会抛异常)
tokenStream.reset();
// 5. 使用while循环遍历TokenStream对象
while (tokenStream.incrementToken()) { //返回true说明有下一个元素
System.out.println(charTermAttribute.toString());
}
// 6. 关闭TokenStream
tokenStream.close();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
analyzer.tokenStream()
方法的第一个参数是 Field,由于我们是直接给定文本,这里就留空。运行结果:可以看到去除了 the,a 等单词和标点符号
spring
framework
provides
comprehensive
programming
configuration
model
2
3
4
5
6
7
# 标准分析器和中文
使用标准分析器,分析英文是没什么问题的;那如果是中文呢?我们可以测试下:
TokenStream tokenStream = analyzer.tokenStream("name", "鲁镇的酒店的格局,是和别处不同的:都是当街一个曲尺形的大柜台,柜里面预备着热水,可以随时温酒。做工的人,傍午傍晚散了工,每每花四文铜钱,买一碗酒,——这是二十多年前的事,现在每碗要涨到十文,——靠柜外站着,热热的喝了休息;倘肯多花一文,便可以买一碟盐煮笋,或者茴香豆,做下酒物了,如果出到十几文,那就能买一样荤菜,但这些顾客,多是短衣帮⑴,大抵没有这样阔绰⑵。只有穿长衫的,才踱进店面隔壁的房子里,要酒要菜,慢慢地坐喝。");
运行结果:一个汉字一行,说明一个关键字就是一行,这肯定是不行的。搜索一个汉字可以搜索得到,但搜索词语的话肯定是不能成功的。
我们可以验证下,比如我们的原始文档“全文检索.txt”中,有“全文”这个词语,试试能不能搜索成功。我们修改 searchIndex
方法中的关键词:
Query query = new TermQuery(new Term("content", "全文"));
运行结果:这是因为一个汉字一个关键字
查询出来的总记录数:0
所以标准分析器,在分词中文的时候,是不准确的,我们搜索一般都是输入关键词来搜索。为此我们使用中文分析器。
# 中文分析器 IKAnalyzer
虽然 Lucene 自带了一个中文分析器 SmartChineseAnalyzer,对中文支持较好,但扩展性差,扩展词库,禁用词库和同义词库等不好处理。
IK-Analyzer 不是官方提供的,而是第三方提供的。我们配置 3 个文件:
- IKAnalyzer.cfg.xml:配置文件
- hotword.dic:扩展词典,里面就是一些中文词语。可以添加一些新词,例如新出现的网络新词,新出现的公司名等
- stopword.dic:停用词词典,无意义的词,或者敏感词
配置文件可以去我的 Gitee (opens new window) 或 GitHub (opens new window) 上下载。
使用方法:
添加依赖
<dependency> <groupId>com.jianggujin</groupId> <artifactId>IKAnalyzer-lucene</artifactId> <version>7.0.0</version> </dependency>
1
2
3
4
5添加配置文件、扩展词典和停用词词典,到 resources 目录下
注意:hotword.dic 和 ext_stopword.dic 文件的格式为 UTF-8,注意是无 BOM 的 UTF-8 编码,否则报错。也就是说禁止使用 windows 记事本编辑扩展词典文件,该记事本默认是会保存为 UTF-8 BOM
IKAnalyzer.cfg.xml :配置文件,可以配置多个词典(用分号分割)
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <comment>IK Analyzer 扩展配置</comment> <!--用户可以在这里配置自己的扩展字典 --> <entry key="ext_dict">hotword.dic;</entry> <!--用户可以在这里配置自己的扩展停止词字典--> <entry key="ext_stopwords">stopword.dic;</entry> </properties>
1
2
3
4
5
6
7
8
9
10
11
测试:关键是第 4 行,创建的是 IKAnalyzer
@Test
public void testIKAnalyzer() throws Exception{
// 1. 创建一个Analyzer对象,这里我们使用StandardAnalyzer对象
Analyzer analyzer = new IKAnalyzer();
// 2. 使用分析器对象的tokenStream方法获得一个TokenStream对象
TokenStream tokenStream = analyzer.tokenStream("", "鲁镇的酒店的格局,是和别处不同的:都是当街一个曲尺形的大柜台,柜里面预备着热水,可以随时温酒。做工的人,傍午傍晚散了工,每每花四文铜钱,买一碗酒,——这是二十多年前的事,现在每碗要涨到十文,——靠柜外站着,热热的喝了休息;倘肯多花一文,便可以买一碟盐煮笋,或者茴香豆,做下酒物了,如果出到十几文,那就能买一样荤菜,但这些顾客,多是短衣帮⑴,大抵没有这样阔绰⑵。只有穿长衫的,才踱进店面隔壁的房子里,要酒要菜,慢慢地坐喝。");
// 3. 向TokenStream对象中设置一个引用,相当于是一个指针。TokenStream对象包含了所有关键词,类似链表一样排列着,我们用一个指针指向关键词,访问指针就相当于访问关键词
CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
// 4. 调用TokenStream对象的rest方法,用处是将指针指向第一个数据(如果不调用会抛异常)
tokenStream.reset();
// 5. 使用while循环遍历TokenStream对象
while (tokenStream.incrementToken()) { //返回true说明有下一个元素
System.out.println(charTermAttribute.toString());
}
// 6. 关闭TokenStream
tokenStream.close();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
部分运行结果:可以看到是有一些词语的,并且显示加载了两个词典。
IKAnalyzer 是一个开源的,基于 java 语言开发的轻量级的中文分词工具包,由国人 林良益 (opens new window) 先生开发,从 2006 年 12 月推出 1.0 版开始。由于林良益先生在 2012 之后未对 IKAnalyzer 进行更新,后续版本的 Lucene 分词接口发生变化,导致不能在 Lucene 中使用了。
因此我们用的是这个分析器:IKAnalyzer-lucene (opens new window),是基于 IKAnalyzer 做了兼容性处理的。
# 自定义词典
有时候我们需要添加一些流行语,此时词典是没有的,那么分词就会失败。例如:
我们加上了一个流行语“鸡你太美”,但是分词后将其分成了 3 部分“鸡”,“你”,“太美”,我们可以在 hotword.dic 中添加这个流行词,然后就能看到正常分词了:
停用词同理,这里就不演示了。
# 使用中文分析器创建索引
之前我们说过,创建索引时默认使用的是标准分析器,我们可以自定义使用什么分析器,只需在构造函数里指定即可,代码如下:
@Test
public void createIndexWithIK() throws Exception {
// 1. 创建一个Director对象,指定索引库保存的位置。
Directory directory = FSDirectory.open(new File("D:\\temp\\index").toPath());
IndexWriterConfig config = new IndexWriterConfig(new IKAnalyzer());
// 2. 基于Directory对象创建一个IndexWriter对象(用来写索引)
IndexWriter indexWriter = new IndexWriter(directory, config);
// 3. 读取磁盘上的文件,对应每个文件创建一个文档对象。
File dir = new File("D:\\temp\\searchsource");
File[] files = dir.listFiles();
for (File file : files) {
String fileName = file.getName();
String filePath = file.getPath();
String fileContent = FileUtils.readFileToString(file, "utf-8");
long fileSize = FileUtils.sizeOf(file);
// 4. 创建Field
// 参数1:域的名称, 参数2:域的内容, 参数3:是否存储
Field fieldName = new TextField("name", fileName, Field.Store.YES);
Field fieldPath = new TextField("path", filePath, Field.Store.YES);
Field fieldContent = new TextField("content", fileContent, Field.Store.YES);
Field fieldSize = new TextField("size", String.valueOf(fileSize), Field.Store.YES);
// 4. 将field添加到document对象中。
Document document = new Document();
document.add(fieldName);
document.add(fieldPath);
document.add(fieldContent);
document.add(fieldSize);
// 5. 使用IndexWriter对象将document对象写入索引库,此过程进行索引创建,并将索引和document对象写入索引库。
indexWriter.addDocument(document);
}
// 6. 关闭IndexWriter对象。
indexWriter.close();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
我们先将之前的索引库删掉,然后运行该测试方法,并用 Luke 重新打开:可以看到有中文的词语了。而使用标准分析器,中文都是一个一个字的
# 源码
已将源码上传到 Gitee (opens new window) 或 GitHub (opens new window) 上。并且创建了分支 demo2IK,读者可以通过切换分支来查看本文的示例代码
- 01
- 中国网络防火长城简史 转载10-12
- 03
- 公告:博客近期 RSS 相关问题10-02