ELK数据检索和分析(六)

今天分析相关数据的检索和数据分析:

为了方便我们学习,我们导入 kibana 为我们提供的范例数据。
 

 

目前为止,我们已经探索了如何将数据放入 Elasticsearch , 现在来讨论下如 何将数据从 Elasticsearch 中拿出来,那就是通过搜索。毕竟,如果不能搜索数据, 那么将其放入搜索引擎的意义又何在呢? 幸运的是, Elasticsearch 提供了丰富的 接口来搜索数据,涵盖了 Lucene 所有的搜索功能。因为 Elasticsearch 允许构建 搜索请求的格式很灵活,请求的构建有无限的可能性。要了解哪些查询和过滤器 的组合适用于你的数据, 最佳的方式就是进行实验,因此不要害怕在项目的数据 上尝试这些组合,这样才能弄清哪些更适合你的需求。
_search 接口
所有的 REST 搜索请求使用 _search 接口,既可以是 GET 请求,也可以是 POST 请求,也可以通过在搜索 URL 中指定索引来限制范围。 _search 接口有两种请求方法,一种是基于 URI 的请求方式,另一种是基于 请求体的方式,无论哪种,他们执行的语法都是基于 DSL ES 为我们定义的查询 语言,基于 JSON 的查询语言),只是形式上不同。我们会基于请求体的方式来 学习。比如说:
get kibana_sample_data_flights/_search
{
  "query": {
    "match_all": {
      
    }
  }
}
get kibana_sample_data_flights/_search
{
  "query": {
    "match_none": {
      
    }
  }
}
当然上面的查询没什么太多的用处,因为他们分别代表匹配所有和全不匹配。 所以我们经常要使用各种语法来进行查询,一旦选择了要搜索的索引,就需 要配置搜索请求中最为重要的模块。这些模块涉及文档返回的数量,选择最佳的 文档返回,以及配置不希望哪些文档出现在结果中等等。
query- 这是搜索请求中最重要的组成部分,它配置了基于评分返回的最 佳文档,也包括了你不希望返回哪些文档。
size- 代表了返回文档的数量。
from- size 一起使用, from 用于分页操作。需要注意的是,为了确定 第 2 页的 10 项结果, Elasticsearch 必须要计算前 20 个结果。如果结果集合不断 增加,获取某些靠后的翻页将会成为代价高昂的操作。■ _source 指定 _ source 字段如何返回。默认是返回完整的 _ source 字段。 通过配置_ source, 将过滤返回的字段。如果索引的文档很大,而且无须结果中的 全部内容,就使用这个功能。请注意,如果想使用它,就不能在索引映射中关闭 _ source 字段。
sort 默认的排序是基于文档的得分。如果并不关心得分,或者期望许多 文档的得分相同,添加额外的 sort 将帮助你控制哪些文档被返回。
结果起始和页面大小
命名适宜的 from size 字段,用于指定结果的开始点,以及每“页 " 结果的 数量。举个例子,如果发送的 from 值是 7 size 值是 5, 那么 Elasticsearch 将返回 第 8 9 10 11 12 项结果 ( 由于 from 参数是从 0 开始,指定 7 就是从第 8 项结果开始) 。如果没有发送这两个参数, Elasticsearch 默认从第一项结果开始 ( 第 0 项结果 ) ,在回复中返回 10 项结果。
例如
get kibana_sample_data_flights/_search
{
  "from": 100,
  "size": 20,
  "query": {
    "term": {
      "DestCountry": "CN"
    }
  }
}
但是注意,
from size 的和不能超过 index. max_result_window 这个索引配 置项设置的值。默认情况下这个配置项的值为 10000, 所以如果要查询 10000 条以 后的文档,就必须要增加这个配置值。例如,要检索第 10000 条开始的 200 条数 据,这个参数的值必须要大于 10200 ,否则将会抛出类似“ Result window is too large'的异常。 由此可见,Elasticsearch 在使用 from size 处理分页问题时会将所有数据 全部取出来,然后再截取用户指定范围的数据返回。所以在查询非常靠后的数据 时,即使使用了 from size 定义的分页机制依然有内存溢出的可能,而 max_ result_ window 设置的 10000 条则是对 Elastiesearch 的一 . 种保护机制。 那么 Elasticsearch 为什么要这么设计呢 ? 首先,在互联网时代的数据检索应 该通过相似度算法,提高检索结果与用户期望的附和度,而不应该让用户在检索 结果中自己挑选满意的数据。以互联网搜索为例,用户在浏览搜索结果时很少会 看到第 3 页以后的内容。假如用户在翻到第 10000 条数据时还没有找到需要的结 果,那么他对这个搜索引擎一定会非常失望。
_source 参数
元字段 _source 中存储了文档的原始数据。如果请求中没有指定 _source , Elasticsearch 默认返回整个 _ source , 或者如果 _ source 没有存储,那么就只返 回匹配文档的元数据:_ id _type _index _score
例如:
get kibana_sample_data_flights/_search
{
  "query": {
    "match_all": {
      
    }
  },
  "_source": [
    "OriginCountry",
    "DestCountry"
  ]
}
你不仅可以返回字段列表 , 还可以指定通配符。例如 , 如果想同时返回 " DestCountry "和 " DestWeather " 字段,可以这样配置 _ source: "Dest*" 。 也可以使 用通配字符串的数组来指定多个通配符,例如_ source:[" Origin*", "* Weather "]
get kibana_sample_data_flights/_search
{
  "query": {
    "match_all": {
      
    }
  },
  "_source": [
    "Origin*",
    "*Weather"
  ]
}
不仅可以指定哪些字段需要返回,还可以指定哪些字段无须返回。比如:
get kibana_sample_data_flights/_search
{
  "_source": {
    "includes": [
      "*.lon",
      "*.lat"
    ],
    "excludes": "DestLocation.*"
  }
}
排序
大多搜索最后涉及的元素都是结果的排序 ( sort ) 。如果没有指定 sort 排序选 项,Elasticsearch 返回匹配的文档的时候,按照 _ score 取值的降序来排列,这样 最为相关的( 得分最高的 ) 文档就会排名在前。为了对字段进行升序或降序排列, 指定映射的数组,而不是字段的数组。通过在 sort 中指定字段列表或者是字段映 射, 可以在任意数量的字段上进行排序。
例如:
get kibana_sample_data_flights/_search
 {
  "from": 100,
  "size": 20,
  "query": {
    "match_all": {
      
    }
  },
  "_source": [
    "Origin*",
    "*Weather"
  ],
  "sort": [
    {
      "DistanceKilometers": "asc"
    },
    {
      "FlightNum": "desc"
    }
  ]
}
 
 
检索
目前为止所进行的几乎所有搜索请求虽然有些条件限制,但是限制条件主要 是在规定 ES 返回的查询结果中哪些字段返回,哪些字段不返回,对于哪些文档 返回,哪些文档不返回,其实没有多少约束。真正如何更精确的找到我们需要的 文档呢?这就需要我们需要使用带条件的搜索,主要包括两类,基于词项的搜索 和基于全文的搜索。
基于词项的搜索
term 查询
对词项做精确匹配,数值、日期等等,如:
get kibana_sample_data_flights/_search
{
  "query": {
    "term": {
      "dayOfWeek": 3
    }
  }
}
对于字符串而言,字符串的精确匹配是指字符的大小写,字符的数量和位置 都是相同的,词条(term )查询使用字符的完全匹配方式进行文本搜索,词条查 询不会分析( analyze )查询字符串,给定的字段必须完全匹配词条查询中指定的 字符串。比如:
get kibana_sample_data_flights/_search
{
  "query": {
    "term": {
      "OriginCityName": "Frankfurt am Main"
    }
  }
}
 
 
 
但是如果我们执行
get kibana_sample_data_flights/_search
{
  "query": {
    "term": {
      "OriginCityName": "Frankfurt"
    }
  }
}
结果却是
 

因此可以把 term 查询理解为 SQL 语句中 where 条件的等于号。
terms 查询
可以把 terms 查询理解为 SQL 语句中 where 条件的 in 操作符:
get kibana_sample_data_flights/_search
{
  "query": {
    "terms": {
      "OriginCityName": [
        "Frankfurt am Main",
        "Cape Town"
      ]
    }
  }
}
Elasticsearch terms 查询中还支持跨索引查询,这类似于关系型数据库中 的一对多或多对多关系。比如,用户与文章之间就是一对多关系,可以在用户索 引中存储文章编号的数组以建立这种对应关系,而将文章的实际内容保存在文章 索引中( 当然也可以在文章中保存用户 ID) 。如果想将 ID 1 的用户发表的所有 文章都找出来,在文章索引中查询时为
POST /articles/ search
{
  "query": {
    "terms": {
      "_id": {
        "index": "users""id": 1,
        "path": "articles"
      }
    }
  }
}
在上面的例子中, terms 要匹配的字段是 id, 但匹配值则来自于另一个索引。 这里用到了 index id path 三个参数,它们分别代表要引用的索引、文档 ID 和字段路径。在上面的例子中,先会到 users 索引中在找 id 1 的文档,然后 取出 articles 字段的值与 articles 索引里的 _id 做对比,这样就将用户 1 的所有文 章都取出来了。
range 查询和 exists 查询
range 查询和过滤器的含义是不言而喻的,它们查询介于一定范围之内的值, 适用于数字、日期甚至是字符串。 为了使用范围查询,需要指定某个字段的上界和下界值。例如:
get kibana_sample_data_flights/_search
{
  "query": {
    "range": {
      "FlightDelayMin": {
        "gte": 100,
        "lte": 200
      }
    }
  }
}
可以查询出延误时间在 100~200 之间的航班。其中:
gte :大于等于 ( greater than and equal
gt :大于 ( greater than
lte :小于等于 ( less than and equal
lt :大于 ( less than
boost :相关性评分(后面的章节会讲到相关性评分)
exists 查询检索字段值不为空的的文档,无论其值是多少,在查询中通过 field 字段设置检查非空的字段名称,只能有一个。
prefix 查询
prefix 查询允许你根据给定的前缀来搜索词条,这里前缀在同样搜索之前是 没有经过分析的。例如:
get kibana_sample_data_flights/_search
{
  "query": {
    "prefix": {
      "DestCountry": "C"
    }
  }
}
找到航班目的国家中所有以 C 开头的文档。
wildcard 查询和 regexp 查询
wildcard 查询就是通配符查询。 使用字符串可以让 Elasticsearch 使用 * 通配符替代任何数量的字符 ( 也可以 不含) 或者是使用 ? 通配符替代单个字符。
例如,有 5 个单词:“ bacon ”“ barn ” “ ban ” 和“ baboon ” “ bam ”, “ba*n ”的查询会匹配“ bacon ”“ barn ” “ ban ” 和“ baboon , 这是因
* 号可以匹配任何字符序列,而查询“ ba?n ” 只会匹配“ barn" ,因为 ? 任何时 候都需要匹配一个单独字符。 也可以混合使用多个* ? 字符来匹配更为复杂的通配模板,比如 f*f?x 就可 以匹配 firefox
get kibana_sample_data_flights/_search
{
"query":{
"wildcard":{
"Dest":"*Marco*"
}
}
}
使用这种查询时,需要注意的是 wildcard 查询不像 match 等其他查询那样 轻量级。查询词条中越早出现通配符( * 或者 ? ), Elasticsearch 就需要做更多的工作 来进行匹配。例如,对于查询词条“h* ”, Elasticsearch 必须匹配所有以“ h ” 开 头的词条。如果词条是“hi* ,Elasticsearch 只需搜索所有“ hi" 开头的词条, 这是“h ” 开头的词条集合的子集,规模更小。考虑到额外开支和性能问题,在 实际生产环境中使用 wildcard 查询之前,需要先考虑清楚,并且尽量不要让通配 符出现在查询条件的第一位。 当然 Elasticsearch 也支持正则 regexp 查询,比如
get kibana_sample_data_flights/_search
{
  "query": {
    "regexp": {
      "字段名": "正则表达式"
    }
  }
}
 
文本分析
词条( term )查询和全文( fulltext )查询最大的不同之处是:全文查询首先 分析(Analyze )查询字符串,使用默认的分析器分解成一系列的分词, term1 , term2, termN ,然后从索引中搜索是否有文档包含这些分词中的一个或多个。 所以,在基于全文的检索里,ElasticSearch 引擎会先分析( analyze )查询字 符串,将其拆分成小写的分词,只要已分析的字段中包含词条的任意一个,或全 部包含,就匹配查询条件,返回该文档;如果不包含任意一个分词,表示没有任 何文档匹配查询条件。 这里就牵涉到了 ES 里很重要的概念,文本分析,当然对应 text 类型字段 来说,本身不存在文本数据词项提取的问题,所以没有文本分析的问题。
什么分析
分析 ( analysis ) 是在文档被发送并加入倒排索引之前, Elasticsearch 在其主体 上进行的操作。在文档被加入索引之前Elasticsearch 让每个被分析字段经过一 系列的处理步骤。
■字符过滤 -- 使用字符过滤器转变字符。
■文本切分为分词 --- 将文本切分为单个或多个分词。
■分词过滤 --- 使用分词过滤器转变每个分词。
■分词索引 -- 将这些分词存储到索引中。
比如有段话“ I like ELK it include Elasticsearch&LogStash&Kibana ”,分析以 后的分词为 : i like elk it include elasticsearch logstash kibana
字符过滤
Elasticsearch 首先运行字符过滤器( char filter )。这些过滤器将特定的字符 序列转变为其他的字符序列。这个可以用于将 HTML 从文本中剥离,或者是将任 意数量的字符转化为其他字符( 也许是将“ I love u 2 ”这种缩写的短消息纠正为“ I love you too”。 在“I like ELK…… ”的例子里使用特定的过滤器将“ & ” 替换为“ and ”。
切分为分词
在应用了字符过滤器之后,文本需要被分割为可以操作的片段。底层的 Lucene 是不会对大块的字符串数据进行操作。相反,它处理的是被称为分词 ( token)的数据。 分词是从文本片段生成的,可能会产生任意数量 ( 甚至是 0) 的分词。例如, 在英文中一个通用的分词是标准分词器,它根据空格、换行和破折号等其他字符, 将文本分割为分词。在我们的例子里,这种行为表现为将字符串“I like ELK it include Elasticsearch&LogStash&Kibana”分解为分词 I like ELK it include Elasticsearch and LogStash Kibana。
分词过滤器
一旦文本块被转换为分词, Elasticsearch 将会对每个分词运用分词过滤器 ( token filter)。 这些分词过滤器可以将一个分词作为输入, 然后根据需要进行 修改,添加或者是删除。最为有用的和常用的分词过滤器是小写分词过滤器, 它 将输人的分词变为小写,确保在搜索词条“nosql" 的时候,可以发现关于“ NoSq" 的聚会。分词可以经过多于 1 个的分词过滤器,每个过滤器对分词进行不同的操 作,将数据塑造为最佳的形式,便于之后的索引。 在上面的例子, 3 种分词过滤器 : 1 个将分词转为小写 , 2 个删除停用词 (停止词 ) and ”,第三个将词条“ tools" 作为“ technologies" 的同义词进行添 加。
分词索引
当分词经历了零个或者多个分词过滤器,它们将被发送到 Lucene 进行文档 的索引。这些分词组成了第 1 章所讨论的倒排索引。
分析器
所有这些不同的部分,组成了一个分析器 ( analyzer ), 它可以定义为零个或多 个字符过滤器、1 个分词器、零个或多个分词过滤器。 Elasticsearch 中提供了很 多预定义的分析器。我们可以直接使用它们而无须构建自己的分析器。
配置分析器
_analyze 接口
GET /_analyze
POST /_analyze
GET /<index>/_analyze
POST /<index>/_analyze
可以使用 _analyze API 来测试 analyzer 如何解析我们的字符串的,在我们下 面的学习中,我们会经常用到这个接口来测试。
分词综述
因为文本分词会发生在两个地方:创建索引 : 当索引文档字符类型为 text 时, 在建立索引时将会对该字段进行分词;搜索:当对一个 text 类型的字段进行全文 检索时,会对用户输入的文本进行分词。 所以这两个地方都可以对分词进行配置。 创建索引时
ES 将按照下面顺序来确定使用哪个分词器:
1 、先判断字段是否有设置分词器,如果有,则使用字段属性上的分词器设 置;
2 、如果设置了 analysis.analyzer.default ,则使用该设置的分词器;
3 、如果上面两个都未设置,则使用默认的 standard 分词器。
设置索引默认分词器
PUT test
{
  "settings": {
    "analysis": {
      "analyzer": {
        "default": {
          "type": "simple"
        }
      }
    }
  }
}
还可以为索引配置内置分词器,并修改内置的部分选项修改它的行为
put test
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {
          "type": "standard",
          "stopwords": [
            "the",
            "a",
            "an",
            "this",
            "is"
          ]
        }
      }
    }
  }
}
如何为字段指定内置分词器
put test
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "standard",
        "search_analyzer": "simple"
      }
    }
  }
}
甚至还可以自定义分词器。 我们综合来看看分词的设置,并且通过_analyzer 接口来测试分词的效果:
PUT /my_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "std_english": {
          "type": "standard",
          "stopwords": "_english_"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "my_text": {
        "type": "text",
        "analyzer": "standard",
        "fields": {
          "english": {
            "type": "text",
            "analyzer": "std_english"
          }
        }
      }
    }
  }
}
我们首先,在索引 my_index 中配置了一个分析器 std_english std_english 中使用了内置分析器 standard ,并将 standard 的停止词模式改为英语模式 _english_(缺省是没有的),对字段 my_text 配置为多数据类型,分别使用了两 种分析器,standard std_english
POST /my_index/_analyze
{
"field": "my_text",
"text": "The old brown cow"
}
POST /my_index/_analyze
{
"field": "my_text.english",
"text": "The old brown cow"
}
 
 

 

通过上述运行我们可以看到,分析器 std_english 中的 The 被删除,而 standard 中的并没有。这是因为 my_text.english 配置了单独的停止词。
文档搜索时
文档搜索时使用的分析器有一点复杂,它依次从如下参数中如果查找文档分 析器,如果都没有设置则使用 standard 分析器 :
1 、搜索时指定 analyzer 参数
2 、创建索引时指定字段的 search_analyzer 属性
3 、创建索引时字段指定的 analyzer 属性
4 、创建索引时 setting 里指定的 analysis.analyzer.default_search
5 、如果都没有设置则使用 standard 分析器
比如:
搜索时指定 analyzer 查询参数
GET my_index/_search
{
  "query": {
    "match": {
      "message": {
        "query": "Quick foxes",
        "analyzer": "stop"
      }
    }
  }
}
指定字段的 analyzer seach_analyzer
PUT my_index
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "whitespace",
        "search_analyzer": "simple"
      }
    }
  }
}
指定索引的默认搜索分词器
PUT my_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "default": {
          "type": "simple"
        },
        "default_seach": {
          "type": "whitespace"
        }
      }
    }
  }
}
内置分析器
前面说过,每个被分析字段经过一系列的处理步骤:
字符过滤 -- 使用字符过滤器转变字符。 文本切分为分词--- 将文本切分为单个或多个分词。 分词过滤--- 使用分词过滤器转变每个分词。 每个分析器基本上都要包含上面三个步骤至少一个。其中字符过滤器可以为 0 个,也可以为多个,分词器则必须,但是也只能有一个,分词过滤器可以为 0 个,也可以为多个。Elasticsearch 已经为我们内置了很多的字符过滤器、分词器和分词过滤器,
以及分析器。不过常用的就是那么几个。
字符过滤器( Character filters
字符过滤器种类不多。 elasticearch 只提供了三种字符过滤器: HTML 字符过滤器( HTML Strip Char Filter
从文本中去除 HTML 元素。
POST _analyze
{
"tokenizer": "keyword",
"char_filter": ["html_strip"],
"text":"<p>I'm so <b>happy</b>!</p>"
}
映射字符过滤器( Mapping Char Filter
接收键值的映射,每当遇到与键相同的字符串时,它就用该键关联的值替换 它们。
PUT pattern_test4
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {
          "tokenizer": "keyword",
          "char_filter": [
            "my_char_filter"
          ]
        }
      },
      "char_filter": {
        "my_char_filter": {
          "type": "mapping",
          "mappings": [
            "James => 666",
            "13 号 => 888"
          ]
        }
      }
    }
  }
}
上例中,我们自定义了一个分析器,其内的分词器使用关键字分词器,字符 过滤器则是自定制的,将字符中的 James 替换为 666 13 号替换为 888
POST pattern_test4/_analyze
{
"analyzer": "my_analyzer",
"text": " James 热爱 13 号,可惜后来 13 号结婚了 "
}
模式替换过滤器( Pattern Replace Char Filter
使用正则表达式匹配并替换字符串中的字符。但要小心你写的糟糕的正则表 达式。因为这可能导致性能变慢!
比如:
POST _analyze
{
"analyzer": "standard",
"text": "My credit card is 123-456-789"
}
这样分词,会导致 123-456-789 被分为 123 456 789 ,但是我们希望 123-456-789 是一个整体,可以使用模式替换过滤器,替换掉“ - ”。
PUT pattern_test5
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {
          "tokenizer": "standard",
          "char_filter": [
            "my_char_filter"
          ]
        }
      },
      "char_filter": {
        "my_char_filter": {
          "type": "pattern_replace",
          "pattern": "(\\d+)-(?=\\d)",
          "replacement": "$1_"
        }
      }
    }
  }
}
POST pattern_test5/_analyze
{
"analyzer": "my_analyzer",
"text": "My credit card is 123-456-789"
}
把数字中间的“ - ”替换为下划线“ _ ”,这样的话可以让“ 123-456-789 ”作 为一个整体,而不至于被分成 123 456 789
分词器( Tokenizer
1. 标准分词器 (standard)
标准分词器 ( standard tokenizer) 是一个基于语法的分词器,对于大多数欧洲 语言来说是不错的。它还处理了 Unicode 文本的切分。它也移除了逗号和句号这 样的标点符号。 “I have, potatoes.”切分后的分词分别是 ” I” ” have” ” potatoes”
2. 关键词分词器 (keyword)
关键词分词器 ( keyword tokenizer ) - - 种简单的分词器,将整个文本作为单 个的分词,提供给分词过滤器。只想应用分词过滤器,而不做任何分词操作时, 它可能非常有用。 'Hi, there.' 唯一的分词是 Hi, there
3. 字母分词器 (letter)
字母分词器根据非字母的符号 , 将文本切分成分词。例如,对于句子 “Hi,there." 分词是 Hi there, 因为逗号、空格和句号都不是字母 : 'Hi, there. '分词是 Hi there
4. 小写分词器 (lowercase)
小写分词器 ( lowercase tokenizer) 结合了常规的字母分词器和小写分词过滤 器( 如你所想,它将整个分词转化为小写 ) 的行为。通过 1 个单独的分词器来实现 的主要原因是,2 次进行两项操作会获得更好的性能。 'Hi, there.'分词是 hi there
5. 空白分词器 (whitespace)
空白分词器 ( whitespace tokenizer ) 通过空白来分隔不同的分词,空白包括空 格、制表符、换行等。请注意,这种分词器不会删除任何标点符号,所以文本“Hi, there." 的分词 . 'Hi, there. ' 分词是 Hi, there.
6. 模式分词器 (pattern)
模式分词器 ( patterm tokenizer) 允许指定一个任 意的模式,将文本切分为分 词。被指定的模式应该匹配间隔符号。例如,可以创建一个定制分析器,它在出 现文本“. -. ”的地方将分词断开。
7. UAX URL 电子邮件分词器 (uax_url_email)
在处理英语单词的时候,标准分词器是非常好的选择。但是,当下存在不少 以网站地址和电子邮件地址结束的文本。标准分析器可能在你未注意的地方对其 进行了切分。例如,有一个电子邮件地址的样本 john.smith@example.com, 用标
准分词器分析它,切分后 : 'john.smith@example.com' 分词是 john.smith example.com 。 它同样将 URL 切分为不同的部分 :
'http://example. com?q=foo' 分词是 http example.com q foo 。 UAX URL 电子邮件分词器 ( UAX URL email tokenizer ) 将电子邮件和 URL 都作 为单独的分词进行保留。
8. 路径层次分词器 (path_hierarchy)
路径层次分词器 ( path hierarchy tokenizer ) 允许以特定的方式索引文件系统 的路径,这样在搜索时,共享同样路径的文件将被作为结果返回。例如,假设有 一个文件名想要索引,看上去是 这样的(ustl0oal/var/log/elasticsearch.log 。路径层次分词器将其切分为 : ' /usr/local/var/1og/elasticsearch. log' 分词是/usr /usr/local /usr/local/var /usr/local/var/ log /usr/local/var/
log/elasticsearch.1og 。 这意味着,一个用户查询时,和上述文件共享同样路径层次( 名字也是如此 ) 的文件也会被匹配上。查询“/usr/local/var/log/es.log" 时,它和 “/usr/local/var/log/elasticsearch.log" 拥有同样的分词,因此它也会被作为结果 返回。
分词过滤器( Token filters
1. 标准分词过滤器( standard
不要认为标准分词过滤器 ( standard token filter ) 进行了什么复杂的计算,实 际上它什么事情也没做。
2. 小写分词过滤器( lowercase
小写分词过滤器 ( lowercase token filter) 只是做了这件事 : 将任何经过的分词 转换为小写。这应该非常简单也易于理解。
3. 长度分词过滤器( length
长度分词过滤器 (length token filter) 将长度超出最短和最长限制范围的单词 过滤掉。举个例子,如果将 min 设置为 2 ,并将 max 设置为 8 ,任何小于 2 个字 符和任何大于 8 个字符的分词将会被移除。
4. 停用词分词过滤器( stop
停用词分词过滤器 (stop token fite) 将停用词从分词流中移除。对于英文而言, 这意味着停用词列表中的所有分词都将会被完全移除。用户也可以为这个过滤器 指定- 个待移除 单词的列表。
什么是停用词 ?
停用词是指在信息检索中,为节省存储空间和提高搜索效率,在处理自然语 言数据(或文本)之前或之后会自动过滤掉某些字或词,这些字或词即被称为 Stop Words(停用词)。
停用词 (Stop Words) 大致可分为如下两类:
1 、使用十分广泛,甚至是过于频繁的一些单词。比如英文的“ i ”、“ is ”、 “what ”,中文的“我”、“就”之类词几乎在每个文档上均会出现,查询这样 的词搜索引擎就无法保证能够给出真正相关的搜索结果,难于缩小搜索范围提高 搜索结果的准确性,同时还会降低搜索的效率。因此,在真正的工作中,Google 和百度等搜索引擎会忽略掉特定的常用词,在搜索的时候,如果我们使用了太多
的停用词,也同样有可能无法得到非常精确的结果,甚至是可能大量毫不相关的 搜索结果。
2 、文本中出现频率很高,但实际意义又不大的词。这一类主要包括了语气 助词、副词、介词、连词等,通常自身并无明确意义,只有将其放入一个完整的 句子中才有一定作用的词语。如常见的“的”、“在”、“和”、“接着”之类。 下面是英文的默认停用词列表: a, an, and, are, as, at, be, but, by, for, if, in, into, is, it, no, not, of, on, or; such,
that, the, their;, then,there, these, they, this, to, was, will, with
系统内置的停止词如下: 种语言中常见的停止词。这些内置的停止词如下:
_arabic_ - armenian_ _ basque _ bengali 1 _ brazilian _ bulgarian_,_
catalan _ _czech_,_ danish_ _ _dutch_ english_ finnish_, french_ _ _galician
_ german_,_greek._hindi_,_ hungarian_ _ indonesian_ _ irish_ _ _italian_
_ latvian_,_norwegian_,_ persian_ _portuguese_,_ romanian_,_ russian_,-
sorani_,- spanish_,_ swedish_ _thai_ _turkish_
5. 截断分词过滤器、修剪分词过滤器和限制分词数量过滤器
下面 3 个分词过滤器,通过某种方式限制分词流。
■截断分词过滤器 ( truncate token filter ) 允许你通过定制配置中的 length 参 数,截断超过一定长度的分词。默认截断多于 10 个字符的部分。
■修剪分词过滤器 ( trim token filter ) 删除 1 个分词中的所有空白部分。例如, 分词" foo " 将被转变为分词 foo
■限制分词数量分词过滤器 ( limit token count token filter) 限制了某个字段可 包含分词的最大数量。例如,如果创建了一个定制的分词数量过滤器,限制是 8, 那么分词流中只有前 8 个分词会被索引。这个设置使用 max_ token_ count 参数,
默认是 1 ( 只有 1 个分词会被索引 )
常用内置分析器
1. 标准分析器
当没有指定分析器的时候,标准分析器 ( standardanalyzer) 是文本的默认分析 器。它综合了对大多欧洲语言来说合理的默认模块,它没有字符过滤器,包括标 准分词器、小写转换分词过滤器和停用词分词过滤器(默认为_none_ ,也就是不
去除停止词)。这里只需要记住,如果不为某个字段指定分析器,那么该字段就 会使用标准分析器。可配置的参数如下:
max_token_length ,默认值 255 ,表示词项最大长度,超过这个长度将按该 长度分为多个词项 stopwords,默认值_none_ ,表示分析器使用的停止词数组,可使用内置停 止词列表,比如_english_ 等 stopwords_path 停止词文件路径
2. 简单分析器
简单分析器 ( simple analyzer) 就是那么简单 ! 它只使用了小写转换分词器,这 意味着在非字母处进行分词,并将分词自动转变为小写。这个分析器对于亚洲语 言来说效果不佳,因为亚洲语言不是根据空白来分词,所以请仅仅针对欧洲语言 使用它。
3. 空白分析器
空白分析器 ( whitespace analyzer ) 什么事情都不做 , 只是根据空白将文本切分 为若干分词。
4. 停用词分析器
停用词分析器 ( stop analyzer ) 和简单分析器的行为很相像,只是在分词流中 额外地过滤了停用词。
5. 关键词分析器
关键词分析器 ( keyword analyzer ) 将整个字段当作一个单独的分词。
6. 模式分析器
模板分析器 ( pattern analyzer ) 允许你指定一个分词切分的模式。 但是,由 于可能无论如何都要指定模式,通常更有意义的做法是使用定制分析器,组合现 有的模式分词器和所需的分词过滤器。
7. 雪球分析器
雪球分析器 ( snowball analyzer ) 除了使用标准的分词器和分词过滤器 ( 和标 准分析器一样), 也使用了小写分词过滤器和停用词过滤器。它还使用了雪球词干 器对文本进行词干提取。
试试自定义分析器
业务需求如下: 去除所有的 HTML 标签 将 & 替换成 and ,使用一个自定义的 mapping 字符过滤器 使用 standard 分词器分割单词 使用 lowercase 分词过滤器将词转为小写 用 stop 分词过滤器去除一些自定义停用词。
PUT pattern_custom
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {
          "char_filter": [
            "html_strip",
            "&_to_and"
          ],
          "filter": [
            "lowercase",
            "my_stopwords"
          ],
          "tokenizer": "standard",
          "type": "custom"
        }
      },
      "char_filter": {
        "&_to_and": {
          "mappings": [
            "&=>and"
          ],
          "type": "mapping"
        }
      },
      "filter": {
        "my_stopwords": {
          "stopwords": [
            "king",
            "james"
          ],
          "type": "stop"
        }
      }
    }
  }
}
POST pattern_custom/_analyze
{
"analyzer": "my_analyzer",
"text": "<br> I & Lison & king & James are handsome<br>"
}
 
 
 
中文分析器
上面的分析器基本都是针对英文的,对中文的处理不是太好,比如
 

 

 

分析后的结果是:

 

Standard 分析器把中文语句拆分为一个个的汉字,并不是太适合。这时候, 就需要中文分析器。
中文分析器有很多,例如 cjk ik 等等,我们选用比较有名的 ik 作为我们的 中文分析器。
安装
进入 elasticsearch 目录下的 plugins 目录,并执行
./elasticsearch-plugin install
https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.7.0/elasti
csearch-analysis-ik-7.7.0.zip

 

如果询问你“ Continue with installation? ”,当然继续进行。 安装完成后,必须重启 elasticsearch 使用
IK 分词器有两种分词效果,一种是 ik_max_word (最大分词)和 ik_smart (最 小分词)ik_max_word: 会将文本做最细粒度的拆分,比如会将
 
 
基于全文的搜索
了解了文本分析以后,就可以学习基于全文的搜索了,这里就需要用到 match 系列查询。
match 查询
比如说:
"query":{
"match":{
"elk":"Elasticsearch LogStash Kibana"
}
查询字符串是“ Elasticsearch LogStash Kibana ”,被分析器分词之后,产生三 个小写的单词:elasticsearch logstash kibana ,然后根据分析的结果构造一个布尔 查询,默认情况下,引擎内部执行的查询逻辑是:只要 elk 字段值中包含有任意 一个关键字 elasticsearch logStash kibana ,那么返回该文档,相对于的伪代 码是:
if(
doc. elk.contains(elasticsearch)
||doc. elk.contains(logstash)
||doc. elk.contains (kibana)
)
return doc ;
匹配查询的行为受到两个参数的控制:
operator :表示单个字段如何匹配查询条件的分词
minimum_should_match :表示字段匹配的数量
通过调整 operator minimum_should_match 属性值,控制匹配查询的逻 辑条件,进而控制引擎返回的结果。默认情况下 operator 的值是 or ,在构造查 询时设置分词之间的逻辑运算符,如果设置为 and ,那么引擎内部执行的查询逻
辑是:
if(
doc. elk.contains(elasticsearch)
&&doc. elk.contains(logstash)
&&doc. elk.contains (kibana)
)
return doc ;
对于 minimum_should_match 属性值,默认值是 1 ,如果设置其值为 2 ,表 示分词必须匹配查询条件的数量为 2 ,这意味着,只要文档的 elk 字段包含任意 两个关键字,就满足查询条件,但是如果文档中只有 1 个关键字,这个文档就不 满足条件。比如:
POST /kibana_sample_data_logs/_search
{
"query": {
"match": {
"message": "firefox chrome"
}
}
}
检索包含 firefox chrome 的文档,如果改为:
POST /kibana_sample_data_logs/_search
{
  "query": {
    "match": {
      "message": {
        "query": "firefox chrome",
        "operator": "and"
      }
    }
  }
}
则不会有任何文档返回,因为没有文档的 message 字段既包含 firefox 又包 含 chrome 。同样:
POST /kibana_sample_data_logs/_search
{
  "query": {
    "match": {
      "message": {
        "query": "firefox chrome",
        "minimum_should_match": 2
      }
    }
  }
}
也不会任何文档返回,原因也是一样的,因为没有文档的 message 字段既包 含 firefox 又包含 chrome
multi_match 查询
多个字段上执行匹配相同的查询,叫做 "multi_match" 查询。比如:
POST /kibana_sample_data_flights/_search
{
  "query": {
    "multi_match": {
      "query": "AT",
      "fields": [
        "DestCountry",
        "OriginCountry"
      ]
    }
  }
}
请求将同时检索文档中 DestCountry OriginCountry 这两个字段,只要有一
个字段包含 AT 词项该文档就满足查询条件。
match_phrase 查询
当你希望寻找邻近的单词时, match_phrase 查询可以帮你达到目的。比如:
假设我们要找到 title 字段包含这么一段文本“ quick brown fox ”的文档,然
后我们用
GET /my_index/my_type/_search
{
  "query": {
    "match_phrase": {
      "title": "quick brown fox"
    }
  }
}
match_phrase 查询首先解析查询字符串来产生一个词条列表。然后会搜索所
有的词条,但只保留包含了所有搜索词条的文档,并且词条的位置要邻接。
但是对于
GET /my_index/my_type/_search
{
  "query": {
    "match_phrase": {
      "title": "quick fox"
    }
  }
}
这个查询查询不会匹配我们的任何文档,因为没有文档含有邻接在一起的
quick fox 词条。也就是说,匹配的文档必须满足:
1 quick brown fox 必须全部出现在 title 字段中。
2 brown 的位置必须比 quick 的位置大 1
3 fox 的位置必须比 quick 的位置大 2
如果以上的任何一个条件没有被满足,那么文档就不能被匹配。 精确短语(Exact-phrase) 匹配也许太过于严格了。也许我们希望含有 "quick brown fox"的文档也能够匹配 "quick fox" 查询,即使位置并不是完全相等的。 我们可以在短语匹配使用 slop 参数来引入一些灵活性:
GET /my_index/my_type/_search
{
  "query": {
    "match_phrase": {
      "title": {
        "query": "quick fox",
        "slop": 1
      }
    }
  }
}
slop 参数缺省为 0 ,它告诉 match_phrase 查询词条能够最远相隔多远时仍然 将文档视为匹配。相隔多远的意思是,你需要移动一个词条多少次来让查询和文 档匹配?比如这样一段文本:hello world, java is very good, spark is also very good.
使用 match_phrase 搜索 java spark 搜不到 如果我们指定了 slop ,那么就允许 java spark 进行移动,来尝试与 doc
行匹配

 

 

 

上面展示了,当固定第一个 term 的时候,后面的 teram 经过移动直到匹 配上搜索词的经过这个移动的次数就是 slop ,实际例子如下:
POST /kibana_sample_data_logs/_search
{
"query": {
"match_phrase": {
"message": "firefox 6.0a1"
}
}
}
match_phrase_prefix 查询
被称为基于前缀的短语匹配,比如:
{
"match_phrase_prefix" : {
"brand" : "johnnie walker bl"
}
}
这种查询的行为与 match_phrase 查询一致,不同的是它将查询字符串的最 后一个词作为前缀使用,换句话说,可以将之前的例子看成如下这样: johnnie
跟着 walker
跟着以 bl 开始的词
或者可以干脆理解为:
"johnnie walker bl*" 与 match_phrase 一样,它也可以接受 slop 参数(参照 slop )让相对词 序位置不那么严格:
{
  "match_phrase_prefix": {
    "brand": {
      "query": "walker johnnie bl",
      "slop": 10
    }
  }
}
prefix 查询存在严重的资源消耗问题,短语查询的这种方式也同样如此。前 缀 a 可能会匹配成千上万的词,这不仅会消耗很多系统资源,而且结果的用处 也不大。可以通过设置 max_expansions 参数来限制前缀扩展的影响,一个合理的值 是是 50 ,这也是系统默认的值:
{
  "match_phrase_prefix": {
    "brand": {
      "query": "johnnie walker bl",
      "max_expansions": 50
    }
  }
}
实际例子:
POST /kibana_sample_data_logs/_search
{
  "query": {
    "match_phrase_prefix": {
      "message": "firefox 6.0"
    }
  }
}
模糊查询、纠错与提示器
编辑距离算法
Elasticsearch 基于全文的查询中,除了与短语相关的查询以外,其余查询 都包含有一个名为 fuzziness 的参数用于支持模糊查询。 Elasticsearch 支持的模糊 查询与 SQL 语言中模糊查询还不一样, SQL 的模糊查询使用“ % keyword%" 的形 式,效果是查询字段值中包含 keyword 的记录。 Elaticsearch 支持的模糊查询比 这个要强大得多,它可以根据一个拼写错误的词项匹配正确的结果,例如根据 firefix 匹配 firefox 。在自然语言处理领域,两个词项之间的差异通常称为距离或 编辑距离,距离的大小用于说明两个词项之间差异的大小。计算词项 编辑距离的算法有多种,在 Elasticsearch 中主要使用 Levenshtein NGram 两种。其他与此相关的算法也都是在这两种算法基础上进行的改造,基本思想都 是一致的。所以理解这两个算法的核心思想是学习这部分内容的关键。
Levenshtein NGram
Levenshein算法是前苏联数学家 Vladimir Levenshein 1965 年开发的一套算 法, 这个算法可以对两个字符申的差异程度做量化。量化结果是一一个正整数, 反映的是一个字符申变成另一个字符申最少需要多少次的处理。由于 Levenshtein 算法是最为普遍接受的编辑距离算法,所以在很多文献中如果没有特殊说明编辑 距离算法就是指 Levenshtein 算法。 在 Levenshtein 算法中定义了三种字符操作,即替换、插人和删除,后来又 由其他科学家补充了一个换位操作。在转换过程中,每执行次操作编辑距离就加
1 , 编辑距离越大越能说明两个字符串之间的差距大。 比如从 firefix firefox 需要将“ i" 替换成“ o ”, 所以编辑距离为 1; 而从 fax
fair 则需要将“ x ”替换为“ i" 并在结尾处插人“ r ”,所以编辑距离为 2 。显然 在编辑距离相同的情况下,单词越长错误与正确就越接近。比如编辑距离同样为
2 的情况下,从 fax fair 与从 elascsearxh elasticsearch, 后者 elastesearsh 是由 拼写错误引起的可能性就更大此。所以编辑距离这种量化标准一般还需要与单词 长度结合起来 明虑,在一些极端情况下编辑距离还应该设置为 0 ,比如像 at 、 on 这类长度只有 2 的短单词。 NGram 一般是指 N 个连续的字符,具体的字符个数被定义为 NGram size 。 size 为 1 NGram 称为 Unigram, size 2 时称为 Bigram, size 3 时则称为 Trigram。如果 NGram 处理的单元不是字符而是单词,一般称之为 Shingle 。使用 NGram 计算编辑距离的基本思路是让字符串分解为 NGram ,然后比较分解后共 有 NGram 的数量。假设有 a b 两个字符申,则 NGram 距离的具体运算公式为 ngram( a )+ngram(b) -2 * ngram(a)∩ ngram( b) 式中,ngram(a) ngram(b) 代表 a b 两个字符串 NGram 的数量 ; ngram(a) ∩ ngram(b)则是两者共有 NGram 的数量。
例如按 Bigram 处理 firefix firefox 两个单词,分别为“ fi,ir,re, ef, fi,ix ”和“ fi
ir, re, ef, fi, ox" 。 那么两个字符申的 Bigram 个数都为 6 ,而共有 Bigram 4 ,则 最终 NGram 距离为 6+6-2x4=4 。 在应用上,Levenshtein 算法更多地应用于对单个词项的模糊查询上,而 NGram 则应用于多词项匹配中。 Elasticseareh 同时应用了两种算法。
模糊查询
返回包含与搜索字词相似的字词文档;为了找到相似的术语, fuzzy 查询将 在指定的编辑距离内创建一组搜索词的所有可能的变体或扩展。查询然后返回每 个扩展的完全匹配。
比如:
get kibana_sample_data_logs/_search
{
  "query": {
    "fuzzy": {
      "message": {
        "value": "firefix",
        "fuzziness": "1"
      }
    }
  }
}
我们想找到文档中 message 字段包含 firefox ,而查询条件中给出的是 firefix , 因为两者的编辑距离为 1 ,所以包含 firefox 的文档依然可以找到,但是,如果使 用 firefit ,因为编辑距离为 2 ,则不会找到任何文档。
相关的参数有:
value ,必填项,希望在 field 中找到的术语
fuzziness ,选填项,匹配允许的最大编辑距离;可以被设置为“ 0 ”, “ 1 ”, “2 ”或“ auto ”。“ auto ”是推荐的选项,它会根据查询词的长度定义距离。 max_expansions,选填项,创建的最大变体项,默认为 50 。应该避免使用较 大的值,尤其是当 prefix_length 参数值为 0 时,如果过多,会影响查找性能。 prefix_length,选填项,创建扩展时保留不变的开始字符数。默认为 0
transpositions ,选填项,指示编辑是否包括两个相邻字符串的转置( ab ba )。 默认为 true
纠错与提示器
纠错是在用户提交了错误的词项时给出正确词项的提示,而输人提示则是在 用户输人关键字时给出智能提示,甚至可以将用户未输人完的内容自动补全。大 多数互联网搜索引擎都同时支持纠错和提示的功能,比如在用户提交了错误的搜 索关键字时会提示: “ 你是不是想查找 .... . 而在用户输人搜索关键字时还能自动 弹出提示框将用户可能要输人的内容全都列出来供用户选择。
Elasticsearch 也同时支持纠错与提示功能,由于这两个功能从实现的角度来 说并没有本质区别,所以它们都由一种被称为提示器或建议器( Suggester) 的特殊 检索实现。由于输人提示需要在用户输人的同时给出提示词,所以这种功能要求 速度必须快,否则就失去了提示的意义。在实现上,输人提示是由单独的提示器 完成。而在使用上,提示器则是通过检索接口_ search 的一个参数设置,
例如 :
POST /kibana_sample_data_logs/_search?filter_path=suggest
{
  "suggest": {
    "msg-suggest": {
      "text": "firefit chrom",
      "term": {
        "field": "message"
      }
    }
  }
}
在示例中, search 接口的 suggest 参数中定义了一个提示 msg- suggest, 并通 过 text 参数给出需要提示的内容。另一个参数 term 实际上是一种提示器的名称, 它会分析 text 参数中的字符串并提取词项,再根据 Levenshtein 算法找到满足编 辑距离的提示词项。所以在返回结果中会包含一个 suguggest 字段,其中列举了 依照 term 提示器找到的提示词项 :

Elaticearch 提供了三种提示器,它们在本质上都是基于编辑距离算法。下面 就来看看这此提示器如何使用。
term 提示器
在示例中使用的提示器就是 term 提示器,这种提示器默认使用的算法是称 为 internal 的编辑距离算法。 intermal 算法本质上就是 Levenshtein 算法,但根 据 Elasticsearch 索引特征做了一些优化而效率更高,可以通过 string _distance 参 数更改算法。
term 提示器使用的编辑距离可通过 max_ edits 参数设置,默认值为 2
phrase 提示器
terms 会将需要提示的文本拆分成词项,然后对每一个词项做单独的提示, 而 phrase 提示器则会使用整个文本内容做提示。所以在 phrase 提示器的返回结 果中,不会看到一个词项一个词项的提示,而是针对整个短语的提示。但从使用 的角度来看几乎是一样的,例如:
POST /kibana_sample_data_logs/_search
{
  "suggest": {
    "msg-suggest": {
      "text": "firefix with chrime",
      "phrase": {
        "field": "message"
      }
    }
  }
}
}
 
 

 

但不要被 phrase 提示器返回结果欺骗,这个提示器在执行时也会对需要提 示的文本内容做词项分析,然后再通过 NGram 算法计算整个短语的编辑距离。 所以本质上来说,phrase 提示是基于 term 提示器的提示器,同时使用了 Levenshtein 和 NGram 算法。
completion 提示器
completion 提示器一般应用于输人提示和自动补全,也就是在用户输人的同 时给出提示或补全未输入内容。这就要求 completion 提示器必须在用户输人结 束前快速地给出提示,所以这个提示器在性能上做了优化以达到快速检索的目的。 首先要求提示词产生的字段为 completion 类型,这是一种专门为 completion 提示器而设计的字段类型,它会在内存中创建特殊的数据结构以满足快速生成提 示词的要求。例如在示例中创建了 aricles 索引,并向其中添加了 1 份文档 :
PUT articles
{
  "mappings": {
    "properties": {
      "author": {
        "type": "keyword"
      },
      "content": {
        "type": "text"
      },
      "suggestions": {
        "type": "completion"
      }
    }
  }
}
POST articles/_doc/
{
"author":"taylor",
"content":"an introduction of elastic stack and elasticsearch",
"suggestions":{
"input":["elastic stack", "elasticsearch"],
"weight":10
}
}
POST articles/_doc/
{
  "author": "taylor",
  "content": "an introduction of elastic stack and elasticsearch",
  "suggestions": [
    {
      "input": "elasticsearch",
      "weight": 30
    },
    {
      "input": "elastic stack",
      "weight": 1
    }
  ]
}
在向 completion 类型的字段添加内容时可以使用两个参数, input 参数设置 字段实际保存的提示词; weight 参数则设置了这些提示词的权重,权重越高它 在返回的提示词中越靠前。在示例 5-29 中给出了两种设置提示词权重的方式, 第一种是将一组提示词的权重设置为统一值,另一种则是分开设置它们的权重值。需要注意的是, completion 类型字段保存的提示词是不会分析词项的,比如示 例 5-29 中的“ elastic stack ”并不会拆分成两个提示词,而是以整体出现在提示 词列表中。 completion 提示器专门用于输人提示或补全,它根据用户已经输人的内容提 示完整词项, 所以在 completion 提示器中没有 text 参数而是使用 prefix 参数。例
:
POST articles/_search
{
  "_source": "suggest",
  "suggest": {
    "article_suggestion": {
      "prefix": "ela",
      "completion": {
        "field": "suggestions"
      }
    }
  }
}
总结一下, term phrase 提示器主要用于纠错, term 提示器用于对单个词 项的纠错而 phrase 提示器则主要针对短语做纠错。 completion 提示器是专门用 于输人提示和自动补全的提示器,在使用上依赖前缀产生提示并且速度更快。

今天分享到此结束,下一篇分析相关性检索和组合查询,敬请期待!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寅灯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值