Lucene 源码分析之倒排索引(二)

Stella981
• 阅读 831

本文以及后面几篇文章将讲解如何定位 Lucene 中的倒排索引。内容很多,唯有静下心才能跟着思路遨游。

我们可以思考一下,哪个步骤与倒排索引有关,很容易想到检索文档一定是要查询倒排列表的,那么就从此处入手。检索文档通过调用 IndexSearcher.search(Query query, int n) 方法返回匹配的文档。

public class IndexSearcher {
    public TopDocs search(Query query, int n) throws IOException {
        return searchAfter(null, query, n);
    }

    public TopDocs searchAfter(ScoreDoc after, Query query, int numHits) throws IOException {
        // ...
        return search(query, manager);
    }

    public <C extends Collector, T> T search(Query query, CollectorManager<C, T> collectorManager) throws IOException {
        if (executor == null) {
            final C collector = collectorManager.newCollector();
            search(query, collector);
            return collectorManager.reduce(Collections.singletonList(collector));
        }
        // ...
    }
}

上面是 search 的调用链,最终调用的核心方法是 reduce(...),也就是说 reduce(...) 会返回匹配的文档。

下文通过聚焦 reduce(...) 方法定位 Lucene 中的倒排索引。

reduce(...) 方法的形参是 Collections.singletonList(collector),collector 是由 CollectorManager.newCollector() 方法创建的,而 CollectorManager 创建于上面代码中第二个方法 searchAfter 方法中的匿名内部类,代码如下。

public class IndexSearcher {
    public TopDocs searchAfter(ScoreDoc after, Query query, int numHits) throws IOException {
        // ...
        final CollectorManager<TopScoreDocCollector, TopDocs> manager = new CollectorManager<TopScoreDocCollector, TopDocs>() {
            @Override
            public TopScoreDocCollector newCollector() throws IOException {
                return TopScoreDocCollector.create(cappedNumHits, after);
            }
            // ...
        };
        // ...
    }
}

public abstract class TopScoreDocCollector extends TopDocsCollector<ScoreDoc> {
    public static TopScoreDocCollector create(int numHits, ScoreDoc after) {
        return new SimpleTopScoreDocCollector(numHits);
    }
}

也就是说 reduce 的形参是一个集合,该集合包含一个 SimpleTopScoreDocCollector 对象。

回到 reduce 的内部实现,调用方也是 searchAfter 方法中的匿名内部类 CollectorManager,代码如下。

public class IndexSearcher {
    public TopDocs searchAfter(ScoreDoc after, Query query, int numHits) throws IOException {
        // ...
        final CollectorManager<TopScoreDocCollector, TopDocs> manager = new CollectorManager<TopScoreDocCollector, TopDocs>() {
            // ...
            @Override
            public TopDocs reduce(Collection<TopScoreDocCollector> collectors) throws IOException {
                final TopDocs[] topDocs = new TopDocs[collectors.size()];
                int i = 0;
                for (TopScoreDocCollector collector : collectors) {
                    topDocs[i++] = collector.topDocs();
                }
                return TopDocs.merge(0, cappedNumHits, topDocs, true);
            }

        };
        // ...
    }
}

由于 reduce(...) 方法的形参仅有一个元素,reduce(...) 方法退化成执行 SimpleTopScoreDocCollector.topDocs(),其结果就是匹配的文档。

public abstract class TopScoreDocCollector extends TopDocsCollector<ScoreDoc> {
    private static class SimpleTopScoreDocCollector extends TopScoreDocCollector {
        // ...
    }
}

public abstract class TopDocsCollector<T extends ScoreDoc> implements Collector {
    public TopDocs topDocs() {
        return topDocs(0, topDocsSize());
    }

    public TopDocs topDocs(int start, int howMany) {
        // ...
        ScoreDoc[] results = new ScoreDoc[howMany];
        // ...
        populateResults(results, howMany);
        return newTopDocs(results, start);
    }

    protected void populateResults(ScoreDoc[] results, int howMany) {
        for (int i = howMany - 1; i >= 0; i--) { 
            results[i] = pq.pop();
        }
    }
}

SimpleTopScoreDocCollector 继承自 TopScoreDocCollector 继承自 TopDocsCollector,实际执行 TopDocsCollector.topDocs()。

时刻记住 reduce() 返回匹配的文档,也就是说 TopDocsCollector. topDocs() 返回匹配的文档。 results 作为 NewTopDocs 的成员变量一定包含了匹配的文档,results 又来自于 pq.pop(),那么 pq 一定包含了匹配的文档。

下面通过聚焦 SimpleTopScoreDocCollector 对象的 pq 定位倒排索引。

回顾 CollectorManager.reduce(...) 所在的 search(...) 方法,在初始化 SimpleTopScoreDocCollector 和 reduce(...) 之间唯一的方法就是另一个 search(…) 方法,一定是在这个方法中赋值了 pq,代码如下。

public class IndexSearcher {
    public void search(Query query, Collector results) throws IOException {
        search(leafContexts, createNormalizedWeight(query, results.needsScores()), results);
    }

    protected void search(List<LeafReaderContext> leaves, Weight weight, Collector collector) throws IOException {
        for (LeafReaderContext ctx : leaves) { // search each subreader
            final LeafCollector leafCollector = collector.getLeafCollector(ctx);
            BulkScorer scorer = weight.bulkScorer(ctx);
            scorer.score(leafCollector, ctx.reader().getLiveDocs());
        }
    }
}

一共就三个方法,究竟是在哪个方法中赋值了 pq 呢?一个个分析。

第一个方法,collector.getLeafCollector(ctx) 实际调用的就是 SimpleTopScoreDocCollector.getLeafCollector(ctx)。

public abstract class TopScoreDocCollector extends TopDocsCollector<ScoreDoc> {
    private static class SimpleTopScoreDocCollector extends TopScoreDocCollector {
        @Override
        public LeafCollector getLeafCollector(LeafReaderContext context) throws IOException {
            final int docBase = context.docBase;
            return new ScorerLeafCollector() {
                @Override
                public void collect(int doc) throws IOException {
                    float score = scorer.score();
                    totalHits++;               
                    pqTop.doc = doc + docBase;
                    pqTop.score = score;
                    pqTop = pq.updateTop();
                }
            };
        }
    }
}

可以看到 getLeafCollector(...) 方法返回的 ScorerLeafCollector 类提供了 collect(doc) 方法对 pq 进行操作。也就是说找到调用 collect(doc) 方法的地方也就找到了倒排索引。

下面通过聚焦找到调用 collect() 方法的来源来定位倒排索引。

第二个方法,weight.bulkScorer(ctx) 创建 BulkScorer,而 weight 由 createNormalizedWeight(…) 创建。

public class IndexSearcher {
    public Weight createNormalizedWeight(Query query, boolean needsScores) throws IOException {
        // ...
        return createWeight(query, needsScores, 1f);
    }

    public Weight createWeight(Query query, boolean needsScores, float boost) throws IOException {
        // ...
        Weight weight = query.createWeight(this, needsScores, boost);
        // ...
        return weight;
    }
}

假设 query 是最简单的 TermQuery,createWeight(…) 代码如下。

public class TermQuery extends Query {
    @Override
    public Weight createWeight(IndexSearcher searcher, boolean needsScores, float boost) throws IOException {
        // ...
        return new TermWeight(searcher, needsScores, boost, termState);
    }
}

最终返回的是 TermWeight 对象,那么 weight.bulkScorer(ctx) 实现类代码如下。

public abstract class Weight implements SegmentCacheable {
    public BulkScorer bulkScorer(LeafReaderContext context) throws IOException {        
        // ...
        return new DefaultBulkScorer(scorer);
    }
}

最终返回的是一个 DefaultBulkScorer 对象。

第三个方法,scorer.score(…),实际调用类是 DefaultBulkScorer,代码如下。

public abstract class Weight implements SegmentCacheable {
    protected static class DefaultBulkScorer extends BulkScorer {
        // ...
    }
}

public abstract class BulkScorer {
    public void score(LeafCollector collector, Bits acceptDocs) throws IOException {
        final int next = score(collector, acceptDocs, 0, DocIdSetIterator.NO_MORE_DOCS);
    }
}

BulkScorer.score(…) 内部调用的还是 DefaultBulkScorer 中重构的 score(…) 方法,代码如下。

public abstract class Weight implements SegmentCacheable {
    protected static class DefaultBulkScorer extends BulkScorer {
        @Override
        public int score(LeafCollector collector, Bits acceptDocs, int min, int max) throws IOException {
            collector.setScorer(scorer);
            if (scorer.docID() == -1 && min == 0 && max == DocIdSetIterator.NO_MORE_DOCS) {
                scoreAll(collector, iterator, twoPhase, acceptDocs);
                return DocIdSetIterator.NO_MORE_DOCS;
            }
        }

        static void scoreAll(LeafCollector collector, DocIdSetIterator iterator, TwoPhaseIterator twoPhase, Bits acceptDocs) throws IOException {
            if (twoPhase == null) {
                for (int doc = iterator.nextDoc(); doc != DocIdSetIterator.NO_MORE_DOCS; doc = iterator.nextDoc()) {
                    if (acceptDocs == null || acceptDocs.get(doc)) {
                        collector.collect(doc);
                    }
                }
            }
        }
    }
}

看到了什么!找到了调用 collect(…) 方法的代码。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Stella981 Stella981
3年前
ELK学习笔记之ElasticSearch的索引详解
0x00ElasticSearch的索引和MySQL的索引方式对比Elasticsearch是通过Lucene的倒排索引技术实现比关系型数据库更快的过滤。特别是它对多条件的过滤支持非常好,比如年龄在18和30之间,性别为女性这样的组合查询。倒排索引很多地方都有介绍,但是其比关系型
Python进阶者 Python进阶者
9个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
京东云开发者 京东云开发者
7个月前
倒排索引关键点普及
倒排索引倒排索引是什么?为什么es、hbase、doris、starrocks都有倒排索引?倒排索引(英文:InvertedIndex),是一种索引方法,常被用于全文检索系统中的一种单词文档映射结构。现代搜索引擎绝大多数的索引都是基于倒排索引来进行构建的,