日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

基于Elasticsearch实现搜索推荐

發(fā)布時間:2024/1/17 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 基于Elasticsearch实现搜索推荐 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

在基于Elasticsearch實現(xiàn)搜索建議一文中我們曾經(jīng)介紹過如何基于Elasticsearch來實現(xiàn)搜索建議,而本文是在此基于上進一步優(yōu)化搜索體驗,在當(dāng)搜索無結(jié)果或結(jié)果過少時提供推薦搜索詞給用戶。

背景介紹

在根據(jù)用戶輸入和篩選條件進行搜索后,有時返回的是無結(jié)果或者結(jié)果很少的情況,為了提升用戶搜索體驗,需要能夠給用戶推薦一些相關(guān)的搜索詞,比如用戶搜索【迪奧】時沒有找到相關(guān)的商品,可以推薦搜索【香水】、【眼鏡】等關(guān)鍵詞。

設(shè)計思路

首先需要分析搜索無結(jié)果或者結(jié)果過少可能的原因,我總結(jié)了一下,主要包括主要可能:

  • 搜索的關(guān)鍵詞在本網(wǎng)不存在,比如【迪奧】;
  • 搜索的關(guān)鍵詞在本網(wǎng)的商品很少,比如【科比】;
  • 搜索的關(guān)鍵詞拼寫有問題,比如把【阿迪達斯】寫成了【阿迪大斯】;
  • 搜索的關(guān)鍵詞過多,由于我們采用的是cross_fields,在一個商品內(nèi)不可能包含所有的Term,導(dǎo)致無結(jié)果,比如【阿迪達斯 耐克 衛(wèi)衣 運動鞋】;
  • 那么針對以上情況,可以采用以下方式進行處理:

  • 搜索的關(guān)鍵詞在本網(wǎng)不存在,可以通過爬蟲的方式獲取相關(guān)知識,然后根據(jù)搜索建議詞去提取,比如去百度百科的迪奧詞條里就能提取出【香水】、【香氛】和【眼鏡】等關(guān)鍵詞;當(dāng)然基于爬蟲的知識可能存在偏差,此時需要能夠有人工審核或人工更正的部分;
  • 搜索的關(guān)鍵詞在本網(wǎng)的商品很少,有兩種解決思路,一種是通過方式1的爬蟲去提取關(guān)鍵詞,另外一種是通過返回商品的信息去聚合出關(guān)鍵詞,如品牌、品類、風(fēng)格、標簽等,這里我們采用的是后者(在測試后發(fā)現(xiàn)后者效果更佳);
  • 搜索的關(guān)鍵詞拼寫有問題,這就需要拼寫糾錯出場了,先糾錯然后根據(jù)糾錯后的詞去提供搜索推薦;
  • 搜索的關(guān)鍵詞過多,有兩種解決思路,一種是識別關(guān)鍵詞的類型,如是品牌、品類、風(fēng)格還是性別,然后通過一定的組合策略來實現(xiàn)搜索推薦;另外一種則是根據(jù)用戶的輸入到搜索建議詞里去匹配,設(shè)置最小匹配為一個匹配到一個Term即可,這種方式實現(xiàn)比較簡單而且效果也不錯,所以我們采用的是后者。
  • 所以,我們在實現(xiàn)搜索推薦的核心是之前講到的搜索建議詞,它提供了本網(wǎng)主要的關(guān)鍵詞,另外一個很重要的是它本身包含了關(guān)聯(lián)商品數(shù)的屬性,這樣就可以保證推薦給用戶的關(guān)鍵詞是可以搜索出結(jié)果的。

    實現(xiàn)細節(jié)

    整體設(shè)計

    整體設(shè)計框架如下圖所示:

    搜索推薦整體設(shè)計

    搜索建議詞索引

    在基于Elasticsearch實現(xiàn)搜索建議一文已有說明,請移步閱讀。此次增加了一個keyword.keyword_lowercase的字段用于拼寫糾錯,這里列取相關(guān)字段的索引:

    PUT /suggest_index {"mappings": {"suggest": {"properties": {"keyword": {"fields": {"keyword": {"type": "string","index": "not_analyzed"},"keyword_lowercase": {"type": "string","analyzer": "lowercase_keyword"},"keyword_ik": {"type": "string","analyzer": "ik_smart"},"keyword_pinyin": {"type": "string","analyzer": "pinyin_analyzer"},"keyword_first_py": {"type": "string","analyzer": "pinyin_first_letter_keyword_analyzer"}},"type": "multi_field"},"type": {"type": "long"},"weight": {"type": "long"},"count": {"type": "long"}}}} }

    商品數(shù)據(jù)索引

    這里只列取相關(guān)字段的mapping:

    PUT /product_index {"mappings": {"product": {"properties": {"productSkn": {"type": "long"},"productName": {"type": "string","analyzer": "ik_smart"},"brandName": {"type": "string","analyzer": "ik_smart"},"sortName": {"type": "string","analyzer": "ik_smart"},"style": {"type": "string","analyzer": "ik_smart"}}}} }

    關(guān)鍵詞映射索引

    主要就是source和dest直接的映射關(guān)系。

    PUT /conversion_index {"mappings": {"conversion": {"properties": {"source": {"type": "string","analyzer": "lowercase_keyword"},"dest": {"type": "string","index": "not_analyzed"}}}} }

    爬蟲數(shù)據(jù)爬取

    在實現(xiàn)的時候,我們主要是爬取了百度百科上面的詞條,在實際的實現(xiàn)中又分為了全量爬蟲和增加爬蟲。

    全量爬蟲

    全量爬蟲我這邊是從網(wǎng)上下載了一份他人匯總的詞條URL資源,里面根據(jù)一級分類包含多個目錄,每個目錄又根據(jù)二級分類包含多個詞條,每一行的內(nèi)容的格式如下:

    李寧!http://baike.baidu.com/view/10670.html?fromTaglist diesel!http://baike.baidu.com/view/394305.html?fromTaglist ONLY!http://baike.baidu.com/view/92541.html?fromTaglist lotto!http://baike.baidu.com/view/907709.html?fromTaglist

    這樣在啟動的時候我們就可以使用多線程甚至分布式的方式爬蟲自己感興趣的詞條內(nèi)容作為初始化數(shù)據(jù)保持到爬蟲數(shù)據(jù)表。為了保證冪等性,如果再次全量爬取時就需要排除掉數(shù)據(jù)庫里已有的詞條。

    增量爬蟲

  • 在商品搜索接口中,如果搜索某個關(guān)鍵詞關(guān)聯(lián)的商品數(shù)為0或小于一定的閾值(如20條),就通過Redis的ZSet進行按天統(tǒng)計;
  • 統(tǒng)計的時候是區(qū)分搜索無結(jié)果和結(jié)果過少兩個Key的,因為兩種情況實際上是有所區(qū)別的,而且后續(xù)在搜索推薦查詢時也有用到這個統(tǒng)計結(jié)果;
  • 增量爬蟲是每天凌晨運行,根據(jù)前一天統(tǒng)計的關(guān)鍵詞進行爬取,爬取前需要排除掉已經(jīng)爬過的關(guān)鍵詞和黑名單中的關(guān)鍵詞;
  • 所謂黑名單的數(shù)據(jù)包含兩種:一種是每天增量爬蟲失敗的關(guān)鍵字(一般會重試幾次,確保失敗后加入黑名單),一種是人工維護的確定不需要爬蟲的關(guān)鍵詞;
  • 爬蟲數(shù)據(jù)關(guān)鍵詞提取

  • 首先需要明確關(guān)鍵詞的范圍,這里我們采用的是suggest中類型為品牌、品類、風(fēng)格、款式的詞作為關(guān)鍵詞;
  • 關(guān)鍵詞提取的核心步驟就是對爬蟲內(nèi)容和關(guān)鍵詞分別分詞,然后進行分詞匹配,看該爬蟲數(shù)據(jù)是否包含關(guān)鍵詞的所有Term(如果就是一個Term就直接判斷包含就好了);在處理的時候還可以對匹配到關(guān)鍵詞的次數(shù)進行排序,最終的結(jié)果就是一個key-value的映射,如{迪奧 -> [香水,香氛,時裝,眼鏡], 紀梵希 -> [香水,時裝,彩妝,配飾,禮服]};
  • 管理關(guān)鍵詞映射

  • 由于爬蟲數(shù)據(jù)提取的關(guān)鍵詞是和詞條的內(nèi)容相關(guān)聯(lián)的,因此很有可能提取的關(guān)鍵詞效果不大好,因此就需要人工管理;
  • 管理動作主要是包括添加、修改和置失效關(guān)鍵詞映射,然后增量地更新到conversion_index索引中;
  • 搜索推薦服務(wù)的實現(xiàn)

  • 首先如果對搜索推薦的入口進行判斷,一些非法的情況不進行推薦(比如關(guān)鍵詞太短或太長),另外由于搜索推薦并非核心功能,可以增加一個全局動態(tài)參數(shù)來控制是否進行搜索推薦;
  • 設(shè)計思路里面我們分析過可能有4中場景需要搜索推薦,如何高效、快速地找到具體的場景從而減少不必要的查詢判斷是推薦服務(wù)實現(xiàn)的關(guān)鍵;這個在設(shè)計的時候就需要綜合權(quán)衡,我們通過一段時間的觀察后,目前采用的邏輯的偽代碼如下:
  • public JSONObject recommend(SearchResult searchResult, String queryWord) {try {String keywordsToSearch = queryWord;// 搜索推薦分兩部分// 1) 第一部分是最常見的情況,包括有結(jié)果、根據(jù)SKN搜索、關(guān)鍵詞未出現(xiàn)在空結(jié)果Redis ZSet里if (containsProductInSearchResult(searchResult)) {// 1.1) 搜索有結(jié)果的 優(yōu)先從搜索結(jié)果聚合出品牌等關(guān)鍵詞進行查詢String aggKeywords = aggKeywordsByProductList(searchResult);keywordsToSearch = queryWord + " " + aggKeywords;} else if (isQuerySkn(queryWord)) {// 1.2) 如果是查詢SKN 沒有查詢到的 后續(xù)的邏輯也無法推薦 所以直接到ES里去獲取關(guān)鍵詞keywordsToSearch = aggKeywordsBySkns(queryWord);if (StringUtils.isEmpty(keywordsToSearch)) {return defaultSuggestRecommendation();}}Double count = searchKeyWordService.getKeywordCount(RedisKeys.SEARCH_KEYWORDS_EMPTY, queryWord);if (count == null || queryWord.length() >= 5) {// 1.3) 如果該關(guān)鍵詞一次都沒有出現(xiàn)在空結(jié)果列表或者長度大于5 則該詞很有可能是可以搜索出結(jié)果的// 因此優(yōu)先取suggest_index去搜索一把 減少后面的查詢動作JSONObject recommendResult = recommendBySuggestIndex(queryWord, keywordsToSearch, false);if (isNotEmptyResult(recommendResult)) {return recommendResult;}}// 2) 第二部分是通過Conversion和拼寫糾錯去獲取關(guān)鍵詞 由于很多品牌的拼寫可能比較相近 因此先走Conversion然后再拼寫檢查String spellingCorrentWord = null, dest = null;if (allowGetingDest(queryWord) && StringUtils.isNotEmpty((dest = getSuggestConversionDestBySource(queryWord)))) {// 2.1) 爬蟲和自定義的Conversion處理keywordsToSearch = dest;} else if (allowSpellingCorrent(queryWord) && StringUtils.isNotEmpty((spellingCorrentWord = suggestService.getSpellingCorrectKeyword(queryWord)))) {// 2.2) 執(zhí)行拼寫檢查 由于在搜索建議的時候會進行拼寫檢查 所以緩存命中率高keywordsToSearch = spellingCorrentWord;} else {// 2.3) 如果兩者都沒有 則直接返回return defaultSuggestRecommendation();}JSONObject recommendResult = recommendBySuggestIndex(queryWord, keywordsToSearch, dest != null);return isNotEmptyResult(recommendResult) ? recommendResult : defaultSuggestRecommendation();} catch (Exception e) {logger.error("[func=recommend][queryWord=" + queryWord + "]", e);return defaultSuggestRecommendation();}}

    其中涉及到的幾個函數(shù)簡單說明下:

    • aggKeywordsByProductList方法用商品列表的結(jié)果,聚合出出現(xiàn)次數(shù)最多的幾個品牌和品類(比如各2個),這樣我們就可以得到4個關(guān)鍵詞,和原先用戶的輸入拼接后調(diào)用recommendBySuggestIndex獲取推薦詞;
    • aggKeywordsBySkns方法是根據(jù)用戶輸入的SKN先到product_index索引獲取商品列表,然后再調(diào)用aggKeywordsByProductList去獲取品牌和品類的關(guān)鍵詞列表;
    • getSuggestConversionDestBySource方法是查詢conversion_index索引去獲取關(guān)鍵詞提取的結(jié)果,這里在調(diào)用recommendBySuggestIndex時有個參數(shù),該參數(shù)主要是用于處理是否限制只能是輸入的關(guān)鍵詞;
    • getSpellingCorrectKeyword方法為拼寫檢查,在調(diào)用suggest_index處理時有個地方需要注意一下,拼寫檢查是基于編輯距離的,大小寫不一致的情況會導(dǎo)致Elasticsearch Suggester無法得到正確的拼寫建議,因此在處理時需要兩邊都轉(zhuǎn)換為小寫后進行拼寫檢查;
    • 最終都需要調(diào)用recommendBySuggestIndex方法獲取搜索推薦,因為通過suggest_index索引可以確保推薦出去的詞是有意義的且關(guān)聯(lián)到商品的。該方法核心邏輯的偽代碼如下:
    private JSONObject recommendBySuggestIndex(String srcQueryWord, String keywordsToSearch, boolean isLimitKeywords) {// 1) 先對keywordsToSearch進行分詞List<String> terms = null;if (isLimitKeywords) {terms = Arrays.stream(keywordsToSearch.split(",")).filter(term -> term != null && term.length() > 1).distinct().collect(Collectors.toList());} else {terms = searchAnalyzeService.getAnalyzeTerms(keywordsToSearch, "ik_smart");}if (CollectionUtils.isEmpty(terms)) {return new JSONObject();}// 2) 根據(jù)terms搜索構(gòu)造搜索請求SearchParam searchParam = new SearchParam();searchParam.setPage(1);searchParam.setSize(3);// 2.1) 構(gòu)建FunctionScoreQueryBuilderQueryBuilder queryBuilder = isLimitKeywords ? buildQueryBuilderByLimit(terms): buildQueryBuilder(keywordsToSearch, terms);searchParam.setQuery(queryBuilder);// 2.2) 設(shè)置過濾條件BoolQueryBuilder boolFilter = QueryBuilders.boolQuery();boolFilter.must(QueryBuilders.rangeQuery("count").gte(20));boolFilter.mustNot(QueryBuilders.termQuery("keyword.keyword_lowercase", srcQueryWord.toLowerCase()));if (isLimitKeywords) {boolFilter.must(QueryBuilders.termsQuery("keyword.keyword_lowercase", terms.stream().map(String::toLowerCase).collect(Collectors.toList())));}searchParam.setFiter(boolFilter);// 2.3) 按照得分、權(quán)重、數(shù)量的規(guī)則降序排序List<SortBuilder> sortBuilders = new ArrayList<>(3);sortBuilders.add(SortBuilders.fieldSort("_score").order(SortOrder.DESC));sortBuilders.add(SortBuilders.fieldSort("weight").order(SortOrder.DESC));sortBuilders.add(SortBuilders.fieldSort("count").order(SortOrder.DESC));searchParam.setSortBuilders(sortBuilders);// 4) 先從緩存中獲取final String indexName = SearchConstants.INDEX_NAME_SUGGEST;JSONObject suggestResult = searchCacheService.getJSONObjectFromCache(indexName, searchParam);if (suggestResult != null) {return suggestResult;}// 5) 調(diào)用ES執(zhí)行搜索SearchResult searchResult = searchCommonService.doSearch(indexName, searchParam);// 6) 構(gòu)建結(jié)果加入緩存suggestResult = new JSONObject();List<String> resultTerms = searchResult.getResultList().stream().map(map -> (String) map.get("keyword")).collect(Collectors.toList());suggestResult.put("search_recommendation", resultTerms);searchCacheService.addJSONObjectToCache(indexName, searchParam, suggestResult);return suggestResult;}private QueryBuilder buildQueryBuilderByLimit(List<String> terms) {FunctionScoreQueryBuilder functionScoreQueryBuilder= new FunctionScoreQueryBuilder(QueryBuilders.matchAllQuery());// 給品類類型的關(guān)鍵詞加分functionScoreQueryBuilder.add(QueryBuilders.termQuery("type", Integer.valueOf(2)),ScoreFunctionBuilders.weightFactorFunction(3));// 按詞出現(xiàn)的順序加分for (int i = 0; i < terms.size(); i++) {functionScoreQueryBuilder.add(QueryBuilders.termQuery("keyword.keyword_lowercase", terms.get(i).toLowerCase()),ScoreFunctionBuilders.weightFactorFunction(terms.size() - i));}functionScoreQueryBuilder.boostMode(CombineFunction.SUM);return functionScoreQueryBuilder;}private QueryBuilder buildQueryBuilder(String keywordsToSearch, Set<String> termSet) {// 1) 對于suggest的multi-fields至少要有一個字段匹配到 匹配得分為常量1MultiMatchQueryBuilder queryBuilder = QueryBuilders.multiMatchQuery(keywordsToSearch.toLowerCase(),"keyword.keyword_ik", "keyword.keyword_pinyin", "keyword.keyword_first_py", "keyword.keyword_lowercase").analyzer("ik_smart").type(MultiMatchQueryBuilder.Type.BEST_FIELDS).operator(MatchQueryBuilder.Operator.OR).minimumShouldMatch("1");FunctionScoreQueryBuilder functionScoreQueryBuilder= new FunctionScoreQueryBuilder(QueryBuilders.constantScoreQuery(queryBuilder));for (String term : termSet) {// 2) 對于完全匹配Term的加1分functionScoreQueryBuilder.add(QueryBuilders.termQuery("keyword.keyword_lowercase", term.toLowerCase()),ScoreFunctionBuilders.weightFactorFunction(1));// 3) 對于匹配到一個Term的加2分functionScoreQueryBuilder.add(QueryBuilders.termQuery("keyword.keyword_ik", term),ScoreFunctionBuilders.weightFactorFunction(2));}functionScoreQueryBuilder.boostMode(CombineFunction.SUM);return functionScoreQueryBuilder;}

    最后,從實際運行的統(tǒng)計來看,有90%以上的查詢都能在1.3)的情況下返回推薦詞,而這一部分還沒有進行拼寫糾錯和conversion_index索引的查詢,因此還是比較高效的;剩下的10%在最壞的情況且緩存都沒有命中的情況下,最多還需要進行三次ES的查詢,性能是比較差的,但是由于有緩存而且大部分的無結(jié)果的關(guān)鍵詞都比較集中,因此也在可接受的范圍,這一塊可以考慮再增加一個動態(tài)參數(shù),在大促的時候進行關(guān)閉處理。

    小結(jié)與后續(xù)改進

    • 通過以上的設(shè)計和實現(xiàn),我們實現(xiàn)了一個效果不錯的搜索推薦功能,線上使用效果如下:
    //搜索【迪奧】,本站無該品牌商品 沒有找到 "迪奧" 相關(guān)的商品, 為您推薦 "香水" 的搜索結(jié)果。或者試試 "香氛" "眼鏡" //搜索【puma 運動鞋 上衣】,關(guān)鍵詞太多無法匹配 沒有找到 "puma 運動鞋 上衣" 相關(guān)的商品, 為您推薦 "PUMA 運動鞋" 的搜索結(jié)果。或者試試 "PUMA 運動鞋 女" "PUMA 運動鞋 男"//搜索【puma 上衣】,結(jié)果太少 "puma 上衣" 搜索結(jié)果太少了,試試 "上衣" "PUMA" "PUMA 休閑" 關(guān)鍵詞搜索//搜索【51489312】特定的SKN,結(jié)果太少 "51489312" 搜索結(jié)果太少了,試試 "夾克" "PUMA" "戶外" 關(guān)鍵詞搜索//搜索【blackjauk】,拼寫錯誤 沒有找到 "blackjauk" 相關(guān)的商品, 為您推薦 "BLACKJACK" 的搜索結(jié)果。或者試試 "BLACKJACK T恤" "BLACKJACK 休閑褲"
    • 后續(xù)考慮的改進包括:1.繼續(xù)統(tǒng)計各種無結(jié)果或結(jié)果太少場景出現(xiàn)的頻率和對應(yīng)推薦詞的實現(xiàn),優(yōu)化搜索推薦服務(wù)的效率;2.爬取更多的語料資源,提升conversion的能力;3.考慮增加個性化的功能,給用戶推薦Ta最感興趣的內(nèi)容。



    作者:ginobefun
    鏈接:https://www.jianshu.com/p/4ab3c69e7b19
    來源:簡書
    簡書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請聯(lián)系作者獲得授權(quán)并注明出處。

    總結(jié)

    以上是生活随笔為你收集整理的基于Elasticsearch实现搜索推荐的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。