原文《Implementing A Modern E-Commerce Search》,作者:Alexander Reelsen.
原文內容比較多,所以翻譯會分三篇發出:
第一篇:講述了好的搜索功能由好的索引資料和好的查詢陳述句(即搜索關鍵詞+特征過濾器)組成,電子商務搜索中的產品資料處理(包含:資料清洗、計量單位、重復資料、庫存資料)和特定場景的資料建模(包含:變體、多語言、分解分詞、價格)
第二篇:一些用例,后續再細化
第三篇:一些用例,后續再細化
在線kibana:http://134.175.121.78:5601/app/dev_tools#/console
(是我自己的服務器搭建的,請大家友好的體驗)
簡介
搜索功能很難,做好電子商務網站的搜索功能更難,實作一個好的搜索包含兩個要素:好的索引資料和好的查詢陳述句(即搜索關鍵詞+特征過濾器),兩個元素同時存在的情況是很難的,但在e-discovery這種平臺上是很常見的(什么是e-discovery??是政府或法律授權機構通過網路技術向有關行業(如法律、稅務機構等)提供的資訊交換平臺,也稱作“電子儲存資訊”(Electronically stored information,即ESI)),使用e-discovery平臺搜索資料的用戶擁有深厚的專業知識,也有能力提出適當的查詢陳述句,但是,在電子商務中基本是相反的,你通常有結構化良好的資料,但你的搜索質量卻很低,用戶并不確切知道他們要搜索什么,他們通常搜索的是品牌名稱、產品名稱或類似“便宜的”這樣的形容詞,而且還可能包含拼寫錯誤,
這篇文章要討論的另一個常用搜索場景是聚合與分析用例,這常見于儀表盤功能,但電子商務搜索通常聚焦于搜索電商產品,盡管聚合常用于深入資料分析,但對我來說,最常見的聚合用例是其可觀察的特性,比如在日志、指標或跟蹤(logs、metrics、traces)資料上的聚合,
我不會為文章中的每一個用例都提供使用Elasticsearch的解決方案,但我會舉例說明我的觀點,
為什么電商搜索這么難?
這是一個非常棒的問題,在這么多年之后,我仍然發現這個問題相當難以回答,因為這不是一個事情導致它難以回答,而是許多小事情的共同影響,有時僅僅一個小事情就足以讓網站的訪問者在眨眼間決定不在你的電子商店中購買,最重要的是,有許多與搜索無關的因素也會把用戶趕出你的網站,
我最近的個人經歷是在新冠疫情封城期間嘗試在Hugendubel商城中訂購一本書,Hugendubel商城是德國的讀書類專業電商,但它不允許我在沒有創建用戶賬戶的情況下下單,而Thalia.de商城允許我這樣做,所以我最后選擇在Thalia.de商城中下單,這和搜索體驗完全沒有關系,
在另一個封城期間的案例中,我嘗試在Ravensburger商城中為我女兒訂購一本書,線上商城告訴我,這本書只能在物體店購買,而且,每當我使用Amazon pay進行支付,卻沒有收到我的信用卡是否被扣款的通知,我向平臺寫了一封電子郵件,兩周后我得到了反饋:我描述的問題已經轉發給負責支付的部門,另一個導致我不想再光顧這個商城的原因是,搜索體驗非常糟糕,
但是,讓我們不要把重點放在我對網上商店的責罵中,而要放在正確的搜索上,
產品資料
讓我們從最高優先級的產品資料開始,沒有資料,何談搜索,經營一個商城意味著,商家提供資料,并且不同商家提供的資料格式會不一樣,
1、資料清洗(clean data)
什么是資料清洗?它是發現并糾正資料檔案中可識別的錯誤的最后一道程式,包括檢查資料一致性,處理無效值和缺失值等,
對于客戶資料,這通常意味著大量的驗證:
#、有效的URLS
#、資料型別約束(eg:庫存必須是int)
#、范圍約束(eg:庫存必須是正整數)
#、值匹配,通過運算式或自定義程式代碼實作
取決于資料供應商的職業,一些供應商公司還在通過Excel來管理他們的資料(eg:手動在Excel中更新庫存資料),一些供應商有一個成熟的軟體系統管理他們的資料并允許你匯出此類資料,
這給我們帶來了另一個有趣的話題,你接受什么樣的資料格式?JSON、XML、EDIFAC或者CSV?你有API或表單上傳嗎?你該如何處理多年沒有更新的資料?
資料清洗是一件很棘手的事情,你需要一個萬無一失的處理程序,如果你的資料清洗程序將商家產品價格更改為原始價格十分之一,并且有人下了1000個訂單,這種情況怎么辦?責任也是很重要的話題,
2、計量單位(UOM)
計量單位(Unit of Measure/Measurement,UOM),這不僅僅是關于系統指標,而且是關于到不同單位之間的轉換,需要對所有資料的值進行規范化,這意味著,如果一個產品的尺寸是英寸,而另一產品的尺寸是厘米,那么就需要一個轉換機制來進行適當的范圍查詢,你還需要確保對不同的產品使用了正確的計量單位,eg:顯示幕、飲料、視頻包裝等等,
3、重復資料
如果你經營一個商城,你會發現這些商家銷售相同商品的幾率很高,
如何處理這種情況?這個問題在圖書品類中已經通過ISBN解決了,如果你是世界上最大的商城,你就有能力創建一個ASIN,
(
ISBN(International Standard Book Number)國際標準書號,是專門為識別圖書等文獻而設計的國際編號,
ASIN:ASIN(Amazon standard identification number),亞馬遜為自家產品編的唯一編號
)
也有一些可以考慮的替代方案,你可以為提供的照片檢查相似性,復雜的檢查方案會浪費很多時間,有時只需簡單的考慮檢測相同哈希值就足夠了,
你也可以比較產品的描述,因為它們通常直接從生產商處復制,另外還有:產品名稱、發布日期或計量單位等,
這些替代方案都不是百分百安全的,
4、庫存資料
擁有近實時的資訊是非常重要的,比如產品是可用的;比如產品不能在2-3天內送達,大多客戶不會下單,因為客戶往往是沖動性消費,
所以,要么你能查詢其他系統(eg:查詢商家系統獲取最新的資料),要么你的商家提供庫存資料,庫存資料更新通常比價格或產品內容更新更頻繁,因此請確保使用一種輕量級的更新方式,
你可能還需要處理庫存資訊陳舊的問題,即在你平臺上標識可用的商品但在商家處已經不再可用,從而導致訂單取消和變更,
資料建模
現在開始為資料建模,首先你獲得了一些屬性,然后為它們標記上text/keyword標記,就可以開始搜索了,
1、變體
(譯者注:變體,即一個產品一個屬性存在不同值,就可能有多個變體,即SKU和SPU的概念)
對我來說,最棘手的問題是產品的變體,首先,你需要為不同的屬性和它們的組合建模,商家總是將多個變體掛載到一個產品中,即使這些變體本應該是獨立的產品,很難制定一個規則來規范什么是變體,什么不是,讓我們先一起來看些簡單又無處不在的商品:衣服,
#、Color(red, green, yellow, black, orange, white, blue)
#、Size(XXS, XS, S, M, L, XL, XXL, XXXL)
簡單的兩個維度,卻已經有56個獨立的產品了,如果是四個維度將會導致變體風暴,而在UI中已經很難顯示哪些變體存在,哪些不存在,Amazon商城解決此問題的方案是:在點擊屬性后,再展現變體資訊,
這種場景如何建模呢?這里有三個方案,其付出的成本相差很大,
方案一:每一個變體擁有自己的索引檔案,這個方案簡單容易實作,但當商品數變多時,會存在很多重復的檔案和內容,另外,如果沒有指定屬性,該如何進行搜索過濾?讓我們通過一個t-shirt示例來說明,
這個t-shirt存在不同的顏色和尺寸,
DELETE products
PUT products/_bulk?refresh
{ "index" : {} }
{ "title" : "Elastic Robot T-Shirt", "size": "M", "color" : "gray" }
{ "index" : {} }
{ "title" : "Elastic Robot T-Shirt", "size": "S", "color" : "gray" }
{ "index" : {} }
{ "title" : "Elastic Robot T-Shirt", "size": "L", "color" : "gray" }
{ "index" : {} }
{ "title" : "Elastic Robot T-Shirt", "size": "M", "color" : "green" }
{ "index" : {} }
{ "title" : "Elastic Robot T-Shirt", "size": "S", "color" : "green" }
{ "index" : {} }
{ "title" : "Elastic Robot T-Shirt", "size": "L", "color" : "green" }
查詢陳述句如下:
GET products/_search
{
"query": {
"bool": {
"must": {
"match": {
"title": "shirt"
}
},
"filter": [
{
"term": {
"color.keyword": "green"
}
},
{
"term": {
"size.keyword": "M"
}
}
]
}
}
}
查詢結果只有一條shift資料,但當我們從兩個filter中移除一個后,將會回傳同一個shift產品的多條document資料,
我們可以通過elasticsearch的field collapsing功能來解決這個問題,但這也意味著在查詢時需要多做一些事情,
方案二:我們可以嘗試使用elasticsearch的嵌套資料型別,把所有的變體放到一個陣列中,如下:
DELETE products
PUT products
{
"mappings": {
"properties": {
"variants" : {
"type": "nested"
}
}
}
}
POST products/_doc
{
"title" : "Elastic Robot T-Shirt",
"variants" : [
{ "size": "S", "color": "gray"},
{ "size": "M", "color": "gray"},
{ "size": "L", "color": "gray"},
{ "size": "S", "color": "green"},
{ "size": "M", "color": "green"},
{ "size": "L", "color": "green"}
]
}
查詢陳述句如下:
GET products/_search
{
"query": {
"bool": {
"must": {
"match": {
"title": "shirt"
}
},
"filter": [
{
"nested": {
"path": "variants",
"query": {
"term": {
"variants.color.keyword": "green"
}
}
}
},
{
"nested": {
"path": "variants",
"query": {
"term": {
"variants.size.keyword": "M"
}
}
}
}
]
}
}
}
我們也可以在不使用任何filter的情況下進行搜索,且只回傳一個檔案,需要注意一點是:通過使用自動映射來防止映射爆炸,如果你控制了屬性名稱,請盡量減少它們的數量并統一規范它們(eg:屬性名稱size,可以用于多種商品上)
使用 inner_hits 功能也很容易找出匹配的嵌套檔案,
那么這個方案有什么問題呢?問題在于產品資料更新,如果你也將庫存存盤在該索引中,那么單個變體的庫存更新將導致整個檔案的索引重建,因為庫存數量的變更頻率,可能會是相當大的開銷,但我仍然傾向于這個解決方案,因為我認為庫存更新在大多數情況下是可管理的,
方案三:使用 join資料型別,允許我們在查詢時將兩個檔案(產品檔案和變體檔案)進行關聯,
DELETE products
PUT products
{
"mappings": {
"properties": {
"join_field": {
"type": "join",
"relations": {
"parent_product": "variant"
}
}
}
}
}
PUT products/_bulk?refresh
{ "index" : { "_id": "robot-shirt" } }
{ "title" : "Elastic Robot T-Shirt", "join_field" : { "name" : "parent_product" } }
{ "index" : { "routing": "robot-shirt" } }
{ "size": "M", "color" : "gray", "join_field" : { "name" : "variant", "parent" : "robot-shirt" } }
{ "index" : { "routing": "robot-shirt" } }
{ "size": "S", "color" : "gray", "join_field" : { "name" : "variant", "parent" : "robot-shirt" } }
{ "index" : { "routing": "robot-shirt" } }
{ "size": "L", "color" : "gray", "join_field" : { "name" : "variant", "parent" : "robot-shirt" } }
{ "index" : { "routing": "robot-shirt" } }
{ "size": "M", "color" : "green", "join_field" : { "name" : "variant", "parent" : "robot-shirt" } }
{ "index" : { "routing": "robot-shirt" } }
{ "size": "S", "color" : "green", "join_field" : { "name" : "variant", "parent" : "robot-shirt" } }
{ "index" : { "routing": "robot-shirt" } }
{ "size": "L", "color" : "green", "join_field" : { "name" : "variant", "parent" : "robot-shirt" } }
查詢陳述句如下:
GET products/_search
{
"query": {
"bool": {
"must": {
"match": {
"title": "shirt"
}
},
"filter": [
{
"has_child": {
"inner_hits": {},
"type": "variant",
"query": {
"bool": {
"filter": [
{
"term": {
"color.keyword": "green"
}
},
{
"term": {
"size.keyword": "M"
}
}
]
}
}
}
}
]
}
}
}
這也會回傳匹配的子產品,請注意,使用join資料型別比使用nested資料型別的查詢開銷要大,因此,只有在高更新負載的情況下我才會考慮使用join資料型別,搜索速度對我來說是最重要的指標之一,
上面這個示例也使用了之前提到的inner_hits 功能,所以你不僅能看到父檔案,也可以看到匹配的子檔案,請注意,這可能不止一次命中,所以你應該小心的將結果回傳到客戶端(我總是試圖只回傳一個變體),為客戶端回傳部分變體資料可能很重要,假設你正在搜索一件XL尺寸的綠色shirt,那么回傳一個綠色shirt影像比回傳尺寸為XL 的shirt影像更加有用,
哪些資料屬于變體,哪些資料屬于父產品,這很難把控,有些商家會為每一個變體撰寫一個描述,我是非常反對的,因為不同變體之間,屬性應該是唯一的區別,
在進入下一個話題之前,有幾個問題是需要我們思考的:
#、如何在UI中處理丟失的變體?
#、如何顯示不可用的變體?
#、你能處理2000個產品變體嗎?
#、沒有任何變體的產品如何展示和建模?
#、確保變體能擁有獨立的單價(eg:手機中不同記憶體會有不同價格)
#、變體的屬性能否支持搜索和過濾?(eg:尺寸、顏色等)
2、多語言
如果你的產品名稱和描述需要支持多語言,你應該為每種語言設定專用欄位,以便你使用自定義分析器,這里包含兩個問題:如何識別語言和如何存盤內容,首先,如果你不懂這種語言,你需要去識別它,最好的情況下,語言資訊和產品資料一起交付給你,
在Elasticsearch中,在推理處理器(inference processor)中內置了一種語言識別(language identification)特性,所以你可以在索引時提取語言資訊,
POST _ingest/pipeline/_simulate
{
"pipeline": {
"processors": [
{
"inference": {
"model_id": "lang_ident_model_1",
"inference_config": { "classification": {}},
"field_map": {}
}
}
]
},
"docs": [
{ "_source": { "text": "Das ist ein deutscher Text" } }
]
}
預測結果為:de(德語)
推測出語言后,你就可以將語言和內容存盤到一個特定的欄位中,如description.de,如果你能分析出用戶搜索關鍵詞使用的語言,你就可以只使用德語分析器搜索德語欄位(description.de),從而得到更好的搜索體驗,
3、Decompounding分解分詞
這是一個德語案例,雖然只針對德語一種語言做處理,但依然很難,尤其是很多產品名稱存在復合詞的情況,著名的:Eiersollbruchenstellenverursacher,如果你覺得好奇,你可以在Amazon網站上搜索試試,這不是一個假冒產品,但也只是一個例外,還有一些簡單的例子,比如Blumentopf(flower pot,花盆)和Kochtopf(cooking pot,烹飪器),當只輸入topf時,是不能搜索出Blumentopf和Kochtopf相關的產品的,因為它們只是這個詞的一部分,但英語通過pot單詞(上面括號中為德語對應的英語單詞)很好的解決了這個問題,pot擁有自己的詞條,也被放入倒排索引中,
幸運的是,Lucene有一個分解分詞過濾器(decompounder token filter),讓我們在德語中可以實作pot的效果,讓我們看下面這個例子,
# returns each term
GET _analyze
{
"tokenizer": "standard",
"text": [ "Blumentopf", "Kochtopf" ]
}
GET _analyze?filter_path=tokens.token
{
"tokenizer": "standard",
"filter": [
{
"type": "dictionary_decompounder",
"word_list": ["topf"]
}
],
"text": [ "Blumentopf", "Kochtopf" ]
}
第一條查詢陳述句不會把topf分解為獨立詞條,第二條查詢陳述句會將topf分解為獨立的詞條:
{
"tokens" : [
{
"token" : "Blumentopf"
},
{
"token" : "topf"
},
{
"token" : "Kochtopf"
},
{
"token" : "topf"
}
]
}
但是請注意,讓我們用相同的方式執行另一個詞條”Stopfwatte”
GET _analyze?filter_path=tokens.token
{
"tokenizer": "standard",
"filter": [
{
"type": "dictionary_decompounder",
"word_list": ["topf"]
}
],
"text": [ "Stopfwatte" ]
}
回傳結果
{
"tokens" : [
{
"token" : "Stopfwatte"
},
{
"token" : "topf"
}
]
}
你可以嘗試向你的用戶解釋,搜索topf時,Stopfwatte為什么是一個有效的回傳結果,但是我相信這會非常難解釋清楚,你也可以在多條件bool查詢中使用多個should來影響搜索結果評分,但這很可能意味著你用錯誤的方式解決了這個問題,更好的解決這個問題的地方應該是在創建索引時,
這個的地方就是:斷詞分解(Hyphenation decompounder)處,這需要一個來自offo專案的XML檔案,
GET _analyze
{
"tokenizer": "standard",
"filter": [
{
"type": "hyphenation_decompounder",
"hyphenation_patterns_path": "analysis/de_DR.xml",
"word_list": ["topf"]
}
],
"text": [ "Blumentopf", "Kochtopf", "Stopfwatte" ]
}
運行結果是
{
"tokens" : [
{
"token" : "Blumentopf"
},
{
"token" : "topf"
},
{
"token" : "Kochtopf"
},
{
"token" : "topf"
},
{
"token" : "Stopfwatte"
}
]
}
如你所見,Stopfwatte就沒有創建獨立的topf詞條,因為現在使用段詞字典更好的拆分了詞條,
最后,當你決定對詞條進行分解時,你需要非常清楚,你需要一個持續更新的單詞串列,
你也可以根據你的業務場景創建和修改斷詞模型(hyphenation patterns),
4、價格
一個產品只有一個價格的想法是錯誤的,可能是2個,因為有執行價格,可能是3個,因為有大量的減免,也可能是4個,因為有不同的銷售稅,可能是52個,因為每個州的銷售稅不同,但至少這些價格是靜態的,
如果某些客戶得到永久的10%的折扣,所有的產品,是否要對每一個客戶群設定一個價格變體?
在執行搜索時,是否考慮了價格優惠的問題?如何顯示價格?你是否想為搜索回傳的每個產品再呼叫一次價格服務來獲取價格?
這些都是棘手的問題,關鍵是你要明白:你的產品不會只有一個價格,
(譯者注:淘寶在根據價格過濾時,是根據折扣之前的價格值進行過濾的)
其他推薦閱讀:
Elasticsearch搜索資料匯總
==============================================================================
over,謝謝查閱,覺得文章對你有識訓,請多幫推薦,歡迎向我提供更好的資料資訊,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/229368.html
標籤:其他
