2.信息检索
信息检索是计算机世界中非常重要的一种功能。信息检索不仅仅是指从数据库检索数据,还包括从文件、网页、邮件、用户手输入的内容中检索数据。通过怎样的高效方式将用户想要的信息快速提取出来,是计算机技术人员研究的重点方向之一。
2.1.数据分类
我们生活中的数据总体分为两种:结构化数据和非结构化数据。
结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。
非结构化数据:指不定长或无固定格式的数据,如邮件,word文档等磁盘上的文件
2.2.结构化数据搜索方法
数据库就是最常见的结构化数据。通过SQL可以非常方便的查询数据。
为什么数据库中的数据能非常方便的搜索出来?
因为数据库中的数据存储在表中,表有行有列有类型有长度,因此才可以通过非常方便的SQL查询结果。也就是说结构化的数据有规律,所以才好进行查找。
试想一下如果数据没有进行结构化,没有任何规律该如何查询?
2.3.非结构化数据查询方法
我们考虑一个小时候学查字典的场景:小时候我们都使用过新华字典,老师叫你翻开第268页从268页到269页,找到“坑爹”的坑,此时你会怎么查找?——毫无疑问,你的眼睛会从268页的第一个字开始从头至尾地扫描,直到找到“坑”字为止。这种按照内容的顺序一个一个字符的查找方法叫做顺序扫描法(Serial Scanning)。对于少量的数据,使用顺序扫描是够用的。
但是如果老师不告诉你你坑爹的“坑”字在哪一页呢?也没有教你如何查字典呢?
你只能从第一页的第一个字逐个的扫描下去,那样你真的是被坑了。查找的过程会相当的慢,甚至会让你崩溃,所以这种坑爹的事情我们不能去做。我们要重新思考此时的查询办法。
思考一下新华字典是怎么解决汉字的快速查找的?
从一堆没有结构的内容中提取出来文字的位置信息(页码)、文字写法(文字本身)、汉语拼音,然后将它们重新整理、排序、归纳,最终形成一张结构化的表,我们叫做汉语拼音音节索引表。
汉语拼音索引中记录了“坑”字在哪一页的信息,只要你知道“坑”字的汉语拼音,就可以快速的查找到“坑”字在哪,这样答案就出来了。
下图是汉语拼音音节索引表:
从新华字典的例子总结一下,如何从一堆没有规律没有结构的信息中快速的查找我们需要的信息?
最有效的方法就是先将信息重新组织(提取、整理、排序、归纳),形成新的集合(即一个更方便更高效查找的集合),然后查询这个结构化的集合,从中找出你要找的信息在原文中的位置。
简单归纳成:
非结构化结构化保存结构化
查找结构化得到在非结构化中的定位
这部分从非结构化数据中提取出来,重新组织的结构化信息,我们称之索引。
这种先对全文建立索引集合,再对索引集合进行检索的查询方式就叫全文检索(Full-text Search)。
创建索引的过程会不会很繁琐费时?
是的,很繁琐费时间,但是值得的,因为索引一旦创建就可以多次使用,最终可以带来高效的查询速度,是一件一劳永逸的事情。
2.4.如何实现全文检索
Apache提供了一个开源的全文检索开发框架——Lucene。它提供了完整的查询模块和索引模块,利用这些核心模块,开发人员可以方便、快速的开发出全文检索应用。
根据上面的简单归纳可以知道我们需要使用Lucene要做两件事情:
非结构化结构化保存结构化————创建索引
查找结构化得到在非结构化中是定位————查询索引
注意:索引的数据来源不仅仅局限于数据库,一切可以采集的数据都可以被建立索引。
2.5.系统的数据查询方案
·基本的数据查询方案在面对查询量大的应用时会对数据库造成极大的压力,而且查询效率会很低。
·改进后的数据查询方案将读写进行了分离,将查询量大的应用的查询请求分发给了索引库,查询直接走索引库,不走数据库,这样就有效的降低了数据库的压力,同时索引库查询的高效特性也能够保证查询效率。
2.6.全文检索的应用场景
全文检索应用最多的就是开发站内搜索服务。尤其是对于电商系统,大数据量的搜索都是使用的站内搜索服务。
还有专业的搜索引擎中也有全文检索技术的使用,比如百度、Google等,但专业的搜索引擎不只使用这一种搜索技术。
3.Lucene实现全文检索的流程
3.1.创建索引和查询索引流程
说明:
1.绿色表示创建索引过程,包括:
采集数据构建文档对象分析文档对象创建索引(保存到索引库)
2.红色表示查询索引过程,包括:
入口提交查询请求(查询关键字)创建查询对象执行查询(从索引库搜索)渲染结果显示查询结果
3.2.索引流程
用户将想要搜索的原始数据创建索引,索引内容存储在索引库(index)中。创建索引时不会改变原始文档的任何内容,只是将有用信息的拷贝重新组织成索引。
假设有如下两个原始文档:
【students.txt】:Students should be allowed to go out with their friends, but not allowed to drink beer.
【myfriends.txt】:My friend Jerry went to school to see his students but found them drunk which is not allowed.
3.2.1.采集数据
(手动编程)
从互联网上、数据库、文件系统中等获取需要搜索的原始信息,这个过程就是信息采集,信息采集的目的是为了对原始内容进行索引。
如何采集数据?
1、互联网上的网页:可以使用工具将网页抓取到本地生成html文件。
2、数据库中的数据:可以直接连接数据库用SQL查询数据。
3、文件系统中的文件:可以通过I/O操作读取文件的内容。
在Internet上采集信息的软件通常称为爬虫或蜘蛛,也称为网络机器人,爬虫访问互联网上的每一个网页,将获取到的网页内容存储起来。
Lucene不提供信息采集的类库,需要自己编写一个爬虫程序实现信息采集,也可以通过一些开源软件实现信息采集,如下:
Nutch(http://lucene.apache.org/nutch), Nutch是apache的一个子项目,包括大规模爬虫工具,能够抓取和分辨web网站数据。
jsoup(http://jsoup.org/ ),jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。
heritrix(http://sourceforge.net/projects/archive-crawler/files/),Heritrix 是一个由 java 开发的、开源的网络爬虫,用户可以使用它来从网上抓取想要的资源。其最出色之处在于它良好的可扩展性,方便用户实现自己的抓取逻辑。
3.2.2.构建文档对象
(手动new)
采集上来的数据格式各式各样,我们需要先统一格式然后才能处理,Lucene中的统一格式就是Document文档对象。
一个Document对象类似数据库表的一条记录,可以包含多个Field域,Field域有两个属性:域名(name)和域值(value),Field就相当于表的字段。
Document结构更加灵活,不限制Field域的数量、种类和是否重复,只要是Field就可以加入Document对象。
和数据库表类似,每个文档都有一个唯一的主键——文档id(docID)。
上图是将磁盘上的一个文件采集出来的数据放入一个Document对象。Document对象中包括四个Field(file_name、file_path、file_size、file_content)
3.2.3.分析文档对象(重点)
(Lucene自动完成)
分析文档主要是对文档的Field域进行分析,目的是为了创建索引做好准备。分析的过程是将域(Field)的内容转换成最基本的索引单元——项(Term)的过程。
3.2.3.1.分词组件(Tokenizer)
分词组件(Tokenizer)会做以下几件事情(这个过程称为:Tokenize):
1. 分词器将Field域内容分成一个一个单独的单词
2. 标点符号过滤器去除内容中的标点符号
3. 停用词过滤器去除停用词(stop word)
什么是停用词?所谓停词(Stop word)就是一种语言中没有具体含义的词,因而大多数情况下不会作为搜索的关键词,这样一来创建索引时能减少索引的大小。英语中停词(Stop word)如:”the”、”a”、”this”,中文有:”的,得”等。不同语种的分词组件(Tokenizer),都有自己的停词(stop word)集合。
经过分词(Tokenize)之后得到的结果称为词汇单元(Token)。上面的两个文档的content域内容经过分析后得到以下词汇单元:
“Students”,“allowed”,“go”,“their”,“friends”,“allowed”,“drink”,“beer”,“My”,“friend”,“Jerry”,“went”,“school”,“see”,“his”,“students”,“found”,“them”,“drunk”,“allowed”
将Token传给语言处理组件。
3.2.3.2.语言处理组件(Linguistic Processor)
语言处理组件(linguistic processor)主要是对得到的词元(Token)做一些语言相关的处理。对于英语,语言处理组件(Linguistic Processor)一般做以下几点:
1. 变为小写(Lowercase)
2. 复数变单数(stemming) 如”cars”到”car”
3. 词形还原(lemmatization) ,如”drove”到”drive”
经过语言处理组件(linguistic processor)处理之后得到的结果称为词项(Term),它是创建索引的最小单元。上面的Token经过处理后得到的词项(Term)如下:
"student","allow","go","their","friend","allow","drink","beer","my","friend","jerry","go","school","see","his","student","find","them","drink","allow"。
经过语言处理后,搜索drive时原文中是drove的也能被搜索出来。对文档中的各个Field域进行逐个分析,最终形成了许多的Term词项。
综上所述,分析文档的最终产物是Term,Term是创建索引的最小单元,也是搜索索引时的最小单元。
3.2.4.创建索引
(Lucene自动完成)
3.2.4.1.创建字典表
利用得到的词项(Term)创建一个字典表,一列是Term词项,一列是文档ID(DocId)
字典表如下:
Term DocId
student 1
allow 1
go 1
their 1
friend 1
allow 1
drink 1
beer 1
my 2
friend 2
jerry 2
go 2
school 2
see 2
his 2
student 2
find 2
them 2
drink 2
allow 2
3.2.4.2.对字典表按字母顺序排序
对字典表按字母顺序排序:
排序结果如下:
Term DocId
allow 1
allow 1
allow 2
beer 1
drink 1
drink 2
find 2
friend 1
friend 2
go 1
go 2
his 2
jerry 2
my 2
school 2
see 2
student 1
student 2
their 1
them 2
3.2.4.3.合并相同词项,归纳文档倒排链表
创建好的Term词项实际是包含两部分信息:一是Term出自哪个域,二是Term的内容。合并相同的词项(Term)成为文档倒排(Posting List)链表。
●合并规则:
●在比较Term是否相同时,不考虑是否在同一个Document对象中,合并时暂时忽略它。
●不同的域(Field)中拆分出来的相同的单词是不同的Term,不能合并。
例如:文件名中包含apache和文件内容中包含的apache是不同的Term。
●同名域(Field)的相同单词是相同的Term,可以合并。
例如:两个文档中都有【文件名】Field域中都含有Java,这两个Java就是一个Term(域和单词都相同)
例子是以两个文档的【content】域作为演示的例子,因此只要单词相同就是相同的Term,就可以合并。合并结果如下:合并的同时要记录这个Term来自于哪个文档以及出现的次数。
●Document Frequency(DF):文档频次,表示多少文档出现过此词(Term)
●Frequency(TF):词频,表示某个文档中该词(Term)出现过几次
例如:对词项(Term) “allow”来讲,总共有两篇(DF)文档包含此Term,Term后面的文档链表总共有两个,第一个表示包含“allow”的第一篇文档,即DocId=1的文档,此文档中“allow”出现了2次(TF),第二个表示包含“allow”的第二个文档,即DocId=2的文档,此文档中,”allow”出现了1次(TF)。
索引表 + 文档倒排链表 + 文档对象集合, 共同组成了索引库
●索引表是保存索引词项的
●文档倒排链表是保存包含词项的文档ID的
●文档对象集合是保存文档具体内容的
3.2.5.索引流程总结
3.3.查询索引
查询索引就是搜索的过程。搜索就是用户输入关键字,从索引(index)中进行搜索的过程。根据关键字搜索索引,根据索引找到对应的文档,从而找到要搜索的内容。
3.3.1.用户
用户可以是自然人,也可以是远程调用的程序。
3.3.2.用户搜索界面
(手动编程)
搜索界面用于提交用户搜索关键字的,也相当于采集数据的作用。
比如:
注意:Lucene不提供制作用户搜索界面的功能,需要根据自己的需求开发搜索界面。
3.3.3.分析用户搜索关键字
(手动调用由Lucene自带的或第三方提供的解析器完成)
此处的分析过程跟索引流程中的分析文档对象的过程必须要一致。经过分析最终形成词项Term,只不过这个Term还缺少是属于那个Field域的部分。
分析过程对于索引流程和查询索引流程要一致的原因是,如果不一致会造成两边最终的分词结果不同,这样会什么也搜索不到的。
3.3.4.创建查询对象
(手动new或手动调用解析器生成)
给上面的Term指定Field域,在实际应用的时候,用户查询时是没有要指定Field域的地方,那我们该如何搜索呢?所有的搜索服务都存在一个默认域,默认域是将多个已知Field合并并优化的Field,所以查询这个默认Field域的效率会更高。
比如上面的淘宝的站内搜索,假设需要对商品名称Field域和商品描述Field域进行关键字的查询,就可以将这两个Field合并成一个新的Field域,并将这个新的Field域指定成默认域,具体的合并过程Lucene中不进行学习,我们会展Solr中学习使用。
例如:默认域名为product_keywords,那么会在Lucene内部形成一个查询对象Query,在Query对象内部会生成查询语句:“product_keywords:台灯”。
创建查询对象在明天会详细讲解。
3.3.5.执行查询
(Lucene自动完成)
比如,在淘宝页面上查询台灯关键字,选择过滤条件:光源类型为LED,开关类型为调光开关以外,创建查询对象后实际生成的查询语句是:
product_keywords:台灯 AND product_keywords:LED NOT product_keywords:调光开关
Lucene的查询语句和SQL查询条件类似,有关键字有条件。如果在程序中调用Lucene全文检索服务时,可以在程序中直接写类似上面的查询语句的,就好我们在JDBC程序中写SQL是一样的作用。
对条件进行解析并执行查询:(三步)
●第一步:对查询语句进行词法分析、语法分析及语言处理
1. 词法分析
如上述例子中,经过词法分析,得到单词有台灯,LED,调光开关, 关键字有AND, NOT。
注意:关键字必须大写,否则就作为普通单词处理。关键字有AND、OR、NOT。
2. 语法分析
如果发现查询语句不满足语法规则,则会报错。如product_keywords:台灯 NOT AND 调光开关,则会出错。
如果查询语句满足语法规则,就会形成语法树如下:
3. 语言处理
如LED变成led等。
经过第三步,我们得到一棵经过语言处理的语法树。
●第二步:搜索索引,得到符合语法树的文档
1. 首先,在反向索引表中,分别找出包含lucene,learn,hadoop的文档链表。
2. 其次,对包含lucene,learn的链表进行合并操作,得到既包含lucene又包含learn的文档链表。
3. 然后,将此链表与hadoop的文档链表进行差操作,去除包含hadoop的文档,从而得到既包含lucene又包含learn而且不包含hadoop的文档链表。
4. 此文档链表就是我们要找的文档。
●第三步:根据得到的文档和查询语句的相关性,对结果进行排序
(Lucene自动计算排序,明天会讲相关性排序)
3.3.6.渲染结果
以一个友好的界面将查询结果展示给用户,用户根据搜索结果找自己想要的信息,为了帮助用户很快找到自己的结果,提供了很多展示的效果,比如搜索结果中将关键字高亮显示,百度提供的快照等。
3.4.总结
综上,采集来的原始数据经过分析处理形成了索引库,通过查询条件查询索引表可以得到相关的Term词项,由此从该Term关联的文档倒排链表中得到在Document对象集合中的定位信息(DocId),然后通过DocId就可以从Document集合中得到相关的Document对象,最终可以从Document对象的指定Field域中取值返回给用户。
4.配置开发环境
4.1.Lucene下载
Lucene是开发全文检索功能的工具包,从官方网站下载Lucene4.10.3,并解压。
官方网站:http://lucene.apache.org/
版本:lucene4.10.3
Jdk要求:1.7以上
IDE:Eclipse
4.2.使用的jar包
下载后的压缩包解压缩:
Lucene基本开发jar包:
lucene-core-4.10.3.jar
lucene-analyzers-common-4.10.3.jar
lucene-queryparser-4.10.3.jar
1) lucene-core-4.10.3.jar的位置:这是Lucene的核心jar包
2) lucene-analyzers-common-4.10.3.jar的位置:这是Lucene的分析器的核心jar包
3) lucene-queryparser-4.10.3.jar的位置:这是Lucene的查询解析器jar包
其它:用于处理文件内容的工具包
commons-io-2.4.jar
4.3.创建java工程
创建一个java工程,编码格式utf-8,并导入jar包并导入Junit测试的jar。
5.入门程序
5.1.需求
实现一个文件的搜索功能,通过关键字搜索文件,凡是文件名或文件内容包括关键字的文件都需要找出来。还可以根据中文词语进行查询,并且需要支持多个条件查询。
本案例中的原始内容就是磁盘上的文件,如下图:
这里我们要搜索的文档是磁盘上的文本文件,我们要把凡是文件名或文件内容中包括关键字的文件都要找出来,所以这里要对文件名和文件内容创建索引。
本案例我们要获取磁盘上文件的内容,可以通过文件流来读取文本文件的内容,对于pdf、doc、xls等文件可通过第三方提供的解析工具读取文件内容,比如Apache POI读取doc和xls的文件内容。
使用IndexWriter的对象创建索引。
5.2.创建索引
5.2.1.实现步骤
第一步:创建IndexWriter对象(创建索引的准备工作)
1)指定索引库的存放位置Directory对象
2)创建一个分析器,对document对象中Field域的内容进行分析
3)创建一个IndexWriterConfig对象,用于配置创建索引所需的信息
参数1:Lucene的版本(可以选择对应的版本,也可以选择LATEST)
参数2:分析器对象
4)根据Directory对象和IndexWriterConfig对象创建IndexWriter对象
第二步:开始创建索引
1)采集原始数据
2)创建document对象
根据业务需求创建Field域对象来保存原始数据中的各部分内容
(参数1:域名、参数2:域值、参数3:是否存储)
把上面创建好的Field对象添加进document对象中。
3)用IndexWriter对象创建索引
(添加过程:用IndexWriter对象添加并分析文档对象,然后创建索引,并写入索引库)
第三步:关闭IndexWriter对象(关闭中带有提交处理)
5.2.2.代码实现
【CreateIndexTest.java】
package cn.baidu.test;
import java.io.File;
import org.apache.commons.io.FileUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import org.junit.Test;
public class CreateIndexTest {
/**
* 创建IndexWriter(创建索引准备工作)
*/
private IndexWriter createIndexWriter(String indexRepositoryPath) throws Exception {
// 创建Directory对象
Directory dir = FSDirectory.open(new File(indexRepositoryPath));
// 索引库还可以存放到内存中
// Directory directory = new RAMDirectory();
// 创建一个标准分析器
Analyzer analyzer = new StandardAnalyzer();
// 创建IndexWriterConfig对象
// 参数1: Lucene的版本信息, 可以选择对应的Lucene版本也可以使用LATEST
// 参数2: 分析器对象
IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, analyzer);
// 创建IndexWriter对象
return new IndexWriter(dir, config);
}
/**
* 创建索引
*/
@Test
public void testCreateIndex() throws Exception {
// 第一步:创建IndexWriter(创建索引的准备工作)
IndexWriter indexWriter = createIndexWriter("C:\\mydir\\03_workspace\\lucene\\index");
// 第二步:开始创建索引
// 采集原始数据(从指定的目录下取得文件对象列表集合)
File dirSource = new File("C:\\mydir\\03_workspace\\searchsource");
// 遍历文件对象列表
for (File f : dirSource.listFiles()) {
// 文件名
String fileName = f.getName();
// 文件内容
String fileContent = FileUtils.readFileToString(f, "utf-8");
// 文件路径
String filePath = f.getPath();
// 文件大小
long fileSize = FileUtils.sizeOf(f);
// 创建文件名域: 参数1:域的名称, 参数2:域的内容, 参数3:是否存储
TextField fileNameField = new TextField("filename", fileName, Store.YES);
// 创建文件内容域
TextField fileContentField = new TextField("content", fileContent, Store.YES);
// 创建文件路径域
TextField filePathField = new TextField("path", filePath, Store.YES);
// 创建文件大小域
TextField fileSizeField = new TextField("size", String.valueOf(fileSize), Store.YES);
// 创建document对象
Document document = new Document();
document.add(fileNameField);
document.add(fileContentField);
document.add(filePathField);
document.add(fileSizeField);
// 创建索引(用indexWriter对象)
indexWriter.addDocument(document);
}
// 第三步:关闭indexWriter
indexWriter.close();
}
}
执行效果:
在文件夹【C:\mydir\03_workspace\lucene\index】中出现了以下文件,表示创建索引成功
5.2.3.使用Luke工具查看索引文件
使用luke工具。Luke是一个便于使用Lucene开发和诊断的第三方工具,它可以访问现有利用Lucene创建的索引,并允许显示和修改。
1.启动工具:直接双击【start.bat】或者在控制台输入【java -jar lukeall-4.10.3.jar】
2.选择索引库位置
3. 索引域的展示效果:
4. 文档域的展示效果:
5.3.查询索引
5.3.1.实现步骤
第一步:查询准备工作(创建IndexReader、IndexSearcher对象)
1)指定索引库的存放位置Directory对象
2)根据Directory对象创建IndexReader对象
3)根据IndexReader对象创建IndexSearcher对象
第二步:创建查询条件对象(创建一个Term的精确查询——方式一)
第三步:执行查询(参数1:查询条件对象,参数2:查询结果返回的最大值)
第四步:处理查询结果
1)输出结果数量
2)遍历查询结果并输出
第五步:关闭IndexReader对象
5.3.2.代码实现
// 查询索引
@Test
public void testSearchIndex() throws Exception {
// 第一步:查询准备工作
// 创建Directory对象
Directory dir = FSDirectory.open(new File("C:\\mydir\\03_workspace\\lucene\\index"));
// 创建IndexReader对象
IndexReader reader = DirectoryReader.open(dir);
// 创建IndexSearcher对象
IndexSearcher searcher = new IndexSearcher(reader);
// 第二步:创建查询条件对象
// 手动创建查询对象时是没有指定任何分析器的, 所以手动创建的查询对象没有分析查询语句的能力,
// 只能设置什么就查什么, 而且指定什么就查询什么
TermQuery query = new TermQuery(new Term("filename", "apache"));
// 第三步:执行查询(参数1:查询条件对象,参数2:查询结果返回的最大值)
TopDocs topDocs = searcher.search(query, 10);
// 第四步:处理查询结果
// 输出结果数量
System.out.println("查询的结果数量:" + topDocs.totalHits);
// 取得结果集
// 这个就是查询索引的结果,但是这个里面没有具体的内容,
// 只是关于文件名中包含apache的文件的文档ID和具体相关度的计算结果值
// 要想取得文件详细内容可以根据文档ID,利用IndexSearcher对象查询
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
// 遍历结果集
for (ScoreDoc scoreDoc : scoreDocs) {
// 根据文档对象ID取得文档对象
Document doc = searcher.doc(scoreDoc.doc);
// 打印搜索结果内容
// 文件名称
System.out.println("文件名称:" + doc.get("filename"));
// 文件路径
System.out.println("文件路径:" + doc.get("path"));
// 文件大小
System.out.println("文件大小:" + doc.get("size"));
}
// 关闭IndexReader对象
reader.close();
}
5.3.3.TopDocs
Lucene搜索结果可通过TopDocs遍历,TopDocs类提供了少量的属性,如下:
方法或属性 说明
totalHits 匹配搜索条件的总记录数
scoreDocs 顶部匹配记录
注意:
Search方法需要指定匹配记录数量n:indexSearcher.search(query, n)
TopDocs.totalHits:是匹配索引库中所有记录的数量
TopDocs.scoreDocs:相关度排名靠前的记录数组,scoreDocs的长度小于等于search方法指定的参数n
6.分析器
分析器是索引的关键,如果分词分不好,创建出来的索引根本没有任何意义没法使用。所以我们来认识一下分析器。
6.1.分析器(Analyzer)的执行过程
一个分析器就是一个管道,其中由一个分词器对象 + 若干个过滤器对象串行组成。输入的内容经过逐层过滤最终分解成语汇单元Token,如下图是语汇单元的生成过程:
Token是分析器的直接产物。Token本身也是一个对象,它里面也包含了一些关于这个词的重要信息。
Token对象详细内容:
·词语本身内容
·在当前这段话(Field域值中保存的)中开始位置、结束位置
·一个可以存储其他杂项信息的payload对象(忽略)。
6.2.分析器的分词效果
如果想要看看分析器的分析效果,只需要看TokenStream中的内容就可以了。每个分析器都有一个方法tokenStream,返回一个tokenStream对象:
//查看标准分析器的分词效果
@Test
public void testTokenStream() throws Exception {
//创建一个标准分析器对象
Analyzer analyzer = new StandardAnalyzer();
//获得tokenStream对象
//第一个参数:域名,可以随便给一个
//第二个参数:要分析的文本内容
TokenStream tokenStream = analyzer.tokenStream("test", "The Spring Framework provides a comprehensive programming and configuration model.");
//添加一个引用,可以获得每个关键词
CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
//添加一个偏移量的引用,记录了关键词的开始位置以及结束位置
OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
//将指针调整到列表的头部
tokenStream.reset();
//遍历关键词列表,通过incrementToken方法判断列表是否结束
while(tokenStream.incrementToken()) {
//关键词的起始位置
System.out.println("start->" + offsetAttribute.startOffset());
//取关键词
System.out.println(charTermAttribute);
//结束位置
System.out.println("end->" + offsetAttribute.endOffset());
}
tokenStream.close();
}
6.3.中文分析器
对于分词来说,不同的语言,分词规则是不同的,比如英语每个单词都是用空格分隔,所以拆分词的规则比较简单,我们可以简单以空格判断某个字符串是否为一个单词,比如I love China,love 和 China很容易被程序区分开来。
汉字就不同了,中文是以字为单位的,字组成词,字和词再组成句子。所以它的词必须根据语义分析后才能正确的拆分,所以拆分词的规则会很复杂。比如:“我爱中国”,电脑不知道“中国”是一个词语还是“爱中”是一个词语。把中文的句子切分成有意义的词就是中文分词,也称切词。“我爱中国”,正确的分词结果是:我、爱、中国。
6.3.1.Lucene自带中文分析器
StandardAnalyzer:
单字分词:就是按照中文一个字一个字地进行分词。如:“我爱中国”,
效果:“我”、“爱”、“中”、“国”。
CJKAnalyzer
二分法分词:按两个字进行切分。如:“我是中国人”,效果:“我是”、“是中”、“中国”“国人”。
上边两个分词器无法满足需求。
SmartChineseAnalyzer
对中文支持较好,但扩展性差,扩展词库,禁用词库和同义词库等不好处理
6.3.2.第三方中文分析器
paoding: 庖丁解牛最新版在 https://code.google.com/p/paoding/ 中最多支持Lucene 3.0,且最新提交的代码在 2008-06-03,在svn中最新也是2010年提交,已经过时,不予考虑。
mmseg4j:最新版已从 https://code.google.com/p/mmseg4j/ 移至 https://github.com/chenlb/mmseg4j-solr,支持Lucene 4.10,且在github中最新提交代码是2014年6月,从09年~14年一共有:18个版本,也就是一年几乎有3个大小版本,有较大的活跃度,用了mmseg算法。
IK-analyzer: 最新版在https://code.google.com/p/ik-analyzer/上,支持Lucene 4.10从2006年12月推出1.0版开始, IKAnalyzer已经推出了4个大版本。最初,它是以开源项目Luence为应用主体的,结合词典分词和文法分析算法的中文分词组件。从3.0版本开 始,IK发展为面向Java的公用分词组件,独立于Lucene项目,同时提供了对Lucene的默认优化实现。在2012版本中,IK实现了简单的分词 歧义排除算法,标志着IK分词器从单纯的词典分词向模拟语义分词衍化。 但是也就是2012年12月后没有在更新。
ansj_seg:最新版本在 https://github.com/NLPchina/ansj_seg tags仅有1.1版本,从2012年到2014年更新了大小6次,但是作者本人在2014年10月10日说明:“可能我以后没有精力来维护ansj_seg了”,现在由”nlp_china”管理。2014年11月有更新。并未说明是否支持Lucene,是一个由CRF(条件随机场)算法所做的分词算法。
imdict-chinese-analyzer:最新版在 https://code.google.com/p/imdict-chinese-analyzer/ , 最新更新也在2009年5月,下载源码,不支持Lucene 4.10 。是利用HMM(隐马尔科夫链)算法。
Jcseg:最新版本在git.oschina.net/lionsoul/jcseg,支持Lucene 4.10,作者有较高的活跃度。利用mmseg算法。
6.4.中文分析器——IKAnalyzer
使用方法:
第一步:把jar包添加到工程中
第二步:把配置文件和扩展词典和停用词词典添加到classpath下
注意:mydict.dic和ext_stopword.dic文件的格式为UTF-8,注意是无BOM 的UTF-8 编码。
使用EditPlus.exe保存为无BOM 的UTF-8 编码格式,如下图:
6.4.1.添加jar包
在【资料\jar\IK】下找到IKAnalyzer的jar包
6.4.2.修改代码
IKAnalyzer继承Lucene的Analyzer抽象类,使用IKAnalyzer和Lucene自带的分析器方法一样,将创建索引的测试代码中的【StandardAnalyzer】改为【IKAnalyzer】测试中文分词效果。
可以和之前使用StandardAnalyzer分析器创建的索引可以对比一下:
StandardAnalyzer分析得出的索引结果:
IKAnalyzer分析得出的索引结果:
从结果看出IKAnalyzer能更好的从语义上识别中文,并做出比较正确的切分词。
6.4.3.扩展词库的使用
IKAnalyzer允许用户扩展自己的中文词库,包括扩展词库和停用词库。
扩展词库:是把一些特殊的专有名词加进来,这样分词的时候就会把专有名词当成一个整体,不会被切分。
停用词库:是把一些想过滤掉的词加进来,这样分词后就会被过滤器过滤掉,不作为索引的语汇单元。
6.4.3.1.扩展词库文件与停用词库文件
下载下来的IK压缩包中可能有停用词库,但没有扩展词库,但可以手动创建,但要注意:在创建词库时,不要用windows自带的记事本保存词库文件,因为windows默认格式是含有bom文件头的,这是个不可见文件标识符号,IK识别的时候会出错,因为非windows系统都是不带bom文件头的。
扩展词库【ext.dic】
编程思想
传智播客
停用词库【stopword.dic】
a
an
and
are
as
at
be
but
by
for
if
in
into
is
it
no
not
of
on
or
such
that
the
their
then
there
these
they
this
to
was
will
with
地
的
得
自带的没有【的】【地】【得】,给它加进去。
6.4.3.2.配置词库
【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">ext.dic;</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords">stopword.dic;</entry>
</properties>
把词库文件和配置文件都放到工程的config下:
6.4.3.3.测试
为了便于测试结果的确认,在数据库book表中把每一条记录的description中都加入:【《计算机科学丛书:Java编程思想(第4版)》【传智播客】】这一段话,这样可以增加【编程思想】和【传智播客】的出现频率,搜索排名会靠前。
1.不加扩展词库和停用词库时创建索引的结果:
停用词没有被过滤掉:and,的,the等都被加进了索引库
扩展词【编程思想】【传值播客】被分开了
2.添加停用词库后重新创建索引(将原来的索引文件删除,注意:要先关闭Luke)
如果加入log4j,再次运行的log:
已经看不到被停用的单词了:
3.添加扩展词库后重新创建索引(将原来的索引文件删除,注意:要先关闭Luke)
再次运行的log:
已经看到扩展词没有被切分:
【传值播客】是纯粹的专有名词,所以完全的被保留,没有切分
【编程思想】并不是纯粹的专有名词,在IK的内部的中文分词器中仍然会识别【编程】和【思想】,然后你又追加了【编程思想】,所以最终是三个词【编程】【思想】【编程思想】
6.5.分析器Analyzer使用时机
6.5.1.索引时使用的Analyzer
创建索引时对文档对象的内容进行分析是一个必要的过程,大部分文档内容都是需要被分析的,但也有一些特殊的Field域的内容是不用分析,可以直接作为Term创建索引。
对于一些Field可以不用分析:
1、不作为查询条件的内容,比如文件路径
2、不是匹配内容中的词而匹配Field的整体内容,比如订单号、身份证号等。
6.5.2.搜索时使用Analyzer
用户输入的查询内容也需要进行分析,这个过程和创建索引时的分析是一样的,因此他们必须使用一致的分析器对象,否则会出现双方分析出来的Term对应不上,这样就无法进行查询了。
注意:搜索使用的分析器要和索引使用的分析器一致。
和索引时一样,查询是也存在一些特殊的查询是不需要分析的,比如根据订单号、身份证号查询等。