天天看點

ElasticSearch 2 (18) - 深入搜尋系列之控制相關度ElasticSearch 2 (18) - 深入搜尋系列之控制相關度

ElasticSearch 2 (18) - 深入搜尋系列之控制相關度

摘要

處理結構化資料(比如:時間、數字、字元串、枚舉)的資料庫隻需要檢查一個文檔(或行,在關系資料庫)是否與查詢比對。

布爾是/非比對是全文搜尋的基礎部分,但不止這些,我們也同樣需要知道每個文檔與查詢的相關度,在全文搜尋引擎中我們不僅需要找到比對的文檔,還需要根據他們相關度的高低,對他們進行排序。

全文相關的公式或相似算法(similarity algorithms) 會将多個因素合并起來,為每個文檔生成一個相關度分數 *_score*。本章中,我們會驗證這些不确定的部分,然後讨論如何來控制它們。

當然,相關度不隻與全文查詢有關,它也需要将結構化的資料考慮其中。可能我們正在找一個度假屋,它需要有一些的詳細特征(空調、海景、免費WiFi),比對的特征越多,越有可能是我們想要找的度假地。可能我們還希望有一些其他的考慮因素,如回頭率,價格,流行度,或距離,當然也同時考慮全文查詢的相關度。

所有的這些都可以通過ElasticSearch強大的計分基礎來實作。

我們會先從理論上介紹Lucene是如何計算相關度的,然後通過實際例子說明如何控制相關度的計算過程的。

版本

elasticsearch版本: elasticsearch-2.x

内容

背後的理論(Theory Behind Relevance Scoring)

Lucene(即ElasticSearch)使用布爾模型(Boolean model) 查找比對文檔,并用一個名為 實用分數函數(practical scoring function) 來計算相關度。這個公式借鑒了 詞頻/逆向文檔頻率(term frequency/inverse document frequency) 和 向量空間模型(vector space model),同時也加入了一些現代的新特性,如協調性因素(coordination factor),字段長度正則化(field length normalization),以及術語或查詢語句權重提升(term or query clause boosting)。

不要緊張!這些概念并沒有像他們字面看起來那麼複制,盡管本部分提到了算法、公式和數學模型,但内容還是讓人容易了解的,與了解算法本身相比,了解這些因素是如何影響結果要重要得多。

布爾模型(Boolean Model)

布爾模型(Boolean Model)隻是将 AND,OR,和 NOT這樣的條件在查詢中使用以比對文檔,一個下面這樣的查詢:

full AND text AND search AND (elasticsearch OR lucene)
           

會将所有包括術語 full,text和search,以及elasticsearch或lucene的文檔作為結果集。

這個過程簡單且快速,将所有不比對的文檔排除在外。

詞頻/逆向文檔頻率(TF/IDF)

當我們比對到一組文檔後,需要排列這些文檔的相關度,不是所有的文檔都包含所有術語,有些術語也比其他術語重要。一個文檔的相關度分數部分取決于每個查詢術語在文檔中的權重。

這個術語的權重由三個因素決定,在什麼是相關(What Is Relevance)中介紹過,有興趣的可以了解下面的公式,但并不要求記住。

詞頻(Term frequency)

術語在文檔中出現的頻度是多少?頻度越高,權重越大。一個5次提到同一術語的字段比一個隻有1次提到的更相關。詞頻的計算方式如下:

tf(t in d) = √frequency #1
           
  • #1 術語 t 在檔案 d 的詞頻(tf)是這個術語在文檔中出現次數的平方根。

如果不在意術語在某個字段中出現的頻次,而隻在意術語是否出現過,則可以在字段映射中關掉詞頻統計:

PUT /my_index
{
  "mappings": {
    "doc": {
      "properties": {
        "text": {
          "type":          "string",
          "index_options": "docs" #1
        }
      }
    }
  }
}
           
  • #1 将參數index_options 設定為 docs 可以關掉詞頻統計以及詞頻位置,這個映射的字段不會計算術語出現的次數,對于短語或近似查詢也不可以用,預設使用準确值 not_analyzed 的字元串。
逆向文檔頻率(Inverse document frequency)

術語在集合所有文檔裡出現的頻次。頻次越高,權重越低。常用詞如 and 或 the 對于相關度貢獻非常低,因為他們在多數文檔中都會出現,一些不常見術語如 elastic 或 hippopotamus 可以幫助我們快速縮小範圍找到感興趣的文檔。逆向文檔頻率的計算公式如下:

idf(t) = 1 + log ( numDocs / (docFreq + 1)) #1
           
  • #1 術語t的逆向文檔頻率(Inverse document frequency)是:索引中文檔數量除以所有包含該術國文檔數量後的對數值。
字段長度正則值(Field-length norm)

字段的長度是多少?字段越短,字段的權重越高。如果術語出現在類似标題 title 這樣的字段,要比它出現在内容 body 這樣的字段中的相關度更高。字段長度的正則值公式如下:

norm(d) = 1 / √numTerms #1
           
  • #1 字段長度正則值是字段中術語數平方根的倒數。

字段長度的正則值對全文搜尋非常重要,許多其他字段不需要有正則值。無論文檔是否包括這個字段,索引中每個文檔的每個 string 字段都大概占用1個byte的空間。準确值 not_analyzed 字元串字段的正則值預設是打開的,但是可以通過修改字段映射将其關閉:

PUT /my_index
{
  "mappings": {
    "doc": {
      "properties": {
        "text": {
          "type": "string",
          "norms": { "enabled": false } #1
        }
      }
    }
  }
}
           
  • #1 這個字段不會将字段長度正則值考慮在内,長字段和短字段會以相同長度計算分數。

對于有些應用場景(比如:日志),正則值不是很有用,所需要關心的隻是一個字段是否包含一個特殊的錯誤碼或者一個特定的浏覽器。字段的長度對結果沒有影響,将正則值關閉可以節省大量記憶體空間。

結合(Putting it together)

下面三個因素——詞頻(term frequency)、逆向文檔頻率(inverse document frequency)和字段長度正則值(field-length norm)——是在索引時計算并存儲的。将他們放一起用來計算單個術語在特定文檔中的權重。

我們前面公式中提到的文檔指的實際上是文檔裡的某個字段,每個字段都有它自己的反向索引,是以字段的TF/IDF值就是文檔的TF/IDF值。

當我們用 explain 檢視一個簡單的 term 查詢時,我們可以發現與計算相關度分數的因子就是我們前面介紹的這些:

PUT /my_index/doc/1
{ "text" : "quick brown fox" }

GET /my_index/doc/_search?explain
{
  "query": {
    "term": {
      "text": "fox"
    }
  }
}
           

上面請求的簡化的 explaination 如下:

weight(text:fox in 0) [PerFieldSimilarity]:  0.15342641 #1
result of:
    fieldWeight in 0                         0.15342641
    product of:
        tf(freq=1.0), with freq of 1:        1.0 #2
        idf(docFreq=1, maxDocs=1):           0.30685282 #3
        fieldNorm(doc=0):                    0.5 #4
           
  • #1 術語 fox 在文檔的内部Lucene doc ID 為0,字段是 text 裡的最終分數。
  • #2 術語 fox 在該文檔的 text 字段中隻出現了1次。
  • #3 fox 在所有文檔 text 字段索引的逆向文檔頻率。
  • #4 該字段的字段長度正則值

當然,查詢通常包含不止一個術語,是以我們需要一種合并多個術語權重的方式,向量空間(Vector Space Model)可以回答這個問題。

向量空間(Vector Space Model)

向量空間(Vector Space Model)提供比較多術語查詢的一種方式,單個分數代表文檔與查詢的比對程度,為了做到這點,這個模型将文檔和查詢都以向量的形式表示:

一個向量實際上就是包含很多數字的一個一維數組,例如:

[1,2,5,22,3,8]
           

在向量空間模型裡,向量裡的每個數字都代表一個術語的權重,和TF/IDF計算方式類似。

盡管 TF/IDF 是向量空間模型計算術語權重的預設方式,但不是唯一的方式,ElasticSearch還有其他模型(如:Okapi-BM25)。TF/IDF是預設的因為它是個經檢驗過的簡單、高效算法,可以提供高品質的搜尋結果。

如果我們有查詢“happy hippopotamus”,常見詞 happy 的權重較低,不常見詞 hippopotamus 權重較高,假設 happy 的權重是2,hippopotamus 的權重是5,我們可以将這個二維向量——[2,5]——在坐标系下作條直線,線起于(0,0)點終于(2,5)點:

ElasticSearch 2 (18) - 深入搜尋系列之控制相關度ElasticSearch 2 (18) - 深入搜尋系列之控制相關度

現在,假想我們有三個文檔:

  1. I am happy in summer.
  2. After Christmas I'm a hippopotamus.
  3. The happy hippopotamus helped Harry.

我們可以為每個文檔都建立一個包括每個查詢術語(happy 和 hippopotamus)權重的向量,然後将這些向量置入同一個坐标系中:

  • 文檔 1:(happy,____________)——[2,0]
  • 文檔 2:(_____,hippopotamus)——[0,5]
  • 文檔 3:(happy,hippopotamus)——[2,5]

向量之間是可以比較的,隻需要測量查詢向量和文檔向量之間的角度就可以得到每個文檔的相關度,文檔1與查詢之間的角度最大,是以相關度低;文檔2與查詢間的角度較小,是以更相關;文檔3與查詢的角度正好吻合,完全比對。

在實際中,隻有二維向量(兩個術語的查詢)可以在平面上表示,幸運的是,線性代數——作為數學中處理向量的一個分支——為我們提供了計算兩個多元向量間角度工具,這意味着我們可以使用如上同樣的方式來解釋多個術語的查詢。

關于比較兩個向量的更多資訊可以在 餘弦近似度(cosine similarity) 中看到

現在我們讨論了分數計算的基本理論,我們可以繼續了解Lucene是如何實作分數計算的。

Lucene的計分函數(Lucene’s Practical Scoring Function)

對于多術語查詢,Lucene采用布爾模型(Boolean model)、TF/IDF、以及向量空間模型(Vector Space Model),然後将他們合并到單個包中來收集比對文檔和分數計算。

一個多術語查詢如下:

GET /my_index/doc/_search
{
  "query": {
    "match": {
      "text": "quick fox"
    }
  }
}
           

會在内部被重寫為:

GET /my_index/doc/_search
{
  "query": {
    "bool": {
      "should": [
        {"term": { "text": "quick" }},
        {"term": { "text": "fox"   }}
      ]
    }
  }
}
           

bool 查詢實作了布爾模型,在這個例子中,它會将包括術語 quick 和 fox 或兩者兼有的文檔作為查詢結果。

隻要一個文檔與查詢比對,Lucene就會為查詢計算分數,然後合并每個比對術語的分數。這裡使用的分數計算公式叫做 實用計分函數(practical scoring function)。看似很高達上,但是别擔心——多數的元件都已經介紹過,我們下一步會介紹它引入的一些新元素。

score(q,d)  =  #1
            queryNorm(q)  #2
          · coord(q,d)    #3
          · ∑ (           #4
                tf(t in d)   #5
              · idf(t)²      #6
              · t.getBoost() #7
              · norm(t,d)    #8
            ) (t in q)    #9
           
  • #1 score(q, d) 是文檔 d 與 查詢 q 的相關度分數
  • #2 queryNorm(q) 是查詢正則因子(query normalization factor) (新)。
  • #3 coord(q, d) 是協調因子(coordination factor) (新)。
  • #4 #9 查詢 q 中每個術語 t 對于文檔 d 的權重和。
  • #5 tf(t in d) 是術語 t 在文檔 d 中的詞頻。
  • #6 idf(t) 是術語 t 的逆向文檔頻次。
  • #7 t.getBoost() 是查詢中使用的 boost (新)。
  • #8 norm(t,d) 是字段長度正則值,與索引時字段級的boost的和(如果存在)(新)。

我們應該認識 score,tf 和 idf。queryNorm,coord,t.getBoost和norm是新的。

我們會在本章後面繼續探讨查詢時的權重提升問題,但是首先讓我們了解查詢正則化、協調和索引時字段層面的權重提升等概念。

查詢正則因子(Query Normalization Factor)

查詢正則因子(queryNorm)試圖将查詢正則化,這樣就能比較兩個不同查詢結果。

盡管查詢正則值的目的是為了使查詢結果之間能夠互相比較,但是它并不十分有效,因為相關度分數 *_score* 的目的是為了将目前查詢的結果進行排序,比較不同查詢結果的相關度分數沒有太大意義。

這個因子是在查詢開始前計算的,具體的計算依賴于具體查詢,一個典型的實作如下:

queryNorm = 1 / √sumOfSquaredWeights #1
           
  • #1 sumOfSquaredWeights 是查詢裡每個術語的IDF的平方和。

相同查詢正則因子會被應用到每個文檔,我們無法改變它,總而言之,可以被忽略。

查詢協調(Query Coordination)

協調因子(coord)可以為那些查詢術語包含度高的文檔提供“獎勵”,文檔裡出現的查詢術語越多,它越有機會成為一個好的比對結果。

假如我們有個查詢是“quick brown fox”,每個術語的權重都是1.5。如果沒有協調因子,最終分數會是文檔裡所有術語權重之和。例如:

  • 文檔裡有 fox -> 分數是: 1.5
  • 文檔裡有 quick fox -> 分數是: 3.0
  • 文檔裡有 quick brown fox -> 分數是: 4.5

協調因子将分數與文檔裡比對術語的數量相乘,然後除以查詢裡所有術語的數量,如果使用協調因子,分數會變成:

  • 文檔裡有 fox -> 分數是: 1.5 * 1 / 3 = 0.5
  • 文檔裡有 quick fox -> 分數是: 3.0 * 2 / 3
  • 文檔裡有 quick brown fox -> 分數是: 4.5 * 3 / 3 = 4.5

協調因子能使包含所有三個術語的文檔比隻包含兩個術語的文檔得分要高處許多。

将 quick brown fox 查詢寫成 bool 查詢:

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "term": { "text": "quick" }},
        { "term": { "text": "brown" }},
        { "term": { "text": "fox"   }}
      ]
    }
  }
}
           

bool 查詢預設會對所有 should 語句使用協調功能,不過我們也可以将其關閉。為什麼要這樣做?通常的回答是——無須這樣。查詢協調通常是件好事,當我們使用 bool 查詢将多個進階查詢如 match 查詢包裹的時候,讓協調功能打開是有意義的,比對的語句越多,查詢請求與傳回文檔間的重疊度就越高。

但在某些進階應用中,我們将協調功能關閉可能更好。假如我們正在查找同義詞 jump、leap 和 hop。我們并不關心會出現多少個同義詞,因為它們都表示一個意思,實際上,隻有其中一個同義詞會出現,這是不使用協調功能的一個好的例子:

GET /_search
{
  "query": {
    "bool": {
      "disable_coord": true,
      "should": [
        { "term": { "text": "jump" }},
        { "term": { "text": "hop"  }},
        { "term": { "text": "leap" }}
      ]
    }
  }
}
           

當我們使用同義詞的時候(參照:同義詞(Synonyms)),Lucene内部是這樣的:重寫的的查詢會為同義詞關閉協調功能,大多數關閉操作的應用場景是自動處理的,無須為此擔心。

索引時字段層權重提升(Index-Time Field-Level Boosting)

我們将會讨論在查詢時,讓字段(Field)的權重提升(即讓某個字段比其他字段更重要),當然在索引時也能做到如此。實際上,權重的提升會被應用到字段的每個詞條(term)中,而不是字段本身。

将提升值存儲與索引中不需要使用更多空間,這個字段層次索引時的提升值與字段長度正則值(參照:字段長度正則值(Field-length norm))一起作為單個byte被存放于索引中,norm(t,d) 是前面公式的傳回值。

由于以下原因,我們不建議使用字段層索引時的權重提升方法:

  • 将提升值與字段長度正則值合在一個byte中存儲會丢失字段長度正則值的精度,這樣會導緻ElasticSearch不知如何區分包含三個詞的字段和包含五個詞的字段。
  • 我要想改變索引時的提升值,就必須重新為所有文檔建立索引,與此不同的是,查詢時的提升值可以随着每次查詢的不同而更改。
  • 如果一個索引時權重提升的字段有多個值,提升值會按照每個值來自乘,這會導緻該字段的權重會急劇上升。

查詢時的權重提升(Querying-time boosting)更為簡單、清楚,而且有靈活的選擇。

了解了查詢正則化,協同,和索引時權重提升這些方式後,我們可以進一步了解相關度計算的最有用工具:查詢時權重提升(query-time boosting)。

查詢時權重提升(Query-Time Boosting)

在語句優先級(Prioritizing Clauses)中,我們解釋了如何在搜尋時使用權重提升參數讓一個查詢語句比其他語句更重要。如:

GET /_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "title": {
              "query": "quick brown fox",
              "boost": 2 #1
            }
          }
        },
        {
          "match": { #2
            "content": "quick brown fox"
          }
        }
      ]
    }
  }
}
           
  • #1 title 查詢語句的重要性是 content 的2倍,因為它的權重提升值為2。
  • #2 一個沒有設定權重提升的查詢語句的值為1。

查詢時的權重提升是我們可以用來影響相關度的主要工具,任意一種類型的查詢都能接受權重提升(boost)參數。将權重提升值設定為2,并不代表最終的分數會是原值的2倍;權重提升值會經過正則化和一些其他内部優化過程。盡管如此,它确實想要表明一個提升值為2的句子的重要性是提升值為1句子的2倍。

實際應用中,無法通過一個簡單的公式得出某個特定查詢語句的正确權重提升值,隻能通過不同嘗試獲得。需要明白的是權重提升值隻是影響相關度分數的其中一個因子。在前面的例子中,title 字段相對 content 字段可能已經有一個隐性的權重提升值,因為在字段長度正則值中,标題往往比相關内容要短,是以不要想當然的去盲目提升一些字段的權重。選擇權重,檢查結果,如此往複。

提升索引權重(Boosting an Index)

當在多個索引中進行搜尋時,我們可以使用參數 indices_boost 來提升整個索引的權重,在下面這個例子中,當我們想要為最近索引的文檔配置設定更高權重時,可以這麼做:

GET /docs_2014_*/_search #1
{
  "indices_boost": { #2
    "docs_2014_10": 3,
    "docs_2014_09": 2
  },
  "query": {
    "match": {
      "text": "quick brown fox"
    }
  }
}
           
  • #1 這個多索引查詢涵蓋了所有以字元串 docs_2014_ 開始的索引
  • #2 其中,索引 docs_2014_10 中的所有檔案的權重提升值是3,索引 docs_2014_10 中是2,其他所有比對的索引的提升值為1。

t.getBoost()

這些提升值在Lucene實用計分函數(Lucene's Practical Scoring Function)中可以通過 t.getBoost() 獲得。權重提升不會被應用于它在查詢DSL中出現的層次,而是會被合并下轉至每個詞項中。t.getBoost()始終傳回目前詞項的權重或者目前分析鍊上目前查詢的權重。

實際上,要想解讀 explain 的輸出是非常複雜的,我們看不到提升值,也無法通路上面提到的t.getBoost()方法,權重值融合在 queryNorm 中并應用到每個詞項。盡管我們說,queryNorm 對于每個術語都是相同的,我們還是會發現一個權重提升過的詞項的 queryNorm 值要高于一個沒有提升過的。

使用查詢結構修改相關度(Manipulating Relevance with Query Structure)

ElasticSearch的查詢DSL相當靈活,我們可以通過在查詢結構中,移動查詢語句的位置來或多或少的改變它們的重要性,比如,我們有下面這個查詢:

quick OR brown OR red OR fox
           

我們可以将所有詞項都放在 bool 查詢的同一層中:

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "term": { "text": "quick" }},
        { "term": { "text": "brown" }},
        { "term": { "text": "red"   }},
        { "term": { "text": "fox"   }}
      ]
    }
  }
}
           

這個查詢可能最終給包含 quick、red 和 brown詞項的文檔的分數與包含 quick、red和fox詞項文檔的分數相同,這裡 red 和 brown 是同義詞,可能隻需要保留其中一個,而我們真正要表達的意思是想查詢:

quick OR (brown OR red) OR fox
           

根據标準的布爾邏輯,這與原始的查詢是完全一樣的,但是我們已經在 組合查詢(Combining Queries) 看到,一個 bool 查詢不關心文檔的相關性,而關心文檔是否能比對。

上述查詢有個更好的方式:

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "term": { "text": "quick" }},
        { "term": { "text": "fox"   }},
        {
          "bool": {
            "should": [
              { "term": { "text": "brown" }},
              { "term": { "text": "red"   }}
            ]
          }
        }
      ]
    }
  }
}
           

現在,red 和 brown 處于互相競争的層次,quick、fox 以及 red OR brown則是處于頂層且互相競争的詞項。

我們已經讨論過是如何使用 match、multi_match、term、bool 和 dis_max 查詢來修改相關度分數的。本章後面的内容會介紹另外三個與相關度分數有關的查詢:boosting 查詢、constant_score 查詢和 function_score 查詢。

不完全不(Not Quite Not)

在網際網路上搜尋“Apple”,傳回的結果很可能是一個公司、水果和各種食譜。我們可以将查詢結果的範圍縮小至隻傳回公司,然後排除 pie、tart、crumble 和 tree 這樣的詞,在 bool 查詢中用 must_not 語句來實作:

GET /_search
{
  "query": {
    "bool": {
      "must": {
        "match": {
          "text": "apple"
        }
      },
      "must_not": {
        "match": {
          "text": "pie tart fruit crumble tree"
        }
      }
    }
  }
}
           

但是誰又敢保證在排除 tree 或 crumble 這種詞後,不會讓我們錯失一個與蘋果公司特别相關的文檔呢?有時,must_not 條件會過于嚴格。

權重提升查詢(boosting Query)

權重提升查詢(boosting Query) 恰恰解決了我們的問題。它仍然允許我們将關于水果或甜點的結果包含到結果中,但是将他們降級——即降低他們原來可能占據的排名。

GET /_search
{
  "query": {
    "boosting": {
      "positive": {
        "match": {
          "text": "apple"
        }
      },
      "negative": {
        "match": {
          "text": "pie tart fruit crumble tree"
        }
      },
      "negative_boost": 0.5
    }
  }
}
           

它接受 postive 和 negative 查詢,将隻有與 postive 查詢比對的文檔包括在結果急中,而将與 negative 查詢比對的文檔的原始分數 *_score* 與 negative_boost 相乘。

為了達到效果,negative_boost 的值必須小于 1.0。在這個例子中,所有包含負向詞項的文檔的分數都會減半。

忽略TF/IDF(Ignoring TF/IDF)

有些時候我們根本不關心 TF/IDF,我們隻想知道一個詞是否在某個字段中出現過。我們可能回想搜尋一個可以度假的屋子并希望它能有以下特性:

  • WiFi
  • Garden
  • Pool

這個度假屋的文檔如下:

{ "description": "A delightful four-bedroomed house with ... " }
           

我們可以用簡單的 match 查詢進行比對:

GET /_search
{
  "query": {
    "match": {
      "description": "wifi garden pool"
    }
  }
}
           

但這并不是真正的全文搜尋,此種情況下,TF/IDF并無卵用。我們既不關心 wifi 是否為一個普通詞項,也不關心它在文檔中出現是否頻繁,我們關心的隻是它出現過就行。實際上,我們希望根據房屋的特性對其排名——特性越多越好。如果特性出現,則記 1 分,不出現記 0 分。

constant_score 查詢

在 constant_score 查詢中,它可以包含一個查詢或一個過濾,為任意一個比對的文檔指定分數 0,忽略TF/IDF資訊:

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "constant_score": {
          "query": { "match": { "description": "wifi" }}
        }},
        { "constant_score": {
          "query": { "match": { "description": "garden" }}
        }},
        { "constant_score": {
          "query": { "match": { "description": "pool" }}
        }}
      ]
    }
  }
}
           

可能不是所有的特性都同等重要——盡管對于使用者來說有些特性更具價值,是以我們可以為更重要的特性指定權重提升值,讓它更具相關性:

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "constant_score": {
          "query": { "match": { "description": "wifi" }}
        }},
        { "constant_score": {
          "query": { "match": { "description": "garden" }}
        }},
        { "constant_score": {
          "boost":   2 #1
          "query": { "match": { "description": "pool" }}
        }}
      ]
    }
  }
}   
           

pool 語句的權重提升值為 2,而其他的語句為 1。

最終的分數并不是所有比對語句的簡單加和,協調因子(coordination factor)和查詢規則化因子(query normalization factor)仍然會被考慮。

我們可以加一個 not_analyzed features 字段來改進度假屋文檔:

{ "features": [ "wifi", "pool", "garden" ] }    
           

預設情況下,一個 not_analyzed 字段會關閉 字段長度正則功能(field-length norms)并且将 index_options 設為 docs 選項,關閉詞頻統計(term frequencies),但還有個問題:每個詞項的逆向文檔頻率(inverse document frequency)仍然會被考慮。

我們在 constant_score 查詢中用前面用過的方法來解決這個問題:

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "constant_score": {
          "query": { "match": { "features": "wifi" }}
        }},
        { "constant_score": {
          "query": { "match": { "features": "garden" }}
        }},
        { "constant_score": {
          "boost":   2
          "query": { "match": { "features": "pool" }}
        }}
      ]
    }
  }
}
           

實際上,每個特性都應該看成一個過濾器,對于度假屋來說要麼具有某個特性要麼沒有——過濾器因為其特性正合适。而且,如果我們使用過濾器,還可以從緩存那裡得到好處。

這裡的問題是:過濾器無法計算分數。這樣我們就需要尋求一種方式将過濾器和查詢間的坑填平。function_score 查詢不僅正好可以扮演這個角色,而且有更強大的功能。

function_score 查詢(function_score Query)

function_score 查詢(function_score query) 是用來控制算分過程的終極武器,它允許我們為每個與主查詢比對的文檔應用一個函數,以達到改變甚至完全替換原始分數的目的。

實際上,我們也能用過濾器對結果的子集應用不同的函數,這讓我們一箭雙雕:既能高效算分,也能利用過濾器的緩存特性。

ElasticSearch預定義了一些函數:

  • weight

    為每個文檔應用一個簡單的而不被正則化的權重提升值:當 weight 為 2 時,最終結果為 2 _score*

  • field_value_factor

    使用這個值來修改 _score,如将流行度或贊成數作為考慮因素。

  • random_score

    為每個使用者都使用一個不同的随機分數來對結果排序,但對某一具體使用者來說,看到的順序始終是一緻的。

  • Decay functions — linear, exp, gauss

    将分數與浮動值一起使用(如:publish_date、geo_location、或 price)提供偏好結果:最近釋出的文檔,某一具體位置附近的文檔,或某一具體價格附近的文檔。

  • script_score

    如果需求超出以上範圍時,用自定義腳本完全控制分數計算的邏輯。

如果沒有 function_score 查詢,我們就不能将全文查詢與最近時間這種因子一起結合,而需要對分數或時間進行排序;這會互相影響抵消兩種排序各自效果。這個查詢可以讓我們将兩個效果融合,仍然按照全文相關度進行排序,隻是會更多考慮:最近釋出的文檔、流行的文檔、或使用者在意的價格區間。正如我們想象的一樣,一個查詢要想考慮所有這些因素會非常複雜,讓我們先從簡單的例子開始,然後順着梯子慢慢向上爬。

流行度提升權重(Boosting by Popularity)

假設我們有一個網站供使用者釋出部落格并且可以讓他們為自己喜歡的部落格點贊,我們希望将更受歡迎的部落格放在搜尋結果清單中相對較上的位置,但是全文搜尋的分數仍然作為相關度的主要部分,簡單的,我們可以根據每個部落格的點贊數進行排序:

PUT /blogposts/post/1
{
  "title":   "About popularity",
  "content": "In this post we will talk about...",
  "votes":   6
}
           

在搜尋時,我們可以将 function_score 查詢與 field_value_factor 結合使用,即将點贊數量與全文相關度分數結合:

GET /blogposts/post/_search
{
  "query": {
    "function_score": { #1
      "query": { #2
        "multi_match": {
          "query":    "popularity",
          "fields": [ "title", "content" ]
        }
      },
      "field_value_factor": { #3
        "field": "votes" #4
      }
    }
  }
}
           
  • #1 function_score 查詢将主查詢和函數包括在内。
  • #2 主查詢優先執行。
  • #3 field_value_factor 函數會被應用到每個與主查詢比對的文檔。
  • #4 每個文檔的votes 字段都必須有值供 function_score 計算。

在前面的例子中,每個文檔的最終分數會被修改:

new_score = old_score * number_of_votes 
           

然而這并不會給我們帶來出人意料的結果,全文分數通常處于0到10之間,如下圖中,即使有10個贊的部落格也會被全文分數掩蓋,而0個贊的部落格的分數會被置為0。

Figure 29. Linear popularity based on an original _score of 2.0

ElasticSearch 2 (18) - 深入搜尋系列之控制相關度ElasticSearch 2 (18) - 深入搜尋系列之控制相關度

修飾語(modifier)

修飾語(modifier)是一種可以将受歡迎度(即點贊數)平滑融合的一種方式。換句話說,我們希望最開始的一些贊更重要,但是其重要性會随着數字的增加而降低。0個贊與1個贊的差別應該比10個贊與11個贊的差別大很多。

modifier的一個典型的應用場景是 log1p,它的公式為:

new_score = old_score * log(1 + number_of_votes)
           

對數函數使贊這個字段的分數曲線更平滑:

Figure 30. Logarithmic popularity based on an original _score of 2.0

ElasticSearch 2 (18) - 深入搜尋系列之控制相關度ElasticSearch 2 (18) - 深入搜尋系列之控制相關度

使用 modifier 的請求如下:

GET /blogposts/post/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query":    "popularity",
          "fields": [ "title", "content" ]
        }
      },
      "field_value_factor": {
        "field":    "votes",
        "modifier": "log1p" 
      }
    }
  }
}
           
  • #1 modifier 為 log1p

修飾語(modifier)的值可以為:none(預設狀态)、log、log1p、log2p、ln、ln1p、ln2p、square、sqrt 以及 reciprocal。想要了解更多資訊請參照文檔:field_value_factor。

因子(factor)

可以通過将點贊數與一個因子(factor)相乘來調節受歡迎程度效果的高低:

GET /blogposts/post/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query":    "popularity",
          "fields": [ "title", "content" ]
        }
      },
      "field_value_factor": {
        "field":    "votes",
        "modifier": "log1p",
        "factor":   2 #1
      }
    }
  }
}
           
  • #1 雙倍效果。

将 factor 加入到公式中:

new_score = old_score * log(1 + factor * number_of_votes)
           

factor值大于1會提升效果,factor值小于1會降低效果,如下圖:

Figure 31. Logarithmic popularity with different factors

ElasticSearch 2 (18) - 深入搜尋系列之控制相關度ElasticSearch 2 (18) - 深入搜尋系列之控制相關度

提升模式(boost_mode)

可能将全文分數與 field_value_factor 函數值相乘的效果仍然可能會是放大的,我們可以通過 參數 boost_mode來控制函數與查詢分數合并後的結果,參數接受的值為:

  • multiply

    将分數與函數值相乘(預設)

  • sum

    将分數與函數值相加

  • min

    分數與函數值的較小值

  • max

    分數與函數值的較大值

  • replace

    函數值替代分數

與使用相乘的方式相比,使用分數與函數值相加的方式可以讓我們弱化最終效果,特别是當我們用一個較小的因子時:

GET /blogposts/post/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query":    "popularity",
          "fields": [ "title", "content" ]
        }
      },
      "field_value_factor": {
        "field":    "votes",
        "modifier": "log1p",
        "factor":   0.1
      },
      "boost_mode": "sum" #1
    }
  }
}
           
  • #1 将分數與函數值相乘。

之前請求的公式現在變成下面這樣(參考圖:Figure 32. Combining popularity with sum):

new_score = old_score + log(1 + 0.1 * number_of_votes)
           

Figure 32. Combining popularity with sum

ElasticSearch 2 (18) - 深入搜尋系列之控制相關度ElasticSearch 2 (18) - 深入搜尋系列之控制相關度

最大提升值(max_boost)

最後,我們可以使用 max_boost 參數限制一個函數的最大效果。

GET /blogposts/post/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query":    "popularity",
          "fields": [ "title", "content" ]
        }
      },
      "field_value_factor": {
        "field":    "votes",
        "modifier": "log1p",
        "factor":   0.1
      },
      "boost_mode": "sum",
      "max_boost":  1.5 #1
    }
  }
}
           
  • /#1 無論 field_value_factor 函數的結果如何,最終結果都不會大于 1.5。

注意:

max_boost 隻對函數的結果進行限制,不會對最終分數産生直接影響。

提升過濾集權重(Boosting Filtered Subsets)

讓我們回到忽略TF/IDF裡提到的問題,我們希望根據每個度假屋的特性數量來算分,當時我們希望能用緩存的過濾器來影響分數,現在 function_score 查詢正好可以達成我們的願望。

到目前為止,我們展現的都是為所有文檔使用單個函數的使用方式,現在我們會用過濾器将結果劃分為多個子集(每個特性一個過濾器),并為每個子集使用不同的函數。

在下面這個例子中,我們會使用 weight 函數,它與 boost 參數類似可以用于任何查詢。有一點差別是 weight 沒有被Lucene正則成生澀的浮點數,而是使用它自己。

查詢的結構需要做相應調整以适應多個函數:

GET /_search
{
  "query": {
    "function_score": {
      "filter": { #1
        "term": { "city": "Barcelona" }
      },
      "functions": [ #2
        {
          "filter": { "term": { "features": "wifi" }}, #3
          "weight": 1
        },
        {
          "filter": { "term": { "features": "garden" }}, #4
          "weight": 1
        },
        {
          "filter": { "term": { "features": "pool" }}, #5
          "weight": 2 #6
        }
      ],
      "score_mode": "sum", #7
    }
  }
}
           
  • /#1 function_score 查詢有一個過濾器而不是查詢。
  • /#2 functions 關鍵字存儲着一個将被應用的函數清單。
  • /#3 #4 #5 函數會被應用于和(可選的)過濾器比對的文檔。
  • /#6 pool 比其他特性更重要,是以它有更高的權重。
  • /#7 score_mode 指明每個函數的值是如何合并計算的。

這個新特性需要注意的地方會在下面介紹:

過濾vs.查詢(filter Versus query)

首先要注意的事情是我們用過濾器(filter)替代了查詢(query),在這個例子中,我們無須使用全文搜尋,我們隻想找到所有 city 字段是 Barcelona 的文檔,邏輯用 filter 而不是 query 來表達會更清晰。過濾器傳回的所有文檔的分數值為1。function_score 查詢接受 query 或 filter,如果沒有特别申明,則預設使用 match_all 查詢。

函數(functions)

functions 關鍵字保持着一個将要被使用的函數清單。可以為數組裡的每個項都指定一個過濾器(filter),這種情況下,函數隻會被應用到那些與過濾器比對的文檔,例子中,我們為與過濾器比對的文檔指定權重值為 1(為與 pool 比對的文檔指定權重值為 2)。

score_mode

每個函數傳回一個結果,是以我們需要一種将多個結果縮減到一個值的方式,然後才能将其與原始分數合并。score_mode 正好扮演了這樣的角色,它接受以下值:

  • multiply

    函數結果求積(預設)

  • sum

    函數結果求和

  • avg

    函數結果求平均值

  • max

    函數結果求最大值

  • min

    函數結果求最小值

  • first

    将第一個函數(可以有過濾器,也可沒有)的結果作為最終結果

例子中,我們将每個過濾器比對結果的權重求和,并将其作為最終分數,是以我們使用 sum 算分模式。

不與任何過濾器比對的文檔會保有其原始分數:1。

随機計分(Random Scoring)

我們可以會想知道 一緻随機算分(consistently random scoring)是什麼,我們又為什麼會使用它。之前一個例子是個很好的應用場景,前例中所有的結果都會傳回1、2、3、4或5這樣的最終分數,可能隻有少數房子的分數是5分,而有大量房子的分數是 2 或 3 。

作為一個網站的所有者,我們希望讓廣告有更高的展現率。在目前查詢下,有相同分數的結果會每次都以相同次序出現,為了提高展現率,我們在此引入一些随機性可能是個好主意,這能保證具有相同分數的文檔都能有均等相似的展現機率。

我們想讓每個使用者看到不同的随機次序,但是我們希望如果是同一使用者浏覽翻頁時,結果的相對次序能始終保持一緻。這種行為被稱為一緻随機(consistently random)。

random_score 函數會輸出一個 0 到 1之間的數,當 種子(seed) 值相同時,它生成的随機結果是一緻的,就如一個使用者的對話ID(session ID)一樣:

GET /_search
{
  "query": {
    "function_score": {
      "filter": {
        "term": { "city": "Barcelona" }
      },
      "functions": [
        {
          "filter": { "term": { "features": "wifi" }},
          "weight": 1
        },
        {
          "filter": { "term": { "features": "garden" }},
          "weight": 1
        },
        {
          "filter": { "term": { "features": "pool" }},
          "weight": 2
        },
        {
          "random_score": { #1
            "seed":  "the users session id" #2
          }
        }
      ],
      "score_mode": "sum",
    }
  }
}
           
  • #1 random_score 語句沒有任何過濾器,是以會被應用到所有文檔。
  • #2 将使用者的 session ID 作為 種子(seed),讓該使用者的随機始終保持一緻,相同的 種子(seed) 會産生相同的随機結果。

當然,如果我們索引與查詢比對的建立文檔,無論是否使用一緻随機結果的順序都會發生變化。

越近越好(The Closer, The Better)

很多變量都可以影響使用者對于度假屋的選擇,也許使用者希望離市中心近點,但但價格足夠便宜時也有可能選擇一個更遠的住處,又可能反過來是正确的:願意為最好的位置付更多的價錢。

如果我們添加一個過濾器排除所有市中心方圓1千米以外的度假屋,或所有價格超過100英鎊每晚的,我們可能會将使用者願意考慮妥協的那些選擇排除在外。

function_score 查詢會提供一組衰減函數(decay functions),讓我們有能力在兩個滑動标準(如:地點和價格)之間進行權衡。

有三種衰減函數——線性(linear)、指數(exp)和高斯(gauss)函數,它們可以操作數值、時間以及 經緯度地理坐标點這樣的字段。三個都能接受以下參數:

  • origin

    代表中心點(central point)或字段可能的最佳值,落在原點(origin)上的文檔分數為滿分 1.0。

  • scale

    代表衰減率,即一個文檔從原點(origin)下落時,分數改變的速度。(如,每10歐元或每100米)。

  • decay

    從原點(origin)衰減到 scale 所得到的分數,預設值為 0.5。

  • offset

    以原點(origin)為中心點,為其設定一個非零的偏移量(offset)覆寫一個範圍,而不隻是原點(origin)這單個點。在此範圍内(-offset <= origin <= +offset)的所有值的分數都是 1.0。

這三個函數的唯一不同就是他們衰減曲線的形狀,用圖形說明會更為直覺(參考:Figure 33, “Decay function curves”)

Figure 33. Decay function curves

ElasticSearch 2 (18) - 深入搜尋系列之控制相關度ElasticSearch 2 (18) - 深入搜尋系列之控制相關度

圖中所有曲線的原點(origin)(即中心點)的值都是 40,偏移量(offset)是 5,也就是在範圍 40 - 5 <= value <= 40 + 5 内的所有值都會被當作原點(origin)處理——所有這些點的分數都是滿分 1.0。

在此範圍之外,分數開始衰減,衰減率由 scale 值(此例中的值為 5)和 衰減值(decay)(此例中為預設值 0.5)共同決定。結果是所有三個曲線在 origin +/- (offset + scale) 處的分數都是 0.5,即點 30 和 50 處。

線性(linear)、指數(exp)和高斯(gauss)函數三者之間的差別在于範圍(-offset <= origin <= +offset)之外的曲線形狀:

  • 線性(linear)函數是條直線,一旦直線與橫軸0相交,所有其他值的分數都是 0.0。
  • 指數(exp = exponential)函數是先劇烈衰減然後變緩。
  • 高斯(gauss = Gaussian)函數是鐘形的——它的衰減速率是先緩慢,然後變快,最後又放緩。

選着曲線的依據完全由我們希望分數的衰減速率來決定,即距離原點(origin)的遠近值。

回到我們的例子:我們的使用者希望租一個離倫敦市中心近且每晚不超過100英鎊的度假屋,而且與距離相比,我們的使用者對價格更為敏感,這樣查詢可以寫成:

GET /_search
{
  "query": {
    "function_score": {
      "functions": [
        {
          "gauss": {
            "location": { #1
              "origin": { "lat": 51.5, "lon": 0.12 },
              "offset": "2km",
              "scale":  "3km"
            }
          }
        },
        {
          "gauss": {
            "price": { #2
              "origin": "50", #3
              "offset": "50",
              "scale":  "20"
            }
          },
          "weight": 2 #4
        }
      ]
    }
  }
}
           
  • #1 location 字段以地理坐标點(geo_point) 映射。
  • #2 price 字段是數值。
  • #3 可以參考 了解價格語句( Understanding the price Clause ) 了解 origin為什麼是 50 而不是 100。
  • #4 price 語句有 location 語句2倍的權重。

location 語句可以簡單了解為:

  • 我們以倫敦市中作為原點(origin)。
  • 所有原點(origin) 2千米範圍内的位置的分數是 1.0。
  • 距中心5千米(offset + scale)的位置的分數是 0.5。

了解價格語句(Understanding the price Clause)

price 語句使用了一個小技巧:使用者希望選擇100英鎊以下的度假屋,但是例子中的原點(origin)被設定成50英鎊,價格不能為負,但肯定是越低越好,是以0到100英鎊内的所有價格都認為是比較好的。

如果我們将原點(origin)被設定成100英鎊,那麼低于這個數的度假屋的分數會變低,是以,我們将原點(origin) 和 偏移量(offset)同時設定成50英鎊,這樣就能使隻有在價格高于100英鎊(origin + offset)時分數才會變低。

weight 參數可以被用來調整每個語句的貢獻度,預設值是 1.0。這個值會先與每個句子的分數相乘,然後再通過 score_mode 的設定方式合并。

腳本評分(Scoring with Scripts)

最後,如果所有 function_score 内置的函數都無法滿足我們的應用場景,可以使用 script_score 函數來自行實作邏輯。

舉個例子,我們想要将利潤空間作為因子加入到相關度評分計算中,業務中,利潤空間和以下三點相關:

  • price 度假屋每晚的價格。
  • 會員使用者的級别——某些等級的使用者可以在每晚房價高于某個價格的時候享受折扣。
  • 使用者享受折扣後,經過議價的每晚房價。

計算每個度假屋利潤的算法如下:

if (price < threshold) {
  profit = price * margin
} else {
  profit = price * (1 - discount) * margin;
}
           

我們很可能不想用絕對利潤作為評分,這會弱化其他其他因子(如:地點、受歡迎度和特性)的作用,而是将利潤以我們目标利潤的百分比來表示,高于我們目标的利潤空間會有一個正向分數(大于 1.0),低于我們目标的利潤空間會有一個負向分數(小于 1.0):

if (price < threshold) {
  profit = price * margin
} else {
  profit = price * (1 - discount) * margin
}
return profit / target
           

ElasticSearch裡使用Groovy作為預設的腳本語言,它與JavaScript很像,上面這個算法用Groovy腳本來表示如下:

price  = doc['price'].value #1
margin = doc['margin'].value #2

if (price < threshold) { #3
  return price * margin / target
}
return price * (1 - discount) * margin / target #4
           
  • #1 #2 price 和 margin 變量可以分别從文檔的 price 和 margin 字段提取。
  • #3 #4 threshold、discount 和 target 是作為參數(params)傳入的。

最終我們将 script_score 函數與其他函數一起使用:

GET /_search
{
  "function_score": {
    "functions": [
      { ...location clause... }, #1
      { ...price clause... }, #2
      {
        "script_score": {
          "params": { #3
            "threshold": 80,
            "discount": 0.1,
            "target": 10
          },
          "script": "price  = doc['price'].value; margin = doc['margin'].value;
          if (price < threshold) { return price * margin / target };
          return price * (1 - discount) * margin / target;" #4
        }
      }
    ]
  }
}
           
  • #1 #2 location 和 better 語句在 越近越好(The Closer,The Better) 中解釋過。
  • #3 将這些變量作為參數 params傳遞,我們可以查詢時動态的改變腳本而無須重新編譯。
  • #4 JSON不能接受内嵌的換行符,腳本中的換行符可以用 \n 或 ; 替代。

這個查詢根據使用者對地點和價格的需求,傳回最為之滿意的文檔,同時也考慮到我們對于盈利的要求。

scirpt_score 函數提供了巨大的靈活性,我們可以通過腳本通路文檔裡的所有字段、目前評分甚至詞頻、逆向文檔頻率和字段長度正則值這樣的資訊(參見 文本評分腳本(Text scoring in scripts))。

有人會說,使用腳本對性能會有影響,如果我們确實發現了執行較慢的腳本,可以有以下三個選擇:

  • 盡可能多的提前計算各種資訊并将結果存入每個文檔中。
  • Groovy很快,但沒Java那麼快。可以将我們的腳本用 native Java script 重新實作。(參見 Naive Java Scripts)。
  • 僅對那些最佳評分的文檔應用腳本,用 Rescoring Results 中提到的 rescore功能。

可插拔的相似度算法(Pluggable Similarity Algorithms)

在我們進一步讨論相關度和評分之前,我們會以一個更進階的話題結束本章節的内容:可插拔的相似度算法(Pluggable Similarity Algorithms)。Elasticsearch将 Lucene's Practical Scoring Function 作為他的預設相似度算法,它也能夠支援其他的一些算法,這些算法可以從文檔 相似度子產品(Similarity Models) 中找到。

Okapi BM25

能與 TF/IDF 和 向量空間模型 一個強力競争對手就是 Okapi BM25,它被認為是目前最先進的排序函數。BM25源自機率相關模型(probabilistic relevance model),而不是向量空間模型(vector space model),但這個算法也和Lucene's Practical Scoring Function有很多共通之處。

BM25同樣使用詞頻(term frequency)、逆向文檔頻率(inverse document frequency)以及字段長正則化,但是每個因子的定義都有細微差別。與其詳細解釋BM25公式,倒不如将關注點放在BM25能為我們帶來的實際好處上。

詞頻飽和度(Term-frequency saturation)

TF/IDF 和 BM25 同樣使用 逆向文檔頻率(IDF)來區分普通詞(不重要)和非普通詞(重要),同樣認為文檔裡的某個詞出現次數越頻繁,文檔與這個詞就越相關。

不幸的是,普通詞随處可見,實際上一個普通詞在文檔裡大量出現會抵消該詞在所有文檔中大量出現的作用。

曾經有個時期,将最普通的詞(或停用詞,參見 停用詞:性能與準确度)從索引中移除被認為是一種标準實踐,TF/IDF正是在這種背景下誕生的。TF/IDF沒有考慮詞頻上限的問題,因為高頻停用詞已經被移除了。

Elasticsearch的标準分析器(string 字段預設使用)不會移除停用詞,因為盡管這些詞的重要性很低,但也不是毫無用處。這導緻了一個結果:在一個相當長的文檔中,像 the 和 and 這樣詞出現的數量會高得離譜,以緻它們的權重會被人為放大。

與之相比,BM25有一個上限,文檔裡出現5到10次的詞項會比那些隻出現1、2次的對相關度有着顯著影響。但是通過圖(Figure 34. Term frequency saturation for TF/IDF and BM25)可以看到,文檔中出現20次的詞項幾乎與那些出現上千次的詞項有着相同的影響。

這就是 非線性詞頻飽和度(nonlinear term-frequency saturation)。

Figure 34. Term frequency saturation for TF/IDF and BM25

ElasticSearch 2 (18) - 深入搜尋系列之控制相關度ElasticSearch 2 (18) - 深入搜尋系列之控制相關度
字段長正則化(Field-length normalization)

在字段長正則化(Field-length norm)中,我們提到過Lucene會認為較短字段比較長字段更重要:一個字段某個詞項的頻度所帶來的重要性會被這個字段長度抵消,但是實際的評分函數會将所有字段以同等方式對待。它認為所有的 title 字段(因為它短)比所有的 body 字(因為它長)段更重要。

BM25當然也認為較短字段應該有更多的權重,但是它會分别考慮每個字段内容的平均長度,這樣就能區分一個長的 title 字段和一個短的 title 字段。

在 查詢時權重提升(Query-Time Boosting)中,我們提到 title 字段因為其長度比 body 字段有更為中性的權重提升值。

BM25調優(Tuning BM25)

不像TF/IDF,BM25有一個比較好的特性就是它為我們提供了兩個可調參數:

  • k1

    這個參數控制着詞頻結果在詞頻飽和度中的上升速度。預設值為 1.2。值越小飽和度變化越快,值越大飽和度變化越慢。

  • b

    這個參數控制着字段長正則值所起的作用,0.0 會禁用正則化,1.0 會啟用完全正則化。預設值為 0.75

在實踐中,調試BM25是另外一回事,k1 和 b 的預設值适用于絕大多數文檔集合,但最優值還是會因為文檔集不同而不同,為了找到文檔集合的最優值,我們就必須對參數進行反複修改驗證。

修改相似度(Changing Similarities)

相似度算法可以按字段來指定,隻需在映射中為不同字段標明即可:

PUT /my_index
{
  "mappings": {
    "doc": {
      "properties": {
        "title": {
          "type":       "string",
          "similarity": "BM25" 
        },
        "body": {
          "type":       "string",
          "similarity": "default" 
        }
      }
  }
}
           
  • #1 title 字段使用 BM25 相似度算法。
  • #2 body 字段用預設相似度算法。(參見 Lucene's Practical Scoring Function)

目前,Elasticsearch還不支援改變一個已有字段的相似度算法,隻能通過為資料重建立立索引來達到目的。

配置BM25(Configuring BM25)

配置相似度算法和配置分析器很像,自定義相似度算法可以在建立索引時指定,例如:

PUT /my_index
{
  "settings": {
    "similarity": {
      "my_bm25": { #1
        "type": "BM25",
        "b":    0 #2
      }
    }
  },
  "mappings": {
    "doc": {
      "properties": {
        "title": {
          "type":       "string",
          "similarity": "my_bm25" #3
        },
        "body": {
          "type":       "string",
          "similarity": "BM25" #4
        }
      }
    }
  }
}
           
  • #1 建立一個基于内置BM25算法,名為 my_bm25 的自定義相似度算法。
  • #2 禁止使用字段長正則化。參見 Tuning BM25。
  • #3 title 字段使用自定義相似度算法 my_bm25。
  • #4 body 字段使用内置相似度算法 BM25。

一個自定義的相似度算法可以通過關閉索引,更新索引設定,開啟索引的過程進行更新。這讓我們無需重建索引又能配置試驗不同的相似度算法結果。

調試相關度是最後10%的事情(Relevance Tuning Is the Last 10%)

本章我們介紹了Lucene是如何基于TF/IDF生成評分的。了解評分過程對于我們非常重要,它使我們能為針對具體商業領域調試、調節、減弱并操縱評分。

在實踐中,簡單的查詢組合就能為我們提供很好的搜尋結果,但是為了更高品質的結果,我們就必須反複推敲修改前面介紹的這些調試方法。

通常,經過對政策字段應用權重提升,或通過對查詢語句結構的調整來強調某個句子的重要性這些方法,就足以讓我們獲得非常好的搜尋結果。有時,如果Lucene基于詞的TF/IDF模型不再滿足我們的評分需求(比如:我們希望基于時間或距離來評分),我們又需要一些激進的調整。

除此之外,相關度的調試就有如一個兔子洞,一旦跳進去了就很難再出來。最相關 這個概念是一個難以觸及的模糊目标,通常不同人對于與文檔排序又有着不同的想法,這讓我們很容易陷入一個持續來回調整而沒有任何明顯進步的怪圈。

我們強烈建議不要陷入這種怪圈,而是監控并測量我們的搜尋結果。監控使用者點選最頂端結果的頻次,這可以是前10個文檔,也可以是第一頁;使用者不檢視首次搜尋的結果而直接執行第二次查詢的頻次;使用者來回點選并檢視搜尋結果的頻次。

這些都是用來評價搜尋結果與使用者之間相關程度的名額。如果查詢能傳回高相關的文檔,使用者會選擇頭五個結果中的一個,找到想要的結果,然後離開。不相關的結果會讓使用者來回點選并嘗試新的搜尋條件。

一旦我們有了這些監控儀器,想要調試一個查詢是非常簡單的。稍作修改,監控使用者的行為改變,并作适當重複。本章介紹的一些工具就隻是工具而已,我們需要恰當的利用它們并将搜尋結果推進到高品質一檔,能做到如此的唯一方式就是有個能評價度量使用者行為強大能力。

參考

elastic.co: Controlling Relevance