Apache Lucene简介

2025/04/01

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 分词

分词是将给定的文本转换为更小、更精确的单位,以便于搜索。

文本经过提取关键词、删除常用词和标点符号、将单词改为小写等各种操作。

为此,有多个内置分词器:

  1. StandardAnalyzer:基于基本语法进行分词,删除“a”、“an”等停用词,还可转换为小写
  2. SimpleAnalyzer:根据无字母字符分解文本并转换为小写
  3. 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的入门知识,此外,我们还执行了各种查询并对检索到的文档进行了排序。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章