Elasticsearch(ES)概述

这篇具有很好参考价值的文章主要介绍了Elasticsearch(ES)概述。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


一.什么是Elasticsearch?

Elasticsearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎。它能很方便的使大量数据具有搜索、分析和探索的能力。充分利用Elasticsearch的水平伸缩性,能使数据在生产环境变得更有价值。

Elasticsearch是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容。

Elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域。

Elasticsearch是elastic stack的核心(不可替换),负责存储、搜索、分析数据。

es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot


1.正向索引和倒排索引

文档(document):每条数据就是一个文档

词条(term):文档按照语义分成的词语


正向索引(Forward Index):
定义: 正向索引是根据文档-词项对的方式建立的索引。每个文档都有一个记录,其中包含了文档中的所有词项及其位置信息。

例如Mysql就是使用的正向索引,根据id检索一个文档非常快,但是根据文档中某个字段检索文档只能逐条检索


倒排索引(Inverted Index):
定义: 倒排索引是根据词项-文档对的方式建立的索引。每个词项都有一个记录,其中包含了包含该词项的所有文档及其位置信息。即对文档内容分词,对词条创建索引,并记录词条所在文档的信息。查询时先根据词条查询到文档id,而后获取到文档

Elasticsearch就使用了倒排索引,将文档按照语义分成词条,根据词条建立词条表,这样就形成了词条-文档的结构,导致检索字段时非常快

倒排索引中包含两部分内容:

  • 词条词典(Term Dictionary):记录所有词条,以及词条与倒排列表(Posting List)之间的关系,会给词条创建索引,提高查询和插入效率

  • 倒排列表(Posting List):记录词条所在的文档id、词条出现频率 、词条在文档中的位置等信息

    • 文档id:用于快速获取文档

    • 词条频率(TF):文档在词条出现的次数,用于评分

es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot

文档:

elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。
文档数据会被序列化为json格式后存储在elasticsearch中。

索引:

相同类型的文档的集合

映射

索引中文档的字段约束信息,类似表的结构约束

es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot


2.Mysql和ES的概念对比

es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot

Mysql:擅长事务类型(ACID特性)操作,可以确保数据的安全和一致性

Elasticsearch:擅长海量数据的搜索、分析、计算

总而言之:

  • 正向索引适合于文档级别的查询,因为它直接提供了文档中的词项信息。

  • 倒排索引适合于词项级别的查询,因为它直接提供了包含某个词项的文档信息。

3.安装elasticsearch、kibana

通过Dokcer拉取镜像安装即可

二.IK分词器

ES在创建倒排索引时需要对文档分词;在搜索时,需要对用户输入内容分词。但默认的分词规则对中文处理并不友好。故需要更好的分词策略:IK分词器

安装(安装到es-plugins/_data即可):IK分词器官方地址,重启es后IK分词器生效

ik分词器包含两种模式:

  • ik_smart:最少切分,粗粒度
  • ik_max_word:最细切分,细粒度

IK分词器-拓展词库

IK分词器虽然说按照字典查找词语进行组合,但是随着网络文化发展和新词的逐渐产生,IK分词器不可能马上更新这些词汇,这时候就需要进行拓展词汇库和增加禁用词汇

要拓展ik分词器的词库,只需要修改一个ik分词器目录中的config目录中的IkAnalyzer.cfg.xml文件:
es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot

在ext.dic文件中添加拓展词汇

es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot

在stopword.dic文件下添加禁用词汇

es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot

三.索引库操作

mapping属性

mapping是对索引库中文档的约束,常见的mapping属性包括:

  • type:字段数据类型,常见的简单类型有:

    • 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)

    • 数值:long、integer、short、byte、double、float

    • 布尔:boolean

    • 日期:date

    • 对象:object

  • index:是否创建索引,默认为true

  • analyzer:使用哪种分词器

  • properties:该字段的子字段


创建索引库

PUT /索引库名称
{
  "mappings": {
    "properties": {
      "字段名":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "字段名2":{
        "type": "keyword",
        "index": "false"
      },
      "字段名3":{
        "properties": {
          "子字段": {
            "type": "keyword"
          }
        }
      },
      // ...略
    }
  }
}

查看索引库语法:

GET /索引库名 

删除索引库的语法:

DELETE /索引库名 

修改索引库

索引库和mapping一旦创建无法修改,但是可以添加新的字段,语法如下:

PUT /索引库名/_mapping
{
  "properties": {
    "新字段名":{
      "type": "integer"
    }
  }
}

四.文档操作

添加文档

POST /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    "字段3": {
        "子属性1": "值3",
        "子属性2": "值4"
    },
    // ...
}

查看文档语法:

GET /索引库名/_doc/文档id 

删除文档:

DELETE /索引库名/_doc/文档id 

修改文档

方式一:全量修改,会删除旧文档,添加新文档

PUT /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    // ... 略
}

方式二:增量修改,修改指定字段值

POST /索引库名/_update/文档id
{
    "doc": {
         "字段名": "新的值",
    }
}

文档操作-动态映射

当我们向ES中插入文档时,如果文档中字段没有对应的mapping,ES会帮助我们字段设置mapping,规则如下:

es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot

五.RestClient操作索引库

什么是RestClient
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。

使用:

1.初始化RestClient

1.引入es的RestHighLevelClient依赖:

		<dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
        </dependency>

2.可以发现客户端的版本号,引入的依赖版本必须和客户端一致(不一致则需修改)

es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot

修改版本号

	<properties>
        <java.version>1.8</java.version>
        <elasticsearch.version>客户端版本号</elasticsearch.version>
    </properties>

3.初始化RestHighLevelClient:

	private RestHighLevelClient client;

    //测试类中使用@BeforeEach注解来标记一个方法,该方法将在每个测试方法执行之前执行(@AfterEach同理)
    @BeforeEach
    void setUp(){
        this.client=new RestHighLevelClient(RestClient.builder(
                HttpHost.create("自己的Linux局域网ip地址:9200")
        ));
    }
    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }

2.创建索引库

	@Test
    void testCreateHotelIndex() throws IOException {
        CreateIndexRequest request = new CreateIndexRequest("索引库名");

        request.source("DSL语句", XContentType.JSON);

        client.indices().create(request, RequestOptions.DEFAULT);
    }

其中:indices()包含了所有操作索引库的API

3.删除索引库

	@Test
	void testDeleteHotelIndex() throws IOException {   
		// 1.创建Request对象
		DeleteIndexRequest request = new DeleteIndexRequest("索引库名");
		// 2.发起请求
		client.indices().delete(request, RequestOptions.DEFAULT);
}

4.判断索引库是否存在

	@Test
	void testExistsHotelIndex() throws IOException {    
		// 1.创建Request对象    
		GetIndexRequest request = new GetIndexRequest("索引库名");    
		// 2.发起请求     
		boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);    
		// 3.输出    
		System.out.println(exists);
}

六.RestClient操作文档

1.新增文档

IndexRequest request = new IndexRequest("索引库名").id(设置id);

request.source(JSON文档, XContentType.JSON);

client.index(request, RequestOptions.DEFAULT);

2.查询数据

GetRequest request = new GetRequest(索引库名, id);    

GetResponse response = client.get(request, RequestOptions.DEFAULT);   

String json = response.getSourceAsString();

3.修改数据

UpdateRequest request = new UpdateRequest(索引库名,id);

request.doc("键1","值1","键2","值2"...);

client.update(request, RequestOptions.DEFAULT);

4.删除数据

DeleteRequest request = new DeleteRequest(索引库名,id);

client.delete(request, RequestOptions.DEFAULT);

5.批量插入数据

request.add(new IndexRequest(索引库名).id(id1).source(JSON文档1, XContentType.JSON));
request.add(new IndexRequest(索引库名).id(id2).source(JSON文档2, XContentType.JSON));
request.add(new IndexRequest(索引库名).id(id3).source(JSON文档3, XContentType.JSON));
//request可以添加多个IndexRequest
client.bulk(request, RequestOptions.DEFAULT);

七.DSL查询文档

DSL Query的分类

Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:

  • 查询所有:查询出所有数据,一般测试用。例如:match_all
  • 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
    • match_query
    • multi_match_query
  • 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:
    • ids
    • range
    • term
  • 地理(geo)查询:根据经纬度查询。例如:
    • geo_distance
    • geo_bounding_box
  • 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
    • bool
    • function_score

查询的基本语法如下:

GET /indexName/_search
{
  "query": {
    "查询类型": {
      "查询条件": "条件值"
    }
  }
}

全文检索查询

match查询:全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索,语法:

GET /indexName/_search
{
  "query": {
    "match": {
      "FIELD": "TEXT"
    }
  }
}

multi_match:与match查询类似,只不过允许同时查询多个字段,语法:

GET /indexName/_search
{
  "query": {
    "multi_match": {
      "query": "TEXT",
      "fields": ["FIELD1", " FIELD12"]
    }
  }
}

需要注意的是:根据多个字段查询,参与查询字段越多,查询性能越差,建议使用多个字段拷贝到一个字段进行多字段的查询

精确查询

精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有:

  • term:根据词条精确值查询
  • range:根据的范围查询

term查询:

// term查询
GET /indexName/_search
{
  "query": {
    "term": {
      "FIELD": {
        "value": "VALUE"
      }
    }
  }
}

range查询:

// range查询
GET /indexName/_search
{
  "query": {
    "range": {
      "FIELD": {
        "gte": 10,
        "lte": 20
      }
    }
  }
}

地理查询

geo_bounding_box(不常用):查询geo_point值落在某个矩形范围的所有文档

// geo_bounding_box查询
GET /indexName/_search
{
  "query": {
    "geo_bounding_box": {
      "FIELD": {
        "top_left": {
          "lat": 31.1,
          "lon": 121.5
        },
        "bottom_right": {
          "lat": 30.9,
          "lon": 121.7
        }
      }
    }
  }
}

geo_distance(常用):查询到指定中心点小于某个距离值的所有文档

// geo_distance 查询
GET /indexName/_search
{
  "query": {
    "geo_distance": {
      "distance": "15km",
      "FIELD": "31.21,121.5"
    }
  }
}

复合查询
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑,例如:

fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名。例如百度竞价

当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。

常见的三个算分函数:

es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot

TF-IDF:在elasticsearch5.0之前,会随着词频增加而越来越大

BM25:在elasticsearch5.0之后,会随着词频增加而增大,但增长曲线会趋于水平

Function Score Query

使用 function score query,可以修改文档的相关性算分(query score),根据新得到的算分排序。

es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot
function score query定义的三要素:

  • 过滤条件:哪些文档要加分
  • 算分函数:如何计算function score
  • 加权方式:function score 与 query score如何运算

复合查询 Boolean Query
布尔查询是一个或多个查询子句的组合。子查询的组合方式有:

  • must:必须匹配每个子查询,类似“与”
  • should:选择性匹配子查询,类似“或”
  • must_not:必须不匹配,不参与算分,类似“非”
  • filter:必须匹配,不参与算分

例子:搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。

GET /hotel/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {"name": "如家"}
        }
      ],
      "must_not": [
        {
          "range": { "price": {"gt": 400}}
        }
      ],
      "filter": [
        {
          "geo_distance": {
            "distance": "10km", "location": {"lat": 31.21, "lon": 121.5}
          }
        }
      ]
    }
  }
}

搜索结果处理

1.排序
elasticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。

GET /indexName/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "FIELD": "desc"  // 排序字段和排序方式ASC、DESC
    }
  ]
}
GET /indexName/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "_geo_distance" : {
          "FIELD" : "纬度,经度",
          "order" : "asc",
          "unit" : "km"
      }
    }
  ]
}

2.分页
elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。

elasticsearch中通过修改from、size参数来控制要返回的分页结果:

GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "from": 990, // 分页开始的位置,默认为0
  "size": 10, // 期望获取的文档总数
  "sort": [
    {"price": "asc"}
  ]
}

深度分页问题(ES设定结果集查询的上限是10000)

针对深度分页,ES提供了两种解决方案:

  • search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
  • scroll:原理将排序数据形成快照,保存在内存。官方已经不推荐使用。

3.高亮
高亮:就是在搜索结果中把搜索关键字突出显示。

原理是这样的:

将搜索结果中的关键字用标签标记出来

在页面中给标签添加css样式

语法:

GET /hotel/_search
{
  "query": {
    "match": {
      "FIELD": "TEXT"
    }
  },
  "highlight": {
    "fields": { 
      "FIELD": {// 指定要高亮的字段
        "pre_tags": "<em>",  // 用来标记高亮字段的前置标签
        "post_tags": "</em>" // 用来标记高亮字段的后置标签
      }
    }
  }
}

八.RestClient检索查询文档

需要说明的是,这里的RestClient查询文档不同于上面使用的GetRequest查询,GetRequest查询是简单查询,传入的参数只限制以下几个:

es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot

1.match_all查询

es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot
es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot

	@Test
void testMatchAll() throws IOException {
        SearchRequest request = new SearchRequest("hotel");

        request.source().query(QueryBuilders.matchAllQuery());

        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        SearchHits searchHits = response.getHits();

        long total = searchHits.getTotalHits().value;

        System.out.println("共搜索到" + total + "条数据");

        SearchHit[] hits = searchHits.getHits();

        for (SearchHit hit : hits) {
            String json = hit.getSourceAsString();

            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);

            System.out.println("hotelDoc=" + hotelDoc);
        }
    }

可以把request.source()理解为查询的整体,在source()下又有sort(),highlighter(),size(),from(),query()等等,这些方法均是已经学习过的DSL查询的query()的平级,即RestAPI中其中构建DSL是通过HighLevelRestClient中的resource()来实现的,其中包含了查询、排序、分页、高亮等所有功能

RestAPI中其中构建查询条件的核心部分是由一个名为QueryBuilders的工具类提供的,其中包含了各种查询方法

es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot

2.全文检索查询

全文检索的match和multi_match查询与match_all的API基本一致。==差别是查询条件,也就是query的部分。==同样是利用QueryBuilders提供的方法:
es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot

其实就是query里面的参数不同,对应到java代码中就是QueryBuilders调用的API不同而已

// 单字段查询
QueryBuilders.matchQuery("字段", "字符串");
// 多字段查询
QueryBuilders.multiMatchQuery("字符串", "字段1", "字段2");

3.精确查询

精确查询常见的有term查询和range查询

// 词条查询
QueryBuilders.termQuery("字段", "字符串"); 
// 范围查询
QueryBuilders.rangeQuery("字段").gte(xxx).lte(xxx);

4.复合查询-boolean query
精确查询常见的有term查询和range查询,由于在DSL语句中bool包含了多个属性,故需要先创建一个BoolQueryBuilder对象,依次向对象中添加条件属性

// 创建布尔查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 添加must条件
boolQuery.must(QueryBuilders.termQuery("字段", "字符串")); 
// 添加filter条件
boolQuery.filter(QueryBuilders.rangeQuery("字段").lte(xxx));
//添加must_not条件
boolQuery.mustNot((QueryBuilders.xxx);

5.排序和分页
搜索结果的排序和分页是与query同级的参数,故改变source()调用的API即可

request.source().from(起始页码).size(每页显示条数);

request.source().sort("字段", SortOrder.ASC);//升序

6.高亮

request.source().highlighter(new HighlightBuilder()        
	.field("字段")        
	// 是否需要与查询字段匹配        
	.requireFieldMatch(false)//不填写pre_tags和post_tags属性默认为<em>标签
);

高亮的结果处理(其实就是获取每一个hit里面的highlight值,就可以取出高亮字段中的值了):
es依赖版本,elasticsearch,jenkins,大数据,spring cloud,spring boot文章来源地址https://www.toymoban.com/news/detail-768406.html

到了这里,关于Elasticsearch(ES)概述的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 二、Elasticsearch入门必读指南:到底选择哪个ES版本更合适

    为什么写这篇,一是没人讲,二是网上很多同学分享ES相关知识、问题排查等,很多都不讲版本,导致新手在技术选型时不知道选择什么版本,更多的是问题排查时看半天文章却发现版本对不上。所以想通过这篇文章告诉你2024年了,选择什么版本更合适。     ES从发布之初到

    2024年01月21日
    浏览(51)
  • 怎样查询 elasticsearch es doc 文档中的 version 版本号

    参考链接:https://stackoverflow.com/questions/39016589/how-to-get-the-docs-version-field 记录一下: 需要加一个 version=true 的参数,返回结果中就会返回 version 。 返回结果:

    2024年02月16日
    浏览(41)
  • ElasticSearch学习(十一)—— es7.2升级log4j版本

    下载log4j2.17 下载地址: Apache Logging Services https://logging.apache.org/ 查找es安装目录下需要替换的log4j文件 备份旧文件 将2.11.1文件替换为2.17.2 修改替换的文件权限 权限给到es用户 重新启动 启动报错 解决 修改elasticsearch.yml文件中的host 将默认的0.0.0.0修改为对应的服务器ip地址

    2023年04月09日
    浏览(37)
  • ElasticSearch(7.8版本)聚合查询使用javaHighLevelRestClient实现(从MySQL聚合查询概念->ES聚合概念及实操)

    申明:本文是在实现ES聚合功能中,将过程中查找的多篇博客文献拼接在一起,参考到的博文全部在标题中附上了原文的超链接,分享出来仅是为了提做一个笔记以防忘记,并给大家提供一个参考。 聚合操作指的是在数据查找基础上对于数据进一步整理筛选行为,聚合操作也

    2023年04月24日
    浏览(58)
  • docker-compose部署6.8.23版本elasticsearch+es-head+kibana多节点集群及部分排错处理

    现阶段很多elasticsearch部署都是7版本之上的,但部分要求是6.8版本,俩者很多配置参数存在差异问题,elasticsearch有状态服务,kubernetes和docker启动有点麻烦,最好用docker-compose启动,可以保证数据稳定。 节点信息 hosts信息 192.168.121.137 es-master 192.168.121.138 es-node1 master执行,node改

    2024年02月14日
    浏览(46)
  • Elasticsearch(ES)(版本7.x)创建索引报错:Faile to parse mapping [_doc] Root mapping definition has unsupported

    Elasticsearch(ES)(版本7.x)创建索引报错: 因es7.0版本之后不再需要type doc,把上面语句中的doc删掉,再运行就可以创建索引了。 如果还需要type doc则需要增加include_type_name=true即可解决。 示例:

    2024年02月16日
    浏览(43)
  • 【elasticsearch】elasticsearch es读写原理

    今天来学习下 es 的写入原理。 Elasticsearch底层使用Lucene来实现doc的读写操作: 没有并发设计 lucene 只是一个搜索引擎库,并没有涉及到分布式相关的设计,因此要想使用Lucene来处理海量数据,并利用分布式的能力,就必须在其之上进行分布式的相关设计。 非实时 将文件写入

    2023年04月08日
    浏览(49)
  • ES(elasticsearch)报错elasticsearch.keystore

    准备启动ES发现报错如下elasticsearch.keystore,一直没弄清楚怎么回事,即便我按照keystore搜索也没有,后来我才意识到是权限的问题啊兄弟们!你们看其他文件都是lin:lin,只有这个elasticsearch.keystore归属于root,因为它是在我启动后才生成的,自然在我设置归属的时候没有

    2024年02月12日
    浏览(40)
  • ElasticSearch第二讲:ES详解 - ElasticSearch基础概念

    在学习ElasticSearch之前,先简单了解下ES流行度,使用背景,以及相关概念等。本文是ElasticSearch第二讲,ElasticSearch的基础概念。 根据DB Engine的排名显示,ElasticSearch是最受欢迎的企业级搜索引擎。 下图红色勾选的是我们前面的系列详解的,除此之外你可以看到搜索库ElasticSea

    2024年02月09日
    浏览(51)
  • elasticsearch es数据迁移es-dump

    由于现在接管了同事的代码,同事的代码操作es时,导致了线上问题,我又对es不熟悉,怕操作影响了其他线上业务,于是现在将es数据复制一版到本地来。 远程es服务elasticsearch.tencent.xxx.com:80 本地es服务192.168.9.102:9200 现在将远程转换为本地,我们采用【elasticdump工具】来进行

    2024年02月11日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包