Elasticsearch权威指南 部分知识点总结整理

声明:本博客根据《ES权威指南》内容总结整理而成,转载请注明出处:https://blog.csdn.net/qingmou_csdn

 

time_out

time_out 值告诉我们查询超时与否。一般的,搜索请求不会超时。如果响应速度比完整的结果更重要,你可以定义 timeout 参数为10 或者10ms(10毫秒),或者1s(1秒)

GET /_search?timeout=10ms

Elasticsearch将返回在请求超时前收集到的结果。

超时不是一个断路器(circuit breaker)(译者注:关于断路器的理解请看警告)。

警告:

需要注意的是 timeout 不会停止执行查询,它仅仅告诉你目前顺利返回结果的节点然后关闭连接。在后台,其他分片可能依旧执行查询,尽管结果已经被发送。

使用超时是因为对于你的业务需求(译者注:SLA,Service-Level Agreement服务等级协议,在此我翻译为业务需求)来说非常重要,而不是因为你想中断执行长时间运行的查询。


在集群系统中深度分页

为了理解为什么深度分页是有问题的,让我们假设在一个有5个主分片的索引中搜索。当我们请求结果的第一页(结果1到10)时,每个分片产生自己最顶端10个结果然后返回它们给请求节点(requesting node),它再排序这所有的50个结果以选出顶端的10个结果。

现在假设我们请求第1000页——结果10001到10010。工作方式都相同,不同的是每个分片都必须产生顶端的10010个结果。然后请求节点排序这50050个结果并丢弃50040个!

你可以看到在分布式系统中,排序结果的花费随着分页的深入而成倍增长。这也是为什么网络搜索引擎中任何语句不能返回多于1000个结果的原因。


搜索

 

1.查询字符串(query string):将所有参数通过查询字符串定义;

适合在命令行下运行点对点查询;

优点:简洁明快的表示复杂查询,对于命令行下一次性查询或开发模式下非常有用;

缺点:简洁带来了隐晦、调试困难、脆弱(一个细小的语法错误就会导致返回错误而不是结果);允许任意用户在索引中任何一个字段上运行潜在的慢查询语句,可能暴露私有信息甚至使集群瘫痪(因此不建议直接暴露查询字符串搜索给用户)。

 

2.结构化查询(DSL):使用JSON完整的表示请求体(request body)


映射(mapping)机制用于进行字段类型确认,将每个字段匹配为一种确定的数据类型。

分析(analysis)机制用于进行全文文本(Full Text)的分词,以建立供搜索用的反向索引。

 

确切值(Exact values) vs. 全文文本(Full text)


倒排索引

 

Elasticsearch使用一种叫做倒排索引(inverted index)的结构来做快速的全文搜索。倒排索引由在文档中出现的唯一的单词列表,以及对于每个单词在文档中的位置组成。

 

为了创建倒排索引,我们首先切分每个文档的 content 字段为单独的单词(我们把它们叫做词(terms)或者表征(tokens)),把所有的唯一词放入列表并排序

 

表征化和标准化的过程叫做分词(analysis)


分析和分析器

 

分析(analysis):

首先,表征化一个文本块为适用于倒排索引单独的词(term);

然后,标准化这些词为标准形式,提高它们的“可搜索性”或“查全率”。

 

这个工作由分析器(analyzer)完成。一个分析器只是一个包装用于将三个功能放到一个包里:

字符过滤器(character filter):

字符串先经过字符过滤器,在表征化(断词)前处理字符串,可去除HTML标记或转换“&”为“and”

分词器(tokenizer):

字符串通过分词器被表征化(断词)为独立的词。一个简单的分词器可根据空格或逗号将单词分开(不适用中文)

表征过滤(token filters):

每个词都通过所有表征过滤,它可以修改词(如将“Quick”转换为小写),去掉词(如停用词像“a"、"and"、”the"等),或增加词(如同义词像“jump”和“leap”)

 

ES内置分析器

 

"Set the shape to semi-transparent by calling set_trans(5)"

 

标准分析器:(standard)

标准分析器是Elasticsearch默认使用的分析器。对于文本分析,它对于任何语言都是最佳选择(译者注:就是没啥特殊需求,对于任何一个国家的语言,这个分析器就够用了)。它根据Unicode Consortium的定义的单词边界(word boundaries)来切分文本,然后去掉大部分标点符号。最后,把所有词转为小写。产生的结果为:

set, the, shape, to, semi, transparent, by, calling, set_trans, 5

 

简单分析器:(simple)

简单分析器将非单个字母的文本切分,然后把每个词转为小写。产生的结果为:

set, the, shape, to, semi, transparent, by, calling, set, trans

 

空格分析器:(whitespace)

空格分析器依据空格切分文本。它不转换小写。产生结果为:

Set, the, shape, to, semi-transparent, by, calling, set_trans(5)

 

语言分析器:(english)

特定语言分析器适用于很多语言。它们能够考虑到特定语言的特性。例如, english 分析器自带一套英语停用词库——像 and 或 the 这些与语义无关的通用词。这些词被移除后,因为语法规则的存在,英语单词的主体含义依旧能被理解。english 分析器将会产生以下结果:

set, shape, semi, transpar, call, set_tran, 5

注意 "transparent" 、 "calling" 和 "set_trans" 是如何转为词干的。

 

当分析器被使用

 

当我们索引(index)一个文档,全文字段会被分析为单独的词来创建倒排索引。不过,当我们在全文字段搜索(search)时,我们要让查询字符串经过同样的分析流程处理,以确保这些词在索引中存在。

 

当你查询全文(full text)字段,查询将使用相同的分析器来分析查询字符串,以产生正确的词列表。

当你查询一个确切值(exact value)字段,查询将不分析查询字符串,但是你可以自己指定。

 


index

 

index 参数控制字符串以何种方式被索引。它包含以下三个值当中的一个:

analyzed:首先分析这个字符串,然后索引(即 以全文形式索引此字段);

not_analyzed:索引这个字段使之可以被搜索,但索引内容和指定值一样。不分析此字段;

no:不索引这个字段。这个字段不能被搜索到。

 

2.x以上版本,想要查看索引中某个字段的分析内容,分词结果:

GET /index/type/id/_termvectors?fields=your_type1,your_type2....

空字段

 

Lucene没法存放 null 值,所以一个 null 值的字段被认为是空字段

 

这四个字段将被识别为空字段而不被索引:

"empty_string": " ",

"null_value": null,

"empty_array": [ ],

"array_with_null_value": [ null ]

 


查询与过滤

 

结构化查询(Query DSL) 结构化过滤(Filter DSL)

 

一条过滤语句会询问每个文档的字段值是否包含着特定值;

一条查询语句会询问每个文档的字段值与特定值的匹配程度如何。

 

一条查询语句会计算每个文档与查询语句的相关性,会给出一个相关性评分 _score ,并且按照相关性对匹配到的文档进行排序。这种评分方式非常适用于一个没有完全配置结果的全文本搜索。

 

性能差异

 

使用过滤语句得到的结果集---一个简单的文档列表,快速匹配运算并存入内存是十分方便的,每个文档仅需要1个字节。这些缓存的过滤结果集与后续请求的结合使用是非常高效的。

查询语句不仅要查找相匹配的文档,还需要计算每个文档的相关性,所以一般来说查询语句要比过滤语句更耗时,并且查询结果也不可缓存。

 

幸亏有了倒排索引,一个只匹配少量文档的简单查询语句在百万级文档中的查询效率会与一条经过缓存的过滤语句旗鼓相当,甚至略占上风。 但是一般情况下,一条经过缓存的过滤查询要远胜一条查询语句的执行效率。

 

过滤语句的目的就是缩小匹配的文档结果集,所以需要仔细检查过滤条件。

 

原则上讲,做全文本搜索或其他需要进行相关性评分的时候使用查询语句,剩下的全部用过滤语句


term 过滤

term 主要用于精确匹配哪些值,比如数字,日期,布尔值或 not_analyzed 的字符串(未经分析的文本数据类型)

terms 过滤

terms跟 term有点类似,但 terms允许指定多个匹配条件。如果某个字段指定了多个值,那么文档需要一起去做匹配

range 过滤

exists 和 missing 过滤

exists和missing过滤可以用于查找文档中是否包含指定字段或没有某个字段,类似于SQL语句中的 IS_NULL 条件;这两个过滤只是针对已经查出一批数据来,但是想区分出某个字段是否存在的时候使用。

bool 过滤

bool 过滤可以用来合并多个过滤条件查询结果的布尔逻辑,它包含一下操作符:

must :: 多个查询条件的完全匹配,相当于 and 。

must_not :: 多个查询条件的相反匹配,相当于 not 。

should :: 至少有一个查询条件匹配, 相当于 or 。

这些参数可以分别继承一个过滤条件或者一个过滤条件的数组 。

bool 查询

bool 查询与 bool过滤相似,用于合并多个查询子句。不同的是, bool 过滤可以直接给出是否匹配成功, 而 bool查询要计算每一个查询子句的 _score(相关性分值)。

must :: 查询指定文档一定要被包含。

must_not :: 查询指定文档一定不要被包含。

should :: 查询指定文档,有则可以为文档相关性加分。

字段值排序

对结果按时间排序,date字段在内部被转为毫秒;

计算 _score是比较消耗性能的,而且通常主要用作排序--我们不是用相关性进行排序的时候,就不需要统计其相关性。如果你想强制计算其相关性,可以设置 track_scores 为 true。

多级排序

e.g.

"sort": [
    {
      "time": { "order": "desc" },
      "_score": { "order": "desc" }
    }
  ]

先按时间倒序排序,时间一样时再按评分倒序排序


相关性简介

查询语句会为每个文档添加一个_score字段。评分的计算方式取决于不同的查询类型--不同的查询语句用于不同的目的: fuzzy查询会计算与关键词的拼写相似程度, terms 查询会计算找到的内容与关键词组成部分匹配的百分比,但是一般意义上我们说的全文本搜索是指计算内容与关键词的类似程度。

ElasticSearch的相似度算法被定义为TF/IDF,即检索词频率/反向文档频率


数据字段

当搜索的时候,我们需要用检索词去遍历所有的文档。

当排序的时候,我们需要遍历文档中所有的值,我们需要做反倒序排列操作。

为了提高排序效率,ElasticSearch 会将所有字段的值加载到内存中,这就叫做"数据字段"。

重要:ElasticSearch将所有字段数据加载到内存中并不是匹配到的那部分数据,而是索引下所有文档中的值,包括所有类型。

 

将所有字段数据加载到内存中是因为从硬盘反向倒排索引是非常缓慢的。尽管你这次请求需要的是某些文档中的部分数据,但你下个请求却需要另外的数据,所以将所有字段数据一次性加载到内存中是十分必要的。

 

ElasticSearch中的字段数据常被应用到以下场景:

  • 对一个字段进行排序
  • 对一个字段进行聚合
  • 某些过滤,比如地理位置过滤
  • 某些与字段相关的脚本计算

毫无疑问,这会消耗掉很多内存,尤其是大量的字符串数据。

内存不足可以通过横向扩展解决,我们可以增加更多的节点到集群。


分布式搜索

注:可增加对系统如何工作的了解,不必淹没在细节里。

 

搜索的执行过程分两个阶段,查询 和 取回 (query then fetch )

查询:找到所有匹配文档;

取回:来自多个分片的结果被组合放到一个有序列表中。

 

查询阶段

在初始化查询阶段(query phase),查询被向索引中的每个分片副本(原本或副本)广播。每个分片在本地执行搜索并且建立了匹配document的优先队列(priority queue)。

一个优先队列(priority queue is)只是一个存有前n个(top-n)匹配document的有序列表。这个优先队列的大小由分页参数from和size决定。

查询阶段包含以下三步:

  • 1.客户端发送一个 search(搜索) 请求给 Node3 , Node3 创建了一个长度为from+size的空优先级队列。
  • 2. Node3 转发这个搜索请求到索引中每个分片的原本或副本。每个分片在本地执行这个查询并且将结果汇总到一个大小为 from+size 的有序本地优先队列里去。
  • 3.每个分片返回document的ID和它优先队列里所有document的排序值给协调节点Node3。 Node3把这些值合并到自己的优先队列里产生全局排序结果。

 

当一个搜索请求被发送到一个节点Node,这个节点就变成了协调节点。这个节点的工作是向所有相关的分片广播搜索请求并且把它们的响应整合成一个全局的有序结果集。这个结果集会被返回给客户端。

 

第一步是向索引里的每个节点的分片副本广播请求。就像document的GET 请求一样,搜索请求可以被每个分片的原本或任意副本处理。这就是更多的副本(当结合更多的硬件时)如何提高搜索的吞吐量的方法。对于后续请求,协调节点会轮询所有的分片副本以分摊负载。

 

每一个分片在本地执行查询和建立一个长度为 from+size 的有序优先队列——这个长度意味着它自己的结果数量就足够满足全局的请求要求。分片返回一个轻量级的结果列表给协调节点。只包含documentID值和排序需要用到的值,例如 _score 。

 

协调节点将这些分片级的结果合并到自己的有序优先队列里。这个就代表了最终的全局有序结果集。到这里,查询阶段结束。

 

注意:一个索引可以由一个或多个原始分片组成,所以一个对于单个索引的搜索请求也需要能够把来自多个分片的结果组合起来。一个对于多(multiple)或全部(all)索引的搜索的工作机制和这完全一致——仅仅是多了一些分片而已。

 

取回阶段

查询阶段辨别出那些满足搜索请求的document,但我们仍然需要取回那些document本身。这就是取回阶段的工作,如图分布式搜索的取回阶段所示。

分发阶段由以下步骤构成:

  • 1.协调节点辨别出哪个document需要取回,并且向相关分片发出GET 请求。
  • 2.每个分片加载document并且根据需要丰富(enrich)它们,然后再将document返回协调节点。
  • 3.一旦所有的document都被取回,协调节点会将结果返回给客户端。
  •  

协调节点先决定哪些document是实际(actually)需要取回的。例如,我们指定查询 {"from": 90, "size":10} ,那么前90条将会被丢弃,只有之后的10条会需要取回。这些document可能来自与原始查询请求相关的某个、某些或者全部分片。

 

协调节点为每个持有相关document的分片建立多点get请求然后发送请求到处理查询阶段的分片副本。

 

分片加载document主体——source field。如果需要,还会根据元数据丰富结果和高亮搜索片断。一旦协调节点收到所有结果,会将它们汇集到单一的回答响应里,这个响应将会返回给客户端。


深分页

根据document数量,分片数量以及所用硬件,对10,000到50,000条结果(1000到5000页)深分页是可行的(一般控制最多分页数1000);

如果确实需要从集群里获取大量documents,可设置搜索类型scan禁用排序,来高效地做这件事。


preference(偏爱)

preference参数允许你控制使用哪个分片或节点来处理搜索请求。她接受如下一些参数_primary,_primary_first,_local, _only_node:xyz, _prefer_node:xyz和 _shards:2,3。这些参数在文档搜索偏好(search preference)里有详细描述。

 

然而通常最有用的值是一些随机字符串,它们可以避免结果震荡问题(the bouncing results problem)。

 

结果震荡(Bouncing Results):

想像一下,你正在按照timestamp 字段来对你的结果排序,并且有两个document有相同timestamp。由于搜索请求是在所有有效的分片副本间轮询的,这两个document可能在原始分片里是一种顺序,在副本分片里是另一种顺序。

 

这就是被称为结果震荡(bouncing results)的问题:用户每次刷新页面,结果顺序会发生变化。避免这个问题方法是对于同一个用户总是使用同一个分片。方法就是使用一个随机字符串例如用户的会话ID(session ID)来设置 preference参数。


分析器

standard分析器是用于全文字段的默认分析器,对于大部分西方语系来说是一个不错的选择。它考虑了以下几点:

  • standard 分词器,在词层级上分割输入的文本。
  • standard 表征过滤器,被设计用来整理分词器触发的所有表征(但是目前什么都没做)。
  • lowercase 表征过滤器,将所有表征转换为小写。
  • stop 表征过滤器,删除所有可能会造成搜索歧义的停用词,如 a,he, and , is 。

 

默认情况下,停用词过滤器是被禁用的。如需启用它,你可以通过创建一个基于 standard分析器的自定义分析器,并且设置 stopwords参数。可以提供一个停用词列表,或者使用一个特定语言的预定停用词列表。

 

e.g.创建一个新分析器 new_std ,并使用预定义的停用词 stop :

PUT /es_standard_test
{
  "settings": {
    "analysis": {
      "analyzer": {
        "new_std":{
          "type":"standard",
          "stopwords":"stop"
        }
      }
    }
  }
}

new_std 分析器不是全局的,它仅存在于定义的 es_standard_test 索引中,因此测试它需使用特定的索引名:

GET /es_standard_test/_analyze
{
  "text": ["test stop es"],
  "analyzer": "new_std"
}

响应结果如下:

{
  "tokens": [
    {
      "token": "test",
      "start_offset": 0,
      "end_offset": 4,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "es",
      "start_offset": 10,
      "end_offset": 12,
      "type": "<ALPHANUM>",
      "position": 2
    }
  ]
}

别名

两种管理别名的途径: _alias 用于单个操作, _aliases 用于原子化多个操作

 

假设你的应用采用一个叫 my_index 的索引。而事实上, my_index 是一个指向当前真实索引的别名。真实的索引名将包含一个版本号: my_index_v1 , my_index_v2 等

 

首先,创建一个索引 my_index_v1 , 然后将别名 my_index 指向它:

PUT my_index_v1

PUT my_index_v1/_alias/my_index

可以检测这个别名指向哪个索引:

GET /*/_alias/my_index

或哪些别名指向这个索引:

 GET my_index_v1/_alias/*

上述操作均返回结果:

{
  "my_index_v1": {
    "aliases": {
      "my_index": {}
    }
  }
}

然后,我们决定修改索引中一个字段的映射。当然我们不能修改现存的映射,索引我们需要重新索引数据。首先,我们创建有新的映射的索引 my_index_v2 。

PUT my_index_v2
{
  "mappings": {
    "test":{
      "properties":{
        "tags":{
          "type":"keyword"
        }
      }
    }
  }
}

然后我们从将数据从 my_index_v1 迁移到 my_index_v2 ,一旦我们认为数据已经被正确的索引了,我们就将别名指向新的索引。

POST _reindex
{
  "source": {
    "index": "my_index_v1"
  },
  "dest": {
    "index": "my_index_v2"
  }
}

别名可以指向多个索引,所以我们需要在新索引中添加别名的同时从旧索引中删除它。这个操作需要原子化,所以我们需要用 _aliases 操作:


POST _aliases
{
  "actions": [
    {
      "remove": {
        "index": "my_index_v1",
        "alias": "my_index"
      }
    },
    {
      "add": {
        "index": "my_index_v2",
        "alias": "my_index"
      }
    }
  ]
}

这样,你的应用就从旧索引迁移到了新的,而没有停机时间


查找准确值

 

对于准确值,你需要使用过滤器。过滤器的重要性在于它们非常的快。它们不计算相关性(避过所有计分阶段)而且很容易被缓存。

 

用于数字的 term 过滤器

这个过滤器旨在处理数字,布尔值,日期,和文本;

过滤器不会执行计分和计算相关性。分值由 match_all 查询产生,所有文档一视同仁,所有每个结果的分值都是 1;

 

用于文本的 term 过滤器

not_indexed、

keyword

 

查询多个准确值 - terms 过滤器

 

理解 term 和 terms 是包含操作,而不是相等操作

 

term 过滤器是怎么工作的:它检查倒排索引中所有具有短语的文档,然后组成一个字节集。

 

提示:倒排索引的特性让完全匹配一个字段变得非常困难。你将如何确定一个文档只能包含你请求的短语?你将在索引中找出这个短语,解出所有相关文档 ID,然后扫描索引中每一行来确定文档是否包含其他值。由此可见,这将变得非常低效和开销巨大。因此,term和 terms 是必须包含操作,而不是必须相等

 


范围

日期范围

用于日期字段时,range 过滤器支持日期数学操作。例如,我们想找到所有最近一个小时的文档:

"range":{
    "timestamp":{  "gt": "now-1h"   }
}

这个过滤器将始终能找出所有时间戳大于当前时间减 1 小时的文档,让这个过滤器像移窗一样通过你的文档。

 

日期计算也能用于实际的日期,而不是仅仅是一个像 now 一样的占位符。只要在日期后加上双竖线 || ,就能使用日期数学表达式了:

"range":{
    "timestamp":{
        "gt" : "2018-01-01 00:00:00",
         "lt" : "2018-01-01 00:00:00 || +1M"
     }
}

早于2018年1月1号加一个月(即大于2018年1月1号小于2018年2月1号)

 

字符串范围

倒排索引中的短语按照字典顺序排序,也是为什么字符串范围使用这个顺序

假如我们想让范围从 a开始而不包含 b ,我们可以用类似的 range过滤器语法:

"range":{
    "title":{
        "gte":"a",
        "lt":"b"
    }
}

当心基数:

数字和日期字段的索引方式让他们在计算范围时十分高效。但对于字符串来说却不是这样。为了在字符串上执行范围操作,Elasticsearch会在这个范围内的每个短语执行 term 操作。这比日期或数字的范围操作慢得多。

字符串范围适用于一个基数较小的字段,一个唯一短语个数较少的字段。你的唯一短语数越多,搜索就越慢。


处理Null值

倒排索引是表征和包含它们的文档的一个简单列表。假如一个字段不存在,它就没有任何表征,也就意味着它无法被倒排索引的数据结构表达出来。

本质上来说,null, [] (空数组)和 [null] 是相等的。它们都不存在于倒排索引中!

 

为应对数据缺失字段,或包含空值或空数组,ES有一些工具来处理空值或缺失字段。

 

  • exists过滤器: 返回任何包含这个字段的文档
  • missing过滤器: 返回没有特定字段值的文档

缓存

过滤器的核心是一个字节集来表示哪些文档符合这个过滤器。Elasticsearch主动缓存了这些字节集留作以后使用。一旦缓存后,当遇到相同的过滤时,这些字节集就可以被重用,而不需要重新运算整个过滤。

缓存的字节集很“聪明”:他们会增量更新。你索引中添加了新的文档,只有这些新文档需要被添加到已存的字节集中,而不是一遍遍重新计算整个缓存的过滤器。过滤器和整个系统的其他部分一样是实时的,你不需要关心缓存的过期时间。

 

独立的过滤缓存

每个过滤器都被独立计算和缓存,而不管它们在哪里使用。如果两个不同的查询使用相同的过滤器,则会使用相同的字节集。同样,如果一个查询在多处使用同样的过滤器,只有一个字节集会被计算和重用。

 

控制缓存

大部分直接处理字段的枝叶过滤器(例如 term )会被缓存,而像 bool 这类的组合过滤器则不会被缓存。

 

提示:

枝叶过滤器需要在硬盘中检索倒排索引,所以缓存它们是有意义的。另一方面来说,组合过滤器使用快捷的字节逻辑来组合它们内部条件生成的字节集结果,所以每次重新计算它们也是很高效的。

然而,有部分枝叶过滤器,默认不会被缓存,因为它们这样做没有意义:

  • 脚本过滤器
  • Geo过滤器
  • 日期范围

 

有时候默认的缓存测试并不正确。可能你希望一个复杂的 bool 表达式可以在相同的查询中重复使用,或你想要禁用一个 date 字段的过滤器缓存。你可以通过 _cache 标记来覆盖几乎所有过滤器的默认缓存策略:

"range":{
    "timestamp":{
        "gt":"2018-01-02 16:15:14"
    },
    "_cache":false	//在这个过滤器上禁用缓存
}

“_cache” : ES 2.X版本;5.X版本起已经弃用,改用如下方式:

GET gaoyh/_search?request_cache=false
{
  "query": {
    "range": {
      "time": {
        "lt": "now-1h"
      }
    }
  }
}

监控缓存使用情况

可以通过索引查看缓存的大小(以字节为单位)和驱逐的数量,使用indices-statsAPI:

GET /_stats/request_cache?human

或者通过nodes-statsAPI 的节点:

GET /_nodes/stats/indices/request_cache?human

可以使用clear-cacheAPI手动过期缓存:

POST /index_name/_cache/clear?request=true

默认情况下启用缓存,但在创建新索引时可以禁用缓存:

PUT /my_index { "settings": { "index.requests.cache.enable": false } }

也可以使用update-settingsAPI 在现有索引上动态启用或禁用 :

PUT /my_index/_settings { "index.requests.cache.enable": true }

过滤顺序

在bool条件中过滤器的顺序对性能有很大的影响。更详细的过滤条件应该被放置在其他过滤器之前,以便在更早的排除更多的文档。

假如条件 A匹配1000万个文档,而B只匹配100个文档,那么需要将B放在A前面。

缓存的过滤器非常快,所以它们需要被放在不能缓存的过滤器之前。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值