Lucene

Posted by Chenyawei on 2021-06-29
Words 3.6k and Reading Time 14 Minutes
Viewed Times

一、什么是Lucene

Lucene是apache下的一个开源的全文检索引擎工具包。它为软件开发人员提供一个简单易用的工具包(类库),以方便的在目标系统中实现全文检索的功能。

二、全文检索的应用场景

2.1搜索引擎

2.2站内搜索

2.3文件系统的搜索

总结:Lucene和搜索引擎是不同的,Lucene是一套用java或其它语言写的全文检索的工具包。它为应用程序提供了很多个api接口去调用,可以简单理解为是一套实现全文检索的类库。搜索引擎是一个全文检索系统,它是一个单独运行的软件系统。

三、全文检索的定义

全文检索首先将要查询的目标文档中的词提取出来,组成索引,通过查询索引达到搜索目标文档的目的。这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-text Search)。

四、Lucene实现全文检索的流程

image-20210629220625290

全文检索的流程分为两大部分:索引流程、搜索流程:

  • 索引流程:即采集数据—>构建文档对象—>分析文档(分词)—>创建索引。
  • 搜索流程:即用户通过搜索界面—>创建查询—>执行搜索,搜索器从索引库搜索—>渲染搜索结果。

Lucene本身不能进行视图渲染。

五、入门程序

5.1需求

使用lucene完成对数据库中图书信息的索引和搜索功能。

5.2环境

  • Jdk:1.7及以上
  • Lucene:4.10(从4.8版本以后,必须使用jdk1.7及以上)
  • Ide:indigo
  • 数据库:mysql

5.3数据库脚本

image-20210629220758666

5.4Lucene下载

Lucene是开发全文检索功能的工具包,使用时从官方网站下载,并解压。

官方网站:http://lucene.apache.org/
目前最新版本:5.4.0

1
2
3
下载地址:http://archive.apache.org/dist/lucene/java/
下载版本:4.10.3
JDK要求:1.7以上(从版本4.8开始,不支持1.7以下)

image-20210629220928312

1.png

image-20210629221000971

2.png

image-20210629221022041

3.png

image-20210629221044996

4.png

5.5工程搭建

所需要的jar包:

  • mysql5.1驱动包:mysql-connector-java-5.1.7-bin.jar
  • 核心包:lucene-core-4.10.3.jar
  • 分析器通用包:lucene-analyzers-common-4.10.3.jar
  • 查询解析器包:lucene-queryparser-4.10.3.jar
  • junit包:junit-4.9.jar(非必须)

image-20210629221113070

工程.PNG

5.6实现全文检索

第一步:索引流程

对文档索引的过程,就是将用户要搜索的文档内容进行索引,然后把索引存储在索引库(index)中。

1.为什么要采集数据

全文检索搜索的内容的格式是多种多样的,比如:视频、mp3、图片、文档等等。对于这种格式不同的数据,需要先将他们采集到本地,然后统一封装到lucene的文档对象中,也就是说需要将存储的内容进行统一才能对它进行查询。

2.采集数据的方式

  • 对于互联网中的数据,使用爬虫工具(http工具)将网页爬取到本地
  • 对于数据库中的数据,使用jdbc程序进行数据采集
  • 对于文件系统的数据,使用io流采集

因为目前搜索引擎主要搜索数据的来源是互联网,搜索引擎使用一种爬虫程序抓取网页( 通过http抓取html网页信息),以下是一些爬虫项目:

  • Solr(http://lucene.apache.org/solr) ,solr是apache的一个子项目,支持从关系数据库、xml文档中提取原始数据。
  • 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.索引文件的逻辑结构

image-20210629221854999

索引文件的逻辑结构.PNG

文档域:

文档域存储的信息就是采集到的信息,通过Document对象来存储,具体说是通过Document对象中field域来存储数据。

比如:数据库中一条记录会存储一个一个Document对象,数据库中一列会存储成Document中一个field域。

文档域中,Document对象之间是没有关系的。而且每个Document中的field域也不一定一样。

索引域:

索引域主要是为了搜索使用的。索引域内容是经过lucene分词之后存储的。

倒排索引表

传统方法是先找到文件,如何在文件中找内容,在文件内容中匹配搜索关键字,这种方法是顺序扫描方法,数据量大就搜索慢。
倒排索引结构是根据内容(词语)找文档,倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它是在索引中匹配搜索关键字,由于索引内容量有限并且采用固定优化算法搜索速度很快,找到了索引中的词汇,词汇与文档关联,从而最终找到了文档。

image-20210629221923462

倒排索引表.PNG

4.索引

4.1 采集数据

Book:

1
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
public class Book {

private Integer id;
private String name;
private Float price;
private String pic;
private String description;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Float getPrice() {
return price;
}
public void setPrice(Float price) {
this.price = price;
}
public String getPic() {
return pic;
}
public void setPic(String pic) {
this.pic = pic;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

BookDao:

1
2
3
4
5
public interface BookDao {

public List<Book> queryBooks();

}

BookDaoImpl:

1
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
40
41
42
43
44
45
46
47
48
49
50
public class BookDaoImpl implements BookDao {

@Override
public List<Book> queryBooks() {
// 数据库链接
Connection connection = null;

// 预编译statement
PreparedStatement preparedStatement = null;

// 结果集
ResultSet resultSet = null;

// 图书列表
List<Book> list = new ArrayList<Book>();

try {
// 加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
// 连接数据库
connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/solr", "root", "root");

// SQL语句
String sql = "SELECT * FROM book";
// 创建preparedStatement
preparedStatement = connection.prepareStatement(sql);

// 获取结果集
resultSet = preparedStatement.executeQuery();

// 结果集解析
while (resultSet.next()) {
Book book = new Book();
book.setId(resultSet.getInt("id"));
book.setName(resultSet.getString("name"));
book.setPrice(resultSet.getFloat("price"));
book.setPic(resultSet.getString("pic"));
book.setDescription(resultSet.getString("description"));
list.add(book);
}

} catch (Exception e) {
e.printStackTrace();
}

return list;
}

}

4.2 创建索引

创建索引流程:

image-20210629222000040

创建索引流程.PNG

IndexWriter是索引过程的核心组件,通过IndexWriter可以创建新索引、更新索引、删除索引操作。IndexWriter需要通过Directory对索引进行存储操作。

Directory描述了索引的存储位置,底层封装了I/O操作,负责对索引进行存储。它是一个抽象类,它的子类常用的包括FSDirectory(在文件系统存储索引)、RAMDirectory(在内存存储索引)。

代码:

1
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Test
public void createIndex() throws Exception {
// 采集数据
BookDao dao = new BookDaoImpl();
List<Book> list = dao.queryBooks();

// 将采集到的数据封装到Document对象中
List<Document> docList = new ArrayList<>();
Document document;
for (Book book : list) {
document = new Document();
// store:如果是yes,则说明存储到文档域中
// 图书ID
// 不分词、索引、存储 StringField
Field id = new StringField("id", book.getId().toString(), Store.YES);
// 图书名称
// 分词、索引、存储 TextField
Field name = new TextField("name", book.getName(), Store.YES);
// 图书价格
// 分词、索引、存储 但是是数字类型,所以使用FloatField
Field price = new FloatField("price", book.getPrice(), Store.YES);
// 图书图片地址
// 不分词、不索引、存储 StoredField
Field pic = new StoredField("pic", book.getPic());
// 图书描述
// 分词、索引、不存储 TextField
Field description = new TextField("description",
book.getDescription(), Store.NO);

// 设置boost值
if (book.getId() == 4)
description.setBoost(100f);

// 将field域设置到Document对象中
document.add(id);
document.add(name);
document.add(price);
document.add(pic);
document.add(description);

docList.add(document);
}

// 创建分词器,标准分词器
// Analyzer analyzer = new StandardAnalyzer();
// 使用ikanalyzer
Analyzer analyzer = new IKAnalyzer();

// 创建IndexWriter
IndexWriterConfig cfg = new IndexWriterConfig(Version.LUCENE_4_10_3,
analyzer);
// 指定索引库的地址
File indexFile = new File("E:\\11-index\\hcx\\");
Directory directory = FSDirectory.open(indexFile);
IndexWriter writer = new IndexWriter(directory, cfg);

// 通过IndexWriter对象将Document写入到索引库中
for (Document doc : docList) {
writer.addDocument(doc);
}

// 关闭writer
writer.close();
}

4.3 分词

在对Docuemnt中的内容索引之前需要使用分词器进行分词 ,主要过程就是分词、过虑两步。

  • 分词:将采集到的文档内容切分成一个一个的词,具体应该说是将Document中Field的value值切分成一个一个的词。
  • 过滤:将分好的词进行过滤,包括去除标点符号、去除停用词(的、是、a、an、the等)、大写转小写、词的形还原(复数形式转成单数形参、过去式转成现在式。。。)等。

什么是停用词?

停用词是为节省存储空间和提高搜索效率,搜索引擎在索引页面或处理搜索请求时会自动忽略某些字或词,这些字或词即被称为Stop Words(停用词)。比如语气助词、副词、介词、连接词等,通常自身并无明确的意义,只有将其放入一个完整的句子中才有一定作用,如常见的“的”、“在”、“是”、“啊”等。

Lucene作为了一个工具包提供不同国家的分词器,如下图:

image-20210629222118394

工具包.png

注意由于语言不同分析器的切分规则也不同,本例子使用StandardAnalyzer,它可以对用英文进行分词。

如下是org.apache.lucene.analysis.standard.standardAnalyzer的部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected TokenStreamComponents createComponents(final String fieldName, final Reader reader) {
final StandardTokenizer src = new StandardTokenizer(getVersion(), reader);
src.setMaxTokenLength(maxTokenLength);
TokenStream tok = new StandardFilter(getVersion(), src);
tok = new LowerCaseFilter(getVersion(), tok);
tok = new StopFilter(getVersion(), tok, stopwords);
return new TokenStreamComponents(src, tok) {
@Override
protected void setReader(final Reader reader) throws IOException {
src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength);
super.setReader(reader);
}
};
}
  • Tokenizer是分词器,负责将reader转换为语汇单元即进行分词,Lucene提供了很多的分词器,也可以使用第三方的分词,比如IKAnalyzer一个中文分词器。
  • tokenFilter是分词过滤器,负责对语汇单元进行过滤,tokenFilter可以是一个过滤器链儿,Lucene提供了很多的分词器过滤器,比如大小写转换、去除停用词等。

如下图是语汇单元的生成过程:

image-20210629222134227

语汇单元的生成过程.png

从一个Reader字符流开始,创建一个基于Reader的Tokenizer分词器,经过三个TokenFilter生成语汇单元Token。

比如下边的文档经过分析器分析如下:

1
2
3
4
5
原文档内容:
Lucene is a Java full-text search engine.

分析后得到的语汇单元:
lucene、java、full、text、search、engine

同一个域中相同的语汇单元(Token)对应同一个Term(词),它记录了语汇单元的内容及所在域的域名等,还包括该token出现的频率及位置。

  • 不同的域中拆分出来的相同的单词对应不同的term。
  • 相同的域中拆分出来的相同的单词对应相同的term。

例如:图书信息里面,图书名称中的java和图书描述中的java对应不同的term

4.4 使用luke工具查看索引

image-20210629222151796

luke工具.png

Luke作为Lucene工具包中的一个工具(http://www.getopt.org/luke/),可以通过界面来进行索引文件的查询、修改。

打开Luke方法:

  • 命令运行:cmd运行:java -jar lukeall-4.10.3.jar
  • 手动执行:双击lukeall-4.10.3.jar

image-20210629222224913

luke1.png

image-20210629222245371

luke2.png

第二步:搜索流程

搜索过程:

image-20210629222301626

搜索过程.PNG

  1. 用户定义查询语句,用户确定查询什么内容(输入什么关键字)
    指定查询语法,相当于sql语句。
  2. IndexSearcher索引搜索对象,定义了很多搜索方法,程序员调用此方法搜索。
  3. IndexReader索引读取对象,它对应的索引维护对象IndexWriter,IndexSearcher通过IndexReader读取索引目录中的索引文件
  4. Directory索引流对象,IndexReader需要Directory读取索引库,使用FSDirectory文件系统流对象
  5. IndexSearcher搜索完成,返回一个TopDocs(匹配度高的前边的一些记录)

1.输入查询语句:

同数据库的sql一样,lucene全文检索也有固定的语法:
最基本的有比如:AND, OR, NOT 等

举个例子,用户想找一个description中包括java关键字和lucene关键字的文档。
它对应的查询语句:description:java AND lucene
如下是使用luke搜索的例子:

image-20210629222319734

使用luke搜索.png

2.搜索分词

和索引过程的分词一样,这里要对用户输入的关键字进行分词,一般情况索引和搜索使用的分词器一致。

比如:输入搜索关键字“java学习”,分词后为java和学习两个词,与java和学习有关的内容都搜索出来了

代码:

1
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
40
41
42
43
44
45
@Test
public void indexSearch() throws Exception {
// 创建query对象
// 使用QueryParser搜索时,需要指定分词器,搜索时的分词器要和索引时的分词器一致
// 第一个参数:默认搜索的域的名称
QueryParser parser = new QueryParser("description",
new StandardAnalyzer());

// 通过queryparser来创建query对象
// 参数:输入的lucene的查询语句(关键字一定要大写)
Query query = parser.parse("description:java AND lucene");

// 创建IndexSearcher
// 指定索引库的地址
File indexFile = new File("E:\\11-index\\hcx\\");
Directory directory = FSDirectory.open(indexFile);
IndexReader reader = DirectoryReader.open(directory);
IndexSearcher searcher = new IndexSearcher(reader);

// 通过searcher来搜索索引库
// 第二个参数:指定需要显示的顶部记录的N条
TopDocs topDocs = searcher.search(query, 10);

// 根据查询条件匹配出的记录总数
int count = topDocs.totalHits;
System.out.println("匹配出的记录总数:" + count);
// 根据查询条件匹配出的记录
ScoreDoc[] scoreDocs = topDocs.scoreDocs;

for (ScoreDoc scoreDoc : scoreDocs) {
// 获取文档的ID
int docId = scoreDoc.doc;

// 通过ID获取文档
Document doc = searcher.doc(docId);
System.out.println("商品ID:" + doc.get("id"));
System.out.println("商品名称:" + doc.get("name"));
System.out.println("商品价格:" + doc.get("price"));
System.out.println("商品图片地址:" + doc.get("pic"));
System.out.println("==========================");
// System.out.println("商品描述:" + doc.get("description"));
}
// 关闭资源
reader.close();
}

notice

欢迎访问 chenyawei 的博客, 若有问题或者有好的建议欢迎留言,笔者看到之后会及时回复。 评论点赞需要github账号登录,如果没有账号的话请点击 github 注册, 谢谢 !

If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !