使用Lucene实现全文检索

1. Lucene介绍

Lucene是apache下的一个开源的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。
它为软件开发人员提供一个简单易用的工具包(类库),以方便的在目标系统中实现全文检索的功能。

全文检索

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

Lucene的优点

  1. 索引文件格式独立于应用平台。Lucene定义了一套以8位字节为基础的索引文件格式,使得兼容系统或者不同平台的应用能够共享建立的索引文件。
  2. 在传统全文检索引擎的倒排索引的基础上,实现了分块索引,能够针对新的文件建立小文件索引,提升索引速度。然后通过与原有索引的合并,达到优化的目的。
  3. 优秀的面向对象的系统架构,使得对于Lucene扩展的学习难度降低,方便扩充新功能。
  4. 设计了独立于语言和文件格式的文本分析接口,索引器通过接受Token流完成索引文件的创立,用户扩展新的语言和文件格式,只需要实现文本分析的接口。
  5. 已经默认实现了一套强大的查询引擎,用户无需自己编写代码即可使系统可获得强大的查询能力,Lucene的查询实现中默认实现了布尔操作、模糊查询(Fuzzy Search[11])、分组查询等等。

2. Lucene检索流程

  • 创建索引流程:Gather Data(采集数据) –> 构造文档对象 –> 分词 –> 创建索引并存入索引库
  • 搜索流程:用户发起搜索请求 –> 创建查询对象 –> 从索引库中搜索 –> 渲染并返回搜索结果

3. 索引的逻辑结构

  • 一个非结构化的数据统一格式为document文档,一个document可以有多个field
  • 当用户搜索时,Lucene会从索引域中搜索,并找到对应的document,将document中的filed进行分词,然后根据分词创建索引。

4. 创建索引

创建索引的流程

  1. IndexWriter是核心对象,它可以完成创建索引、更新索引、删除索引等操作。
  2. Directory负责对索引进行存储,它是一个抽象类,子类为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
@Test
public void createIndex() {
// 获得原始数据
ItemsDao itemsDao = new ItemsDaoImpl();
List<Items> items = itemsDao.findAllItems();
// 创建Document
List<Document> documents = new ArrayList<Document>();
Document document = null;
// 遍历原始数据并封装到field
for (Items item : items) {
document = new Document();
/**
* 参数1:field的域名 参数2:要封装的数据 参数3:是否存储
*/
Field name = new TextField("name", item.getName(), Store.YES);
Field id = new StringField("id", item.getId().toString(), Store.YES);
Field price = new TextField("price", item.getPrice().toString(), Store.YES);
// 将field封装到document
document.add(name);
document.add(id);
document.add(price);
documents.add(document);
}
// 创建一个标准分析器
Analyzer analyzer = new StandardAnalyzer();
// 索引库目录
Directory directory = FSDirectory.open(new File("D:\\Repository\\indexDatabase\\test"));
// 创建IndexWriterConfig
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
IndexWriter indexWriter = null;
try {
// 创建IndexWriter
indexWriter = new IndexWriter(directory, indexWriterConfig);
for (Document doc : documents) {
indexWriter.addDocument(doc);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭IndexWriter
indexWriter.close();
}
}

5. 搜索索引

使用QueryParse

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
@Test
public void searchIndex() throws ParseException {
// 创建标准分析器
Analyzer analyzer = new StandardAnalyzer();
// 创建QueryParser,c
// 参数1:Field域名 参数2:分析器
QueryParser queryParser = new QueryParser("name", analyzer);
// 创建Query 查找name为冰箱的
Query query = queryParser.parse("name:冰箱");
// 索引库目录
Directory directory = FSDirectory.open(new File("D:\\Repository\\indexDatabase\\test"));
IndexReader indexReader = null;
try {
// 创建IndexReader
indexReader = DirectoryReader.open(directory);
// 创建IndexSearcher
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
// 执行搜索,并返回TopDoc对象 参数1:query对象 参数2:最大记录数
TopDocs topDocs = indexSearcher.search(query, 10);
// 获得TopDocs中的记录对象
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
// 获得doc的ID
int docId = scoreDoc.doc;
// 根据id查找到doc
Document doc = indexSearcher.doc(docId);
// 打印数据
System.out.println("商品名: " + doc.get("name"));
System.out.println("价格: " + doc.get("price"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭reader
indexReader.close();
}
}

使用Query的子类

TermQuery

TermQuery使用搜索关键词进行查询。

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
@Test
public void searcher(Query query) {
// 索引库
Directory directory = FSDirectory.open(new File("D:\\Repository\\indexDatabase\\test"));
// IndexReader
IndexReader reader = null;
try {
reader = DirectoryReader.open(directory);
// IndexSearcher
IndexSearcher searcher = new IndexSearcher(reader);
TopDocs topDocs = searcher.search(query, 10);
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
int docId = scoreDoc.doc;
Document document = searcher.doc(docId);
System.out.println("商品id :" + document.get("id"));
System.out.println("商品名称 :" + document.get("name"));
System.out.println("商品价格 :" + document.get("price"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
reader.close();
}
}
@Test
public void termQuery() {
// 创建TermQuery 查询name中有冰箱的 等效于 name:冰箱
Query query = new TermQuery(new Term("name", "冰箱"));
searcher(query);
}

NumericRangeQuery

数字范围查询

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void numericRangeQuery(){
// 创建查询
// 第一个参数:域名
// 第二个参数:最小值
// 第三个参数:最大值
// 第四个参数:是否包含最小值
// 第五个参数:是否包含最大值
Query query = NumericRangeQuery.newLongRange("price", l00, 1000, true,true);
// 2、 执行搜索
searcher(query);
}

BooleanQuery

布尔查询,用于组合条件查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void booleanQuery() throws Exception {
BooleanQuery query = new BooleanQuery();
Query query1 = new TermQuery(new Term("id", "3"));
Query query2 = NumericRangeQuery.newFloatRange("price", 10f, 200f,
true, true);
//MUST:查询条件必须满足,相当于AND
//SHOULD:查询条件可选,相当于OR
//MUST_NOT:查询条件不能满足,相当于NOT非
query.add(query1, Occur.MUST);
query.add(query2, Occur.SHOULD);
System.out.println(query);
searcher(query);
}
  1. MUST和MUST表示“与”的关系,即“交集”。
  2. MUST和MUST_NOT前者包含后者不包含。
  3. MUST_NOT和MUST_NOT没有结果,没有意义。
  4. SHOULD与MUST表示MUST,SHOULD失去意义。
  5. SHOUlD与MUST_NOT相当于MUST与MUST_NOT。
  6. SHOULD与SHOULD表示“或”的概念。

6. 删除索引

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void deleteIndex(){
// 指定索引库
Directory directory = FSDirectory.open(new File("D:\\Repository\\indexDatabase\\test"));
// 创建IndexWriterConfig
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(new StandardAnalyzer());
// 创建IndexWriter
IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
// 删除指定的索引
indexWriter.deleteDocuments(new Term("name","冰箱"));
// 关闭indexWriter
indexWriter.close();
}

7. 修改索引

在Lucene中修改索引即是替换索引,先将原来的索引删除,再保存新的索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void modifyIndex() {
// 指定索引库
Directory directory = FSDirectory.open(new File("D:\\Repository\\indexDatabase\\test"));
// IndexWriterConfig
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(new StandardAnalyzer());
// IndexWriter
IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
// 创建一个Document
Document document = new Document();
Field name = new TextField("name", "比利海灵顿", Store.YES);
document.add(name);
// 修改索引
indexWriter.updateDocument(new Term("name", "冰箱"), document);
indexWriter.close();
}

8. 相关度排序

Lucene通过计算Term的权重,对查询关键字和索引文档的相关度进行打分,分越高的就排在越前面。

影响Term的权重有两个因素:

  • Term Frequency:Term在文档中的出现频率,次数越多,则代表这个Term对该文档越重要,即权重越高。
  • Document Frequency:指多少文档包含这个Term的频率,频率越高,则代表这个Term越不重要,即权重越低。

手动设置权值

在创建索引时设置权值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (Items item : items) {
document = new Document();
/**
* 参数1:field的域名 参数2:要封装的数据 参数3:是否存储
*/
Field name = new TextField("name", item.getName(), Store.YES);
Field id = new StringField("id", item.getId().toString(), Store.YES);
Field price = new TextField("price", item.getPrice().toString(), Store.YES);
// 给name域增加权值
name.setBoost(100f);
// 将field封装到document
document.add(name);
document.add(id);
document.add(price);
documents.add(document);
}

在搜索时设置权值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void setBoosts() throws Exception {
// 搜索的域名数组
String[] fields = { "name", "price" };
// 设置权值
Map<String, Float> boosts = new HashMap<String, Float>();
// 给name域设置权重
boosts.put("name", 100f);
// 创建MultiFieldQueryParse
MultiFieldQueryParser multiFieldQueryParser = new MultiFieldQueryParser(fields, new StandardAnalyzer(), boosts);
// 创建Query
Query query = multiFieldQueryParser.parse("冰箱");
searcher(query);
}
分享