Lucene查询的底层实现IndexSearch(上)

系列文章目录

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


前言

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

在这里插入图片描述

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树

IndexSearchpublic void search(Query query, Collector results)
            throws IOException {
        query = rewrite(query);  //创建Query树
        search(leafContexts, createWeight(query, results.scoreMode(), 1), results);
    }
……………………
(Querypublic Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
        throw new UnsupportedOperationException("Query " + this + " does not implement createWeight");
    }
……………………
//在本例中实际调用的是
(BooleanQuery)
@Override
    public 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()方法

 @Override
    public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
        //从IndexSearcher获取了IndexReaderContext
        // IndexReaderContext就是CompositeReaderContext,持有了一个或多个LeafReaderContext
        final 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 IS
            termState = 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中查询term
                if (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=1
                collectionStats = 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);
 @Override
  public 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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值