Solr 遲到的Payloads

– 沒有了索引時的Boost,但你還有我啊!

1. What is this?

Payload在Lucene中表述是一個任意 byte[],它可以(只是可以不是必須)爲它每個Term存儲額外的信息到倒排索引裏面。而且這些Payloads可以被用作Query的一部分來過濾,或者生成對應值用於打分,甚至替代文檔的返回值。

Payload能幹嘛呢

前面提到了,Payloads可以被用作Query的一部分來過濾,或者生成對應值用於打分,甚至替代文檔的返回值。這個非常強大的東西,你理解起來非常容易,你直接當她是一個Hive中的JSON類型即可,如果還是有點陌生的話,直接當她是一個Map吧。即是可以keyvalue

到這裏,你可能要問了。我們本來就是一個動態文檔,原本就是鍵值對的式子,那要你幹嘛呢?
其實,此時你應該把她當成一個nested document。後面會舉這個例子,請繼續往下閱讀。

如果你應該理解上面說的,把她當成一個文檔的子文檔的話。其實她的功能應該很好理解,顯而易見了。

  1. 更人性化的文檔組織方式
  2. 用內嵌文檔(子文檔)的某個字段進行操作
    1. 排序
    2. Faceting
    3. 返回指定key的value
  3. 子文檔flatMap

2. Show Cases

這個例子有個名字,它叫Per-Store Pricing,在 lucidworks 多博客文出現過,感覺非常好,先借來改改先用着。原例子用的相對比較穩定的價格作爲示例,但我覺得用庫存這個動態的屬性作爲示例會更加容易解釋,但不需要過來多說明。

pre-setting :
這是一個在線連鎖店的中央倉存系統,它會記錄每個商品在每家的庫存。由於店鋪的位置和大小的原因,商品a不是所有店鋪都會銷售。
假定這是一個 Snapshot

  • 有了上面這個大前提之後,我們繼續來設定一些假定的操作:

    1. 找出A店所有商品,並按庫存量排序
    2. 找出A店庫存在100-200之間的所有商品
    3. 想某些商品在某些店的庫存情況
  • 針對上面設定和問題,我們嘗試組織文檔和定義操作。

    1. 再商品信息中加入庫存信息,每家店鋪都是一個獨立的字段
      這種作法最大的問題是當然店鋪數量很多的時候,一個文檔的字段非常多,即是多少店鋪就得冗餘多少字段。
    2. 在庫存信息中加入商品信息,每家店鋪都是一個獨立的文檔
      這種作法即是文檔數會很多,但是並不要緊,關鍵是想查商品的情況時,需要做groupBy的操作,這是比較耗性能的操作。
    3. 採用payload的方式,在商品信息加入店鋪的庫存信息作爲payload
      這就是我們要講的內容了。

2. Payloads in Solr

a. 如何在Solr上啓用Payload

你把Payload說得這麼好,那我怎麼Solr裏把它上用起來呢,讓全世界都知道它的好。在Solr6.6之後,我們知道它是隻是一個字段類型,那麼其實很好理解的,即是在schema.xml多配一個字段類型咯,並把它加對應的字段即可了。

1. Change your schema.xml file

<fieldtype name="payloads" stored="false" indexed="true" class="solr.TextField" >
  <analyzer>
    <tokenizer class="solr.WhitespaceTokenizerFactory"/>
    <filter class="solr.DelimitedPayloadTokenFilterFactory" encoder="integer"/>
  </analyzer>
  <similarity class="payloadexample.PayloadSimilarityFactory" />
</fieldtype>

<field name="stock_dpi" type="payloads" indexed="true" stored="true"/>
  • 這裏similarity默認是SchemaSimilarity,它決定payload_scoreQuery得分的關係。
  • encoder 的取值 ${float,integer}$

2. Add the payloaded term to the document

我們已經加了這麼一個字段類型和字段了,接下來我們需要把文檔帶着payload信息文檔提交給Solr進行索引。當然這也很簡單,它就是我們配置的一個字段類型嘛,那麼我們要求進行組織字段的值即可了。

按我們上面的配置(爲簡單起見我們就配置三個字段)


{
    "id":"123",
    "stock_dpi":"a|100 b|200",
    "name":"cafe"
}

b. Payload神器,三把斧

正因爲這三把神器,從而真正牽引我來寫這篇博客。其實非常簡單,也非常實用。好了,我們開始吧。

1. payload() function

payload() 函數並不能作爲查詢條件,只能用於修改顯示的結構作爲僞字段出現在文檔中。即只用在fl的條件,不能用於除fl之外的場景。

payload是對word而言,它是有位置的。即是同一個詞在同個字段中的不同位置可以賦不同的payload值的。

payload()函數簽名:

payload(field, term [,default[,min|max|average|first]])

函數參數說明
- f 必填,字段名
- v 必填,需要轉化的Key
- def 選填, 當對應的key沒有對應的value時的默認
- func 選填,當有一個key對應有多個value時,進什麼計算,默認是average

Example

payloads = ‘a|1.0 a|2.0 a|3.0 b|0.1’

payload(payloads, a) = 2.0  
payload(payloads, c, -1.0) = -1.0   
payload(payloads, a, min) = 1.0     
payload(payloads, c, -1.0, min) = -1.0  

2. Query Function Parser

這裏我們來介紹一下兩個遲到Query Parser,PayloadScoreParser和PayloadCheckParser兩個查詢解釋器。按理說它應該很早應該出現,即使不能跟Lucene4.x一起出現,但實在不至於到現在Solr6.6纔出生。當然,在此之前有過很多民間的實現。因此這兩個解釋器的出現,使得Payload變得有意義、完整。

關於payload的索引存儲原理這裏我們只做簡單的解釋,不做深入的解析。
這些查詢解析器利用term|payload在索引期間最終會翻譯成SpanQuery,SpanNearQuery或者SpanTermQuery。也就是Term與payload其實是成對存在於索引表上的,跟坡度查詢和短語查詢差不多意思。

a. PayloadScoceParser : payload_score

簡單的說,如果用某個子文檔的某個字段的值來參與評分呢?這就是我們接下來要討論的payload_score了。

這些與boost有幾分相似了。多說一句,索引時boost已經Solr6.6棄用了。爲何棄用Boost呢,我想你應該能理解,即是Boost的功能相當單一且不夠靈活。當然啦,也是因爲Payload的引用,使得Boost更加被弱化了。

我們已經payload字段是按一定的規則組織Term和Payload之間的關係的,如上例中我們是這樣的組織:

stock_dpi = a|100 b|200 c|300 a|101 a|102 a|103

  • 其中等號左邊爲字段名,即是下面參數f需要指定的字段名
  • 等號右邊是字段值,它是一系列鍵值對。即這裏的每個鍵便是term,值爲payload

參數說明:
- f 指定使用哪個payloads字段,必填。

  • v 拿哪個 keyterm)來查詢,必填。

    相當於說,需要指哪個子文檔的哪個字段,它裏v即是這的哪個字段

  • func 它的計算結果作爲payload_score的分數值,必填。

    payload function必須填,對payload的進行計算的函數。

  • includeSpanScore 是否把QueryFunction的查詢拼入一般的查詢條件中共同打分

    是把 payload_score 的查詢函數作爲SpanQuery的條件,參與原查詢的打分規則。

    如果是,$score = score(Q_{org} + Q_{pay}) + pay$
    否則的話,$score = score(Q_{org}) + pay$

Example

q={!payload_score f=my_field_dpf v=some_term func=max}

q={!payload_score f=stock_dpi v=a func=average}

對func而言,它僅對f中的v有多值時,纔會有意義。
1. {!payload_score f=payloads v=a func=min} = 1
2. {!payload_score f=payloads v=a func=max} = 3
3. {!payload_score f=payloads v=a func=average} = 2

b. PayloadCheckParser : payload_check

PayloadCheckParser提供的是一個針對Term-Payload檢索功能,即是通過指定鍵值,搜索含有的所有文檔。這麼說可能不好理解,簡單的說,指定了子文檔的字段名和值,希望找到子文檔的該字段名下的值能匹配上的所有文檔。

因此,它跟普通的查詢一樣必須需要指定字段名,和對應的值。

參數說明
- f 指定字段名,必填。

  • payloads 指定payload, 支持多值,之間用空格分開。必填。

  • v 指定key,必填

Example

方式一:
{!payload_check f=words_dps payloads=”VERB NOUN”}searching stuff
方式二:
{!payload_check f=words_dps payloads=”VERB NOUN” v=searching stuff}

3. Faceting on numeric payloads

Faceting出生很早,是Lucene統計分析的基石,大家絕對不會陌生。Faceting在分析方面那麼好、那麼優秀,那麼她能不能延伸到PayloadQuery呢?
其實facet.range只支持實際字段,而不支持僞字段。然而她又那麼好,不忍捨棄,所以在現在版本得用一些比較偏門的方法,即是facet.query,對payload()再進行{!frange} query function查詢來完成類facet.range查詢。

後面版本可能會實現對payload進行facet.range,等着吧。

  • facet.query={!frange key=up_to_400 l=0 u=400}${computed_price} // includes price=400.00
  • facet.query={!frange key=above_400 l=400 incl=false}${computed_price} // excludes price=400.00

3. 回顧例子,Payload On Solr

前面提到了,Payloads可以被用作Query的一部分來過濾,或者生成對應值用於打分,甚至替代文檔的返回值。

A. Solr下的原例1 per-store pricing

上面挖了坑,現在我們來把它填上。在Solr6.6之後的Solr如何優雅解決這個問題的呢。前面我們討論per-store pricing問題時,已經給出結論,但我們並沒有說明這個方案應當如何操作,接下來我們就來說說它應該如何操作。

在上面我們提出了這如下幾問題,如今是時候把他解決掉了。
1. 找出商品A所有商鋪,並按庫存量排序
2. 找出A店庫存在100-200之間的所有商品
3. 想某些商品在某些店的庫存情況

現在我們按商品信息進行組織文檔,然後把庫存信息作爲payloads字段類型加入到文檔中。即是

{
    "id":"product00001"
    "name":"product_a",
    "price":100.0,
    "stock_dpi":"stock_a|100 stock_b|200 stock_c|300",
    ...
}

現在我們可以這麼做
1. q=stock_dpi:stock_a&sort=payloads(stock_dpi, stock_a, 0, min) desc
2. facet.query={!frange key=up_to_400 l=0 u=400}payload(stock_dpi, stock_a, 0)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章