1. 概述
Apache Lucene是一个全文搜索引擎,可以通过多种编程语言使用。
在本文中,我们将尝试理解该库的核心概念并创建一个简单的应用程序。
2. Maven设置
首先,我们来添加必要的依赖:
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>7.1.0</version>
</dependency>
最新版本可以在这里找到。
此外,为了解析我们的搜索查询,我们需要:
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>7.1.0</version>
</dependency>
在此处查看最新版本。
3. 核心概念
3.1 索引
简而言之,Lucene使用数据的“倒排索引”-它不是将页面映射到关键字,而是将关键字映射到页面,就像任何书末尾的词汇表一样。
这样可以实现更快的搜索响应,因为它是通过索引进行搜索,而不是直接通过文本进行搜索。
3.2 文档
这里,文档是字段的集合,每个字段都有一个与之关联的值。
索引通常由一个或多个文档组成,搜索结果是最佳匹配的文档集。
它并不总是一个纯文本文档,它也可以是数据库表或集合。
3.3 字段
文档可以包含字段数据,其中字段通常是包含数据值的键:
title: Goodness of Tea
body: Discussing goodness of drinking herbal tea...
请注意,这里的标题和正文是字段,可以一起搜索,也可以单独搜索。
3.4 分词
分词是将给定的文本转换为更小、更精确的单位,以便于搜索。
文本经过提取关键词、删除常用词和标点符号、将单词改为小写等各种操作。
为此,有多个内置分词器:
- StandardAnalyzer:基于基本语法进行分词,删除“a”、“an”等停用词,还可转换为小写
- SimpleAnalyzer:根据无字母字符分解文本并转换为小写
- WhiteSpaceAnalyzer:根据空格中断文本
还有更多的分词器可供我们使用和自定义。
3.5 搜索
一旦建立了索引,我们就可以使用Query和IndexSearcher搜索该索引。搜索结果通常是一个结果集,包含检索到的数据。
请注意,IndexWriter负责创建索引,IndexSearcher负责搜索索引。
3.6 查询语法
Lucene提供了非常动态且易于编写的查询语法。
要搜索自由文本,我们只需使用文本字符串作为查询。
要搜索特定字段中的文本,我们使用:
fieldName:text
eg: title:tea
范围搜索:
timestamp:[1509909322,1572981321]
我们还可以使用通配符进行搜索:
dri?nk
将在通配符“?”的位置搜索单个字符。
d*k
搜索以“d”开头并以“k”结尾的单词,中间包含多个字符。
uni*
将找到以“uni”开头的单词。
我们还可以组合这些查询并创建更复杂的查询,并包含逻辑运算符,如AND、NOT、OR:
title: "Tea in breakfast" AND "coffee"
有关查询语法的更多信息请参见此处。
4. 一个简单的应用程序
让我们创建一个简单的应用程序,并索引一些文档。
首先,我们将创建一个内存索引,并向其中添加一些文档:
// ...
Directory memoryIndex = new RAMDirectory();
StandardAnalyzer analyzer = new StandardAnalyzer();
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
IndexWriter writter = new IndexWriter(memoryIndex, indexWriterConfig);
Document document = new Document();
document.add(new TextField("title", title, Field.Store.YES));
document.add(new TextField("body", body, Field.Store.YES));
writter.addDocument(document);
writter.close();
这里,我们创建一个包含TextField的文档,并使用IndexWriter将它们添加到索引中。TextField构造函数中的第3个参数表示是否也要存储该字段的值。
分词器用于将数据或文本拆分成块,然后从中过滤掉停用词。停用词是“a”、“am”、“is”等词,这些完全取决于给定的语言。
接下来,让我们创建一个搜索查询并在索引中搜索添加的文档:
public List<Document> searchIndex(String inField, String queryString) {
Query query = new QueryParser(inField, analyzer)
.parse(queryString);
IndexReader indexReader = DirectoryReader.open(memoryIndex);
IndexSearcher searcher = new IndexSearcher(indexReader);
TopDocs topDocs = searcher.search(query, 10);
List<Document> documents = new ArrayList<>();
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
documents.add(searcher.doc(scoreDoc.doc));
}
return documents;
}
在search()方法中,第2个整数参数表示应返回多少个顶级搜索结果,整数参数值可以根据需求设置。
现在我们来测试一下:
@Test
public void givenSearchQueryWhenFetchedDocumentThenCorrect() {
InMemoryLuceneIndex inMemoryLuceneIndex
= new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer());
inMemoryLuceneIndex.indexDocument("Hello world", "Some hello world");
List<Document> documents = inMemoryLuceneIndex.searchIndex("body", "world");
assertEquals("Hello world", documents.get(0).get("title"));
}
在这里,我们向索引添加一个简单的文档,其中包含两个字段“title”和“body”,然后尝试使用搜索查询来搜索相同的文档。
5. Lucene查询
现在我们已经熟悉了索引和搜索的基础知识,让我们深入挖掘一下。
在前面的部分中,我们已经了解了基本的查询语法,以及如何使用QueryParser将其转换为Query实例。
Lucene也提供了各种具体的实现:
5.1 TermQuery
Term是搜索的基本单位,包含字段名称以及要搜索的文本。
TermQuery是所有查询中最简单的,由单个术语组成:
@Test
public void givenTermQueryWhenFetchedDocumentThenCorrect() {
InMemoryLuceneIndex inMemoryLuceneIndex = new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer());
inMemoryLuceneIndex.indexDocument("activity", "running in track");
inMemoryLuceneIndex.indexDocument("activity", "Cars are running on road");
Term term = new Term("body", "running");
Query query = new TermQuery(term);
List<Document> documents = inMemoryLuceneIndex.searchIndex(query);
assertEquals(2, documents.size());
}
5.2 PrefixQuery
要搜索包含“以…开头”单词的文档:
@Test
public void givenPrefixQueryWhenFetchedDocumentThenCorrect() {
InMemoryLuceneIndex inMemoryLuceneIndex
= new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer());
inMemoryLuceneIndex.indexDocument("article", "Lucene introduction");
inMemoryLuceneIndex.indexDocument("article", "Introduction to Lucene");
Term term = new Term("body", "intro");
Query query = new PrefixQuery(term);
List<Document> documents = inMemoryLuceneIndex.searchIndex(query);
assertEquals(2, documents.size());
}
5.3 WildcardQuery
顾名思义,我们可以使用通配符“*”或“?”进行搜索:
// ...
Term term = new Term("body", "intro*");
Query query = new WildcardQuery(term);
// ...
5.4 PhraseQuery
它用于搜索文档中的文本序列:
// ...
inMemoryLuceneIndex.indexDocument(
"quotes",
"A rose by any other name would smell as sweet.");
Query query = new PhraseQuery(1, "body", new BytesRef("smell"), new BytesRef("sweet"));
List<Document> documents = inMemoryLuceneIndex.searchIndex(query);
// ...
请注意,PhraseQuery构造函数的第1个参数称为slop,它是匹配的术语之间的单词数距离。
5.5 FuzzyQuery
我们可以使用它来搜索类似但不一定相同的东西:
// ...
inMemoryLuceneIndex.indexDocument("article", "Halloween Festival");
inMemoryLuceneIndex.indexDocument("decoration", "Decorations for Halloween");
Term term = new Term("body", "hallowen");
Query query = new FuzzyQuery(term);
List<Document> documents = inMemoryLuceneIndex.searchIndex(query);
// ...
我们尝试搜索文本“Halloween”,但拼写错误为“hallowen”。
5.6 BooleanQuery
有时我们可能需要执行复杂的搜索,组合两种或多种不同类型的查询:
// ...
inMemoryLuceneIndex.indexDocument("Destination", "Las Vegas singapore car");
inMemoryLuceneIndex.indexDocument("Commutes in singapore", "Bus Car Bikes");
Term term1 = new Term("body", "singapore");
Term term2 = new Term("body", "car");
TermQuery query1 = new TermQuery(term1);
TermQuery query2 = new TermQuery(term2);
BooleanQuery booleanQuery = new BooleanQuery.Builder()
.add(query1, BooleanClause.Occur.MUST)
.add(query2, BooleanClause.Occur.MUST)
.build();
// ...
6. 对搜索结果进行排序
我们还可以根据某些字段对搜索结果文档进行排序:
@Test
public void givenSortFieldWhenSortedThenCorrect() {
InMemoryLuceneIndex inMemoryLuceneIndex = new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer());
inMemoryLuceneIndex.indexDocument("Ganges", "River in India");
inMemoryLuceneIndex.indexDocument("Mekong", "This river flows in south Asia");
inMemoryLuceneIndex.indexDocument("Amazon", "Rain forest river");
inMemoryLuceneIndex.indexDocument("Rhine", "Belongs to Europe");
inMemoryLuceneIndex.indexDocument("Nile", "Longest River");
Term term = new Term("body", "river");
Query query = new WildcardQuery(term);
SortField sortField = new SortField("title", SortField.Type.STRING_VAL, false);
Sort sortByTitle = new Sort(sortField);
List<Document> documents = inMemoryLuceneIndex.searchIndex(query, sortByTitle);
assertEquals(4, documents.size());
assertEquals("Amazon", documents.get(0).getField("title").stringValue());
}
我们尝试按标题字段(即河流名称)对获取的文档进行排序,SortField构造函数的布尔参数用于反转排序顺序。
7. 从索引中删除文档
让我们尝试根据给定的Term从索引中删除一些文档:
// ...
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
IndexWriter writer = new IndexWriter(memoryIndex, indexWriterConfig);
writer.deleteDocuments(term);
// ...
我们将测试一下:
@Test
public void whenDocumentDeletedThenCorrect() {
InMemoryLuceneIndex inMemoryLuceneIndex = new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer());
inMemoryLuceneIndex.indexDocument("Ganges", "River in India");
inMemoryLuceneIndex.indexDocument("Mekong", "This river flows in south Asia");
Term term = new Term("title", "ganges");
inMemoryLuceneIndex.deleteDocument(term);
Query query = new TermQuery(term);
List<Document> documents = inMemoryLuceneIndex.searchIndex(query);
assertEquals(0, documents.size());
}
8. 总结
本文简要介绍了Apache Lucene的入门知识,此外,我们还执行了各种查询并对检索到的文档进行了排序。
Post Directory
