Elasticsearch(Lucene)检索关联性匹配算法(BM25)的源码解析

计算单个doc的相似性评分


private class BM25DocScorer extends SimScorer {

	private final BM25Stats stats;
    private final float weightValue; // boost * idf * (k1 + 1)
    private final NumericDocValues norms;
    /**
     * precomputed cache for all length values
     */
    private final float[] lengthCache;
    /**
     * precomputed norm[256] with k1 * ((1 - b) + b * dl / avgdl)
     */
    private final float[] cache;


	BM25DocScorer(BM25Stats stats, int indexCreatedVersionMajor, NumericDocValues norms) throws IOException {
	    this.stats = stats;
	    // 权重 = 原始权重 * (k1 +1), k1:控制非线性的词频的标准化因子,就是减少非常高的词频的影响程度,默认值1.2
	    this.weightValue = stats.weight * (k1 + 1);
	    this.norms = norms;
	    // 当前Lucene版本一般都 > 7 , 所以能使用LENGTH_TABLE
	    if (indexCreatedVersionMajor >= 7) {
	        lengthCache = LENGTH_TABLE;
	        cache = stats.cache;
	    } else {
	        lengthCache = OLD_LENGTH_TABLE;
	        cache = stats.oldCache;
	    }
	}

	/**
	  * 给单个document算分
	   *
	   * @param doc  document id within the inverted index segment
	   * @param freq sloppy term frequency
	   * @return
	   * @throws IOException
	   */
	  @Override
	  public float score(int doc, float freq) throws IOException {
	      // if there are no norms, we act as if b=0
	      float norm;
	      if (norms == null) {
	          norm = k1;
	      } else {
	          if (norms.advanceExact(doc)) {
	          	  // cache里每个元素的值得生成方式:  k1 * ((1 - b) + b * LENGTH_TABLE[i] / avgdl);
	          	  // 根据当前field的docValues来从cache里获取长度因子元素
	              norm = cache[((byte)norms.longValue()) & 0xFF];
	          } else {
	              norm = cache[0];
	          }
	      }
	      // weightValue  = stats.weight * (k1 + 1)
	      // weightValue  = idf.getValue() * boost * (k1 + 1)
	      // weightValue  = Math.log(1 + (docCount - docFreq + 0.5D) / (docFreq + 0.5D)) * boost * (k1 + 1)
	      return weightValue * freq / (freq + norm);
	  }
}
  

static {
    for (int i = 1; i < 256; i++) {
        float f = SmallFloat.byte315ToFloat((byte)i);
        OLD_LENGTH_TABLE[i] = 1.0f / (f * f);
    }
    OLD_LENGTH_TABLE[0] = 1.0f / OLD_LENGTH_TABLE[255]; // otherwise inf

    for (int i = 0; i < 256; i++) {
    	// LENGTH_TABLE 存储 的值, 在前40个数字是递增的,之后就逐渐增加,且幅度越来越大
    	// 0.0	1.0	2.0	3.0	4.0	5.0	6.0	7.0	8.0	9.0	10.0	11.0	12.0	13.0	14.0	15.0	16.0	17.0	18.0	19.0	
    	// ......	
    	//35.0	36.0	37.0	38.0	39.0	40.0	42.0	44.0	46.0	48.0	50.0	52.0	54.0	56.0	60.0	
    	//64.0	68.0	72.0	76.0	80.0	84.0	88.0	96.0	104.0	112.0	120.0	128.0	136.0	144.0	152.0	
    	//168.0	184.0	200.0	216.0	232.0	248.0	264.0	280.0	312.0	344.0	376.0	408.0	440.0	472.0	504.0	
    	//......
    	//1.61061274E9	1.74483046E9	1.87904819E9	2.01326592E9
        LENGTH_TABLE[i] = SmallFloat.byte4ToInt((byte)i);
    }
}



计算原始相似性权重

/**
  * 计算相似性权重
  */
public final SimWeight computeWeight(float boost, CollectionStatistics collectionStats,TermStatistics... termStats) {
    // 计算IDF, 如果是短语搜索,也就是有多个term,则idf算法有区别
    Explanation idf = termStats.length == 1 ? idfExplain(collectionStats, termStats[0]) : idfExplain(collectionStats, termStats);
    // 此Field的平均term个数
    float avgdl = avgFieldLength(collectionStats);

    float[] oldCache = new float[256];
    float[] cache = new float[256];
    // 缓存长度因子
    // k1:控制非线性的词频的标准化因子,就是减少非常高的词频的影响程度,默认值1.2, 控制tf 在 0~k+1 之间
    // b:控制doc长度因素对评分的影响程度, 越大则长度越重要,越小则约不重要,默认0.75
    for (int i = 0; i < cache.length; i++) {
    	// 老版本使用此缓存
        oldCache[i] = k1 * ((1 - b) + b * OLD_LENGTH_TABLE[i] / avgdl);
        // 目前大部分使用的是这个逻辑
        cache[i] = k1 * ((1 - b) + b * LENGTH_TABLE[i] / avgdl);
    }
    return new BM25Stats(collectionStats.field(), boost, idf, avgdl, oldCache, cache);
}

BM25Stats(String field, float boost, Explanation idf, float avgdl, float[] oldCache, float[] cache) {
    this.field = field;
    this.boost = boost;
    this.idf = idf;
    this.avgdl = avgdl;
    // 原始权重 = idf * boost
    this.weight = idf.getValue() * boost;
    this.oldCache = oldCache;
    this.cache = cache;
}

计算单个term的idf
/**
  * 计算单个term的idf
  */
public Explanation idfExplain(CollectionStatistics collectionStats, TermStatistics termStats) {
    final long df = termStats.docFreq();
    final long docCount = collectionStats.docCount() == -1 ? collectionStats.maxDoc() : collectionStats.docCount();
    final float idf = idf(df, docCount);
    return Explanation.match(idf, "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:",
        Explanation.match(df, "docFreq"),
        Explanation.match(docCount, "docCount"));
}

/**
  * Implemented as <code>log(1 + (docCount - docFreq + 0.5)/(docFreq + 0.5))</code>.
  */
 protected float idf(long docFreq, long docCount) {
     return (float)Math.log(1 + (docCount - docFreq + 0.5D) / (docFreq + 0.5D));
 }
计算多个term的idf之和
/**
  * 计算多个term的idf,当一个doc中匹配多个term时,每个term的idf相加得到总的idf
  */
public Explanation idfExplain(CollectionStatistics collectionStats, TermStatistics termStats[]) {
    double idf = 0d; // sum into a double before casting into a float
    List<Explanation> details = new ArrayList<>();
    for (final TermStatistics stat : termStats) {
        Explanation idfExplain = idfExplain(collectionStats, stat);
        details.add(idfExplain);
        idf += idfExplain.getValue();
    }
    return Explanation.match((float)idf, "idf(), sum of:", details);
}
计算符合条件的所有doc的平均term长度
/**
 * 计算满足条件的doc的平均term个数,默认是所有的doc的有效term freq之和 / 有效doc个数
 * The default implementation computes the average as <code>sumTotalTermFreq / docCount</code>
 */
protected float avgFieldLength(CollectionStatistics collectionStats) {
    final long sumTotalTermFreq;
    // 如果此filed为被store或者index
    if (collectionStats.sumTotalTermFreq() == -1) {
        // frequencies are omitted (tf=1), its # of postings
        if (collectionStats.sumDocFreq() == -1) {
            // theoretical case only: remove!
            return 1f;
        }
        // field 每个doc里的有效term的总数,一个term出现多少算一个
        sumTotalTermFreq = collectionStats.sumDocFreq();
    } else {
    	// field里所有term出现的次数的总数,一个term可能出现多次
        sumTotalTermFreq = collectionStats.sumTotalTermFreq();
    }
    // 如果field未被store或者index,则用计算所有的doc
    final long docCount = collectionStats.docCount() == -1 ? collectionStats.maxDoc() : collectionStats.docCount();
    return (float)(sumTotalTermFreq / (double)docCount);
}

代码比较复杂,靠寥寥几段代码和注释是说不清楚的,不过看看代码还是能够对评分计算有一些了解,希望能对熟悉ES的打分机制能够有所帮助,文笔不行见谅

可以看看大神写的总结博客:
elasticSearch(5.3.0)的评分机制的研究

## 算法原理与程序使用 BM25算法原理参见我的博文:[【NLP】非监督文本匹配算法——BM25] 测试程序: ```python bm25 = BM25() result = bm25.cal_similarity("自然语言处理并不是一般地研究自然语言") for line, score in result: print(line, score) ``` 测试结果如下: ```python 自然语言处理是计算机科学领域与人工智能领域中的一个重要方向。 1.012567843290477 它研究能实现人与计算机之间用自然语言进行有效通信的各种理论和方法。 2.0911221271793545 自然语言处理是一门融语言学、计算机科学、数学于一体的科学。 1.012567843290477 因此,这一领域的研究将涉及自然语言,即人们日常使用的语言, 2.2068046420905443 所以它与语言学的研究有着密切的联系,但又有重要的区别。 1.4616618736274032 自然语言处理并不是一般地研究自然语言, 3.2072055608059036 而在于研制能有效地实现自然语言通信的计算机系统, 1.201522188129132 特别是其中的软件系统。因而它是计算机科学的一部分。 0 在信息搜索中,我们做的第一步就是检索。 0 再延展一下,搜索这项功能在我们生活中也是太多太多。 0 大众一点就是搜索引擎,商品搜索等,在问题系统中可以匹配相似的问题,然后返回对应答案等。 0 文本匹配包括监督学习方法以及非监督学习方法。 0 或者分为传统方法和深度学习方法。 0 BM25 在 20 世纪 70 年代到 80 年代被提出,到目前为止已经过去二三十年了,但是这个算法依然在很多信息检索的任务中表现优异,是很多工程师首选的算法之一。 0 有时候全称是 Okapi BM25,这里的“BM”是“最佳匹配”(Best Match)的简称。 0 那么,当通过使用不同的语素分析方法,语素权重判定方法以及语素与文档的相关性判定方法,可以衍生很多不同的搜索相关性计算方法,灵活性也比较大。 0 ``` -------- 该资源内项目源码是个人的毕设,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! <项目介绍> 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 --------
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值