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返回值這個問題,翻閱資料后依舊未找到解釋,希望了解的人能解答我這個困惑。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM