搜索引擎进行信息检索的优化策略方法(Java中Lucene执行索引、查询等工作原理及解决办法 )

优采云 发布时间: 2022-04-15 18:21

  搜索引擎进行信息检索的优化策略方法(Java中Lucene执行索引、查询等工作原理及解决办法

)

  一、Lucene 简介1.1 什么是 Lucene?1.2 Lucene使用场景

  适用于需要少量数据索引的场景。当索引量过大时,需要使用ES、Solr等全文搜索服务器来实现搜索功能。

  1.3 你能从这篇文章中学到什么?

  本文旨在分享Lucene搜索引擎源码阅读和功能开发的心得体会。Lucene 采用 7.3.1 版本。

  二、Lucene 基本工作流程

  索引的生成分为两部分:

  1. 创建阶段:

  2. 搜索阶段:

  索引创建和搜索过程如下图所示:

  

  三、Lucene索引构成3.1个前向索引

  Lucene 的基本层次结构由五个部分组成:索引、段、文档、域和单词。前向索引的生成是基于Lucene的基本层次结构逐级处理文档,分解领域存储词的过程。

  

  索引文件的层次关系如图1所示:

  3.2 倒排索引

  Lucene全文索引的核心是一种基于倒排索引的快速索引机制。

  倒排索引的原理如图2所示。倒排索引就是简单的基于分析器对文本内容进行分词,记录每个词出现在哪个文章中,从而通过搜索词进行查询用户输入 文章 收录该单词。

  

  **问题:** 使用上述倒排索引时,每次都需要将索引词加载到内存中。到达内存后,内存损失很大。

  解决方案:从Lucene4开始,Lucene使用FST来减少索引词造成的空间消耗。

  FST(Finite StateTransducers),中文名有限状态机转换器。其主要特点在于以下四点:

  具体存储方式如图3所示:

  

  倒排索引相关文件包括三个文件:.tip、.tim和.doc,其中:

  3.3 索引查询和文档搜索过程

  Lucene 使用倒排索引来定位需要查询的文档号。通过文档编号搜索文档后,使用词重等信息对文档进行排序并返回。

  文件格式如图4所示:

  

  以上主要讲解了Lucene的工作原理,下面将介绍Lucene在Java中的相关代码,进行索引、查询等操作。

  四、Lucene的增删改操作

  Lucene项目中文本的解析、存储等操作都是由IndexWriter类实现的。IndexWriter 文件主要由 Directory 和 IndexWriterConfig 两个类组成。其中:

  目录:用于指定存放索引文件的目录类型。既然需要搜索文本内容,自然是先将文本内容和索引信息写入目录。目录是一个抽象类,它允许索引存储的许多不同实现。常见的存储方式一般有本地存储(FSDirectory)、内存(RAMDirectory)等。

  IndexWriterConfig:用于在写入文件内容时指定IndexWriter的相关配置,包括OpenMode索引构建方式、相似度相关算法等。

  IndexWriter 究竟是如何对索引进行操作的?下面简单分析下IndexWriter索引操作的相关源码。

  4.1. 文档补充

  一种。Lucene 会为每个文档创建一个 ThreadState 对象,该对象持有 DocumentWriterPerThread 来执行文件的增删改查操作;

  ThreadState getAndLock(Thread requestingThread, DocumentsWriter documentsWriter) {

ThreadState threadState = null;

synchronized (this) {

if (freeList.isEmpty()) {

// 如果不存在已创建的空闲ThreadState,则新创建一个

return newThreadState();

} else {

// freeList后进先出,仅使用有限的ThreadState操作索引

threadState = freeList.remove(freeList.size()-1);

// 优先使用已经初始化过DocumentWriterPerThread的ThreadState,并将其与当前

// ThreadState换位,将其移到队尾优先使用

if (threadState.dwpt == null) {

for(int i=0;i IndexWriter.MAX_STORED_STRING_LENGTH) {

throw new IllegalArgumentException("stored field \"" + field.name() + "\" is too large (" + value.length() + " characters) to store");

}

try {

storedFieldsConsumer.writeField(fp.fieldInfo, field);

} catch (Throwable th) {

throw AbortingException.wrap(th);

}

}

}

// 建立DocValue(通过文档查询文档下包含了哪些词)

DocValuesType dvType = fieldType.docValuesType();

if (dvType == null) {

throw new NullPointerException("docValuesType must not be null (field: \"" + fieldName + "\")");

}

if (dvType != DocValuesType.NONE) {

if (fp == null) {

fp = getOrAddField(fieldName, fieldType, false);

}

indexDocValue(fp, dvType, field);

}

if (fieldType.pointDimensionCount() != 0) {

if (fp == null) {

fp = getOrAddField(fieldName, fieldType, false);

}

indexPoint(fp, field);

}

  C。要分析Field,首先需要构造一个TokenStream类,用于生成和转换token流。TokenStream 有两个重要的派生类,Tokenizer 和 TokenFilter,其中 Tokenizer 用于通过 java.io.Reader 类读取字符,生成 Token 流,然后通过任意数量的 TokenFilter 处理这些输入的 Token 流。具体源码如下:

  // invert:对Field进行分词处理首先需要将Field转化为TokenStream

try (TokenStream stream = tokenStream = field.tokenStream(docState.analyzer, tokenStream))

// TokenStream在不同分词器下实现不同,根据不同分词器返回相应的TokenStream

if (tokenStream != null) {

return tokenStream;

} else if (readerValue() != null) {

return analyzer.tokenStream(name(), readerValue());

} else if (stringValue() != null) {

return analyzer.tokenStream(name(), stringValue());

}

public final TokenStream tokenStream(final String fieldName, final Reader reader) {

// 通过复用策略,如果TokenStreamComponents中已经存在Component则复用。

TokenStreamComponents components = reuseStrategy.getReusableComponents(this, fieldName);

final Reader r = initReader(fieldName, reader);

// 如果Component不存在,则根据分词器创建对应的Components。

if (components == null) {

components = createComponents(fieldName);

reuseStrategy.setReusableComponents(this, fieldName, components);

}

// 将java.io.Reader输入流传入Component中。

components.setReader(r);

return components.getTokenStream();

}

  d。根据IndexWriterConfig中配置的分词器,通过策略模式返回分词器对应的分词器组件。针对不同的语言和不同的分词需求,分词组件有很多不同的实现方式。

  以 StandardAnalyzer 为例:

  // 标准分词器创建Component过程,涵盖了标准分词处理器、Term转化小写、常用词过滤三个功能

protected TokenStreamComponents createComponents(final String fieldName) {

final StandardTokenizer src = new StandardTokenizer();

src.setMaxTokenLength(maxTokenLength);

TokenStream tok = new StandardFilter(src);

tok = new LowerCaseFilter(tok);

tok = new StopFilter(tok, stopwords);

return new TokenStreamComponents(src, tok) {

@Override

protected void setReader(final Reader reader) {

src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength);

super.setReader(reader);

}

};

}

  e. 获取到TokenStream后,通过TokenStream中的incrementToken方法分析获取属性,然后通过TermsHashPerField下的add方法构造倒排表,最后将Field的相关数据存储在FreqProxPostingsArray类型的freqProxPostingsArray中, TermVectorsPostingsArray 的 termVectorsPostingsArray。构成一个倒置表;

  // 以LowerCaseFilter为例,通过其下的increamentToken将Token中的字符转化为小写

public final boolean incrementToken() throws IOException {

if (input.incrementToken()) {

CharacterUtils.toLowerCase(termAtt.buffer(), 0, termAtt.length());

return true;

} else

return false;

}

   try (TokenStream stream = tokenStream = field.tokenStream(docState.analyzer, tokenStream)) {

// reset TokenStream

stream.reset();

invertState.setAttributeSource(stream);

termsHashPerField.start(field, first);

// 分析并获取Token属性

while (stream.incrementToken()) {

……

try {

// 构建倒排表

termsHashPerField.add();

} catch (MaxBytesLengthExceededException e) {

……

} catch (Throwable th) {

throw AbortingException.wrap(th);

}

}

……

}

  4.2 删除文件

  一种。Lucene下要删除一个文档,首先将要删除的Term或者Query添加到删除队列中;

  synchronized long deleteTerms(final Term... terms) throws IOException {

// TODO why is this synchronized?

final DocumentsWriterDeleteQueue deleteQueue = this.deleteQueue;

// 文档删除操作是将删除的词信息添加到删除队列中,根据flush策略进行删除

long seqNo = deleteQueue.addDelete(terms);

flushControl.doOnDelete();

lastSeqNo = Math.max(lastSeqNo, seqNo);

if (applyAllDeletes(deleteQueue)) {

seqNo = -seqNo;

}

return seqNo;

}

  湾。根据 Flush 策略触发删除操作;

  private boolean applyAllDeletes(DocumentsWriterDeleteQueue deleteQueue) throws IOException {

// 判断是否满足删除条件 --> onDelete

if (flushControl.getAndResetApplyAllDeletes()) {

if (deleteQueue != null) {

ticketQueue.addDeletes(deleteQueue);

}

// 指定执行删除操作的event

putEvent(ApplyDeletesEvent.INSTANCE); // apply deletes event forces a purge

return true;

}

return false;

}

  public void onDelete(DocumentsWriterFlushControl control, ThreadState state) {

// 判断并设置是否满足删除条件

if ((flushOnRAM() && control.getDeleteBytesUsed() > 1024*1024*indexWriterConfig.getRAMBufferSizeMB())) {

control.setApplyAllDeletes();

if (infoStream.isEnabled("FP")) {

infoStream.message("FP", "force apply deletes bytesUsed=" + control.getDeleteBytesUsed() + " vs ramBufferMB=" + indexWriterConfig.getRAMBufferSizeMB());

}

}

}

  4.3 文档更新

  文档的更新是一个先删除再插入的过程,本文不再赘述。

  4.4 索引刷新

  写入一定数量的文档后,某个线程会触发IndexWriter的Flush操作生成segment,将内存中的Document信息写入硬盘。Flush 操作目前只有一种策略:FlushByRamOrCountsPolicy。FlushByRamOrCountsPolicy 基于两种策略自动执行 Flush 操作:

  其中,activeBytes()是dwpt采集的索引占用的内存量,deleteByteUsed是删除索引的量。

  @Override

public void onInsert(DocumentsWriterFlushControl control, ThreadState state) {

// 根据文档数进行Flush

if (flushOnDocCount()

&& state.dwpt.getNumDocsInRAM() >= indexWriterConfig

.getMaxBufferedDocs()) {

// Flush this state by num docs

control.setFlushPending(state);

// 根据内存使用量进行Flush

} else if (flushOnRAM()) {// flush by RAM

final long limit = (long) (indexWriterConfig.getRAMBufferSizeMB() * 1024.d * 1024.d);

final long totalRam = control.activeBytes() + control.getDeleteBytesUsed();

if (totalRam >= limit) {

if (infoStream.isEnabled("FP")) {

infoStream.message("FP", "trigger flush: activeBytes=" + control.activeBytes() + " deleteBytes=" + control.getDeleteBytesUsed() + " vs limit=" + limit);

}

markLargestWriterPending(control, state, totalRam);

}

}

}

  将内存信息写入索引库。

  

  索引冲洗分为主动冲洗和自动冲洗。该策略触发的Flush操作为Automatic Flush。Active Flush 的执行与 Automatic Flush 的执行有很大的不同。本文不会详细介绍 Active Flush。如果您需要了解,请跳至链接。

  4.5 索引段合并

  在索引 Flush 时,每个 dwpt 都会生成一个单独的段。当段数过多时,全文搜索可能会跨越多个段,导致多次加载。因此,需要合并太多的段。

  通过 MergeScheduler 管理段合并的执行。mergeScheduler 还收录多种管理策略,包括 NoMergeScheduler、SerialMergeScheduler 和 ConcurrentMergeScheduler。

  合并操作首先需要通过updatePendingMerges方法根据段合并策略查询需要合并的段。有许多类型的段合并策略。本文只介绍Lucene默认使用的两种段合并策略:TieredMergePolicy和LogMergePolicy。

<p>private synchronized boolean updatePendingMerges(MergePolicy mergePolicy, MergeTrigger trigger, int maxNumSegments)

throws IOException {

final MergePolicy.MergeSpecification spec;

// 查询需要合并的段

if (maxNumSegments != UNBOUNDED_MAX_MERGE_SEGMENTS) {

assert trigger == MergeTrigger.EXPLICIT || trigger == MergeTrigger.MERGE_FINISHED :

"Expected EXPLICT or MERGE_FINISHED as trigger even with maxNumSegments set but was: " + trigger.name();

spec = mergePolicy.findForcedMerges(segmentInfos, maxNumSegments, Collections.unmodifiableMap(segmentsToMerge), this);

newMergesFound = spec != null;

if (newMergesFound) {

final int numMerges = spec.merges.size();

for(int i=0;i

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线