zoukankan      html  css  js  c++  java
  • es score限制

    背景

    在搜索个性化改造中,由于个性化打分耗时较长,所以不能对所有匹配的商品进行个性化打分排序,因此使用es rescore机制,第一次打分按相关性召回window size个商品,第二次对window size个商品进行个性化打分。

    原先的排序逻辑为 A字段、function A(自定义相关性打分)、B字段、C字段 使用sort机制进行排序,但是rescore是基于score机制,两者只能取一种逻辑排序,因此将原先sort逻辑改造成打分插件,将sort字段整合至score中。

    问题

    自定义打分插件返回的分数与es 返回结果中的score不一致,导致doc A打分结果比doc B大,但是doc A在doc B后面。

    来看下面一个case:

    query:
    -XPOST test_score/show/_search
    {
        "query": {
            "function_score": {
                "functions": [
                    {
                        "script_score": {
                            "script": "[doc].iid[0].value"
                        }
                    }
                ],
                "boost_mode": "replace"
            }
        }
    }
    
    
    结果:
    {
        ...
        "hits": {
            "total": 2,
            "max_score": 40000020000,
            "hits": [
                {
                    "_index": "test_score",
                    "_type": "show",
                    "_id": "AXPghioobslXry4H06lE",
                    "_score": 40000020000,
                    "_source": {
                        "iid": 40000019186.82314,
                        "id": 1
                    }
                },
                {
                    "_index": "test_score",
                    "_type": "show",
                    "_id": "AXPghitBbslXry4H06lF",
                    "_score": 40000020000,
                    "_source": {
                        "iid": 40000019413.37173,
                        "id": 2
                    }
                }
            ]
        }
    }
    

    score返回iid,但是在es结果中,_score都变成了40000020000,且iid为194的排在191后面,大概能猜测到是es的score有精度限制,但在哪一个过程中被限制了,是query阶段、多分片数据排序阶段还是最终返回的时候呢,限制的原因又是什么呢?

    带着这个疑问,下面来看下es对score做了些什么。

    源码跟进

    准备

    自定义打分插件打分是在Query阶段,调用lucene的search接口,打分返回结果后的任意阶段都有可能将score精度降低。

    由于对es使用groovy脚本逻辑不太熟悉,因此决定通过java编写的打分插件进行debug,随后编写了一个插件,在启动es时,InternalNode会加载PluginsService,插件就是通过PluginService加载的,此处为了方便直接通过硬编码方式将打分插件进行加载(笔者的es版本为1.6,高版本加载方式各不相同,例如5.5版本可以通过启动初始化Node类的时候增加加载插件)。

    debug过程

    首先来确定打分插件返回值,在return处返回的确实为4.000001918682314E10精确值,下面是打分插件部分代码。

    public static class SourceScoreScript extends AbstractDoubleSearchScript {
    
    
        public SourceScoreScript(Map<String, Object> params) {
    
    
        }
    
    
        @Override
        public double runAsDouble() {
            DocLookup docLookup = this.doc();
            ScriptDocValues.Doubles iid = (ScriptDocValues.Doubles) docLookup.get("iid");
            return iid.getValue();
        }
    }
    

    然后跟着调用栈慢慢往外走,到ScriptScoreFunction类调用插件进行打分,返回的结果也是精确值。

    public class ScriptScoreFunction extends ScoreFunction {
        //es调用该方法,使用自定义的打分插件进行打分
        public double score(int docId, float subQueryScore) {
            script.setNextDocId(docId);
            scorer.docid = docId;
            scorer.score = subQueryScore;
            return script.runAsDouble();
        }
    }
    

    继续往外,一下就发现了有问题的地方,FunctionFactorScorer类的innerScore的返回值竟然是float,会不会就是这个转换导致精度丢失呢?先来看下代码:

    static class FunctionFactorScorer extends CustomBoostFactorScorer {
    
    
        ...
        // 方法内部将自定义打分插件的分值和es召回的评分做整合
        @Override
        public float innerScore() throws IOException {
            //返回的分值是float
            float score = scorer.score();
            if (function == null) {
                return subQueryBoost * score;
            } else {
                return scoreCombiner.combine(subQueryBoost, score,
                        function.score(scorer.docID(), score), maxBoost);
            }
        }
    }
    
    
    public enum CombineFunction {
        ...
        //query中设置replace用打分插件分数替代es的tf-idf分数
        REPLACE {
            @Override
            //toFloat将double的funScore转换成float
            public float combine(double queryBoost, double queryScore, double funcScore, double maxBoost) {
                return toFloat(queryBoost * Math.min(funcScore, maxBoost));
            }
    
    
            ...
        }
    }
    
    
    //toFloat强转
    public static float toFloat(double input) {
        assert deviation(input) <= 0.001 : "input " + input + " out of float scope for function score deviation: " + deviation(input);
        return (float) input;
    }
    

    测试4.000001918682314E10用float存储,确实输出的值为40000020000,到这里其实就已经明白为什么自定义score计算值与Es最终返回值不同,在query阶段就已经被降精度了。

    了解精度丢失的原因,那么float最多能支持多少位的精度呢?查阅资料后,发现float的尾数位是23位,因此精度为2^23=8388608,最多能保证6-7位的精确度,因此使用es自定义打分需要注意score值最好不大于8388608这个值。

    转换的原因

    为什么es提供一个double的打分接口,却又转换成float返回呢?有没有可能我修改FunctionFactorScorer的innerScore接口,保证精度不丢失呢?

    再查看方法栈,Lucene是通过collector收集器进行文档的召回,在collector调用collect()方法召回数据时,内部通过Score.score()方法,而该抽象方法就是float的,es为了适配collect方法,进行了一层转换。

    private static class OutOfOrderTopScoreDocCollector extends TopScoreDocCollector {
        @Override
        public void collect(int doc) throws IOException {
            //通过此处进行打分
          float score = scorer.score();
    
    
          // This collector cannot handle NaN
          assert !Float.isNaN(score);
    
    
          totalHits++;
          if (score < pqTop.score) {
            // Doesn't compete w/ bottom entry in queue
            return;
          }
          doc += docBase;
          if (score == pqTop.score && doc > pqTop.doc) {
            // Break tie in score by doc ID:
            return;
          }
          pqTop.doc = doc;
          pqTop.score = score;
          pqTop = pq.updateTop();
        }
    
    
      }
    
    
    
    
    public abstract class Scorer extends DocsEnum {
        //float的抽象方法
      /** Returns the score of the current document matching the query.
       * Initially invalid, until {@link #nextDoc()} or {@link #advance(int)}
       * is called the first time, or when called from within
       * {@link Collector#collect}.
       */
      public abstract float score() throws IOException;
    }
    

    总结

    到这里,上面的几个疑惑基本解开了,在此小结一下:

    1. es的打分插件返回的分数会被强转成float类型,只能保证6-7位的精度。
    2. es进行强转的原因主要是Lucene收集器的打分接口是返回float类型,查看8.4版本Lucene该接口依然为float类型。
    3. 至于为什么lucene要将该方法抽象成float返回值这个问题,翻阅资料后依旧未找到解释,希望了解的人能解答我这个困惑。
  • 相关阅读:
    c++获取时间戳
    指针数组学习
    Matlab小波工具箱的使用2
    matlab 小波工具箱
    指针
    低通滤波参数
    git 合并分支到master
    matlab json文件解析 需要下载一个jsonlab-1.5
    matlab2017b
    数据结构-链式栈c++
  • 原文地址:https://www.cnblogs.com/zzzdp/p/13504069.html
Copyright © 2011-2022 走看看