【后端必看】什么是 Elasticsearch?都要学什么?

你是小阿巴,刚入职的后端程序员。 这天,产品经理给你安排任务:阿巴阿巴,咱们网站要加一个文章搜索功能。 你心想:简单,直接写一句 SQL 查询数据库就搞定了~ ```sql SELECT * FROM article WHERE title LIKE '%关键词%' ``` 结果上线没几天,就收到了大量用户的投诉! - 怎么什么搜索结果都没有啊? - 搜索结果乱七八糟,我想找的那篇内容竟然排在最后面? - 搜索一次竟然要等好几秒才出结果?什么破系统! 你汗流浃背了:明明 SQL 写对了啊,难道是 MySQL 数据库不行? ![](https://pic.code-nav.cn/post_picture/1601072287388278786/73qmOi7L113fN998.jpg) 这时,号称 "后端之狗" 的鱼皮路过。他瞄了一眼你的代码,嘲笑道:肯定要用 Elasticsearch 来做搜索功能啊! 你一脸懵:Elasticsearch?那是啥? ⭐️ 推荐观看视频版,更生动易懂:https://bilibili.com/video/BV1gkf1BtETY ![](https://pic.code-nav.cn/post_picture/1601072287388278786/NzS5GAkAO5Xinged.jpg) ## 第一阶段:认识 Elasticsearch 鱼皮:**Elasticsearch** 简称 **ES**,是一个专门为搜索而生的分布式数据库,也叫 **搜索引擎数据库**。 它能存储和管理大量文本数据,提供快速、准确、灵活的全文检索功能。你刚才用 `LIKE` 查询搞不定的那些问题,用 ES 都能轻松解决。 你挠了挠头:真有这么神? 鱼皮:当然。打个比方,MySQL 就像图书馆的书架,书按照分类整整齐齐地摆放着,你想找某本书得自己一排一排去翻;而 ES 就像图书馆的电子检索系统,你输入关键词,它立刻就能告诉你书在哪儿,还会把最相关内容的排在最前面。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/2J2o5JLNkw9Q0QjR.jpg) 像全文搜索、日志分析、数据统计这些需要搜索能力的场景,ES 都能轻松搞定。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/Qo3ZqIcV9pTXU4c6.jpg) 你眼前一亮:听起来有点儿夯啊,那我赶紧装一个试试。 ## 第二阶段:实战应用 ### 安装 Elasticsearch 机智如你,直接打开 [ES 官网](https://www.elastic.co/downloads/elasticsearch) 下载了安装包: ![](https://pic.code-nav.cn/post_picture/1601072287388278786/VZnJwGZOnNq9WKZm.jpg) 并且成功安装运行: ![](https://pic.code-nav.cn/post_picture/1601072287388278786/vM8F2tx61MbTxEAq.jpg) 你:安装好之后,我怎么操作它呢? 鱼皮:ES 本身提供了 RESTful API,默认在 9200 端口提供服务,你可以用 curl 命令或者 Postman 等接口测试工具直接发 HTTP 请求来操作它。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/TtV6gv8IlcxGNcXw.jpg) 不过对新手来说,更推荐先安装一个官方的可视化工具 **Kibana**。 有了它,你可以直观地查看分析数据、对数据进行操作。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/KmH1fKCzztSfgEWO.jpg) 只需要到官网下载安装包并运行,启动之后访问本机的 5601 端口,就能打开 Kibana 的管理界面了。在开发工具控制台里,你可以直接输入查询语句,能够立刻看到结果,非常方便。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/AXvMi3wXz2A6Ts4w.jpg) ### 基本操作 下面我来带你实操一波 ES 的基本操作。 1)首先是 **创建索引**。ES 的 **索引(Index)** 相当于 MySQL 里的表,是存放数据的容器。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/3jSWPUhZymTMRYng.jpg) 创建索引的时候,还要定义 **Mapping(映射)**,类似 MySQL 的表结构,用来规定每个字段的类型、是否需要分词、使用什么分词器等等。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/NeeIoN2a0wlcZdpN.jpg) 在 Kibana 开发工具中输入这段代码: ```json PUT /article { "mappings": { "properties": { "title": { "type": "text", "analyzer": "standard" }, "content": { "type": "text" }, "tags": { "type": "keyword" }, "viewCount": { "type": "long" }, "isPublished": { "type": "boolean" }, "createTime": { "type": "date" } } } } ``` 这段代码创建了一个叫 `article` 的索引。其中 `text` 类型表示需要分词的文本字段,适合做全文检索;`keyword` 类型不会分词,适合存标签、状态这种需要精确匹配的内容。其他的类型就比较好理解了,`long` 存数字,`boolean` 存 true 或者 false,`date` 存日期。设计索引的时候要根据业务需求合理选择字段类型。 2)然后是 **插入文档**。**文档(Document)** 相当于 MySQL 里的一行数据。ES 的文档是用 JSON 格式存储的,不需要像 MySQL 那样提前定义好所有字段,而是随时可以加新字段,非常灵活。 ```json POST /article/_doc/1 { "title": "鱼皮的 Elasticsearch 入门教程", "content": "鱼皮带你学习 ES", "cover": "封面图地址", "tags": ["ES", "搜索"], "viewCount": 1000, "isPublished": true, "createTime": "2025-01-30" } ``` ![](https://pic.code-nav.cn/post_picture/1601072287388278786/IQVwHlZhHBPfUCo6.jpg) 3)有了数据之后,就可以体验 ES 最核心的能力 **搜索文档**。比如在 `article` 索引中搜索标题包含 "鱼皮教程" 的文章: ```json GET /article/_search { "query": { "match": { "title": "鱼皮教程" } } } ``` 你执行完这条查询,惊喜地发现:搜 "鱼皮教程" 居然能匹配到 "鱼皮的 ES 入门教程" 这篇文章! ![](https://pic.code-nav.cn/post_picture/1601072287388278786/CZiXBGmMol4q2dnZ.jpg) 鱼皮点点头:虽然标题里并没有 "鱼皮教程" 这 4 个连着的字,但因为 ES 会自动分词,把 "鱼皮" 和 "教程" 拆开分别匹配,所以就搜到了。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/5c336qsLowrsNoGK.jpg) 你感叹道:哇,这才是搜索该有的样子啊! ### 查询语法 DSL 鱼皮:没错,ES 的搜索能力非常灵活强大。刚才你写的那些操作语句,其实用的就是 ES 的 **DSL**(Domain Specific Language 领域特定语言)。就像学数据库要学 SQL 一样,学 ES 就得学 DSL。不管是创建索引、插入文档,还是搜索查询,都是用这套 JSON 格式的语法来描述的。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/AfYlJD9ku71h2s8X.jpg) 其中最常用的就是查询语法,常见的查询类型有这么几种: - `match` 是全文检索,会对搜索词分词之后再匹配 - `term` 是精确匹配,不分词,适合查 id、状态这种 - `bool` 可以组合多个条件,用 `must`(必须满足)、`should`(最好满足)、`must_not`(必须不满足)来灵活控制 - `range` 用来做范围查询,比如查某个时间段内的数据。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/AdXnxoRdyEoqloYT.jpg) 你不需要背这些语法,用到的时候问 AI 或者查文档就行,多写几次就熟了。 ### 用代码操作 ES 你皱了皱眉:感觉写这些 JSON 格式的 DSL 还是有点麻烦啊,我用 Java 代码操作 ES 的时候,总不会也要手动拼这堆 JSON 吧? 鱼皮:当然不用!ES 官方提供了各种语言的客户端。比如你用 Java 语言,对应的是 **Java API Client**,支持链式调用和类型安全。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/hb3KAYoZXqognFfF.jpg) 对于 Spring 项目来说,更推荐用 **Spring Data Elasticsearch**,它可以让你像用 MyBatis-Plus 操作 MySQL 一样操作 ES。只需要定义一个实体类,加上 `@Document` 注解指定要操作的索引,再写个 Repository 接口继承依赖包内置的 ES 操作接口。 ```java @Document(indexName = "article") public class Article { @Id private Long id; private String title; private String content; } public interface ArticleRepository extends ElasticsearchRepository<Article, Long> { // 根据标题搜索 List<Article> findByTitle(String title); } ``` 框架会根据方法名自动生成查询逻辑,基本的增删改查方法就自动实现了。 ```java // 使用示例 // 插入文档 articleRepository.save(article); // 根据 id 查询 articleRepository.findById(1L); // 根据标题搜索 articleRepository.findByTitle("鱼皮"); // 删除文档 articleRepository.deleteById(1L); ``` 你感叹道:这才是人写的代码啊!优雅,真是优雅~ 我这就给 Java 代码整上 ES! 鱼皮:要注意,ES 版本更新很快,你用的客户端版本要跟安装的 ES 服务保持一致,不然会出各种奇奇怪怪的 Bug。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/7Kb8CMNoIBgSIYsz.jpg) ## 第三阶段:实用特性 学会了基本操作之后,你兴冲冲地把 MySQL 数据库里的文章数据全部导入到了 ES,然后把网站的搜索功能改成从 ES 查询。上线后效果立竿见影,搜索又快又准,用户好评如潮。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/AWHAVFXvaPTAMei2.jpg) 你非常开心:阿巴,俺可真厉害! ![](https://pic.code-nav.cn/post_picture/1601072287388278786/Hxgg7Z8au6RxbH3S.jpg) 鱼皮:不错不错,你已经掌握了 ES 的基本操作,算是学会 80% 了。不过 ES 还有很多值得学习的实用特性,进一步优化你的搜索功能。 ### 倒排索引 鱼皮:先来考考你,你知道为什么 ES 能搜得又快又准么? 你挠挠头:阿巴阿巴…… 鱼皮笑道:关键在于它使用了 **倒排索引** 来存储数据,这是 ES 最核心的特性。 举个例子,假设咱们要存 3 篇博客文档,用 MySQL 数据库的话,存储结构是这样的: | 文档 id | 文档内容 | | ------- | ---------------- | | 1 | 感谢关注鱼皮 | | 2 | 鱼皮是一名程序员 | | 3 | 感谢关注编程导航 | 这种结构下,如果用户搜 “鱼皮程序员”,MySQL 会傻乎乎地把它当成一整个词去匹配,结果可能啥也搜不到。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/2VvabS9OjlGNa5dv.jpg) 而 ES 的做法不一样。它会先把文档内容按照单词进行切分,这个过程叫 **分词**。然后再构建 **单词到文档 id 的映射关系**,也就是 **倒排索引**。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/3PCWBK54WtoCEh0P.jpg) 有了上述的倒排索引,当用户搜索 “鱼皮程序员” 时,搜索引擎数据库会先对搜索词进行分词,得到 “鱼皮” 和 “程序员”,然后根据这两个词汇就能找到文档 id 1、2 了。不用再一行一行遍历表内所有的数据,实现了更灵活、快速的 **模糊搜索** 。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/p5lm13DlkAgvm5H4.jpg) 你两眼放光:原来如此,牛啊牛啊! 但是 ES 怎么知道一句话该拆成哪些词呢? ### 分词器 鱼皮:好问题,这就要靠 **分词器** 了,它负责把一段文本拆成一个个词。 ES 内置了标准分词器,它基于 Unicode 文本分割算法设计,会按空格和标点符号等来切分文本。但这个规则只适合英文,对中文基本是一个字一个字地拆,效果很差。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/OANtO3en3H0M0Hqk.jpg) 所以如果你要做中文搜索,必须安装 [**IK 分词器**](https://github.com/infinilabs/analysis-ik)。它是专门为中文设计的,能够智能识别中文词汇的边界,把句子正确地拆分成有意义的词语。 IK 提供了两种分词模式: - `ik_smart` 是智能分词,尽量把词分得少一点,比如 "好学生" 就只会拆成 "好学生" 一个词 - `ik_max_word` 是最大化分词,能拆的都拆,"好学生" 会被拆成 "好学生"、"好学"、"学生" 三个词。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/KUdFTT9SYgsVKqDU.jpg) 一般建议索引的时候用 `ik_max_word` 尽可能多分词,搜索的时候用 `ik_smart` 提高精确度。 此外,IK 还支持自定义词典。比如你想让 “程序员鱼皮” 作为一个完整的词不被拆开,加到词典里就行了。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/6cbFBM89Xrx2Jqbb.jpg) ### 高亮显示 你好奇道:既然 ES 能分词,那能不能在搜索结果中把命中的关键词标红啊? ![](https://pic.code-nav.cn/post_picture/1601072287388278786/ARsDxt2Jzu3WNbEE.jpg) 鱼皮:当然可以,ES 支持 **高亮显示** 功能。只需要在查询里加个 `highlight` 参数,指定要高亮的字段就行: ```json GET /article/_search { "query": { "match": { "title": "鱼皮教程" } }, "highlight": { "fields": { "title": {} } } } ``` 返回结果里,命中的关键词会自动被 `<em>` 标签包起来,前端拿到之后加个颜色样式就搞定了。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/KyP1ry7q2nOCvzzu.jpg) 你两眼放光:这也太方便了吧! ![](https://pic.code-nav.cn/post_picture/1601072287388278786/NqShEHvRDScZ2ny4.jpg) 不过还有个问题,现在虽然能够搜索到内容了,但怎么把最相关的结果排到前面呢? ### 相关性评分 鱼皮:好问题。ES 会给每个搜索结果计算一个分数,放到 `_score` 字段中,分数高的排在前面。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/1zJDqrlNa2fiY9e3.jpg) 你好奇道:这个分数是怎么算的呢? 鱼皮:ES 默认用的是 **BM25 算法**,主要考虑三个因素: - **词频**,关键词在文档里出现的次数越多,分数越高。这很好理解,一篇文章里反复提到 "鱼皮",说明它很可能就是在讲鱼皮相关的内容。 - **文档长度**,同样出现一次关键词,在短文档里占的比例更大,所以短文档的分数会更高一点。 - **稀有度**,如果一个词在所有文档里都很常见,比如 "的"、"是",那它对搜索结果的区分度就不大。反过来,如果一个词很少见,只在少数文档里出现,那命中这个词的文档就更有价值,分数也更高。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/iHBvt7oEQcbVZiDT.jpg) ### 聚合分析 鱼皮:除了搜索,ES 还有个很实用的功能叫 **聚合分析**,有点像 MySQL 的 `GROUP BY` 分组查询。 比如你想统计每个标签下有多少篇文章,写个聚合查询就行: ```json GET /article/_search { "size": 0, "aggs": { "tag_count": { "terms": { "field": "tags" } } } } ``` ![](https://pic.code-nav.cn/post_picture/1601072287388278786/OPSkVhsqdNHSWfgG.jpg) 除了分组统计数量,ES 的聚合还能做求和、求平均值、找最大最小值、甚至多层嵌套聚合,能够满足开发各类数据报表的需求。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/EmoWcaCgF5l8kAjD.jpg) ## 第四阶段:生产环境实践 用了一段时间 ES 后,你开始有点儿飘了。 没事儿就对着新来的实习生阿坤吹牛皮:ES 我闭着眼睛都能写!什么分词、高亮、聚合,我都玩得贼溜儿~ ![](https://pic.code-nav.cn/post_picture/1601072287388278786/x1fzH9KPBxg4C6i0.jpg) 结果没多久,老板黑着脸找到你:有用户投诉,说明明改了自己文章的标题,但是搜索出来还是旧的,怎么回事? ![](https://pic.code-nav.cn/post_picture/1601072287388278786/ycAZrC14SOHkuS8f.jpg) 你排查后发现:原来是 ES 里的数据和 MySQL 数据库里的不一样!当初俺只是把数据一次性导入 ES,后来文章在数据库里更新了,但 ES 里还是旧数据。 你有些头大:唉,ES 和 MySQL 是两套独立的系统,数据不会自动同步啊,咋办啊? ![](https://pic.code-nav.cn/post_picture/1601072287388278786/6BuORjBEgdM8U2MB.jpg) 这时,旁边的阿坤突然鸡叫起来:我来! ### 数据同步方案 阿坤一边打篮球一边说:MySQL 和 ES 的数据同步,一般有这么几种方案。 1)定时任务 每隔几分钟扫一遍数据库,把最近更新的数据同步到 ES。优点是实现简单,缺点是有一定延迟。适合数据更新不频繁、对实时性要求不高的场景。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/COcNRo0riBYuo6Xr.jpg) 2)双写 每次把数据写入 MySQL 的时候顺便也写一份到 ES。优点是能做到实时同步,缺点是会影响写入性能,而且如果 ES 写失败了还得处理数据不一致的问题。适合数据写入量不大的场景。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/DALfmHXFAZl8zpl4.jpg) 3)用 Logstash 它是 ES 官方提供的数据收集工具,可以配置从 MySQL 定时拉取数据同步到 ES。优点是不用写代码,全靠配置驱动,缺点是需要额外部署组件,灵活性也有限。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/FTg0RI8MBrItSRYK.jpg) 4)用 Canal 监听数据库 Canal 是阿里开源的一个工具,它会伪装成 MySQL 的从库,实时监听数据库的变更日志。数据库一有改动,Canal 立刻就能感知到,然后同步到 ES。优点是能做到实时同步,缺点是部署和运维相对麻烦一点。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/65dxiWn74R8M2WtG.jpg) 像咱们这个文章系统,更新又不频繁,用户也能接受几分钟的延迟,用定时任务就完全够了。如果以后做电商那种对实时性要求高的系统,再考虑上 Canal。 ### 集群部署 鱼皮走过来拍了拍阿坤的肩膀:不错不错,我再考考你们,如果 ES 服务器挂了怎么办? 你支支吾吾:重…… 重启? ![](https://pic.code-nav.cn/post_picture/1601072287388278786/gNasEljtsqurdKiO.jpg) 鱼皮摇头:用户等得起吗? 阿坤:生产环境肯定不能只部署一台 ES 啊,得搭建 **集群**。 ES 集群中有几种角色的节点。**主节点** 负责管理集群的状态,比如哪些节点在线、索引的元数据等等。**数据节点** 负责存储实际的数据,处理读写请求。一般生产环境至少部署 3 个节点,保证高可用。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/WYXspQPdpXm877rS.jpg) 鱼皮追问:那如果数据量特别大,一个节点存不下怎么办? 你眼前一亮,终于等到自己会的问题了,抢答道:删除数据! 阿坤用看流浪狗的眼神看了你一眼,回答道:这就要说到 **分片** 了。分片就是把一个索引的数据拆成多份,分别存到不同的节点上。这样单个节点存不下的海量数据,也能通过多节点分担。而且多个节点可以并行处理查询请求,性能也更好。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/Pgm5GLN5FNQ8UerT.jpg) 你有些不服气:那万一某个节点挂了,上面的数据不就丢了? 阿坤:所以还需要 **副本**。副本就是分片的备份。每个分片可以配置若干个副本,存在其他节点上。万一某个节点挂了,副本可以顶上,这样数据就不会丢失,服务也不会中断。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/IIhEACibTla5MkFa.jpg) ### 其他生产实践 鱼皮拍了拍阿坤的肩膀:小伙子年轻有为啊! 这些都是 ES 在生产环境必须考虑的问题,此外还要学习: - 深度分页问题:ES 默认只允许查询前 10000 条数据,再往后翻就会报错。这是为了保护集群性能。如果确实需要给用户深度翻页,推荐使用更高效的 search_after。如果需要导出全量数据,可以结合 Point-in-Time API 使用。 - 性能调优技巧:合理设计 Mapping,该用 keyword 的别用 text;查询的时候多用 filter 少用 query,因为 filter 会缓存结果;还有控制返回字段的数量,别动不动就查全部字段。 - ELK 日志方案:ES 最经典的应用场景之一就是做日志系统。ELK 是三个组件的缩写,E 是 Elasticsearch 负责存储和搜索日志,L 是 Logstash 负责收集和处理日志,K 是 Kibana 负责可视化展示。大厂排查线上问题,基本都靠这一套。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/qNofsdpruJHSn3x6.jpg) 你羞愧地抬不起头:我以为自己已经掌握了 ES,原来只是学了个皮毛…… 鱼皮:小阿巴,你还要好好跟阿坤学习啊。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/Hx0kPlu6BvXLvWPp.jpg) ## 第五阶段:深入原理 被连环拷问后,你主动找到阿坤:坤哥,我想深入学习 ES 的底层原理,你是怎么学的? 阿坤有些惊讶:咦?你不背八股文的么?去 [面试刷题网站 - 面试鸭](https://www.mianshiya.com/) 刷刷题就好了呀! ![](https://pic.code-nav.cn/post_picture/1601072287388278786/a2q32WDhhQjoiK5L.jpg) 你震惊了:现在的实习生,竟然恐怖如斯! 鱼皮笑了笑:阿坤你别逗他了。其实可以带着问题去学习,比如 **ES 为什么这么快**? 你抢答道:因为倒排索引! 鱼皮:没错,但这只是一方面。ES 底层是基于 Lucene 搜索引擎库的,它的倒排索引结构经过了高度优化。另外 ES 会把常用的数据缓存在内存里,查询时优先从内存读取,速度自然快。再加上 ES 是分布式的,可以把数据分片存储到多个节点,并行处理查询请求,几方面加起来,性能就上去了。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/Ce79oPuvkSjTFEbO.jpg) 再比如数据是怎么写入的、查询请求是怎么执行的? 从这些问题出发,去阅读相关的文章,或者像阿坤说的刷一刷 [ES 面试题](https://www.mianshiya.com/bank/1805423815382736897),就能快速学会很多核心知识点。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/dNKeUjDWscB1w4WT.jpg) 如果想系统学习,推荐看 ES 官方文档,因为 ES 的更新太快了,很多书籍可能已经跟不上节奏了。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/FXAwzZJZJgGUCB3p.jpg) ## 结尾 若干年后,你已经成为了公司的 ES 搜索专家。不仅能熟练使用 ES 解决各种搜索问题,搭个集群架构也是手拿把掐的。 你也像鱼皮当时一样,耐心地给新人分享学习 ES 的经验,让他们谨记一句话:**ES 是实战型技术,一定要多动手实践!** 更详细的完整版 Elasticsearch 保姆级学习路线,可以在 [编程导航](https://www.codefather.cn/course/1789189862986850306/section/1990755182346022913) 查看。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/rgJyB4FJYEQgm5q9.jpg) 再次遇到鱼皮是在一条昏暗的小巷,此时的他年过 35,灰头土脸。你什么都没说,只是给他点了个赞,投了 2 个币。 不打扰,是你的温柔~ ![](https://pic.code-nav.cn/post_picture/1601072287388278786/kw22nMpNrkVWBCvY.jpg)

下载 APP