系列文章目录

(一)问答系统的文段检索
(二)lucene全文检索底层原理理解
(三)Lucene查询的底层实现IndexSearch(上)
(四)Lucene查询的底层实现IndexSearch(下)

文章目录

  • 系列文章目录
  • 前言
  • IndexReader
    • LeafReader
    • CompositeReader
  • IndexReaderContext
  • IndexReader 指向索引文件夹
  • QueryParser 解析查询语句生成查询对象
  • 搜索查询对象
  • 重写Query对象树
  • 多态
  • 创建weight树
    • 获取termstates
    • 构造TermWeight

前言

搜索的过程理解:
从索引中读出词典及倒排表信息
依据查询语句合并倒排表
得到结果文档集并对文档进行打分

IndexReader

  • 读取索引的媒介,但是它读取索引并不是“实时”的,如果索引发生了更新,对于已经创建的IndexReader是不可见的,除非重新打开一个Reader,或者使用Lucene为我们提供的DirectoryReader#openIfChanged。
  • IndexReader主要分为两个大类型:LeafReader 和 CompositeReader。

LeafReader

  • 抽象类,索引检索工作,最终都是依赖这个类型的Reader实现,是“原子的”,不再包含其它子Reader。
  • 出于效率考虑,LeafReader通过docId返回文档,docId是唯一的正整数,但是它随着文档的更新或删除,可能会发生改变。

CompositeReader

  • 一种复合Reader,持有多个LeafReader,只能通过它持有的LeafReader获取字段,它不能直接进行检索。
  • DirectoryReader#open(Directory)创建的Reader其实就是一种CompositeReader。它通过List类型的变量持有多个SegmentReader,而我们上面提到了,SegmentReader就是一种LeafReader。

IndexReaderContext

可以理解为IndexReader的“上下文”,而它同样表征了IndexReader之间的层次关系,每个IndexReader都有一个IndexReaderContext。它通过IndexReader创建。就像一个CompositeReader下可能包含有多个LeafReader 一样,一个CompositeReaderContext可能有多个LeafReaderContext,一个IndexReaderContext则有它的父级CompositeReaderContext。我们前面提到了,它包含一些segment的基础信息,通过它我们可以方便的获取该Leaf(可以简单理解为segment)的docBase(docId在该segment中的起始值)、ord(sengment序号)等等。

IndexReader 指向索引文件夹

对应代码分析

Directory directory = FSDirectory.open(new File("file_path");
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
IndexReader indexReader = DirectoryReader.open(directory);

调用了 DirectoryReader.open(Directory, IndexDeletionPolicy, IndexCommit, boolean, int) 函数,其主要作用是生成一个 SegmentInfos.FindSegmentsFile 对象,并用它来找到此索引文件中所有的段,并打开这些段。

QueryParser 解析查询语句生成查询对象

此过程相对复杂,涉及 JavaCC,QueryParser,分词器,查询语法等,要说明的是,根据查询语句生成的是一个 Query 树,这棵树很重要,并且会生成其他的树,一直贯穿整个索引过程。

QueryParser queryParser = new QueryParser("content", new IKAnalyzer());//参数1:默认搜索域,参数2:分析器对象//queryParser.parse: 根据查询语句生成的是一个 Query 树Query query = queryParser.parse("好好学习呀");

搜索查询对象

TopDocs topDocs = indexSearcher.search(query, 3);
ScoreDoc[] scoreDocs = topDocs.scoreDocs;

其最终调用 search(createWeight(query), filter, n);

重写Query对象树

递归
对每个Query对象进行重写
重写后的对象加入新的对象树

?问题:Query是什么结构?

public Query rewrite(Query original) throws IOException {Query query = original;for (Query rewrittenQuery = query.rewrite(reader);rewrittenQuery != query;rewrittenQuery = query.rewrite(reader)) {query = rewrittenQuery;}return query;}

Weight的创建,会“递归”创建,对所有Query都会创建Weight,上层Weight通过一个List存有下层Weight列表的引用,就像一棵树一样:

多态

涉及到java语法:

多态是同一个行为具有多个不同表现形式或形态的能力
多态就是同一个接口,使用不同的实例而执行不同操作
使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。

创建weight树

(IndexSearch)
public void search(Query query, Collector results)throws IOException {query = rewrite(query);  //创建Query树search(leafContexts, createWeight(query, results.scoreMode(), 1), results);}
……………………
(Query)
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {throw new UnsupportedOperationException("Query " + this + " does not implement createWeight");}
……………………
//在本例中实际调用的是
(BooleanQuery)
@Overridepublic Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {BooleanQuery query = this;if (scoreMode.needsScores() == false) {query = rewriteNoScoring();}return new BooleanWeight(query, searcher, scoreMode, boost);}


weight的实现,不断递归调用

BooleanWeight(BooleanQuery query, IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {super(query);this.query = query;this.scoreMode = scoreMode;this.similarity = searcher.getSimilarity();weightedClauses = new ArrayList<>();for (BooleanClause c : query) {Weight w = searcher.createWeight(c.getQuery(), c.isScoring() ? scoreMode : ScoreMode.COMPLETE_NO_SCORES, boost);weightedClauses.add(new WeightedBooleanClause(c, w));}}

这里结合BooleanQuery,其子query都是TermQuery,故c.getQuery()得到的query类型为TermQuery。
计算子query的权重时根据多态,调用TermQuery对应的createweight()方法

 @Overridepublic Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {//从IndexSearcher获取了IndexReaderContext// IndexReaderContext就是CompositeReaderContext,持有了一个或多个LeafReaderContextfinal IndexReaderContext context = searcher.getTopReaderContext();final TermStates termState;if (perReaderTermState == null|| perReaderTermState.wasBuiltFor(context) == false) {termState = TermStates.build(context, term, scoreMode.needsScores());} else {// PRTS was pre-build for this IStermState = this.perReaderTermState;}return new TermWeight(searcher, scoreMode, boost, termState);}

理解:

  • 先从IndexSearcher获取了IndexReaderContext,这里的IndexReaderContext就是我们前面提到的CompositeReaderContext,它持有了一个或多个LeafReaderContext。
  • perReaderTermState可以理解为一个“缓存”。在创建一次之后,我们可以将其保留起来,只要Context没发生变更就可以重用该TermStates 。
  • termStates是通过TermStates.build()方法创建的,该方法是一个静态方法,参数包含IndexReaderContext和Term。它会在所有的LeafReaderContext中寻找指定的Term
termState = TermStates.build(context, term, scoreMode.needsScores());

获取termstates

即获取包含term的content的相关信息,包括该Term在当前Context中一些状态信息的描述,它我们可以查找该term的频率信息、倒排信息、处于哪一个block等等。

核心代码如下:

    //从顶级 IndexReaderContext 和给定的术语创建术语状态。// 此方法将在所有上下文的叶读取器中查找给定的术语,// 并使用叶读取器的序号在返回的术语状态中注册包含该术语的每个读取器。// 注意:给定的上下文必须是顶级上下文。public static TermStates build(IndexReaderContext context, Term term, boolean needsStats)throws IOException {assert context != null && context.isTopLevel;final TermStates perReaderTermState = new TermStates(needsStats ? null : term, context);if (needsStats) {for (final LeafReaderContext ctx : context.leaves()) {//if (DEBUG) System.out.println("  r=" + leaves[i].reader);TermsEnum termsEnum = loadTermsEnum(ctx, term);//从context中查询termif (termsEnum != null) {final TermState termState = termsEnum.termState();//如果找到到了该term,则返回其TermState/**TermState,就相当于该Term在当前Context中一些状态信息的描述,* 它我们可以查找该term的频率信息、倒排信息、处于哪一个block等等。* 比如:docFreq:此context中,包含该term的文档数量(注:从这里往后说到的contex可以简单对应segment)*     totalTermFreq:此context中,该term在所有文档中出现的总次数**///if (DEBUG) System.out.println("    found");perReaderTermState.register(termState, ctx.ord, termsEnum.docFreq(), termsEnum.totalTermFreq());}}}return perReaderTermState;}

TermsEnum是一个抽象类,可以把它理解该context下指定field下的terms的迭代器描述。
TermState,就相当于该Term在当前Context中一些状态信息的描述,它我们可以查找该term的频率信息、倒排信息、处于哪一个block等等。比如:

docFreq:此context中,包含该term的文档数量(注:从这里往后说到的contex可以简单对应segment)
totalTermFreq:此context中,该term在所有文档中出现的总次数

>  perReaderTermState.register(termState, ctx.ord, termsEnum.docFreq(), termsEnum.totalTermFreq());
  • 如果某一个LeafReaderContext包含指定的Term,那么就会将对应的Context通过其序号(oridinal)注册到TermStates中,最后将termStates返回。
termState = this.perReaderTermState;

有了term的频率信息、倒排信息、处于哪一个block等信息之后进行权重计算

new TermWeight(searcher, scoreMode, boost, termState);

构造TermWeight

TermWeight的构造方法需要三个参数:

  • searcher:就是我们的IndexSearcher;
  • needsScores:布尔型变量,表明是否需要进行评分(还记得前面提到的Collector#needsScores()吗?);
  • termState:就是上面我们获取的TermState。
public TermWeight(IndexSearcher searcher, ScoreMode scoreMode,float boost, TermStates termStates) throws IOException {super(TermQuery.this);if (scoreMode.needsScores() && termStates == null) {throw new IllegalStateException("termStates are required when scores are needed");}this.scoreMode = scoreMode;this.termStates = termStates;this.similarity = searcher.getSimilarity();//从searcher中获取了一个Similarity。这个Similarity是一个抽象类,相当于我们的评分组件,可以在初始化IndexSearcher的时候指定。//如果我们要自定义该组件,就继承该抽象类实现对应的方法,然后设置到IndexSearcher中即可。final CollectionStatistics collectionStats;final TermStatistics termStats;if (scoreMode.needsScores()) {collectionStats = searcher.collectionStatistics(term.field());termStats = termStates.docFreq() > 0 ? searcher.termStatistics(term, termStates.docFreq(), termStates.totalTermFreq()) : null;} else {// we do not need the actual stats, use fake stats with docFreq=maxDoc=ttf=1collectionStats = new CollectionStatistics(term.field(), 1, 1, 1, 1);/*** maxDoc:该context中的文档数量(无论文档是否包含该field)。* docCount:该context中,包含该field,且field下至少有一个term(不需要和指定term相同)的文档数量。* sumDocFreq:该context的所有文档中,该field下,所有词的docFreq的总和。* sumTotalTermFreq:该context中的所有文档中,该field下,所有词的totalTermFreq的总和。*/termStats = new TermStatistics(term.bytes(), 1, 1);}if (termStats == null) {this.simScorer = null; // term doesn't exist in any segment, we won't use similarity at all} else {this.simScorer = similarity.scorer(boost, collectionStats, termStats);}}

需要进行评分则需通过searcher获取该Field的一些基础信息(CollectionStatistics),比如:

maxDoc:该context中的文档数量(无论文档是否包含该field)。
docCount:该context中,包含该field,且field下至少有一个term(不需要和指定term相同)的文档数量。
sumDocFreq:该context的所有文档中,该field下,所有词的docFreq的总和。
sumTotalTermFreq:该context中的所有文档中,该field下,所有词的totalTermFreq的总和。

承接上个方法

this.simScorer = similarity.scorer(boost, collectionStats, termStats);
 @Overridepublic final SimScorer scorer(float boost, CollectionStatistics collectionStats, TermStatistics... termStats) {Explanation idf = termStats.length == 1 ? idfExplain(collectionStats, termStats[0]) : idfExplain(collectionStats, termStats);float avgdl = avgFieldLength(collectionStats);float[] cache = new float[256];for (int i = 0; i < cache.length; i++) {cache[i] = 1f / (k1 * ((1 - b) + b * LENGTH_TABLE[i] / avgdl));}return new BM25Scorer(boost, k1, b, idf, avgdl, cache);}

idf的计算:

public Explanation idfExplain(CollectionStatistics collectionStats, TermStatistics termStats) {final long df = termStats.docFreq();final long docCount = collectionStats.docCount();final float idf = idf(df, docCount);return Explanation.match(idf, "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",Explanation.match(df, "n, number of documents containing term"),Explanation.match(docCount, "N, total number of documents with field"));}

默认实现将平均值计算为

 /** The default implementation computes the average as <code>sumTotalTermFreq / docCount</code> */protected float avgFieldLength(CollectionStatistics collectionStats) {return (float) (collectionStats.sumTotalTermFreq() / (double) collectionStats.docCount());}

得到BM25Score

 return new BM25Scorer(boost, k1, b, idf, avgdl, cache);
BM25Scorer(float boost, float k1, float b, Explanation idf, float avgdl, float[] cache) {this.boost = boost;this.idf = idf;this.avgdl = avgdl;this.k1 = k1;this.b = b;this.cache = cache;this.weight = boost * idf.getValue().floatValue();}

参考博客链接:https://blog.csdn.net/huangzhilin2015/article/details/89329854

Lucene查询的底层实现IndexSearch(上)相关推荐

  1. lucene 查询示例_高级Lucene查询示例

    lucene 查询示例 本文是我们名为" Apache Lucene基础知识 "的学院课程的一部分. 在本课程中,您将了解Lucene. 您将了解为什么这样的库很重要,然后了解Lu ...

  2. 高级Lucene查询示例

    本文是我们名为" Apache Lucene基础知识 "的学院课程的一部分. 在本课程中,您将了解Lucene. 您将了解为什么这样的库很重要,然后了解Lucene中搜索的工作方式 ...

  3. lucene 查询示例_Lucene查询(搜索)语法示例

    lucene 查询示例 本文是我们名为" Apache Lucene基础知识 "的学院课程的一部分. 在本课程中,您将了解Lucene. 您将了解为什么这样的库很重要,然后了解Lu ...

  4. lucene查询原理

    lucene查询原理 1. lucene 数据模型 2. lucene 查询过程 3. SkipList 哨兵数组 skipDoc docDeltaBuffer Lucene中使用读取跳表SkipLi ...

  5. Lucene查询语法和使用

    Lucene查询语法和使用 (1)直接输入单词查询,比如输入Windows: 也可以根据一个关键词(key) 配上该关键词的值,精确查找,如选择关键词为ip,查找一个已知的ip地址: 也可以这样查询, ...

  6. lucene查询语法,适用于ELk:kibana查询

    lucene查询语法,适用于ELk:kibana查询 Kibana在ELK中扮演着数据可视化角色,用来查询及展示数据: Elasticsearch查询采用的是luncene搜索引擎,其4过滤查询语法和 ...

  7. Lucene查询语法详解

    Lucene查询 Lucene查询语法以可读的方式书写,然后使用JavaCC进行词法转换,转换成机器可识别的查询. 下面着重介绍下Lucene支持的查询: Terms词语查询 词语搜索,支持 单词 和 ...

  8. 【示例】Lucene查询索引库编程步骤

    Lucene查询索引库编程步骤

  9. 接口查询的数据生成excel上传到七牛云

    接口查询的数据生成excel上传到七牛云 一.注册七牛云并新建一个存储空间 我们公司已经有一个七牛云的账号,登录进去之后创建一个新的对象存储空间.命名为eval_mobile.酒会有一个默认的融合 C ...

最新文章

  1. java.lang.ClassCastException: com.sun.proxy.$Proxy2 cannot be cast to...异常
  2. php 将数组导出excel,#php 怎样将 数组导出excel文件#前端导出excel表格
  3. Gartner魔力象限IBM被评为固态阵列市场领导厂商
  4. html iframe 播放视频播放,播放iframe视频点击链接javascript
  5. 加速时光,让你永远70岁的「变老神器」FaceAPP突然爆红,却恐遭美国封杀
  6. 删除git库中untracked files(未监控)的文件
  7. pythonwhile循环怎么修改数据类型_python基础--数据类型循环
  8. 使用Servlet上传多张图片——Service层(ProductInfoService.java和ProductInfoServiceImpl)
  9. Docker - 实战TLS加密通讯
  10. python 入门基础-如何学习Python,以及新手如何入门?
  11. python画图程序没有图_Python实现画图软件功能方法详解
  12. 链家爬虫python_python爬虫-链家租房信息获取
  13. D3D9学习笔记之基础几何体的深入应用(一)
  14. (PTA)7-2 比较大小 (10分)
  15. 问题:我的xmindpro从桌面打开就弹窗发生错误
  16. react手脚架安装
  17. 7-2 实验二 银行利息结算
  18. 利用Python爬取《囧妈》豆瓣短评数据,并进行snownlp情感分析
  19. 2019年美赛建模D题后续(2)
  20. 代码没写完,哪里有脸睡觉!程序员专属壁纸 3.0 版

热门文章

  1. java 分块上传_Java 文件分块上传客户端和服务器端源代码
  2. 基于DDD的微服务架构设计
  3. 微信小程序 登录 服务器 c,asp.net core 3.x 微信小程序登录库(也可用于abp)
  4. 一个QQ靓号,还不如一碗牛肉面?
  5. js获取 url 参数
  6. ENSP之静态、缺省路由的配置实验
  7. Web前端-jQuery(四)
  8. 博恩.崔西七步成功公式
  9. 虚拟化系列-Citrix XenServer 6.1 安装与配置
  10. spi nor flash驱动