一、前言
上篇介紹了搜索結果高亮的實現方法,本篇主要介紹搜索結果相關性排序優化。
二、相關概念
2.1 排序
默認情況下,返回結果是按照「相關性」進行排序的——最相關的文檔排在最前。
2.1.1 相關性排序(默認)
在 ES 中相關性評分 由一個浮點數表示,並在搜索結果中通過「 _score 」參數返回,默認是按照 _score 降序排列。
2.1.2 按照字段值排序
使用「 sort 」參數實現,可指定一個或多個字段。然而使用 sort 排序過於絕對,它會直接忽略文檔本身的相關度,因此僅適合在某些特殊場景使用。
注:如果以字符串字段進行排序,除了索引一份用於全文查詢的數據,還需要索引一份原始的未經分析器處理(即 not_analyzed )的數據。這就需要使用
「 fields 」參數實現同一個字段多種索引方式,這里的「索引」是動詞相當於「存儲」的概念。
2.2 相關性算法
ES 5.X 版本將相關性算法由之前的「 TF/IDF 」算法改為了更先進的「 BM25 」算法。
2.2.1 TF/IDF 評分算法
ES版本 < 5 的評分算法,即詞頻/逆向文檔頻率。
① 詞頻( Term frequency )
搜索詞在文檔中出現的頻率,頻率越高,相關度越高。計算公式如下:
$$tf(t\ \ in\ \ d) = \sqrt{frequency}$$
搜索詞「 t 」在文檔「 d 」的詞頻「 tf 」是該詞在文檔中出現次數的平方根。
② 逆向文檔頻率( Inverse document frequency )
搜索詞在索引(單個分片)所有文檔里出現的頻率,頻率越高,相關度越低。用人話描述就是「物以稀為貴」,計算公式如下:
$$idf(t) = 1 + log \frac{docCount}{docFreq + 1}$$
搜索詞「 t 」的逆向文檔頻率「 idf 」是索引中的文檔總數除以所有包含該詞的文檔數,然后求其對數。
③ 字段長度歸一值( Field length norm )
字段的長度,字段越短,相關度越高。計算公式如下:
$$norm(d) = \frac{1}{\sqrt{numTerms}}$$
字段長度歸一值「 norm 」是字段中詞數平方根的倒數。
注:前面公式中提到的「文檔」實際上是指文檔里的某個字段
2.2.2 BM25 評分算法
ES版本 >= 5 的評分算法;BM25 的 BM 是縮寫自 Best Match, 25 貌似是經過 25 次迭代調整之后得出的算法。它也是基於 TF / IDF 算法進化來的。
對於給定查詢語句「Q」,其中包含關鍵詞「$q_{1}$,...$q_{n}$」,那么文檔「D」的 BM25 評分計算公式如下:
$$score(D,Q) = \sum_{i=1}^NIDF(q_{i})\ ·\ \frac{f(q_{i},D)\ ·\ (k_{1}+1)}{f(q_{i},D)+k_{1}\ ·\ (1-b+b\ ·\ \frac{|D|}{avgdl})}$$
這個公式看起來很唬人,尤其是那個求和符號,不過分解開來還是比較好理解的。
總體而言,主要還是分三部分,TF - IDF - Document Length
- IDF 的計算公式調整為如下所示,其中N 為文檔總數, $n(q_{i})$ 為包含搜索詞 $q_{i}$ 的文檔數。
$$IDF(q_{i}) = 1 + log\frac{N-n(q_{i})+0.5}{n(q_{i})+0.5}$$ - $f(q_{i},D)$ 為搜索詞 $q_{i}$ 在文檔 D 中的「 TF 」,| D | 是文檔的長度,avgdl 是平均文檔長度。
先不看 IDF 和 Document Length 的部分, 則公式變為 TF * ($k_{1}$ + 1) / (TF + $k_{1}$),
相比傳統的 TF/IDF 而言,BM25 抑制了 TF 對整體評分的影響程度,雖然同樣都是增函數,但是 BM25 中,TF 越大,帶來的影響無限趨近於 ($k_{1}$ + 1),這里 $k_{1}$ 值通常取 [1.2, 2.0],而傳統的 TF/IDF 則會沒有臨界點的無限增長。 - 至於文檔長度 | D | 的影響,可以看到在命中搜索詞的情況下,文檔越短,相關性越高,具體影響程度又可以由公式中的 b 來調整,當設值為 0 的時候,就跟將 norms 設置為 false 一樣,忽略文檔長度的影響。
- 最后再對所有搜索詞的計算結果求和,就是 ES5 中一般查詢的得分了。
三、實際案例
3.1 現實需求
要求搜索文章時,搜索詞出現在標題時的權重要比出現在內容中高,同時要考慮「引用次數」對最終排序的影響。
3.2 實現方法
3.2.1 調整搜索字段權重
通過調整字段的 boost 參數實現自定義權重,此處將標題的權重調整為內容的兩倍。
private SearchQuery getKnowledgeSearchQuery(KnowledgeSearchParam param) {
...省略其余部分...
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
boolQuery.must(QueryBuilders.termQuery("isDeleted", IsDeletedEnum.NO.getKey()));
boolQuery.should(QueryBuilders.matchQuery(knowledgeTitleFieldName, param.getKeyword()).boost(2.0f));
boolQuery.should(QueryBuilders.matchQuery(knowledgeContentFieldName, param.getKeyword()));
return new NativeSearchQueryBuilder()
.withPageable(pageable)
.withQuery(boolQuery)
.withHighlightFields(knowledgeTitleField, knowledgeContentField)
.build();
}
3.2.2 按引用次數提升權重
這里通過 function score 實現重打分操作。根據上面的需求,我們將使用 field value factor 函數指定「 referenceCount 」字段計算分數並與 _score 相加作為最終評分進行排序。
private SearchQuery getKnowledgeSearchQuery(KnowledgeSearchParam param) {
...省略其余部分...
// 引用次數更多的知識點排在靠前的位置
// 對應的公式為:_score = _score + log (1 + 0.1 * referenceCount)
ScoreFunctionBuilder scoreFunctionBuilder = ScoreFunctionBuilders
.fieldValueFactorFunction("referenceCount")
.modifier(FieldValueFactorFunction.Modifier.LN1P)
.factor(0.1f);
FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders
.functionScoreQuery(boolQuery, scoreFunctionBuilder)
.boostMode(CombineFunction.SUM);
return new NativeSearchQueryBuilder()
.withPageable(pageable)
.withQuery(functionScoreQuery)
.withHighlightFields(knowledgeTitleField, knowledgeContentField)
.build();
}
上述的 function score 是 ES 用於處理文檔分值的 DSL(領域專用語言),它預定義了一些計算分值的函數:
① weight
為每個文檔應用一個簡單的權重提升值:當 weight 為 2 時,最終結果為 2 * _score
② field_value_factor
通過文檔中某個字段的值計算出一個分數且使用該值修改 _score,具有以下屬性:
| 屬性 | 描述 |
|---|---|
| field | 指定字段名 |
| factor | 對字段值進行預處理,乘以指定的數值,默認為 1 |
| modifier | 將字段值進行加工,默認為 none |
| boost_mode | 控制函數與 _score 合並的結果,默認為 multiply |
③ random_score
為每個用戶都使用一個隨機評分對結果排序,可以實現對於用戶的個性化推薦。
④ 衰減函數
提供一個更復雜的公式,描述了這樣一種情況:對於一個字段,它有一個理想值,而字段實際的值越偏離這個理想值就越不符合期望。具有以下屬性:
| 屬性 | 描述 |
|---|---|
| origin(原點) | 該字段的理想值,滿分 1.0 |
| offset(偏移量) | 與原點相差在偏移量之內的值也可以得到滿分 |
| scale(衰減規模) | 當值超出原點到偏移量這段范圍,它所得的分數就開始衰減,衰減規模決定了分數衰減速度的快慢 |
| decay(衰減值) | 該字段可以被接受的值,默認為 0.5 |
⑤ script_score
支持自定義腳本完全控制評分計算
3.2.3 理解評分標准
通過JAVA API 實現相關功能后,輸出評分說明可以幫助我們更好的理解評分過程以及后續調整算法參數。
① 首先定義一個打印搜索結果的方法,設置 explain = true 即可輸出 explanation 。
public void debugSearchQuery(SearchQuery searchQuery, String indexName) {
SearchRequestBuilder searchRequestBuilder = elasticsearchTemplate.getClient().prepareSearch(indexName).setTypes(indexName);
searchRequestBuilder.setSearchType(SearchType.DFS_QUERY_THEN_FETCH);
searchRequestBuilder.setFrom(0).setSize(10);
searchRequestBuilder.setExplain(true);
searchRequestBuilder.setQuery(searchQuery.getQuery());
SearchResponse searchResponse;
try {
searchResponse = searchRequestBuilder.execute().get();
long totalCount = searchResponse.getHits().getTotalHits();
log.info("總條數 totalCount:" + totalCount);
//遍歷結果數據
SearchHit[] hitList = searchResponse.getHits().getHits();
for (SearchHit hit : hitList) {
log.info("SearchHit hit explanation:{}\nsource:{}", hit.getExplanation().toString(), hit.getSourceAsString());
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
② 之后調用接口,其 explanation 結果展示如下:
19.491358 = sum of
19.309036 = sum of:
19.309036 = sum of:
19.309036 = weight(knowledgeTitle.pinyin:test in 181) [PerFieldSimilarity], result of:
19.309036 = score(doc=181,freq=1.0 = termFreq=1.0
), product of:
2.0 = boost
6.2461066 = idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:
2.0 = docFreq
1289.0 = docCount
1.5456858 = tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:
1.0 = termFreq=1.0
1.2 = parameter k1
0.75 = parameter b
29.193172 = avgFieldLength
4.0 = fieldLength
0.0 = match on required clause, product of:
0.0 = # clause
1.0 = isDeleted:[0 TO 0], product of:
1.0 = boost
1.0 = queryNorm
0.18232156 = min of:
0.18232156 = field value function: ln1p(doc['referenceCount'].value * factor=0.1)
3.4028235E38 = maxBoost
其中 idf = 6.2461066,tfNorm = 1.5456858,boost = 2.0,由於此時只有一個搜索字段,因此 score = idf * tfNorm * boost = 19.309036;與此同時 field value function = 0.18232156;最終得分 sum = 19.309036 + 0.18232156 = 19.491358 。
四、結語
至此一個簡單需求的相關性排序優化已經實現完畢,由於業務的關系暫時未涉及其他復雜的場景,所以此篇僅僅作為一個入門介紹。
