全文检索技术应用系列(一):对Lucene的认知

【引言】如果放在几年前,或许Lucene是什么我都无所知;所以年岁的成长势必还是要伴随着经验和认知的提升的,否则真就是白长了!提到Lucene甚至全文检索可能有些人还有些陌生,但是如果说起Google、Baidu,想必大家都耳熟能详,而他们检索业务的核心就包括了这里提到的全文检索技术。


定义

官方定义

  The Apache LuceneTM project develops open-source search software, including:

  • Lucene Core, our flagship sub-project, provides Java-based indexing and search technology, as well as spellchecking, hit highlighting and advanced analysis/tokenization capabilities.
  • SolrTM is a high performance search server built using Lucene Core, with XML/HTTP and JSON/Python/Ruby APIs, hit highlighting, faceted search, caching, replication, and a web admin interface.
  • PyLucene is a Python port of the Core project.

通俗定义

  Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。(来自:百度百科)
  所以Lucene本身并不能提供完整全文检索服务,它只是一个架构;基于这个架构,目前比较常见的两个实现一个是Solr,一个是ElasticSearch;当然,Solr才是亲生的(不然官方定义里面也不会把Solr写进去了),但具体哪个好用还真的用过才知道。

基础概念

正排索引

  正排索引(正向索引):正排表是以文档的ID为关键字,表中记录文档中每个字的位置信息,查找时扫描表中每个文档中字的信息直到找出所有包含查询关键字的文档。尽管正排表的工作原理非常的简单,但是由于其检索效率太低,除非在特定情况下,否则实用性价值不大。

倒排索引

  倒排索引(反向索引):倒排表以字或词为关键字进行索引,表中关键字所对应的记录表项记录了出现这个字或词的所有文档,一个表项就是一个字表段,它记录该文档的ID和字符在该文档中出现的位置情况。

全文检索

  我们生活中的数据总体分为两种:结构化数据和非结构化数据。非结构化数据又一种叫法叫全文数据。按照数据的分类,搜索也分为两种:

  • 对结构化数据的搜索:如对数据库的搜索,用SQL语句。再如对元数据的搜索,如利用windows搜索对文件名,类型,修改时间进行搜索等。
  • 对非结构化数据的搜索:如利用windows的搜索也可以搜索文件内容,Linux下的grep命令,再如用Google和百度可以搜索大量内容数据。

  下图就是一个标准的全文检索基本流程:

非结构化数据搜索方法

顺序扫描法

  所谓顺序扫描,比如要找内容包含某一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件。
  如利用windows的搜索也可以搜索文件内容,只是相当的慢。如果你有一个80G硬盘,如果想在上面找到一个内容包含某字符串的文件,不花他几个小时,怕是做不到。
  Linux下的grep命令也是这一种方式。大家可能觉得这种方法比较原始,但对于小数据量的文件,这种方法还是最直接,最方便的。但是对于大量的文件,这种方法就很慢了。

全文索引

  全文检索的基本思路:将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。
  这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引。这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-text Search)。

索引存什么?

  比如有4篇文章,按照不同的词和文章的对应关系组合就形成了右侧类似于Map的一个结构;左边一系列字符串(Vocabulary),称为词典;每个字符串都指向包含此字符串的文档(Document)链表,此文档链表称为倒排表(Posting List)。

创建索引的过程

  • 将文档(Document)交给分词组件(Tokenizer),分词组件会按如下步骤处理文档
    • 将文档分成一个一个单独的单词;
    • 去除标点符号;
    • 去除停用词(Stop word;就是一种语言中最普通的一些单词,没有什么实际意义的词);
    • 经过分词(Tokenizer)后得到的结果称为词次(Token)。
  • 将词次(Token)传给语言处理组件(Linguistic Processor)
    • 变为小写(Lowercase)。
    • 将单词缩减为词根形式,如“cars”到“car”等。这种操作称为:stemming。
    • 将单词转变为词根形式,如“drove”到“drive”等。这种操作称为:lemmatization。
    • 语言处理组件(linguistic processor)的结果称为词元(Term)。
  • 将词元(Term)传给索引组件(Indexer)
    • 利用得到的词(Term)创建一个字典(Term-DocumentID)
    • 对字典按字母顺序进行排序。
    • 合并相同的词元(Term)成为文档倒排(Posting List)链表。在此表中,有几个定义:

      Document Frequency 即文档频次,表示总共有多少文件包含此词(Term)。
      Frequency 即词频率,表示此文件中包含了几个此词(Term)。

反向索引的过程

  比如要寻找既包含字符串“china”又包含字符串“search”的文档,步骤如下:

  • 客户端输入查询词(比如china search)
  • 取出包含字符串“lucene”的文档链表。
  • 取出包含字符串“solr”的文档链表。
  • 通过合并链表,找出既包含“lucene”又包含“solr”的文件。

反向索引的优缺点

  • 缺点:加上新建索引的过程,全文检索不一定比顺序扫描快,尤其是在数据量小的时候更是如此。而对一个很大量的数据创建索引也是一个很慢的过程。
  • 优点:顺序扫描是每次都要扫描,而全文索引可一次索引,多次使用;检索速度快。

流程微总结

  • 绿色表示索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:确定原始内容即要搜索的内容→采集文档→创建文档→分析文档→索引文档
  • 红色表示搜索过程,从索引库中搜索内容,搜索过程包括:用户通过搜索界面→创建查询→执行搜索,从索引库搜索→渲染搜索结果

Demo演示

  这里提供一个小小的Demo参考,既然是Demo,基本上也就是基础的调用代码和测试方法了,这里只是一个很简单很简单的演示效果,跟实际应用还差着十万八千里,不过通过这段简短的Demo,我们可以一窥Lucene的究竟。

Maven依赖

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.cc</groupId>
<artifactId>lucene</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-core -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>7.4.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-queryparser -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>7.4.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-analyzers-common -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>7.4.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-highlighter -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>7.4.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

</dependencies>
</project>

Demo源码

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package com.cc.lucene;

import org.apache.commons.io.FileUtils;
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.StoredField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.*;
import org.apache.lucene.store.FSDirectory;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.util.ArrayList;
import java.util.List;

/**
* Created by chenglin on 2018/8/16.
*/
public class LuceneDemo {

/**
* 根据srcPath下的文件创建索引库
* @param srcPath
* @param indexPath
*/
public static void createIndex(String srcPath, String suffix, String indexPath) {
System.out.println("Create index start... ");
IndexWriter indexWriter = null;
try {
// 创建Document集合
List<Document> docs = createDocuments(srcPath, suffix);
if (docs.isEmpty()) {
System.out.println("No document found, exit.");
return;
}

// 定义索引操作对象indexWriter
indexWriter = new IndexWriter(FSDirectory.open(FileSystems.getDefault().getPath(indexPath)),
new IndexWriterConfig(new StandardAnalyzer()));

// 创建索引
for (Document document : docs) {
indexWriter.addDocument(document);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != indexWriter) {
try {
// 关闭流
indexWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("Create index finish... ");
}

/**
* 取目录下固定后缀的文件生成Document
* @param srcPath
* @param suffix
* @return
* @throws IOException
*/
public static List<Document> createDocuments(String srcPath, String suffix) throws IOException {

List<Document> docList = new ArrayList<Document>();
File folder = new File(srcPath);
if (!folder.isDirectory()) {
System.out.println("The srcPath must be a folder... ");
return null;
}

// 以固定后缀获取文件
File[] files = folder.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(suffix);
}
});

for (File file : files) {
if (file.isFile()) {
// 创建文档
Document doc = new Document();

// 将Field添加到文档中(TextField会进行语汇化也就是会剔除无意义的词,StringField则不会进行语汇化)
doc.add(new StringField("fileName", file.getName(), Store.YES));
doc.add(new TextField("fileContent", FileUtils.readFileToString(file, "UTF-8"), Store.YES));
doc.add(new TextField("fileSize", String.valueOf(FileUtils.sizeOf(file)), Store.YES));
doc.add(new StoredField("filePath", file.getAbsolutePath()));

// 加入集合
docList.add(doc);
}
}
return docList;
}

/**
* 根据索引目录查询
* @param indexPath
* @throws IOException
*/
public static void indexQuery(String indexPath, String termName, String queryContent) throws IOException {
// 创建查询对象
Query query = new TermQuery(new Term(termName, queryContent));

// 根据索引目录创建indexSearcher
IndexSearcher indexSearcher =
new IndexSearcher(DirectoryReader.open(FSDirectory.open(FileSystems.getDefault().getPath(indexPath))));

// 搜索TopN
TopDocs topDocs = indexSearcher.search(query, 100);
System.out.println("Total hit records :" + topDocs.totalHits);

for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
Document doc = indexSearcher.doc(scoreDoc.doc);

System.out.println("Matched file name = " + doc.get("fileName"));
System.out.println("Matched file size = " + doc.get("fileSize"));
System.out.println("Matched file content = " + doc.get("fileContent"));
}
}

/**
* Have a demo
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
// Params
String srcPath = "D:/lucene/src";
String suffix = "txt";
String indexPath = "D:/lucene/index";

// Index
System.out.println("***********************Start building index*************************");
createIndex(srcPath, suffix, indexPath);

// Query
System.out.println("***********************Start term query*************************");
indexQuery(indexPath, "fileContent", "lucene");
System.out.println("-----------------------------------------------");
indexQuery(indexPath, "fileContent", "engine");
}
}

测试文件

文件一:SiteSecurityServiceState.txt

内容:Lucene: Apache LuceneTM is a high-performance, full-featured text search engine library written entirely in Java.

文件二:luceneIntro.txt

内容:2017年10月12日 - Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引…

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
***********************Start building index*************************
Create index start...
Create index finish...
***********************Start term query*************************
Total hit records :2
Matched file name = SiteSecurityServiceState.txt
Matched file size = 114
Matched file content = Lucene: Apache LuceneTM is a high-performance, full-featured text search engine library written entirely in Java.
Matched file name = luceneIntro.txt
Matched file size = 219
Matched file content = 2017年10月12日 - Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引...
-----------------------------------------------
Total hit records :1
Matched file name = SiteSecurityServiceState.txt
Matched file size = 114
Matched file content = Lucene: Apache LuceneTM is a high-performance, full-featured text search engine library written entirely in Java.
------2019 Lin.C ------