基于Elasticsearch實(shí)現(xiàn)搜索推薦

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

背景介紹

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

設(shè)計(jì)思路

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

  1. 搜索的關(guān)鍵詞在本網(wǎng)不存在,比如【迪奧】;
  2. 搜索的關(guān)鍵詞在本網(wǎng)的商品很少,比如【科比】;
  3. 搜索的關(guān)鍵詞拼寫(xiě)有問(wèn)題,比如把【阿迪達(dá)斯】寫(xiě)成了【阿迪大斯】;
  4. 搜索的關(guān)鍵詞過(guò)多,由于我們采用的是cross_fields,在一個(gè)商品內(nèi)不可能包含所有的Term,導(dǎo)致無(wú)結(jié)果,比如【阿迪達(dá)斯 耐克 衛(wèi)衣 運(yùn)動(dòng)鞋】;

那么針對(duì)以上情況,可以采用以下方式進(jìn)行處理:

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

所以,我們?cè)趯?shí)現(xiàn)搜索推薦的核心是之前講到的搜索建議詞,它提供了本網(wǎng)主要的關(guān)鍵詞,另外一個(gè)很重要的是它本身包含了關(guān)聯(lián)商品數(shù)的屬性,這樣就可以保證推薦給用戶的關(guān)鍵詞是可以搜索出結(jié)果的。

實(shí)現(xiàn)細(xì)節(jié)

整體設(shè)計(jì)

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

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

搜索建議詞索引

基于Elasticsearch實(shí)現(xiàn)搜索建議一文已有說(shuō)明,請(qǐng)移步閱讀。此次增加了一個(gè)keyword.keyword_lowercase的字段用于拼寫(xiě)糾錯(cuò),這里列取相關(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"
        }
      }
    }
  }
}

爬蟲(chóng)數(shù)據(jù)爬取

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

全量爬蟲(chóng)

全量爬蟲(chóng)我這邊是從網(wǎng)上下載了一份他人匯總的詞條URL資源,里面根據(jù)一級(jí)分類(lèi)包含多個(gè)目錄,每個(gè)目錄又根據(jù)二級(jí)分類(lèi)包含多個(gè)詞條,每一行的內(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

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

增量爬蟲(chóng)

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

爬蟲(chóng)數(shù)據(jù)關(guān)鍵詞提取

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

管理關(guān)鍵詞映射

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

搜索推薦服務(wù)的實(shí)現(xiàn)

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

其中涉及到的幾個(gè)函數(shù)簡(jiǎn)單說(shuō)明下:

  • aggKeywordsByProductList方法用商品列表的結(jié)果,聚合出出現(xiàn)次數(shù)最多的幾個(gè)品牌和品類(lèi)(比如各2個(gè)),這樣我們就可以得到4個(gè)關(guān)鍵詞,和原先用戶的輸入拼接后調(diào)用recommendBySuggestIndex獲取推薦詞;
  • aggKeywordsBySkns方法是根據(jù)用戶輸入的SKN先到product_index索引獲取商品列表,然后再調(diào)用aggKeywordsByProductList去獲取品牌和品類(lèi)的關(guān)鍵詞列表;
  • getSuggestConversionDestBySource方法是查詢conversion_index索引去獲取關(guān)鍵詞提取的結(jié)果,這里在調(diào)用recommendBySuggestIndex時(shí)有個(gè)參數(shù),該參數(shù)主要是用于處理是否限制只能是輸入的關(guān)鍵詞;
  • getSpellingCorrectKeyword方法為拼寫(xiě)檢查,在調(diào)用suggest_index處理時(shí)有個(gè)地方需要注意一下,拼寫(xiě)檢查是基于編輯距離的,大小寫(xiě)不一致的情況會(huì)導(dǎo)致Elasticsearch Suggester無(wú)法得到正確的拼寫(xiě)建議,因此在處理時(shí)需要兩邊都轉(zhuǎn)換為小寫(xiě)后進(jìn)行拼寫(xiě)檢查;
  • 最終都需要調(diào)用recommendBySuggestIndex方法獲取搜索推薦,因?yàn)橥ㄟ^(guò)suggest_index索引可以確保推薦出去的詞是有意義的且關(guān)聯(lián)到商品的。該方法核心邏輯的偽代碼如下:
    private JSONObject recommendBySuggestIndex(String srcQueryWord, String keywordsToSearch, boolean isLimitKeywords) {
        // 1) 先對(duì)keywordsToSearch進(jìn)行分詞
        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)造搜索請(qǐng)求
        SearchParam searchParam = new SearchParam();
        searchParam.setPage(1);
        searchParam.setSize(3);
    
        // 2.1) 構(gòu)建FunctionScoreQueryBuilder
        QueryBuilder queryBuilder = isLimitKeywords ? buildQueryBuilderByLimit(terms)
                                      : buildQueryBuilder(keywordsToSearch, terms);
        searchParam.setQuery(queryBuilder);
        
        // 2.2) 設(shè)置過(guò)濾條件
        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());
    
        // 給品類(lèi)類(lèi)型的關(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) 對(duì)于suggest的multi-fields至少要有一個(gè)字段匹配到 匹配得分為常量1
        MultiMatchQueryBuilder 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) 對(duì)于完全匹配Term的加1分
            functionScoreQueryBuilder.add(QueryBuilders.termQuery("keyword.keyword_lowercase", term.toLowerCase()),
                ScoreFunctionBuilders.weightFactorFunction(1));
    
            // 3) 對(duì)于匹配到一個(gè)Term的加2分
            functionScoreQueryBuilder.add(QueryBuilders.termQuery("keyword.keyword_ik", term),
                ScoreFunctionBuilders.weightFactorFunction(2));
        }
    
        functionScoreQueryBuilder.boostMode(CombineFunction.SUM);
        return functionScoreQueryBuilder;
    }

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

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

  • 通過(guò)以上的設(shè)計(jì)和實(shí)現(xiàn),我們實(shí)現(xiàn)了一個(gè)效果不錯(cuò)的搜索推薦功能,線上使用效果如下:
//搜索【迪奧】,本站無(wú)該品牌商品
沒(méi)有找到 "迪奧" 相關(guān)的商品, 為您推薦 "香水" 的搜索結(jié)果?;蛘咴囋?"香氛"  "眼鏡" 

//搜索【puma 運(yùn)動(dòng)鞋 上衣】,關(guān)鍵詞太多無(wú)法匹配
沒(méi)有找到 "puma 運(yùn)動(dòng)鞋 上衣" 相關(guān)的商品, 為您推薦 "PUMA 運(yùn)動(dòng)鞋" 的搜索結(jié)果?;蛘咴囋?"PUMA 運(yùn)動(dòng)鞋 女"  "PUMA 運(yùn)動(dòng)鞋 男"

//搜索【puma 上衣】,結(jié)果太少
"puma 上衣" 搜索結(jié)果太少了,試試 "上衣"  "PUMA"  "PUMA 休閑" 關(guān)鍵詞搜索

//搜索【51489312】特定的SKN,結(jié)果太少
"51489312" 搜索結(jié)果太少了,試試 "夾克"  "PUMA"  "戶外" 關(guān)鍵詞搜索

//搜索【blackjauk】,拼寫(xiě)錯(cuò)誤
沒(méi)有找到 "blackjauk" 相關(guān)的商品, 為您推薦 "BLACKJACK" 的搜索結(jié)果?;蛘咴囋?"BLACKJACK T恤"  "BLACKJACK 休閑褲" 
  • 后續(xù)考慮的改進(jìn)包括:1.繼續(xù)統(tǒng)計(jì)各種無(wú)結(jié)果或結(jié)果太少場(chǎng)景出現(xiàn)的頻率和對(duì)應(yīng)推薦詞的實(shí)現(xiàn),優(yōu)化搜索推薦服務(wù)的效率;2.爬取更多的語(yǔ)料資源,提升conversion的能力;3.考慮增加個(gè)性化的功能,給用戶推薦Ta最感興趣的內(nèi)容。
掃一掃 關(guān)注我的微信公眾號(hào)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容