理解Elasticsearch
搜索的原理,是建立反向索引,又叫倒排索引,是根据文章内容中的关键字建立索引,与文章的标题进行对应。比如,古诗索引,让你想与“月光”相关的古诗有哪些,不容易想到。因为我们建立的索引是正向索引,先古诗名字、朝代、作者、古诗内容,但是《静夜思》中“床前明月光”就有“月光”,我们可以将“月光”和“床前明月光”建立索引,但是这首诗中可建立索引的太多了,“故乡”对应“低头思故乡”,我们记不过来,那么可以通过将“月光”、“故乡”跟《静夜思》进行对应,当我们想到了《静夜思》,后面的就都有了。
Elasticsearch是对Lucene的一种封装,能够提供搜索功能。通过api简化索引的创建和访问;实现了分布式搜索引擎。
安装Elasticsearch。
一些概念:索引(index),类似数据库。类型(type)类似数据表。文档(document)类似一条记录。
ES也是master-slave架构,实现了数据的分片和备份。只有简历索引和类型需要经过master,数据的写入有一个简单的routing规则,可以route到集群中的任意节点,所以数据写入压力是分散到整个集群。
ES的典型应用就是ELK日志分析系统,其中 E 就是 Elasticsearch,L 是 Logstash,是一个日志收集系统,K 是 Kibana,是一个数据可视化平台。比如1000台的机器,系统出现故障查看日志,一台台查看非常麻烦。
安装完ES后,需要创建索引,也叫做创建mapping。
创建mapping的语句:index_name/_mapping/type_name,json格式的请求体如下:
{
“settings”:{
"index":{
“number_of_shards”:"4",
"number_of_replicas":"1"
}
},
"mappings":{
"type_name":{
"_ttl":{
"enabled":false
},
"dynamic":false,
"_all":{
"enabled":false
},
"properties":{
"name":"string",
“index”:"not_analyzed"
}
}
}
}
基本概念
-
Cluster集群
一个集群包含多个节点,对外提供服务,每个节点属于哪个集群通过配置文件中的集群名称决定。
-
Node节点
集群中的一个节点,每个节点也有一个名称,默认是随机分配,也可以自己制定,在es集群中通过节点名称进行管理和通信。
-
Index索引
索引是具有相同结构的文档集合,作用相当于mysql中的库。
-
Type类型
一个索引可以对应一个或者多个类型,类型可以当做是索引的逻辑分区,作用相当于mysql中的表。
-
Documnet文档
存在es中的一个JSON格式的字符串,每一个文档有一个文档ID,如果没有自己指定ID,系统会自动生成一个ID,文档的index/type/id必须是唯一的,作用相当于mysql中的行。
-
field字段
一个文档中会包含多个字段,每个字段都对应个字段类型,类似于mysql中的列。
7、shard分片
es中分primary shard主分片和replica shard副本分片。
主分片:当存一个文档的时候会先存储在主分片中,然后复制到不同的副本分片中,默认一个索引会有5个主分片,可以自己指定分片数量,当分片一旦建立,分片数量不能改变。
副本分片:每一个主分片会有零个或者多个副本,副本主要是主分片的复制,通过副本分片可以提供高可用性,当一个主分片挂了,可以从副本分偏重选择一个作为主分片,还可以提高性能,所以主分片不能和副本分片部署在相同的节点上。
8、replica复制
复制是为了防止单点问题,可以做到对故障进行转移,保证系统的高可用。
9、映射
描述数据在每个字段内如何存储,是定义存储和索引的文档类型及字段的过程,索引中的每一个文档都有一个类型,每种类型都有它自己的映射,一个映射定义了文档结构内每个字段的数据类型。
使用GET /index/_mapping/type获取对应的/index/type的映射信息。
类似是获取表结构。如下:
GET /shop_product_es/_mapping/shop_product_es_type
{
"shop_product_es": {
"mappings": {
"shop_product_es_type": {
"dynamic": "false",
"_all": {
"enabled": false
},
"properties": {
"secondCategory": {
"type": "integer"
},
"avgPrice": {
"type": "double"
}
}
}
}
}
}
分词相关
查询文本分词后的效果,使用命令:index_name/type_name/_anaylze,json参数需要填写分词的内容。
Elasticsearch默认的分词模式是将中文每个字都分开,所以搜索的时候会拆开关键词,然后进行索引,查询出来的结果不尽人意。
查询语句
可以使用url查询,也可以使用DSL语句查询。
URL参数搜索
这种方式类似GET请求,将参数拼接到连接上,多个参数用&符号隔开。如:GET index/type/_search?参数
查询所有
命令:GET index/type/_search
返回:
{
"took": 7, //查询耗时,毫秒
"timed_out": false, //是否超时,timeout 不是停止执行查询,它仅仅是告知正在协调的节点返回到目前为止收集的结果并且关闭连接
"_shards": {
"total": 5, //请求的分片数量,索引拆成了5个分片,所以对于搜索请求,会打到所有的primary shard
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 2, //符合条件的总条数,这里查的是所有
"max_score": 1, //匹配分数
"hits": [ //数据
{
"_index": "school",
"_type": "student",
"_id": "2",
"_score": 1,
"_source": {
"name": "houyi",
"age": 23,
"class": 2,
"gender": "男"
}
},
{
"_index": "school",
"_type": "student",
"_id": "1",
"_score": 1,
"_source": {
"name": "吕布",
"age": 21,
"class": 2,
"gender": "男"
}
}
]
}
}
多索引、多type搜索
在URL中指定特殊的索引和类型进行多索引、多type搜索。
-
/_search,在所有的索引中搜索所有的type
-
/school/_search,在school索引中搜索所有的type
-
/school,ad/_search,在school和ad索引中搜索所有的type
-
/school/student/_search,在school索引中搜索student类型
-
/s*,a*/_search,在所有以s和a开头的索引中搜索所有的type
-
/school,ad/student,phone/_search,在school和ad索引上搜索student和phone类型
-
/_all/student,phone/_search,在所有的索引中搜索student和phone类型
按条件查询
查询name是houyi的命令:GET /school/student/_search?q=name:houyi
更多查询参数如下:
参数 |
解释 |
q |
查询字符串,例如:q=syslog |
df |
当查询中没有定义前缀的时候默认使用的字段 |
analyzer |
当分析查询字符串的时候使用的分词器 |
lowercase_expanded_terms |
搜索的时候忽略大小写标志,默认为true |
analyze_wildcard |
通配符或者前缀查询是否被分析,默认为false |
default_operator |
默认多个条件的关系,AND或者OR,默认为OR |
lenient |
如果设置为true,字段类型转换失败的时候将被忽略,默认为false |
explain |
在每个返回结果中,将包含评分机制的解释 |
_source |
是否包含元数据,同时支持_source_include和_source_exclude |
fields |
只返回索引中指定的列,多个列中间用逗号隔开 |
sort |
根据字段名排序,例如fieldName:asc或者fieldName:desc |
track_scores |
评分轨迹,当排序的时候,true表示返回评分的信息 |
timeout |
超时时间设置 |
terminate_after |
在每个分片中查询的最大条数,如果设置,返回结果中会有一个terminated_early字段 |
from |
返回索引匹配结果开始的值,默认为0,用于分页起始 |
size |
搜索结果返回的条数,默认为10 |
search_type |
搜索的类型,可以是dfs_query_then_fetch,query_then_fetch,默认为query_theh_fetch |
todo:补充示例。
查询DSL
https://juejin.im/post/5d2d300b6fb9a07ec56ea9bb
搜索的命令是:index_name/type_name/_search,json参数填写搜索条件。注意index名称不是集群名称。
GET /index_name/type_name/_search
{
"query":{
"bool":{
"must":{
"match":{
"last_name":"smith"
}
}
}
}
}
请求的参数是一个json串,最外层是query节点。
复合查询指的是多条件组合查询。比如一个bool语句,包括must、must_not、should、filter语句。如:
GET /index/type/_search
{
"query": {
"bool": {
"must": [
{"match": {
"name": "phone"
}}
]
, "must_not": [
{"match": {
"color": "red"
}}
]
, "should": [
{"match": {
"price": 5000
}}
]
, "filter": {
"term": {
"label": "phone"
}
}
}
}
}
must:表示文档一定要包含的查询的内容。
must_not:标识文档一定不要包含查询的内容。
should:标识如果文档匹配上可以增加文档相关性得分。
事实上我们可以使用两种结构化语句:结构化查询和结构化过滤
查询某个字段为空的结果
类似SQL中where条件:字段名 is null
GET index/type/_search
{
"query": {
"bool": {
"must_not": {
"exists": {
"field": "字段名"
}
}
}
}
}
反之,查询不为空的语句:
GET index/type/_search
{
"query": {
"bool": {
"must": {
"exists": {
"field": "字段名"
}
}
}
}
}
分页查询
如搜索从第一条数据开始,每页查询25条。
写在url上:index/type/_search?from=0&size=25
也可以写在JSON查询语句中:
{
"query": {
"bool": {
"must_not": {
"exists": {
"field": "secondCategoryName"
}
}
}
},
"from":0,
"size":12
}
分组统计
类似sql的group by语句。
POST shop_product_es/shop_product_es_type/_search
{
"aggs": {
"group_by_firstcategory": {
"terms": {
"field": "firstCategory"
},
"aggs": {
"secondCategory": {
"terms": {
"field": "secondCategory"
},
"aggs": {
"thirdCategory": {
"terms": {
"field": "thirdCategory"
}
}
}
}
}
}
}
}
关键词详解
-
match_all查询
查询简单的匹配所有文档。
GET /ad/phone/_search
{
"query":{
"match_all":{}
}
}
-
match查询
支持全文搜索和精确查询,取决于字段是否支持全文检索。
全文检索:
GET /ad/phone/_search
{
"query":{
"match":{
"ad":"a red"
}
}
}
全文检索会将查询的字符串先进行分词, a red会分成a和red,然后在潮牌索引中进行匹配,所以这条语句会将三条文档都查出来。
精确查询:
GET /ad/phone/_search
{
"query":{
"match":{
"price":"6000"
}
}
}
对于精确值的查询,可以使用filter语句来取代query,因为filter将会被缓存。
operator操作:
match查询还可以接受operator操作符作为输入参数,默认情况下该操作符是or。我们还可以将它修改成and,让所有的指定词项都必须匹配。
GET /ad/phone/_search
{
"query":{
"match":{
"ad":{
"query":"a red",
"operator":"and"
}
}
}
}
精确度匹配:
match查询支持minimum_should_match最下匹配参数,可以指定必须匹配的词项数用来表示一个文档是否相关。我们可以将其设置为某个具体数据(值需要匹配倒排索引的词的数量),更常用的做法是将其设置为一个百分数,因为我们无法控制用户搜索时输入的单词数量。
GET /ad/phone/_search
{
"query":{
"match":{
"ad":{
"query":"a red",
"minimum_should_match":"2"
}
}
}
}
只会返回匹配上a和red两个词的文档。
如果minimum_should_match是1,则只要匹配上其中一个词,文档就会返回。
-
multi_match查询
多字段查询,比如查询color和ad字段包含单词red的文档。
GET /ad/phone/_search
{
"query":{
"multi_match":{
"query":"red",
"fields":["color","ad"]
}
}
}
-
range查询
范围查询,操作符:gt(大于),gte(大于等于),lt(小于),lte(小于等于)
查询价格大运4000小于6000的文档
GET /ad/phone/_search
{
"query":{
"range":{
"price":{
"gt":4000,
"lt":6000
}
}
}
}
-
term查询
精确值查询,查询price字段等于6000的文档
GET /ad/phone/_search
{
"query":{
"term":{
"price":{
"value":"6000"
}
}
}
}
查询name字段等于iphone 8的文档
GET /ad/phone/_search
{
"query":{
"term":{
"name":{
"value":"iphone 8"
}
}
}
}
结果是没有查到相关文档。
原因是term查询会去倒排索引中寻找确切的term,他并不会走分词器,只会去匹配倒排索引,而name字段的type类型为text,会进行分词,将iphone 8分为iphone和8,我们使用term查询iphone 8时倒排索引中并没有iphone 8,所以没有查询到匹配的文档。
term和match查询的区别
term查询时,不会分词,直接匹配倒排索引;
match查询时会进行分词,查询iphone 8时,会先分词成iphone和8,然后去匹配倒排索引,所以结果会将iphone 8和xiaomi 8两个文档都会查询出来。
还有一点需要注意,因为term查询不会走分词器,但是会去匹配倒排索引,所以查询的结果就跟分词器如何分词有关系。比如新增一个/ad/phone类型下的文档,name字段赋值为Oppo,这时使用term查询Oppo不会查询出文档,这是因为es默认用的standard分词器,他在分词后会将单词转换成小写输出,所以使用Oppo查不出来文档,使用小写的oppo可以查询出来。
GET /ad/phone/_search
{
"query": {
"term": {
"name": {
"value": "Oppo" //改成oppo可以查出新添加的文档
}
}
}
}
所有像term这类查询结果跟选择的分词器有关系,了解选择的分词器分词方式有助于我们编写查询语句。
-
terms查询
terms查询与term查询一样,允许你指定多值进行匹配,如果这个字段包含了制定之中的任何一个值,那么这个文档满足条件。
GET /ad/phone/_search
{
"query":{
"terms":{
"ad":["red","blue"]
}
}
}
-
exists查询和missing查询
用于查找指定字段有值(exists)或无值(missing)的文档。
指定name字段有值
GET /ad/phone/_search
{
"query":{
"bool":{
"filter":{
"exists":{
"field":"name"
}
}
}
}
}
指定name字段无值:
GET /ad/phone/_search
{
"query":{
"bool":{
"filter":{
"missing":{
"filed":"name"
}
}
}
}
}
-
match_phrase查询
短语查询,精确匹配,查询a red会匹配ad字段包含a red短语的,而不会进行分词查询,也不会查询出包含“a其他词red”这样的文档。
GET /ad/phone/_search
{
"query":{
"match_phrase":{
"ad":"a red"
}
}
}
-
scroll查询
类似于分页查询,不支持跳页查询,只能一页一页往下查询,scroll查询不是针对实时用户请求,而是针对处理大量数据。
POST /ad/phone/_search?scroll=1m
{
"query": {
"match_all": {}
},
"size": 1,
"from": 0
}
返回值包含一个
"_scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAAQFlV6T3VqY2NaVDBLRG5uZXdiZ0hFYUEAAAAAAAAAERZVek91amNjWlQwS0RubmV3YmdIRWFBAAAAAAAAABIWVXpPdWpjY1pUMEtEbm5ld2JnSEVhQQAAAAAAAAATFlV6T3VqY2NaVDBLRG5uZXdiZ0hFYUEAAAAAAAAAFBZVek91amNjWlQwS0RubmV3YmdIRWFB"
下次查询的时候使用
_scroll_id
就可以查询下一页的文档
POST /_search/scroll
{
"scroll" : "1m",
"scroll_id" : "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAAYFlV6T3VqY2NaVDBLRG5uZXdiZ0hFYUEAAAAAAAAAGRZVek91amNjWlQwS0RubmV3YmdIRWFBAAAAAAAAABYWVXpPdWpjY1pUMEtEbm5ld2JnSEVhQQAAAAAAAAAXFlV6T3VqY2NaVDBLRG5uZXdiZ0hFYUEAAAAAAAAAFRZVek91amNjWlQwS0RubmV3YmdIRWFB"
}
-
multi_get查询
允许基于索引,类型(可选)和id(以及可能的路由)获取多个文档,如果某个文档获取失败则将错误信息包含在响应中。
GET /ad/phone/_mget
{
"ids": ["1","8"]
}
-
bulk批量操作
bulk批量操作可以在单次API调用中实现多个文档的create、index、update和delete。这可以大大提高索引速度。
bulk请求体如下:
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
...
action必须是以下几种
行为 |
解释 |
create |
当文档不存在时创建 |
index |
创建新文档或替换已有文档 |
update |
局部更新文档 |
delete |
删除一个文档 |
在索引、创建、更新或删除时必须指定文档的_index、_type、_id这些元数据。
例如:
PUT _bulk
{ "create" : { "_index" : "ad", "_type" : "phone", "_id" : "6" }}
{ "doc" : {"name" : "bulk"}}
{ "index" : { "_index" : "ad", "_type" : "phone", "_id" : "6" }}
{ "doc" : {"name" : "bulk"}}
{ "delete":{ "_index" : "ad", "_type" : "phone", "_id" : "1"}}
{ "update":{ "_index" : "ad", "_type" : "phone", "_id" : "3"}}
{ "doc" : {"name" : "huawei p20"}}
返回:
{
"took": 137,
"errors": true, //如果任意一个文档出错,这里返回true,
"items": [ //items数组,它罗列了每一个请求的结果,结果的顺序与我们请求的顺序相同
{
//create这个文档已经存在,所以异常
"create": {
"_index": "ad",
"_type": "phone",
"_id": "6",
"status": 409,
"error": {
"type": "version_conflict_engine_exception",
"reason": "[phone][6]: version conflict, document already exists (current version [2])",
"index_uuid": "9F5FHqgISYOra_P09HReVQ",
"shard": "2",
"index": "ad"
}
}
},
{
//index这个文档已经存在,会覆盖
"index": {
"_index": "ad",
"_type": "phone",
"_id": "6",
"_version": 3,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 6,
"_primary_term": 5,
"status": 200
}
},
{
//删除
"delete": {
"_index": "ad",
"_type": "phone",
"_id": "1",
"_version": 1,
"result": "not_found",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 4,
"_primary_term": 5,
"status": 404
}
},
{
//修改
"update": {
"_index": "ad",
"_type": "phone",
"_id": "3",
"_version": 3,
"result": "noop",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"status": 200
}
}
]
}
bulk请求不是原子操作,不能实现事务。每个请求操作是分开的,所以每个请求的成功与否不干扰其他操作。
-
fuzzy查询
模糊查询,fuzzy查询会计算与关键词的拼写相似程度。
GET /ad/phone/_search
{
"query":{
"fuzzy":{
"color":{
"value":"res",
"fuzziness":2,
"prefix_length":1
}
}
}
}
参数设置:
fuzziness:最大编辑距离,默认为AUTO;
prefix_length:不会“模糊化”的初始字符数。这有助于减少必须检查的数据数量,默认为0;
max_expansiions:fuzzy查询将扩展到的最大术语数。默认为50,设置小,有助于优化查询;
transpositions:是否支持模糊专制(ab->ba),默认是false。
-
wildcard查询,模糊搜索
使用wildcard能够实现不分词模糊搜索,即搜索“小米”,只会搜索关键字中连续包含“小米”的记录,前提是要搜索的字段不要进行分词(ES2中设置成类型"type": "string"且"index": "not_analyzed",ES6中设置为keyword类型)。
支持通配符的模糊查询,?匹配单个字符,*匹配任何字符。
为了防止极其缓慢通配符查询,*或?通配符项不应该放在通配符的开始。
GET /es_index/es_index_type/_search
{
"query":{
"wildcard":{
"color":"r?d"
}
}
}
color是其中的一个字段,查询字符r和d之间只有一个字符的。