基于Elasticsearch实现搜索推荐

article/2025/9/18 12:08:28

在基于Elasticsearch实现搜索建议一文中我们曾经介绍过如何基于Elasticsearch来实现搜索建议,而本文是在此基于上进一步优化搜索体验,在当搜索无结果或结果过少时提供推荐搜索词给用户。

背景介绍

在根据用户输入和筛选条件进行搜索后,有时返回的是无结果或者结果很少的情况,为了提升用户搜索体验,需要能够给用户推荐一些相关的搜索词,比如用户搜索【迪奥】时没有找到相关的商品,可以推荐搜索【香水】、【眼镜】等关键词。

设计思路

首先需要分析搜索无结果或者结果过少可能的原因,我总结了一下,主要包括主要可能:

  1. 搜索的关键词在本网不存在,比如【迪奥】;
  2. 搜索的关键词在本网的商品很少,比如【科比】;
  3. 搜索的关键词拼写有问题,比如把【阿迪达斯】写成了【阿迪大斯】;
  4. 搜索的关键词过多,由于我们采用的是cross_fields,在一个商品内不可能包含所有的Term,导致无结果,比如【阿迪达斯 耐克 卫衣 运动鞋】;

那么针对以上情况,可以采用以下方式进行处理:

  1. 搜索的关键词在本网不存在,可以通过爬虫的方式获取相关知识,然后根据搜索建议词去提取,比如去百度百科的迪奥词条里就能提取出【香水】、【香氛】和【眼镜】等关键词;当然基于爬虫的知识可能存在偏差,此时需要能够有人工审核或人工更正的部分;
  2. 搜索的关键词在本网的商品很少,有两种解决思路,一种是通过方式1的爬虫去提取关键词,另外一种是通过返回商品的信息去聚合出关键词,如品牌、品类、风格、标签等,这里我们采用的是后者(在测试后发现后者效果更佳);
  3. 搜索的关键词拼写有问题,这就需要拼写纠错出场了,先纠错然后根据纠错后的词去提供搜索推荐;
  4. 搜索的关键词过多,有两种解决思路,一种是识别关键词的类型,如是品牌、品类、风格还是性别,然后通过一定的组合策略来实现搜索推荐;另外一种则是根据用户的输入到搜索建议词里去匹配,设置最小匹配为一个匹配到一个Term即可,这种方式实现比较简单而且效果也不错,所以我们采用的是后者。

所以,我们在实现搜索推荐的核心是之前讲到的搜索建议词,它提供了本网主要的关键词,另外一个很重要的是它本身包含了关联商品数的属性,这样就可以保证推荐给用户的关键词是可以搜索出结果的。

实现细节

整体设计

整体设计框架如下图所示:

搜索推荐整体设计

搜索建议词索引

在基于Elasticsearch实现搜索建议一文已有说明,请移步阅读。此次增加了一个keyword.keyword_lowercase的字段用于拼写纠错,这里列取相关字段的索引:

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"}}}}
}

商品数据索引

这里只列取相关字段的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"}}}}
}

关键词映射索引

主要就是source和dest直接的映射关系。

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

爬虫数据爬取

在实现的时候,我们主要是爬取了百度百科上面的词条,在实际的实现中又分为了全量爬虫和增加爬虫。

全量爬虫

全量爬虫我这边是从网上下载了一份他人汇总的词条URL资源,里面根据一级分类包含多个目录,每个目录又根据二级分类包含多个词条,每一行的内容的格式如下:

李宁!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

这样在启动的时候我们就可以使用多线程甚至分布式的方式爬虫自己感兴趣的词条内容作为初始化数据保持到爬虫数据表。为了保证幂等性,如果再次全量爬取时就需要排除掉数据库里已有的词条。

增量爬虫

  1. 在商品搜索接口中,如果搜索某个关键词关联的商品数为0或小于一定的阈值(如20条),就通过Redis的ZSet进行按天统计;
  2. 统计的时候是区分搜索无结果和结果过少两个Key的,因为两种情况实际上是有所区别的,而且后续在搜索推荐查询时也有用到这个统计结果;
  3. 增量爬虫是每天凌晨运行,根据前一天统计的关键词进行爬取,爬取前需要排除掉已经爬过的关键词和黑名单中的关键词;
  4. 所谓黑名单的数据包含两种:一种是每天增量爬虫失败的关键字(一般会重试几次,确保失败后加入黑名单),一种是人工维护的确定不需要爬虫的关键词;

爬虫数据关键词提取

  1. 首先需要明确关键词的范围,这里我们采用的是suggest中类型为品牌、品类、风格、款式的词作为关键词;
  2. 关键词提取的核心步骤就是对爬虫内容和关键词分别分词,然后进行分词匹配,看该爬虫数据是否包含关键词的所有Term(如果就是一个Term就直接判断包含就好了);在处理的时候还可以对匹配到关键词的次数进行排序,最终的结果就是一个key-value的映射,如{迪奥 -> [香水,香氛,时装,眼镜], 纪梵希 -> [香水,时装,彩妆,配饰,礼服]};

管理关键词映射

  1. 由于爬虫数据提取的关键词是和词条的内容相关联的,因此很有可能提取的关键词效果不大好,因此就需要人工管理;
  2. 管理动作主要是包括添加、修改和置失效关键词映射,然后增量地更新到conversion_index索引中;

搜索推荐服务的实现

  1. 首先如果对搜索推荐的入口进行判断,一些非法的情况不进行推荐(比如关键词太短或太长),另外由于搜索推荐并非核心功能,可以增加一个全局动态参数来控制是否进行搜索推荐;
  2. 设计思路里面我们分析过可能有4中场景需要搜索推荐,如何高效、快速地找到具体的场景从而减少不必要的查询判断是推荐服务实现的关键;这个在设计的时候就需要综合权衡,我们通过一段时间的观察后,目前采用的逻辑的伪代码如下:
    public JSONObject recommend(SearchResult searchResult, String queryWord) {try {String keywordsToSearch = queryWord;// 搜索推荐分两部分// 1) 第一部分是最常见的情况,包括有结果、根据SKN搜索、关键词未出现在空结果Redis ZSet里if (containsProductInSearchResult(searchResult)) {// 1.1) 搜索有结果的 优先从搜索结果聚合出品牌等关键词进行查询String aggKeywords = aggKeywordsByProductList(searchResult);keywordsToSearch = queryWord + " " + aggKeywords;} else if (isQuerySkn(queryWord)) {// 1.2) 如果是查询SKN 没有查询到的 后续的逻辑也无法推荐 所以直接到ES里去获取关键词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) 如果该关键词一次都没有出现在空结果列表或者长度大于5 则该词很有可能是可以搜索出结果的//      因此优先取suggest_index去搜索一把 减少后面的查询动作JSONObject recommendResult = recommendBySuggestIndex(queryWord, keywordsToSearch, false);if (isNotEmptyResult(recommendResult)) {return recommendResult;}}// 2) 第二部分是通过Conversion和拼写纠错去获取关键词 由于很多品牌的拼写可能比较相近 因此先走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) 执行拼写检查 由于在搜索建议的时候会进行拼写检查 所以缓存命中率高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();}}

其中涉及到的几个函数简单说明下:

  • aggKeywordsByProductList方法用商品列表的结果,聚合出出现次数最多的几个品牌和品类(比如各2个),这样我们就可以得到4个关键词,和原先用户的输入拼接后调用recommendBySuggestIndex获取推荐词;
  • aggKeywordsBySkns方法是根据用户输入的SKN先到product_index索引获取商品列表,然后再调用aggKeywordsByProductList去获取品牌和品类的关键词列表;
  • getSuggestConversionDestBySource方法是查询conversion_index索引去获取关键词提取的结果,这里在调用recommendBySuggestIndex时有个参数,该参数主要是用于处理是否限制只能是输入的关键词;
  • getSpellingCorrectKeyword方法为拼写检查,在调用suggest_index处理时有个地方需要注意一下,拼写检查是基于编辑距离的,大小写不一致的情况会导致Elasticsearch Suggester无法得到正确的拼写建议,因此在处理时需要两边都转换为小写后进行拼写检查;
  • 最终都需要调用recommendBySuggestIndex方法获取搜索推荐,因为通过suggest_index索引可以确保推荐出去的词是有意义的且关联到商品的。该方法核心逻辑的伪代码如下:
    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) 根据terms搜索构造搜索请求SearchParam searchParam = new SearchParam();searchParam.setPage(1);searchParam.setSize(3);// 2.1) 构建FunctionScoreQueryBuilderQueryBuilder queryBuilder = isLimitKeywords ? buildQueryBuilderByLimit(terms): buildQueryBuilder(keywordsToSearch, terms);searchParam.setQuery(queryBuilder);// 2.2) 设置过滤条件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) 按照得分、权重、数量的规则降序排序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) 调用ES执行搜索SearchResult searchResult = searchCommonService.doSearch(indexName, searchParam);// 6) 构建结果加入缓存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());// 给品类类型的关键词加分functionScoreQueryBuilder.add(QueryBuilders.termQuery("type", Integer.valueOf(2)),ScoreFunctionBuilders.weightFactorFunction(3));// 按词出现的顺序加分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;}

最后,从实际运行的统计来看,有90%以上的查询都能在1.3)的情况下返回推荐词,而这一部分还没有进行拼写纠错和conversion_index索引的查询,因此还是比较高效的;剩下的10%在最坏的情况且缓存都没有命中的情况下,最多还需要进行三次ES的查询,性能是比较差的,但是由于有缓存而且大部分的无结果的关键词都比较集中,因此也在可接受的范围,这一块可以考虑再增加一个动态参数,在大促的时候进行关闭处理。

小结与后续改进

  • 通过以上的设计和实现,我们实现了一个效果不错的搜索推荐功能,线上使用效果如下:
//搜索【迪奥】,本站无该品牌商品
没有找到 "迪奥" 相关的商品, 为您推荐 "香水" 的搜索结果。或者试试 "香氛"  "眼镜" //搜索【puma 运动鞋 上衣】,关键词太多无法匹配
没有找到 "puma 运动鞋 上衣" 相关的商品, 为您推荐 "PUMA 运动鞋" 的搜索结果。或者试试 "PUMA 运动鞋 女"  "PUMA 运动鞋 男"//搜索【puma 上衣】,结果太少
"puma 上衣" 搜索结果太少了,试试 "上衣"  "PUMA"  "PUMA 休闲" 关键词搜索//搜索【51489312】特定的SKN,结果太少
"51489312" 搜索结果太少了,试试 "夹克"  "PUMA"  "户外" 关键词搜索//搜索【blackjauk】,拼写错误
没有找到 "blackjauk" 相关的商品, 为您推荐 "BLACKJACK" 的搜索结果。或者试试 "BLACKJACK T恤"  "BLACKJACK 休闲裤" 
  • 后续考虑的改进包括:1.继续统计各种无结果或结果太少场景出现的频率和对应推荐词的实现,优化搜索推荐服务的效率;2.爬取更多的语料资源,提升conversion的能力;3.考虑增加个性化的功能,给用户推荐Ta最感兴趣的内容。



作者:ginobefun
链接:https://www.jianshu.com/p/4ab3c69e7b19
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。


http://chatgpt.dhexx.cn/article/7mRfPRbh.shtml

相关文章

五个小众好用的搜索引擎

一、wikiHow https://zh.wikihow.com/ 我把wikiHow当做一个帮我做任何事的搜索引擎 wikiHow上每一篇详尽明了的指南文章 都能改善成百上千人的生活 与维基百科类似&#xff0c;wikiHow 也采用了维基技术 所有人都可以创建或编辑文章中的内容 来自全球的协作者们已编写了…

阿里搜索推荐系统

一、系统框架 导购升级的优化思路从三个方向着手&#xff1a;1.策略升级。利用深度学习及异构网络的思想&#xff0c;对用户个性化进行更深的理解和建模&#xff1b;同时对因马太效应引起的独立query数下降等问题进行优化。 2.导购外投。在包括会场激活页、猜你喜欢等渠道进行搜…

推荐和搜索系统的多样性研究综述

前言 检索结果的多样化是检索系统的一个重要研究课题&#xff0c;其可以满足用户的各种兴趣和供应商的平等公平曝光。 然而&#xff0c;检索系统中&#xff08;搜索与推荐领域&#xff09;的多样性研究缺乏一个系统的汇总&#xff0c;并且研究点相对零散。本次介绍的paper中&am…

从零开始搭建搜索推荐系统(五十二)ElasticSearch搜索利器

聊的不止技术。跟着小帅写代码&#xff0c;还原和技术大牛一对一真实对话&#xff0c;剖析真实项目筑成的一砖一瓦&#xff0c;了解最新最及时的资讯信息&#xff0c;还可以学到日常撩妹小技巧哦&#xff0c;让我们开始探索主人公小帅的职场生涯吧&#xff01; &#xff08;PS…

《智能搜索和推荐系统》总结

这本书主要分为4部分介绍&#xff0c;分别是搜索和推荐的基础&#xff0c;搜索系统基本原理&#xff0c;推荐系统的基本原理&#xff0c;工程应用。 第一部分&#xff1a;搜索和推荐的基础 主要讲了一下概率统计与应用数学的基础知识&#xff0c;比如概率论基础&#xff08;概率…

推荐系统与搜索引擎的差异

转自&#xff1a;https://blog.csdn.net/cserchen/article/details/50422553 详细分析推荐系统和搜索引擎的差异陈运文 从信息获取的角度来看&#xff0c;搜索和推荐是用户获取信息的两种主要手段。无论在互联网上&#xff0c;还是在线下的场景里&#xff0c;搜索和推荐这两种方…

商品搜索引擎---推荐系统设计

一、前言 结合目前已存在的商品推荐设计&#xff08;如淘宝、京东等&#xff09;&#xff0c;推荐系统主要包含系统推荐和个性化推荐两个模块。 系统推荐&#xff1a; 根据大众行为的推荐引擎&#xff0c;对每个用户都给出同样的推荐&#xff0c;这些推荐可以是静态的由系统管…

推荐一些不常见的搜索引擎

5.雅虎网 来自 Yahoo.com 的屏幕截图&#xff0c;2023 年 2 月 截至 2022 年 1 月&#xff0c;Yahoo.com&#xff08;Verizon Media&#xff09;的搜索市场份额为 11.2%。 雅虎的优势在于多元化&#xff0c;除搜索外还提供电子邮件、新闻、金融等服务。 二十多年来&#xff0c;…

相关搜索 --- 搜索中的推荐

0. 前面的瞎扯淡 互联网从开始出现&#xff0c;如果就信息获取方面的话&#xff0c;到现在经历了三个大的时期&#xff0c;最开始是人工信息的分类时期&#xff0c;作为一个上了岁数的人&#xff0c;是经历过那个时期的&#xff0c;那个时期如何来找信息呢&#xff1f;我们来看…

基于机器学习的搜索推荐系统

目录 一&#xff0e; 引言 1 二&#xff0e; 准备 2 一&#xff0e; 软件工程语言选择 2 二&#xff0e; 服务器的选取 2 三&#xff0e; 搜索服务 5 一&#xff0e; 搜索服务软件目录结构 5 二&#xff0e; 搜索服务功能 6 三&#xff0e; SPARQL语句分析 7 四&#xff0e; 经…

超好用的搜索引擎推荐

搜索引擎是我们信息资料搜集的最重要的渠道之一,用搜索引擎查找信息资料需要使用恰当的关键词和一些搜索技巧。目前国内主要的搜集引擎有如下10个,近期还有较多行业 型搜索冒出来,需找专业型行业资料可以使用行业型搜索引擎。 由于每个搜索引擎都有一定的局限性,可以把要…

搜索推荐相关

搜索算法 Learning to Rank方法&#xff1a; 1、单文档方法&#xff1a;根据query判断每个item的相似度 2、文档对方法&#xff1a;确定文档对的前后顺序 3、文档列表法&#xff1a;确定所有文档的先后顺序 Item&#xff1a;垂域、意图、语义相似性、item的热度、用户的搜索日…

推荐一个搜索引擎

yandex是一个俄罗斯搜索引擎。 https://yandex.com/ 最近很忙&#xff0c;月更。 水下文章。

ES-搜索推荐

1. 概述 搜索一般都会要求具有“搜索推荐”或者叫“搜索补全”的功能&#xff0c;即在用户输入搜索的过程中&#xff0c;进行自动补全或者纠错。以此来提高搜索文档的匹配精准度&#xff0c;进而提升用户的搜索体验&#xff0c;这就是Suggest。 ##四种Suggester 2. term sugge…

ul, li, a怎么用

<style type"text/css"> *{margin:0; padding:0;font-size:14px} body{padding-top:20px} ul,li{list-style: none} ul{background: yellow;} li{float:left; width:60px; height:50px;}//width:60px后加,不然ie li会比a宽 a{display:inline-block;padding:0 3…

html用ul li制作导航条

制作的导航条如图所示&#xff1a; 当鼠标滑过每个导航的时候&#xff0c;背景会变换颜色。技术点&#xff1a;将超链接a标签&#xff0c;转换成block标签&#xff0c;从而设置鼠标滑过时的背景色。代码如下所示&#xff1a; <html ><head><meta charset"…

HTML中的列表ol ul

<!DOCTYPE html> <html lang"en"> <head><title>列表</title> </head> <body><h3 style"background-color: rgb(102, 165, 165);">畅销图书榜</h3><hr/><!-- 有序列表 ol --><ol>…

ul及ol标签list-style-type介绍

ul及ol标签list-style-type介绍 1.ul/ol标签样式介绍 a.不设置ul中标签类型&#xff08;默认&#xff09;&#xff1a; 不设置ol中标签类型&#xff08;默认&#xff09;&#xff1a; b.设置方法代码&#xff1a; 在html中设置或者在CSS样式中设置 <style>#kl{list-s…

ul li 实例

之前使用过很多次的 ul li&#xff0c;不过都怎么规范&#xff0c;今天学习到一个整合“p、span、a”等标签的ul li实例&#xff0c;防止忘记&#xff0c;记录一下。 html代码 <span style"white-space:pre"> </span><li><h2>发明专利</h…

html中列表标签ul、ol、 dl的使用和介绍

一、UL列表介绍 ul 是无需列表&#xff0c;li列表里面的一条值&#xff0c;例如 <ul><li>中国</li><li>四川</li><li>成都</li></ul> 效果如下&#xff1a; 可以改变前面li的样式&#xff0c;通过给ul添加样式&#xff0c;其…