Lucene全文索引与搜索入门篇2 - 可视化理解Lucene

编程教程 > Java (7) 2025-04-29 10:59:09

背景介绍

 

Lucene是一款高性能、可扩展的信息检索工具库,是用于全文检索和搜寻的Java开放源码程序库,最初是由Doug Cutting所撰写,2000年发行了第一个开源版本,2005年成为Apache顶级项目。虽然经过近20年,Lucene在全文检索领域还是独领风骚,蓬勃发展。

优秀的搜索引擎需要复杂的架构和算法,用来支撑对海量数据的存储和搜索,并同时保证搜索质量。搜索引擎最重要的一个数据结构:倒排索引(Inverted Index)  (实现单词->文档的存储形式),能高效实现全文搜索,并且索引数据是"一次检索,可多次搜索"。

Lucene的主要功能包括:

  • 文档索引:用户基于原始文档,创建文档对象Document,Lucene将Document文档对象解析成Index索引文件并持久化到文件系统
  • 搜索查询:用户传入查询语句,Lucene转成Query对象,基于Index索引文件搜索并汇总

 

Lucene的主要功能包括:

  • 文档索引:用户基于原始文档,创建文档对象Document,Lucene将Document文档对象解析成Index索引文件并持久化到文件系统
  • 搜索查询:用户传入查询语句,Lucene转成Query对象,基于Index索引文件搜索并汇总
Lucene全文索引与搜索入门篇_图示-32ddf4aad263416189b289206994a994.png

Lucene仅提供检索工具包,不提供额外的检索应用功能,在Lucene之上构建的应用项目主要有:

  • Apache Nutch:提供可扩展的开源Web爬虫应用
  • Apache Solr:基于Lucene提供企业级搜索引擎,支持分布式部署
  • Elasticsearch:基于Lucene提供企业级搜索引擎,支持分布式部署,并提供 Elastic Stack服务:包括数据采集、检索、展示、APM全链路跟踪

 

核心概念

Lucene索引层级结构主要包括:Index(索引),Segment(索引段),Document(索引文档),Field(索引域),Term(索引项),它们之间关系如下:

Lucene全文索引与搜索入门篇_图示-547e45d98e204a72b1678f624f259a49.png

Index

Index(索引):原始文档经过Lucene的索引流程后,以Index形式存储在文件系统,支持对保存的数据进行快速随机访问。 一个Index由多个Segment(索引段)构成,每个Segment包含多个Document(索引文件)。Index是逻辑概念,是一个索引目录下,所有索引文件的总和,可表示Document文档数据的集合,不同的Document数据结构,建议使用不同的Index。

Segment

Segment(索引段):每个Lucene Index包括多个Segment,每个Segment都是一个独立的索引,是整个Index索引的子集,因此在搜索时支持对每个Segment进行单独访问,最后对结果进行汇总。Segment是逻辑概念,是一系列索引文件的集合,属于同个Segment的索引文件具有相同的文件前缀,各个独立的索引文件组成索引的不同部分(存储Field、词向量、倒排索引等)。

索引流程中基于Segment的主要功能包括:

  1. 新增/更新Document会创建新的Segment;
  2. 刷新/合并已存在的Segment;
  3. 同个Segment下,不同索引文件合成;

Document

Document(索引文档):包含多个Field对象的集合容器,是Lucene索引和搜索的原子单元。Lucene基于Document Numbers(文档编号)对每个Document编号,初始变化为0,后续其编号依次增长1。

Field

Field(索引域):是Document的一部分,里面包含真正搜索的内容,每个Field包括三个部分:name(标识名称)、type(Field属性描述)、value。 Field被分词为Term对象进行索引保存。

Field常用的数据结构子类如下

类型

说明

IntPoint

以int索引,只索引不存储,支持精确/range查询,基于KD-tree 加速range查询速度

LongPoint

以long索引,其他同上

FloatPoint

以float索引,其他同上

DoublePoint

以double索引,其他同上

TextField

可以Reader或String索引,索引并分词,主要用于全文索引

StringField

以String索引,只索引不分词,直接以整个字符串作为一个分词

SortedDocValuesField

以String索引并存储,用于排序(sorting)操作

SortedSetDocValuesField

以String索引并存储,用于聚合(faceting)、分组(grouping)、关联(joining)操作

NumericDocValuesField

以long索引并存储,用于评分、排序和值检索

SortedNumericDocValuesField

与NumericDocValuesField,常用于搜索结果排序

StoredField

存储Field值

 

Field中以FieldType定义索引的属性描述,包括以下内容:

  • indexOptions:Field是否分词
  • stored:Field是否存储
  • tokenized:Field是否分词
  • docValuesType:Field的DocValue类型
  • omitNorms:是否忽略Norm统一因子
  • storeTermVectors:TermVector相关的属性
  • storeTermVectorOffsets:TermVector相关的属性,存储的偏移量
  • storeTermVectorPositions:TermVector相关的属性,存储的位置信息
  • storeTermVectorPayloads:TermVector相关的属性,存储负载
  • frozen:Field是否冻结
  • dimensionCount:字段的维度数量
  • indexDimensionCount:索引的维度数量
  • dimensionNumBytes:每个维度的字节数

Term

Term(词汇项):索引过程中,经过Analyzer分析器,将Field解析成Token,基于Token添加额外索引信息等操作后,得到Term对象进行索引。Term是最小搜索单元,是实现倒排表Posting的基本元素,由两部分组成:词汇的文本信息、词汇所属Field名称。

解析流程如下图:Field1、Field2基于分词操作,从Token转成Term,Field3设置不分词,其FiledValue直接转成独立的Term。

Lucene全文索引与搜索入门篇_图示-adee97e1e791481199acfa8caeb1b09a.png

Analyzer

Analyzer(分词器):是由一组TokenStream串行组成的词汇分析链,定义将Field文本解析为搜索单元Term的策略,可通过IndexWriter构造方法指定Field的分词器。 用户可以很简单的对不同TokenStream组合和实现,得到自定义分词器。

TokenStream是分词处理流的抽象类,主要有两个子类:Tokenizer(定义分词逻辑),TokenFilter(定义分词后的转换操作)。Analyzer一般由一个Tokenizer和多个TokenFilter组成,其中Tokenizer是TokenFilter的前置。

Lucene全文索引与搜索入门篇_图示-2dd60d9a843b4ca1bb19c0e80589e438.png

 

Token

Token(词汇单元):在词汇解析过程中,由Tokenizers和TokenFilters过程中产生的分词对象,包括一系列的Attribute属性信息,定义该分词对象的关注属性,如偏移量、位置、词性等。

Lucene全文索引与搜索入门篇_图示-5821e590a74a49898059193b306c3356.png

倒排索引

Inverted Index(倒排索引):是搜索引擎的核心数据结构,对文档进行逆向排列,以文档Term为Key信息,关联包含该Term的文档信息,即文档原本的数据结构为:document -> terms,而倒排索引的数据结构为:term -> documents,使得基于term-based的全文检索更加高效。

Lucene的倒排索引主要有以下三部分构成:

  • Term Index(Term索引):是Term Directory的索引,使得Term可以被随机访问,判断Term是否存在,从Lucene4后,其数据结构使用FST,为每个Field维护一个FST树
  • Term Directory(Term字典):是Term的列表,每个Term包括了该Term下的统计信息(如文档频次)及元数据(如Term对应的Postings倒排表指针信息),该字典列表是按顺序存储的
  • Postings(倒排表):Term对应的倒排信息,主要包括:包含该Term的所有Document,每个Doc下的词频(TF),Term在Doc下的位置(Position),Term的Payload、Offsets等
Lucene全文索引与搜索入门篇_图示-11fb980b9ac54a1bbd5aa6edd54a08fc.png

整体架构

功能架构

Lucene功能主要包括两部分:索引,搜索

  • 索引:主要基于IndexWriter为执行入口,由DWPT(DocumentsWriterPerThread)实现了并发写,每个DWPT都有独立的内存空间执行索引链流程
  • 搜索:主要基于IndexSearcher为执行入口,可指定查询Executor实现并发搜索,每个搜索线程下,基于LeafReader以Segment为单位进行Term匹配读取
Lucene全文索引与搜索入门篇_图示-ba72c3ecbfb942f4a1a47f85d9b2bccf.png

逻辑架构

索引 和 搜索的逻辑架构图如下所示:

Lucene全文索引与搜索入门篇_图示-50366dbf221f46c2b26464e82fea8f4a.png

Lucene的核心功能索引和搜索都是在lucene.core子项目下实现,对应的源码包关系图如下:

Lucene全文索引与搜索入门篇_图示-cda76446e9254e2eb67e4cacba695843.png

Lucene核心包说明如下(官方文档):

  • org.apache.lucene.analysis:分词解析,定义Analyzer(分析器),从Reader对象读取数据并解析为TokenStream和对应的Attributes(Token属性);
  • org.apache.lucene.codecs:索引编解码,定义Lucene的不同索引文件的数据结构读写方式(编解码),定义抽象类和对应的各种实现;
  • org.apache.lucene.document:索引文件的对象,从Reader读取获取Document对象,Document对象可看做Field的集合;
  • org.apache.lucene.index:索引执行流程,提供两个主要类:IndexWriter创建索引,IndexReader访问读取索引;
  • org.apache.lucene.search:搜索执行流程,提供搜索对象Query和索引搜索的执行入口IndexSearcher,返回搜索结果TopDocs;
  • org.apache.lucene.store:索引存储,定义持久化数据的抽象类Directory,并提供了多种实现方式,其中常用FSDirectory从本地磁盘进行索引读写;
  • org.apache.lucene.util:提供处理数据结构的工具类;

使用操作

索引操作

创建索引

@Test
public void create() throws Exception {
    String content = "hello world index";
    String indexPath = "test/index";
    IndexWriterConfig iwc = new IndexWriterConfig(new StandardAnalyzer());
    try (Directory dir = FSDirectory.open(Paths.get(indexPath));
         IndexWriter writer = new IndexWriter(dir, iwc);
    ) {
        Document doc = new Document();
        doc.add(new StringField("path", indexPath, Field.Store.YES));
        doc.add(new TextField("content", content, Field.Store.YES));
        doc.add(new LongPoint("modified", Clock.systemUTC().millis()));
        writer.addDocument(doc);
    }

更新索引

根据指定Term更新索引信息

@Test
public void update() throws Exception {
    String content = "world index";
    String indexPath = "test/index";
    IndexWriterConfig iwc = new IndexWriterConfig(new StandardAnalyzer());
    try (Directory dir = FSDirectory.open(Paths.get(indexPath));
         IndexWriter writer = new IndexWriter(dir, iwc);
    ) {
        Document doc = new Document();
        doc.add(new StringField("path", indexPath + "_update", Field.Store.YES));
        doc.add(new TextField("content", content, Field.Store.YES));
        doc.add(new LongPoint("modified", Clock.systemUTC().millis()));
        writer.updateDocument(new Term("path", indexPath), doc);
    }
}

 

删除索引

@Test
public void delete() throws Exception {
    String fieldName = "content";
    String indexPath = "test/index";
    IndexWriterConfig iwc = new IndexWriterConfig(new StandardAnalyzer());
    try (Directory dir = FSDirectory.open(Paths.get(indexPath));
         IndexWriter writer = new IndexWriter(dir, iwc);
    ) {
        QueryParser parser = new QueryParser(fieldName, new StandardAnalyzer());
        Query query = parser.parse("hello world");
        long deleteCounts = writer.deleteDocuments(query);
        System.out.println("delete doc by query of count=" + deleteCounts);
    }
}

 

搜索操作

读取索引信息

@Test
public void read() throws Exception {
    String indexPath = "test/index";
    Directory dir = FSDirectory.open(Paths.get(indexPath));
    DirectoryReader reader = DirectoryReader.open(dir);
    IndexReader indexReader = new IndexSearcher(reader).getIndexReader();
    //获取索引信息
    System.out.println("索引leaves信息:" + indexReader.getContext().leaves());
    System.out.println("索引中文档数量:" + indexReader.numDocs());
    System.out.println("索引中文档的最大值:" + indexReader.maxDoc());
    System.out.println("索引中被删除文档数量:" + indexReader.numDeletedDocs());
}

简单搜索

@Test
public void search() throws Exception {
    String indexPath = "test/index";
    String fieldName = "content";
    Directory dir = FSDirectory.open(Paths.get(indexPath));
    IndexSearcher searcher = new IndexSearcher(DirectoryReader.open(dir));
    QueryParser parser = new QueryParser(fieldName, new StandardAnalyzer());
    Query query = parser.parse("hello world");
    TopDocs docs = searcher.search(query, 10);
    System.out.println("匹配的搜索条数:" + docs.totalHits.value);
    for (int i = 0; i < docs.scoreDocs.length; i++) {
        ScoreDoc scoreDoc = docs.scoreDocs[i];
        System.out.println("获取Document文档 docId=" + scoreDoc.doc + ",匹配打分=" + scoreDoc.score);
        Document doc = searcher.doc(scoreDoc.doc);
        Iterator<IndexableField> iterator = doc.iterator();
        while (iterator.hasNext()) {
            IndexableField field = iterator.next();
            String fieldValue = doc.get(field.name());
            System.out.println("[fieldName]=" + field.name() + ",[fieldValue]=" + fieldValue);
        }
        System.out.println();
    }
}

 

多条件搜索

多条件搜索基于BooleanQuery实现,示例如下:

@Test
public void booleanSearch() throws Exception {
    String indexPath = "test/index";
    Directory dir = FSDirectory.open(Paths.get(indexPath));
    IndexSearcher searcher = new IndexSearcher(DirectoryReader.open(dir));
    Query queryContent = new QueryParser("content", new StandardAnalyzer()).parse("hello world");
    BoostQuery boost = new BoostQuery(queryContent, 100);
    Query queryPath = new QueryParser("path",  new SimpleAnalyzer()).parse("test\\/index");
    BooleanQuery.Builder builder = new BooleanQuery.Builder();
    builder.add(queryContent, BooleanClause.Occur.MUST);
    builder.add(boost, BooleanClause.Occur.SHOULD);
    builder.add(queryPath, BooleanClause.Occur.MUST);
    BooleanQuery query = builder.build();
    TopDocs docs = searcher.search(query, 10);
    System.out.println("匹配的搜索条数:" + docs.totalHits.value);
    for (int i = 0; i < docs.scoreDocs.length; i++) {
        ScoreDoc scoreDoc = docs.scoreDocs[i];
        System.out.println("获取Document文档 docId=" + scoreDoc.doc + ",匹配打分=" + scoreDoc.score);
        Document doc = searcher.doc(scoreDoc.doc);
        Iterator<IndexableField> iterator = doc.iterator();
        while (iterator.hasNext()) {
            IndexableField field = iterator.next();
            String fieldValue = doc.get(field.name());
            System.out.println("[fieldName]=" + field.name() + ",[fieldValue]=" + fieldValue);
        }
        System.out.println();
    }
}
Occur

Occur是用来决定各个条件的逻辑关系,具体如下:

l Occur.SHOULD 或

l Occur.MUST 且

l Occur.MUST_NOT 且非

l Occur.FILTER 同 MUST,但该字句不参与评分

而这些逻辑组合之间也有要关注的地方(下面几点关注一下即可,跟版本有关,在8.1.0的版本中是直接意思,并没有出现下面的情况。应该是新版已经更新,6版本之前的可以关注下面的信息。):

  1. MUST和MUST:取得连个查询子句的交集。
  2. MUST和MUST_NOT:表示查询结果中不能包含MUST_NOT所对应得查询子句的检索结果。
  3. SHOULD与MUST_NOT:连用时,功能同MUST和MUST_NOT。
  4. SHOULD与MUST连用时,结果为MUST子句的检索结果,但是SHOULD可影响排序。
  5. SHOULD与SHOULD:表示“或”关系,最终检索结果为所有检索子句的并集。
  6. MUST_NOT和MUST_NOT:无意义,检索无结果。

总结

Lucene 建立了大数据检索的基础,其基于奥卡姆剃刀的原则,提供检索工具包而不提供更多应用功能。使得Lucene项目能够更专注于构建索引和搜索,也便于其他应用项目的集成与扩展。 
 


评论
User Image
提示:请评论与当前内容相关的回复,广告、推广或无关内容将被删除。

相关文章
Apache Lucene一款 Apache托管的全文索引组件,纯Java实现。Lucene的作用用户—&gtl;服务器—&gtl;Lucene API—&gtl;索引库—&gtl;数据库/文
背景介绍 Lucene是一款高性能、可扩展的信息检索工具库,是用于全文检索和搜寻的Java开放源码程序库,最初是由Doug Cutting所撰写,2000年发行
Apache lucene 与Java jdk版本对应关系表参考:Apache Lucene版本JDK版本备注
Lucene Field简介在 Apache Lucene 中,Field 类是文档中存储数据的基础。不同类型的 Field 用于存储不同类型的数据(如文本、数
StandardAnalyzer自带的标准分词器源码示例 public static void main(String[] args) throws IOExc
Hibernate Search 入门配置及简单实用,在本文中,我们将讨论Hibernate Search的基础知识,如何配置它,并且我们将实现一些简单的查询。
Vert.x java 入门,Vert.x这个框架在常规的web业务开发中估计还不是很成熟。但是了解了他的一些原理我觉得可以有一番作为。今天主要简单讲解下eclipse Vert.x是什么有什么...
spring boot入门,spring boot是一个崭新的spring框架分支项目,本文讲解其属性配置相关
spring boot又一个spring框架的经典项目,本文讲解spring boot入门的环境配置以及第一个项目,Spring Boot 入门教程
随着Spring Cloud 的越来越流行,国内很多公司也在开始使用该框架了
Spring 5 入门实战Say Hello,本博客主要讲解spring 5最基础的容器入门实战。
在本教程中,学习从spring boot 入门应用程序创建和使用RSS和Atom提要。 您必须在各种网站上(例如我们的RSS提要)以文本或图像按钮的形式看到这一点,邀请您“通过RSS订阅”。RS...
Vert.x 入门教程通过maven shade plugin插件打包发布项目,上一篇文章,我们主要初步的了解了下eclipse vert.x是什么有什么用。当然一个程序编写完成后最终还是需要打...
spring boot是一个崭新的spring框架分支项目,本文讲解基本的数据库配置
spring boot是一个崭新的spring框架分支项目,本文讲解spring boot中controller的常用注解使用