Lucene检索源码解析(上)

本文详细解析了Lucene查询语句的解析过程,从关键字处理到生成语法树,再到BooleanQuery的构造和条件字句。接着,文章介绍了如何计算IDF,以及在检索前的准备工作,如创建Weight、计算coord和queryNorm。通过对IndexSearcher的search方法的分析,揭示了查询权重的计算和归一化处理,为后续的检索和评分奠定了基础。
摘要由CSDN通过智能技术生成

有了Lucene得分公式(戳这里看详情)的基础,我们现在先跳过写索引的步骤,直接解析查询这块儿的代码(还是基于5.5.0)。另外由于内容实在太多,所以文章分为上下两部分介绍,上部分主要介绍实际检索前的一些处理,下部分介绍检索和评分。

一、场景

假设现在已经有多个文档被索引成功,索引目录为:D:\index。我们要对name域(Field)进行查询,代码如下:

Path path = Paths.get("D:\\index");
Directory directory = FSDirectory.open(path);
IndexReader indexReader = DirectoryReader.open(directory);
IndexSearcher indexSearcher = new IndexSearcher(indexReader);

Analyzer nameanalyzer = new StandardAnalyzer();
QueryParser nameParser = new QueryParser("name", nameanalyzer);
Query nameQuery = nameParser.parse("詹姆斯");

TopDocs topDocs = indexSearcher.search(nameQuery, 2);
......

二、查询语句解析

第一步需要先做关键字识别、词条处理等等,最终生成语法树。

我们创建QueryParser的时候,使用的是StandardAnalyzer分词器,analyzer主要完成分词、去除停用词、转换大小写等操作。QueryParser在parse生成Query的时候,会根据不同的查询类型生成不同的Query,比如WildcardQuery、RegexpQuery、PrefixQuery等等。在本例中,最终生成的是BooleanQuery,“詹姆斯”被分为三个词:詹  姆  斯。(当然也根据实际情况也可以不用分词,比如使用TermQuery)

BooleanQuery为布尔查询,支持四种条件字句:

MUST("+"):表示必须匹配该子句,类似于sql中的AND。

FILTER("#"):和MUST类似,但是它不参与评分。

SHOULD(""):表示可能匹配该字句。类似于sql中的OR,对于没有MUST字句的布尔查询,匹配文档至少需要匹配一个SHOULD字句。

MUST_NOT("-"):表示不匹配该字句。类似于sql中的!=。但是要注意的是,布尔查询不能仅仅只包含一个MUST_NOT字句。并且这些字句不会参与文档的评分。

使用这些条件,可以组成很复杂的复合查询。在我们的例子中,会根据分词结果生成三个查询子句,它们之间使用SHOULD关联:

                                      name:詹 SHOULD name:姆 SHOULD name:斯

将上述查询语句按照语法树分析:

它表示的是:查询“name”中包含“詹”或“姆”或“斯”的文档。当然,我们可以有更加复杂的语法树,比如我们加入“hobby”字段的查询项:在上述基础上还需要爱好篮球或电影:

它表示的是:查询“name”中包含“詹”或“姆”或“斯” 并且 “hobby”中包含“篮球”或“电影”的文档。这里我们发现,整个语法树的所有连接点,也就是非叶子节点,其实就是通过BooleanQuery实现的。像这样就可以生成复杂的复合查询,类似于SQL。

三、计算IDF

本例中,我们调用的:searcher.search(query,n)方法,它表示按照query查询,最多返回包含n条结果的TopDocs,接下来我们详细探索该方法的实现。

该方法默认调用的:searchAfter(after,query,n)方法,after参数可用于分页查询,在这里after为空:

我们来看看searchAfter是如何实现的:

public TopDocs searchAfter(final ScoreDoc after, Query query, int numHits) throws IOException {
    //reader是在我们读取索引目录的时候就生成的,reader.maxDoc会返回索引库总共的文档数量
    final int limit = Math.max(1, reader.maxDoc());
    if (after != null && after.doc >= limit) {
      throw new IllegalArgumentException("after.doc exceeds the number of documents in the reader: after.doc="
          + after.doc + " limit=" + limit);
    }
    //常规操作,numHits为我们指定的查询数量,如果大于文档数量,则直接替换为文档数量
    //但是这里没必要比较两次,个人认为是这个版本此处的“Bug”
    numHits = Math.min(numHits, limit);
    final int cappedNumHits = Math.min(numHits, limit);
	
    final CollectorManager<TopScoreDocCollector, TopDocs> manager = new CollectorManager<TopScoreDocCollector, TopDocs>() {

      @Override
      public TopScoreDocCollector newCollector() throws IOException {
        return TopScoreDocCollector.create(cappedNumHits, after);
      }

      @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(cappedNumHits, topDocs);
      }

    };

    return search(query, manager);
  }

我们发现该方法中创建了一个CollectorManager,这个CollectorManager是一个接口,它用于并行化的处理查询请求。怎么叫并行化的处理呢?我们知道Lucene的索引目录结构中有个很重要的内容:segment(段)。索引由多个段组成,Lucene需要针对所有的segment(段)进行文档收集,然后根据需要将结果进行汇总。IndexSearcher给我们提供了入口设置线程池,通过线程池,我们可以并行的对多个段进行索引,以提高检索效率。该接口中只有两个方法,

public interface CollectorManager<C extends Collector, T> {

  C newCollector() throws IOException;

  T reduce(Collection<C> collectors) throws IOException;
  
}

1:CollectorManager#newCollector()。创建一个Collector,Collector主要用于收集search的原始结果,并且实现排序、自定义结果过滤等等。但是需要保证每次查询都返回一个新的Collector,也就是不能被复用。Collector也是一个接口,包含两个方法:

public interface Collector {

  LeafCollector getLeafCollector(LeafReaderContext context) throws IOException;
  
  boolean needsScores();
}

注:关于Collector在检索阶段再详细介绍,我们现在知道有这么个东西就可以了。

1.1:Collector

1.1.1:Collector#getLeafCollector方法接受LeafReaderContext参数,返回一个LeafCollector。

注:关于LeafReaderContext,我们现在可以简单的将其当做是索引中每个segment(段)的“代表”,它包含segment(段)的一些基础数据和父级Context,后面会介绍。

LeafCollector还是为一个接口,Lucene使用它将收集文档和对文档打分解耦,其包含两个方法:

public interface LeafCollector {

  
  void setScorer(Scorer scorer) throws IOException;

  void collect(int doc) throws IOException;

}

1.1.2:Collector#needsScores,就和方法名描述的一样,此方法返回的布尔值结果表示该collector是否需要对匹配文档进行打分。

1.2 LeafCollector

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值