Lucene介绍
Lucene简介
最受欢迎的java开源全文搜索引擎开发工具包。提供了完整的查询引擎和索引引擎,部分文本分词引擎(英文与德文两种西方语言)。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便在目标系统中实现全文检索功能,或者是以此为基础建立起完整的全文检索引擎。 是Apache的子项目,网址:http://lucene.apache.org/
Lucene用途
为软件开发人员提供一个简单易用的工具包,以方便在目标系统中实现全文检索功能,或者是以此为基础建立起完整的全文检索引擎。
Lucene适用场景
在应用中为数据库中的数据提供全文检索实现。
开发独立的搜索引擎服务、系统
Lucene的特性
1、稳定、索引性能高
每小时能够索引150GB以上的数据。
对内存的要求小——只需要1MB的堆内存
增量索引和批量索引一样快。
索引的大小约为索引文本大小的20%~30%
2、高效、准确、高性能的搜索算法
良好的搜索排序。
强大的查询方式支持:短语查询、通配符查询、临近查询、范围查询等。
支持字段搜索(如标题、作者、内容)。
可根据任意字段排序
支持多个索引查询结果合并
支持更新操作和查询操作同时进行
支持高亮、join、分组结果功能
速度快
可扩展排序模块,内置包含向量空间模型、BM25模型可选
可配置存储引擎
3、跨平台
纯java编写。
作为Apache开源许可下的开源项目,你可在商业或开源项目中使用。
Lucene有多种语言实现版可选(如C、C++、Python等),不光是JAVA。
Lucene架构
1.数据收集
2.创建索引
3.索引存储
4.搜索(使用索引)
Lucene集成
选用的Lucene版本
选用当前最新版 7.3.0 : https://lucene.apache.org/
系统要求
JDK1.8 及以上版本
集成:将lucene core的jar引入到你的应用中
方式一:官网下载 zip,解压后拷贝jar到你的工程
方式二:maven 引入依赖
Lucene 模块说明
core: Lucene core library 核心模块:分词、索引、查询
analyzers-*: 分词器
facet: Faceted indexing and search capabilities 提供分类索引、搜索能力
grouping: Collectors for grouping search results. 搜索结果分组支持
highlighter: Highlights search keywords in results 关键字高亮支持
join: Index-time and Query-time joins for normalized content 连接支持
queries: Filters and Queries that add to core Lucene 补充的查询、过滤方式实现
queryparser: Query parsers and parsing framework 查询表达式解析模块
spatial: Geospatial search 地理位置搜索支持
suggest: Auto-suggest and Spellchecking support 拼写检查、联想提示
先引入lucene的核心模块
<!-- lucene 核心模块 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>7.3.0</version>
</dependency>
了解核心模块的构成
分词器详解
Lucene分词器API
1.org.apache.lucene.analysi.Analyzer
分析器,分词器组件的核心API,它的职责:构建真正对文本进行分词处理的TokenStream(分词处理器)。通过调用它的如下两个方法,得到输入文本的分词处理器。
public final TokenStream tokenStream(String fieldName, Reader reader)
public final TokenStream tokenStream(String fieldName, String text)
这两个方法是final方法,不能被覆盖的,在这两个方法中是如何构建分词处理器的呢?
问题1:从哪里得到了TokenStream?
问题2:方法传入的字符流Reader 给了谁?
问题3: components是什么?components的获取逻辑是怎样?
问题4:createComponents(fieldName) 方法是个什么方法?
问题5:Analyzer能直接创建对象吗?
问题6:为什么它要这样设计?
问题7:请看一下Analyzer的实现子类有哪些?
问题8:要实现一个自己的Analyzer,必须实现哪个方法?
TokenStreamComponents createComponents(String fieldName)
是Analizer中唯一的抽象方法,扩展点。通过提供该方法的实现来实现自己的Analyzer。参数说明:fieldName,如果我们需要为不同的字段创建不同的分词处理器组件,则可根据这个参数来判断。否则,就用不到这个参数。
返回值为 TokenStreamComponents 分词处理器组件。
我们需要在createComponents方法中创建我们想要的分词处理器组件。
2.TokenStreamComponents
分词处理器组件:这个类中封装有供外部使用的TokenStream分词处理器。提供了对source(源)和sink(供外部使用分词处理器)两个属性的访问方法。
问题1:这个类的构造方法有几个?区别是什么?从中能发现什么?
问题2:source 和 sink属性分别是什么类型?这两个类型有什么关系?
问题3:在这个类中没有创建source、sink对象的代码(而是由构造方法传人)。也就是说我们在Analyzer.createComponents方法中创建它的对象前,需先创建什么?
问题4:在Analyzer中tokenStream() 方法中把输入流给了谁?得到的TokenStream对象是谁?TokenStream对象sink中是否必须封装有source对象?如果必须有,这个封装是否也得在Analyzer.createComponents方法中完成?
3,org.apache.lucene.analysis.TokenStream
分词处理器,负责对输入文本完成分词、处理。
问题1:接上一页,TokenStream中有没有对应的给入方法?
问题2:TokenStream是一个抽象类,有哪些方法,它的抽象方法有哪些?它的构造方法有什么特点?
问题3:TokenStream的具体子类分为哪两类?有什么区别?
问题4:TokenStream继承了谁?它是干什么用的?
概念说明:Token: 分项,从字符流中分出一个一个的项. Token Attribute: 分项属性(分项的信息):如 包含的词、位置等
4.TokenStream 的两类子类
Tokenizer:分词器,输入是Reader字符流的TokenStream,完成从流中分出分项
TokenFilter:分项过滤器,它的输入是另一个TokenStream,完成对从上一个TokenStream中流出的token的特殊处理。
问题1:请查看Tokenizer类的源码及注释,这个类该如何使用?要实现自己的Tokenizer只需要做什么?
问题2:Tokenizer的子类有哪些?
问题3:请查看TokenFilter类的源码及注释,如何实现自己的TokenFilter?
问题4:TokenFilter的子类有哪些?
问题5:TokenFilter是不是一个典型的装饰器模式?如果我们需要对分词进行各种处理,只需要按我们的处理顺序一层层包裹即可(每一层完成特定的处理)。不同的处理需要,只需不同的包裹顺序、层数。
5.TokenStream 继承了 AttributeSource
问题1:我们在TokenStream及它的两个子类中是否有看到关于分项信息的存储,如该分项的词是什么、这个词的位置索引?
概念说明:Attribute 属性 Token Attribute 分项属性(分项信息),如 分项的词、词的索引位置等等。这些属性通过不同的Tokenizer /TokenFilter处理统计得出。不同的Tokenizer/TokenFilter组合,就会有不同的分项信息。它是会动态变化的,你不知道有多少,是什么。那该如何实现分项信息的存储呢?
答案就是 AttributeSource、Attribute 、AttributeImpl、AttributeFactory
1、AttribureSource 负责存放Attribute对象,它提供对应的存、取方法
2、Attribute对象中则可以存储一个或多个属性信息
3、AttributeFactory 则是负责创建Attributre对象的工厂,在TokenStream中默认使用了AttributeFactory.getStaticImplementation 我们不需要提供,遵守它的规则即可。
6.AttributeSource使用规则说明
1、某个TokenStream实现中如要存储分项属性,通过AttributeSource的两个add方法之一,往AttributeSource中加入属性对象。
<T extends Attribute> T addAttribute(Class<T> attClass)
该方法要求传人你需要添加的属性的接口类(继承Attribute),返回对应的实现类实例给你。从接口到实例,这就是为什么需要AttributeFactory的原因。
void addAttributeImpl(AttributeImpl att)
2、加入的每一个Attribute实现类在AttributeSource中只会有一个实例,分词过程中,分项是重复使用这一实例来存放分项的属性信息。重复调用add方法添加它返回已存储的实例对象。
3、要获取分项的某属性信息,则需持有某属性的实例对象,通过addAttribute方法或getAttribure方法获得Attribute对象,再调用实例的方法来获取、设置值
4、在TokenStream中,我们用自己实现的Attribute,默认的工厂。当我们调用这个add方法时,它怎么知道实现类是哪个?这里有一定规则要遵守:
1、自定义的属性接口 MyAttribute 继承 Attribute
2、自定义的属性实现类必须继承 Attribute,实现自定义的接口MyAttribute
3、自定义的属性实现类必须提供无参构造方法
4、为了让默认工厂能根据自定义接口找到实现类,实现类名需为 接口名+Impl 。
请查看lucene中提供的Attribute实现是否是这样的。
7.TokenStream 的使用步骤。
我们在应用中并不直接使用分词器,只需为索引引擎和搜索引擎创建我们想要的分词器对象。但我们在选择分词器时,会需要测试分词器的效果,就需要知道如何使用得到的分词处理器TokenStream,使用步骤:
1、从tokenStream获得你想要获得分项属性对象(信息是存放在属性对象中的)
2、调用 tokenStream 的 reset() 方法,进行重置。因为tokenStream是重复利用的。
3、循环调用tokenStream的incrementToken(),一个一个分词,直到它返回false
4、在循环中取出每个分项你想要的属性值。
5、调用tokenStream的end(),执行任务需要的结束处理。
6、调用tokenStream的close()方法,释放占有的资源。
思考:tokenStream是装饰器模式,这个reset / incrementToken / end / close是如何工作的? 请查看 tokenStream / Tokenizer / TokenFilter的源码。
8.简单实现一个我们自己的Analyzer,需求说明
Tokenizer:
实现对英文按空白字符进行分词。
需要记录的属性信息有: 词
TokenFilter:
要进行的处理:转为小写
说明:Tokenizer分词时,是从字符流中一个一个字符读取,判断是否是空白字符来进行分词。
思考:Tokenizer是一个 AttributeSource对象,TokenFilter 又是一个AttributeSource对象。在这两个我们自己的实现类中,我们都调用了addAttribute方法,怎么会只有一个 attribute对象? 请查看源码找到答案。
public interface MyAttribute1 extends Attribute {
String getAttr();
}
public class MyAttribute1Impl extends AttributeImpl {
int value = 0;
Random rd = new Random();
public int getAttr() {
return value;
}
@Override
public void clear() {
value = rd.nextInt(1000);
}
@Override
public void reflectWith(AttributeReflector reflector) {
// TODO Auto-generated method stub
}
@Override
public void copyTo(AttributeImpl target) {
}
}
public class MyWhitespaceAnalyzer extends Analyzer {
@Override
protected TokenStreamComponents createComponents(String fieldName) {
Tokenizer source = new MyWhitespaceTokenizer();
TokenStream filter = new MyLowerCaseTokenFilter(source);
return new TokenStreamComponents(source, filter);
}
static class MyWhitespaceTokenizer extends Tokenizer {
// 需要记录的属性
// 词
MyCharAttribute charAttr = this.addAttribute(MyCharAttribute.class);
// 存词的出现位置
// 存放词的偏移
//
char[] buffer = new char[255];
int length = 0;
int c;
@Override
public boolean incrementToken() throws IOException {
// 清除所有的词项属性
clearAttributes();
length = 0;
while (true) {
c = this.input.read();
if (c == -1) {
if (length > 0) {
// 复制到charAttr
this.charAttr.setChars(buffer, length);
return true;
} else {
return false;
}
}
if (Character.isWhitespace(c)) {
if (length > 0) {
// 复制到charAttr
this.charAttr.setChars(buffer, length);
return true;
}
}
buffer[length++] = (char) c;
}
}
}
public static class MyLowerCaseTokenFilter extends TokenFilter {
public MyLowerCaseTokenFilter(TokenStream input) {
super(input);
}
MyCharAttribute charAttr = this.addAttribute(MyCharAttribute.class);
@Override
public boolean incrementToken() throws IOException {
boolean res = this.input.incrementToken();
if (res) {
char[] chars = charAttr.getChars();
int length = charAttr.getLength();
if (length > 0) {
for (int i = 0; i < length; i++) {
chars[i] = Character.toLowerCase(chars[i]);
}
}
}
return res;
}
}
public static interface MyCharAttribute extends Attribute {
void setChars(char[] buffer, int length);
char[] getChars();
int getLength();
String getString();
}
public static class MyCharAttributeImpl extends AttributeImpl
implements MyCharAttribute {
private char[] chatTerm = new char[255];
private int length = 0;
@Override
public void setChars(char[] buffer, int length) {
this.length = length;
if (length > 0) {
System.arraycopy(buffer, 0, this.chatTerm, 0, length);
}
}
public char[] getChars() {
return this.chatTerm;
}
public int getLength() {
return this.length;
}
@Override
public String getString() {
if (this.length > 0) {
return new String(this.chatTerm, 0, length);
}
return null;
}
@Override
public void clear() {
this.length = 0;
}
@Override
public void reflectWith(AttributeReflector reflector) {
}
@Override
public void copyTo(AttributeImpl target) {
}
}
public static void main(String[] args) {
String text = "An AttributeSource contains a list of different AttributeImpls, and methods to add and get them. ";
try (Analyzer ana = new MyWhitespaceAnalyzer();
TokenStream ts = ana.tokenStream("aa", text);) {
MyCharAttribute ca = ts.getAttribute(MyCharAttribute.class);
ts.reset();
while (ts.incrementToken()) {
System.out.print(ca.getString() + "|");
}
ts.end();
System.out.println();
} catch (IOException e) {
e.printStackTrace();
}
}
}
9.小结
通过API及源码的学习,你是否感受到了作者的一些设计思想。
他是如何处理变与不变的,如Analyzer、TokenStream类的设计?
他是如何处理不同分词器有不同的处理逻辑的问题的?
Lucene提供的分词器
Lucene core模块中的 StandardAnalyzer 英文分词器
看看它都分析存储哪些属性信息 试试它的英文分词效果 中文分词效果
Lucene 的中文分词器 SmartChineseAnalyzer
<!-- Lucene提供的中文分词器模块,lucene-analyzers-smartcn -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-smartcn</artifactId>
<version>7.3.0</version>
</dependency>
试试它的中英文分词效果
Lucene-Analyzers-common包中提供的分词器
看看都有些什么分词器
public class AnalizerTestDemo {
private static void doToken(TokenStream ts) throws IOException {
ts.reset();
CharTermAttribute cta = ts.getAttribute(CharTermAttribute.class);
while (ts.incrementToken()) {
System.out.print(cta.toString() + "|");
}
System.out.println();
ts.end();
ts.close();
}
public static void main(String[] args) throws IOException {
String etext = "Analysis is one of the main causes of slow indexing. Simply put, the more you analyze the slower analyze the indexing (in most cases).";
String chineseText = "张三说的确实在理。";
// String chineseText = "中华人民共和国简称中国。";
try (Analyzer ana = new StandardAnalyzer();) {
TokenStream ts = ana.tokenStream("coent", etext);
System.out.println("标准分词器,英文分词效果:");
doToken(ts);
ts = ana.tokenStream("content", chineseText);
System.out.println("标准分词器,中文分词效果:");
doToken(ts);
} catch (IOException e) {
}
// smart中文分词器
try (Analyzer smart = new SmartChineseAnalyzer()) {
TokenStream ts = smart.tokenStream("content", etext);
System.out.println("smart中文分词器,英文分词效果:");
doToken(ts);
ts = smart.tokenStream("content", chineseText);
System.out.println("smart中文分词器,中文分词效果:");
doToken(ts);
}
// IKAnalyzer 细粒度切分
try (Analyzer ik = new IKAnalyzer4Lucene7();) {
TokenStream ts = ik.tokenStream("content", etext);
System.out.println("IKAnalyzer中文分词器 细粒度切分,英文分词效果:");
doToken(ts);
ts = ik.tokenStream("content", chineseText);
System.out.println("IKAnalyzer中文分词器 细粒度切分,中文分词效果:");
doToken(ts);
}
// IKAnalyzer 智能切分
try (Analyzer ik = new IKAnalyzer4Lucene7(true);) {
TokenStream ts = ik.tokenStream("content", etext);
System.out.println("IKAnalyzer中文分词器 智能切分,英文分词效果:");
doToken(ts);
ts = ik.tokenStream("content", chineseText);
System.out.println("IKAnalyzer中文分词器 智能切分,中文分词效果:");
doToken(ts);
}
}
}
IKAnalyzer集成
需要做集成,是因为Analyzer的createComponents方法API改变了。
集成步骤
1、找到 IkAnalyzer包体提供的Lucene支持类,比较IKAnalyzer的createComponets方法。
2、照这两个类,创建新版本的, 类里面的代码直接复制,修改参数即可。
IKAnalyzer提供两种分词模式:细粒度分词和智能分词,看它的构造参数。
集成后,请测试细粒度分词、智能分词的效果
测试语句:张三说的确实在理
请比较IKAnalyzer和SmartChineseAnalyzer
测试语句:
张三说的确实在理。
Lucene是一个开源的,基于java的搜索引擎开发工具包。
扩展 IKAnalyzer的停用词
Ik中默认的停用词很少,我们往往需要扩展它。可从网址: https://github.com/cseryp/stopwords 下载一份比较全的停用词。
Ik中停用词的扩展步骤:
1、在类目录下创建IK的配置文件:IKAnalyzer.cfg.xml
2、在配置文件中增加配置扩展停用词文件的节点:
<entry key=“ext_stopwords”>my_ext_stopword.dic</entry>
如有多个,以“;”间隔
3、在类目录下创建我们的扩展停用词文件 my_ext_stopword.dic
4、编辑该文件加入停用词,一行一个
注意文件要是UTF-8编码
扩展 IKAnalyzer的词典
每年都有很多的新词产生,往分词器的词典中添加新词的步骤:
1、在类目录下IK的配置文件:IKAnalyzer.cfg.xml 中增加配置扩展词文件的节点:
<entry key="ext_dict">ext.dic</entry>
如有多个,以“;”间隔
2、在类目录下创建扩展词文件 ext.dic
4、编辑该文件加入新词,一行一个
测试语句: 厉害了我的国一经播出,受到各方好评,强烈激发了国人的爱国之情、自豪感! 新词:厉害了我的国
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">my_ext_stopword.dic</entry>
</properties>
来源:oschina
链接:https://my.oschina.net/u/3728166/blog/3002934