Lucene 快速入门

Lucene 快速入门

Apache Lucene 是一个全文搜索引擎,可以在多种编程语言中应用。本文尝试介绍其核心库的概念并创建一个简单示例。

gradle 依赖

首先解决依赖,读者可以在maven仓库中引用最新版本。

    compile "org.apache.lucene:lucene-core:7.2.1"
    compile "org.apache.lucene:lucene-queryparser:7.2.1"

核心概念

索引

简单地说,lucene使用数据的“反向索引”——不使用从页面至关键字的映射,而是从关键字至页面的映射,就像书后面的词汇表。

通过反向索引可以实现快速搜索响应,因为搜索索引,而不是搜整个文本。

文档(Document)

文档是域的集合,每个域有一个值与之关联。索引通常有一个或多个文档组成,搜索结果是一组最佳匹配的文档。
文档不总是纯文本文档,也可以是数据框表或集合。

域(Field)

文档中有域数据,域一般是键和对应的数据值:

title: Goodness of Tea
body: Discussing goodness of drinking herbal tea...

这里的title和body是域,可以单独或以前被搜索。

分析

分析器把文本转换成较小的精确单元,便于搜索。文本通过不同的抽取关键字操作,删除通用词和标点,转换单词至小写形式等。
为此,lucene内置了多种分析器:

  1. StandardAnalyzer – 基于基本语法,删除停顿词,转换至小写,也支持中文分析
  2. SimpleAnalyzer – 基于非字母字符分割文本并转换至小写
  3. WhiteSpaceAnalyzer – 基于空白字符分割文本
    还有其他的分析器,当然也可以自定义。

搜索

一旦建好了索引,我们可以使用Query和IndexSearcher类搜索索引,搜索结果集包含返回的数据。
IndexWritter 负责创建索引,IndexSearcher为搜素索引。

查询语法

lucene提供很多动态易使用的查询语法。
字符串查询文本可以搜索任意文本,搜索特定域的文本,语法如下:
fieldName:text
举例:title:tea

范围搜索示例:timestamp:[1509909322,1572981321]

使用通配符:dri?nk , dk ,uni;和SQL语法类似。

也可以组合这些查询为复杂的查询,使用逻辑运算符,AND,NOT,OR:

title: “Tea in breakfast” AND “coffee”

示例应用

下面创建一个简单示例,对一些文档进行索引,然后应用索引实现快速搜索。
首先我们创建一个基于内存的索引,然后增加往里增加一些文档:

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.*;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.BytesRef;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class InMemoryLuceneIndex {
    private Directory memoryIndex;
    private StandardAnalyzer analyzer;

    public InMemoryLuceneIndex(Directory memoryIndex, StandardAnalyzer analyzer) {
        this.memoryIndex = memoryIndex;
        this.analyzer = analyzer;
    }

    public void indexDocument(String title, String body) {
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
        try {
            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));
            document.add(new SortedDocValuesField("title", new BytesRef(title)));

            writter.addDocument(document);
            writter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public List<Document> searchIndex(String inField, String queryString) {
        try {
            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;
        } catch (IOException | ParseException e) {
            e.printStackTrace();
        }
        return null;
    }

    public void deleteDocument(Term term) {
        try {
            IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
            IndexWriter writter = new IndexWriter(memoryIndex, indexWriterConfig);
            writter.deleteDocuments(term);
            writter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public List<Document> searchIndex(Query query, Sort sort) {
        try {
            IndexReader indexReader = DirectoryReader.open(memoryIndex);
            IndexSearcher searcher = new IndexSearcher(indexReader);
            TopDocs topDocs = searcher.search(query, 10, sort);
            List<Document> documents = new ArrayList<>();
            for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
                documents.add(searcher.doc(scoreDoc.doc));
            }

            return documents;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

上面代码,我们使用TextField 类此创建文档,并使用IndexWriter类加入索引,TextField构造函数的第三个参数指明域对应值是否存储。

分析器是用来把数据或文本分割为块,然后过滤掉停顿词,停顿词一般为如:‘a’,‘am’,‘is’等,不同类型的语言有不同的停顿词。

下面定义搜索查询,基于文档索引。我们定义了searchIndex方法,在search()方法第二个Integer参数指明返回前多少条符合条件的结果数据。

下面开始测试:

@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”,然后测试搜索结果是否相同。

Lucene查询

现在我们已经熟悉了基本的索引和搜索,让我们再深入一点。
在前面,我们已经看到了基本的查询语法,以及如何使用QueryParser转换至Query 实例。
Lucene同时提供了Query不同的具体实现:

TermQuery

项是最基本的搜索单元,包含域名称和搜索文本。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());
}

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());
}

WildcardQuery

通配符查询,可以使用通配符,如“*”或“?”进行查询:

// ...
Term term = new Term("body", "intro*");
Query query = new WildcardQuery(term);
// ...

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构造函数第一个参数被称为slop,即单词之间的最大距离,示例中1表示之间可以有一个或无其他单词可以匹配。

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,也可以搜索到。

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();
// ...

搜索结果排序

针对搜索结果需要根据特定域进行排序:

@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());
}

我们尝试用title域对返回文档进行排序,即river名称。SortField构造函数boolean参数为了实现倒叙排序。

从索引中删除文档

可以基于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());
}

总结

本文简要介绍了Apache lucene,同时通过示例展示了不同类型查询和排序。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值