Elasticsearch中提供了一种强大的检索数据方式,这种检索方式称之为Query DSL(Domain Specified Language) 。Query DSL是利用 Rest API传递 JSON格式的请求体(RequestBody)数据与 ES进行交互,这种方式的丰富查询语法让 ES检索变得更强大,更简洁。其基本语法如下:
GET /es_db/_doc/_search {json请求体数据}
#可以简化为下面写法
GET /es_db/_search {json请求体数据}
1. 文档映射Mapping
ElasticSearch中的映射(Mapping)用来定义一个文档,可以定义所包含的字段以及字段的类型、分词器及属性等等。Mapping类似数据库中的schema的定义。ES中Mapping映射可以分为动态映射和静态映射。
- 动态映射
在关系数据库中,需要事先创建数据库,然后在该数据库下创建数据表,并创建表字段、类 型、长度、主键等,最后才能基于表插入数据。而Elasticsearch中不需要定义Mapping映射(即关系型数据库的表、字段等),在文档写入Elasticsearch时,会根据文档字段自动识 别类型,这种机制称之为动态映射。
- 静态映射
静态映射是在Elasticsearch中也可以事先定义好映射,包含文档的各字段类型、分词器 等,这种方式称之为静态映射
注意
:动态映射(Dynamic Mapping)的机制,使得我们无需手动定义Mappings, Elasticsearch会自动根据文档信息,推算出字段的类型。但是有时候会推算的不对,例如地理位置信息。当类型如果设置不对时,会导致一些功能无法正常运行。
PUT /user/_doc/1
{
"name":"jack",
"age":32,
"address":"武汉马房山"
}
上面我们向es中插入了一条文档,我们现在看看es是怎么样帮我们映射mappins的
GET /user/_mapping
- address:帮我们映射成了text类型
- age:帮我们映射成了long类型
- name:帮我映射成了text类型
那么现在我们思考一下,mapping能否修改?这要分两种情况:
- 新增加字段
dynamic设为true时,一旦有新增字段的文档写入,Mapping 也同时被更新。dynamic设为false,Mapping 不会被更新,新增字段的数据 无法被索引,但是信息会出现在_source中。dynamic设置成strict(严格控制策略),文档写入失败,抛出异常。所以dynamic配置不同我们的es也同样会做出不同的处理。
2. 对已有字段,一旦已经有数据写入,就不再支持修改字段定义
Lucene 实现的倒排索引,一旦生成后,就不允许修改。如果希望改变字段类型,可以利用 reindex API,重建索引,reindex的作用是将数据从一个索引复制到另一个索引(一种数据迁移)。
如果我们修改了原有的字段的数据类型,会导致已经索引的数据无法被检索
PUT /user1
{
"mappings": {
"dynamic":"strict",
"properties": {
"name":{
"type": "text"
},
"address":{
"type": "object",
"dynamic":"true"
}
}
}
}
上面我们使用到了静态映射,自己定义了字段的类型,注意dynamic
为strict
,现在我们插入一条不合法数据。
PUT /user1/_doc/1
{
"name": "fox",
"age": 32,
"address": {
"province": "湖南",
"city": "长沙"
}
}
可以发现报了mapping解析异常
现在我们修改dynamic
为true,再次执行插入就插入成功了
PUT /user1/_mapping
{
"dynamic":true
}
对已有字段的mapping修改,具体方法是:
- 如果要推倒现有的映射, 你得重新建立一个静态索引
- 然后把之前索引里的数据导入到新的索引里
- 删除原创建的索引
- 为新索引起个别名, 为原索引名
首先我们新建立一个索引,并设置静态映射
PUT /user3
{
"mappings": {
"properties": {
"name":{
"type": "text"
},
"address":{
"type": "text",
"analyzer": "standard"
}
}
}
}
然后将之前索引的数据迁移到新的索引中
POST _reindex
{
"source": {"index":"user1"},
"dest": {"index": "user3"}
}
删除原来的索引
DELETE user1
给新建立的索引取个别名,名称和原来删除的索引一样
PUT /user3/_alias/user1
//或使用下面语法
PUT /_aliases
{
"actions": [
{
"add": {
"index": "user3",
"alias": "user1"
}
}
]
}
通过这几个步骤就实现了索引的平滑过渡,并且是零停机
- 常见的mapping参数配置
index: 控制当前字段是否能被索引,如果设置为false,es不会为该字段设置倒排索引 |
index_options:有四种不同基本的index options配置,控制倒排索引记录的内容
docs
: 记录doc idfreqs
:记录doc id 和term frequencies(词频)positions
: 记录doc id / term frequencies / term positionffsets
: doc id / term frequencies / term posistion /character offsets|
对于index_options
,text类型默认记录postions,其他默认为 docs。记录内容越多,占用存储空间越大。
如果我们现在想要插入检索空值,注意只有type类型为keyword
才能设置空值
copy_to设置:将字段的数值拷贝到目标字段,满足一些特定的搜索需求。 copy_to的目标字段不出现在_source中。
PUT /address
{
"mappings": {
"properties": {
"province":{
"type": "keyword",
"copy_to": "{full_address}"
},
"city":{
"type": "text",
"copy_to": "{full_address}"
}
}
},
"settings": {
"index":{
"analysis.analyzer.default.type":"ik_max_word"
}
}
}
映射定义了索引中的字段属性。在这个例子中,定义了两个字段 “province” 和 “city”。“province” 字段的类型是 “keyword”,而 “city” 字段的类型是 “text”。此外,对于两个字段,都定义了 “copy_to” 参数,用于将字段值复制到 “full_address” 字段中。这样做的目的是为了能够在搜索时统一匹配这两个字段的内容。例如,当搜索某个地址时,可以在 “full_address” 字段中查找匹配的内容,而不用分别搜索 “province” 和 “city” 字段。设置定义了索引的配置信息。在这个例子中,设置了 “index” 下的 “analysis.analyzer.default.type” 参数为 “ik_max_word”,表示默认的分析器类型为 IK 中文分词器的 “ik_max_word”。这样配置后,当索引文档时,Elasticsearch 会使用 IK 分词器对文本进行分词处理,以便更好地支持中文搜索。
现在我们插入数据
PUT /address/_bulk
{"index":{"_id":"1"}}
{"province":"湖南","city":"长沙"}
{"index":{"_id":"2"}}
{"province":"湖南","city":"常德"}
{"index":{"_id":"3"}}
{"province":"广东","city":"广州"}
{"index":{"_id":"4"}}
{"province":"湖南","city":"邵阳"}
然后我们现在查询湖南常德:
GET /address/_search
{
"query": {
"match": {
"{full_address}": {
"query":"湖南常德",
"operator":"and"
}
}
}
}
上面代码表示查询的字段就是full_address
,然后operator
表示操作类型,可以是and或or,ik分词器会将湖南常德分词为湖南和常德,and就表示既要有湖南,又要有常德。所以结果只有id2这一条。
如果是or就会有多条:
2. Index Template
顾名思义,它就是一种索引建立的模版,Index Templates可以帮助你设定Mappings和Settings,并按照一定的规则,自动匹配到新创建的索引之上。Index template定义在创建新index时可以自动应用的settings和mappings。 Elasticsearch根据与index名称匹配的index模式将模板应用于新索引。这个对于我们想创建的一系列的Index具有同样的settings及mappings。比如我们希望每一天/月的日志的index都具有同样的设置。
- 模版仅在一个索引被新创建时,才会产生作用。修改模版不会影响已创建的索引
- 你可以设定多个索引模版,这些设置会被“merge”在一起
- 你可以指定“order”的数值,控制“merging”的过程
PUT /_template/template_default
{
"index_patterns": ["*"],
"order": 0,
"version": 1,
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
}
}
上面就创建了一个Index Template,匹配所有的索引,然后匹配这个模版的索引的分片和副本数量都是1。当一个索引被新创建时,会应用Elasticsearch 默认的settings 和mappings,应用order数值低的lndex Template 中的设定,应用order高的 Index Template 中的设定,之前的设定会被覆盖,应用创建索引时,用户所指定的Settings和 Mappings,并覆盖之前模版中的设
定。
即settings和mappings设置的优先级是:用户设定>Template中Order高的>Template中Order低的>系统默认的
GET /_template/tem*
使用GET命令可以查看我们设定的模版的信息
3. DSL
ES中提供了一种强大的检索数据方式,这种检索方式称之为Query DSL(Domain Specified Language) , Query DSL是利用Rest API传递JSON格式的请求体(RequestBody)数据与ES进行交互,这种方式的丰富查询语法让ES检索变得更强大,更简洁。它的基本语法如下:
GET/es_db/_doc/_search{json请求体数据}
GET/es_db/_search{json请求体数据}
下面使用无条件查询语句:
GET /address/_search
_search
默认会返回10条数据
PUT /es_db
{
"settings": {
"index":{
"analysis.analyzer.default.type":"ik_max_word"
}
}
}
上面代码中对es_db文档设定了默认的分词器为ik分词器
PUT /es_db/_doc/1
{
"name":"张三",
"sex":1,
"age":25,
"address":"广州天河公园",
"remark":"javadeveloper"
}
PUT /es_db/_doc/2
{
"name":"李四",
"sex":1,
"age":28,
"address":"广州荔湾大厦",
"remark":"javaassistant"
}
PUT /es_db/_doc/3
{
"name":"王五",
"sex":0,
"age":26,
"address":"广州白云山公园",
"remark":"phpdeveloper"
}
PUT /es_db/_doc/4
{
"name":"王五",
"sex":0,
"age":26,
"address":"广州白云山公园",
"remark":"phpdeveloper"
}
PUT /es_db/_doc/5
{
"name":"张龙",
"sex":0,
"age":19,
"address":"长沙麓谷企业广场",
"remark":"javaarchitectassistant"
}
PUT /es_db/_doc/6
{
"name":"赵虎",
"sex":1,
"age":32,
"address":"长沙麓谷兴工国际产业园",
"remark":"javaarchitect"
}
上面向es_db索引库中插入了多条文档,_search
查询默认采用的是分页查询,每页记录数size的默认值为10,如果想显示更多数据,可以指定size。match_all
默认只会返回10条数据。
GET/es_db/_search
#等同于
GET /es_db/_search
{
"query": {
"match_all": {}
}
}
##返回指定条数size,
GET /es_db/_search
{
"query": {
"match_all": {}
},
"size": 20
}
我们现在思考一个问题,size可以无限增加吗?我们测试一下:
GET /es_db/_search
{
"query": {
"match_all": {}
},
"size": 20000
}
可以发现出现了异常,从错误信息可以看出来,查询结果的窗口太大,from + size的结果必须小于或等于10000,而当前查询结果的窗口为20000,我们可以采用scroll api更高效的请求大量数据集,查询结果的窗口的限制可以通过参数index.max_result_window进行设置。
PUT /es_db/_settings
{
"index.max_result_window" :"20000"
}
//修改现有所有的索引,但新增的索引,还是默认的10000
PUT /_all/_settings
{
"index.max_result_window" :"20000"
}
参数index.max_result_window主要用来限制单次查询满足查询条件的结果窗口的大小,窗口大小由from + size共同决定。不能简单理解成查询返回给调用方的数据量。这样做主要是为了限制内存的消耗。
比如:from为1000000,size为10,逻辑意义是从满足条件的数据中取1000000到 (1000000 + 10)的记录。这时ES一定要先将(1000000 + 10)的记录(即 result_window)加载到内存中,再进行分页取值的操作。尽管最后我们只取了10条数据返 回给客户端,但ES进程执行查询操作的过程中确需要将(1000000 + 10)的记录都加载到内存中,可想而知对内存的消耗有多大。这也是ES中不推荐采用(from + size)方式进行深度分页的原因。同理,from为0,size为1000000时,ES进程执行查询操作的过程中确需要将1000000 条记录都加载到内存中再返回给调用方,也会对ES内存造成很大压力。
- 分页查询from
from 关键字: 用来指定起始返回位置,和size关键字连用可实现分页效果
GET /es_db/_search
{
"query": {
"match_all": {
}
},
"size": 5,
"from": 0
}
- 深分页查询Scroll
改动index.max_result_window参数值的大小,只能解决一时的问题,当索引的数据量持续增长时,在查询全量数据时还是会出现问题。而且会增加ES服务器内存大结果集消耗完 的风险。最佳实践还是根据异常提示中的采用scroll api更高效的请求大量数据集。
es的分页查询看这条博客:https://zhuanlan.zhihu.com/p/624297206?utm_id=0
GET /es_db/_search?scroll=1m
{
"query": {
"match_all": {}
},
"size": 2
}
查询命令中新增scroll=1m,说明采用游标查询,保持游标查询窗口一分钟,实际使用中为了减少游标查询的次数,可以将值适当增大,比如设置为1000。
可以发现返回了一条scroll_id,下一次查询就可以从这个游标开始接着查询。
多次根据scroll_id游标查询,直到没有数据返回则结束查询。采用游标查询索引全量数据, 更安全高效,限制了单次对内存的消耗
- 对字段进行排序
这里注意一点使用排序的时候会让评分失效
#根据年龄将降序查询
GET /es_db/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": {
"order": "desc"
}
}
]
}
//在排序的基础上加入了分页查询
GET /es_db/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": {
"order": "desc"
}
}
],
"from": 0,
"size": 2
}
- 返回指定字段_source
GET /es_db/_search
{
"query": {
"match_all": {}
},
"_source": ["name","address"]
}
从结果看一看出返回的文档原生信息中只有address和name两个字段
- Match
match在匹配时会对所查找的关键词进行分词,然后按分词匹配查找,match支持以下参数:
- query : 指定匹配的值
- operator : 匹配条件类型,and : 条件分词后都要匹配,or : 条件分词后有一个匹配即可(默认)
- minmum_should_match : 最低匹配度,即条件在倒排索引中最低的匹配度
GET /es_db/_search
{
"query": {
"match": {
"address": "广州白云山公园"
}
}
}
由结果可以知道对
广州白云山公园
进行了分词,并查询结果为or类型,即只要匹配到一个分词就可以返回结果
GET /es_db/_search
{
"query": {
"match": {
"address": {
"query": "广州白云山公园",
"operator": "AND"
}
}
}
}
可以看到此时只有匹配
广州白云山公园
分词的所有的各个分词才会返回结果。在match中的应用: 当operator参数设置为or时,minnum_should_match参数用来控制匹配的分词的最少数量。
GET /es_db/_search
{
"query": {
"match": {
"address": {
"query": "广州公园",
"minimum_should_match": 2
}
}
}
}
可以发现匹配到2个分词就可以返回结果了
- 短语查询match_phrase
match_phrase查询分析文本并根据分析的文本创建一个短语查询。match_phrase 将会检索关键词分词。match_phrase的分词结果必须在被检索字段的分词中都包含,而且顺序必须相同,而且默认必须都是连续的。
GET /es_db/_search
{
"query": {
"match_phrase": {
"address": "广州白云山"
}
}
}
我们可以看看ik分词器的分词结果为下:
我们看看其它词的分词效果:
POST /_analyze
{
"analyzer": "ik_max_word",
"text": "广州白云山公园"
}
可以看出广州白云山
和广州白云山公园
的分词,后者的分词结果包含前者的分词结果,并且顺序一样。现在我们的需求是,如果顺序不是一样的,多个分词之间还隔有其它分析,我们可以借助slop参数,slop参数告诉match_phrase查询词条能够 相隔多远时仍然将文档视为匹配。
GET /es_db/_search
{
"query": {
"match_phrase": {
"address": {
"query": "广州云山"
}
}
}
}
我们现在设置一下slop
GET /es_db/_search
{
"query": {
"match_phrase": {
"address": {
"query": "广州云山",
"slop": 2
}
}
}
}
先查看广州白云山公园分词结果,可以知道广州和白云不是相邻的词条,中间会隔一个白云山,而match_phrase匹配的是相邻的词条,所以查询广州白云山有结果,但查询广州白云没有结果。
- 多字段查询multi_match
可以根据字段类型,决定是否使用分词查询,得分最高的在前面
GET /es_db/_search
{
"query": {
"multi_match": {
"query": "长沙张龙",
"fields": ["address","name"]
}
}
}
字段类型分词,将查询条件分词之后进行查询,如果该字段不分词就会将查询条件作为整体进行查询。
“query"字段中指定了要搜索的关键词,这里是"长沙张龙”。"fields"字段中指定了要在哪些字段中搜索,这里是"address"和"name"两个字段。这个查询的意思是:在"address"和"name"字段中搜索包含"长沙张龙"这个词语的文档。本质上就是对
长沙张龙
进行分词,然后分词到我们指定的字段中进行匹配。
- query_string
允许我们在单个查询字符串中指定AND | OR | NOT条件,同时也和 multi_match query一样,支持多字段搜索。和match类似,但是match需要指定字段名,query_string是在所有字段中搜索,范围更广泛。
首先我们可以不指定字段查询:
GET /es_db/_search
{
"query": {
"query_string": {
"query": "张三 OR 橘子洲"
}
}
}
我们没有指定字段查询,它会到所有字段进行查询,我们同时还可以指定一个字段或多个字段进行查询:
GET /es_db/_search
{
"query": {
"query_string": {
"default_field": "address",
"query": "白云山 OR 橘子洲"
}
}
}
GET /es_db/_search
{
"query": {
"query_string": {
"default_field": ["name","address"],
"query": "白云山 OR 橘子洲"
}
}
}
- simple_query_string
类似Query String,但是会忽略错误的语法,同时只支持部分查询语法,不支持AND OR NOT,会当作字符串处理。支持部分逻辑:
+ 替代AND | 替代OR
- 替代NOT
GET /es_db/_search
{
"query": {
"simple_query_string": {
"query": "广州+公园",
"fields": ["name","address"]
}
}
}
- 关键词查询Term
Term用来使用关键词查询(精确匹配),还可以用来查询没有被进行分词的数据类型。Term是表达语意的最小单位,搜索和利用统计语言模型进行自然语言处理都需要处理Term。 match在匹配时会对所查找的关键词进行分词,然后按分词匹配查找,而term会直接对关 键词进行查找。一般模糊查找的时候,多用match,而精确查找时可以使用term。
ES中默认使用分词器为标准分词器(StandardAnalyzer),标准分词器对于英文单词分词,对于中文单字分词。
在ES的Mapping Type 中 keyword , date ,integer, long , double , boolean or ip 这些类型不分词,只有text类型分词。
//广州白云在倒排索引中匹配
GET /es_db/_search
{
"query": {
"term": {
"address": {
"value": "广州白云"
}
}
}
}
//精确匹配
GET /es_db/_search
{
"query": {
"term": {
"address.keyword": {
"value": "广州白云山公园"
}
}
}
}
在ES中,Term查询,对输入不做分词。会将输入作为一个整体,在倒排索引中查找准确的 词项,并且使用相关度算分公式为每个包含该词项的文档进行相关度算分。
创建新的索引库:
PUT /product/_bulk
{"index":{"_id":1}}
{"productId":"xxx123","productName":"iPhone"}
{"index":{"_id":2}}
{"productId":"xxx111","productName":"iPad"}
我们进行查询:
GET /product/_search
{
"query": {
"term": {
"productName": {
"value": "iPhone"
}
}
}
}
可以发现查询结果为空,这是为什么呢,我们查询是term,然后关键词是iPhone
,然后到倒排索引中查询,发现没有发现iPhone,这是因为iPhone在倒排索引中会被转化为小写iphone
,所以查询不到。所以对于英文,我们可以设置忽略大小写:
PUT /product
{
"settings": {
"analysis": {
"normalizer": {
"es_normalizer": {
"filter": [
"lowercase",
"asciifolding"
],
"type": "custom"
}
}
}
},
"mappings": {
"properties": {
"productId": {
"type": "text"
},
"productName": {
"type": "keyword",
"normalizer": "es_normalizer",
"index": "true"
}
}
}
}
现在就可以忽略大小写了,我仔细看看上面的查询结果,发现有max_socre
,说明在使用Trem的时候,es还是会对结果进行算分,但有时我们需要的是精确查询,算分可能会影响性能,我们可以通过 Constant Score 将查询转换成一个 Filtering,避免算分,并利用缓存,提高性能。
将Query 转成 Filter,忽略TF-IDF计算,避免相关性算分的开销 Filter可以有效利用缓存,Filter可以有效利用缓存
GET /es_db/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"address.keyword": "广州白云山公园"
}
}
}
}
}
- ES中的结构化搜索
结构化搜索(Structured search)是指对结构化数据的搜索,应用场景:对bool,日期,数字,结构化的文本可以利用term做精确匹配。
应用场景:对bool,日期,数字,结构化的文本可以利用term做精确匹配
GET /es_db/_search
{
"query": {
"term": {
"age": {
"value": "23"
}
}
}
}
- 前缀查询
它会对分词后的term进行前缀搜索。它不会分析要搜索字符串,传入的前缀就是想要查找的前缀,默认状态下,前缀查询不做相关度分数计算,它只是将所有匹配的文档返回,然 后赋予所有相关分数值为1。它的行为更像是一个过滤器而不是查询。两者实际的 别就是过滤器是可以被缓存的,而前缀查询不行。
prefix的原理:需要遍历所有倒排索引,并比较每个term是否已所指定的前缀开头。
GET /es_db/_search
{
"query": {
"prefix": {
"address": {
"value": "广州"
}
}
}
}
- 通配符查询wildcard
通配符查询:工作原理和prefix相同,只不过它不是只比较开头,它能支持更为复杂的匹配模式。
GET /es_db/_search
{
"query": {
"wildcard": {
"address": {
"value": "*白*"
}
}
}
}
- 范围查询
gte 大于等于,lte 小于等于,gt 大于,lt 小于,now 当前时间
POST /es_db/_search
{
"query": {
"range": {
"age": {
"gte": 25,
"lte": 28
}
}
}
}
- 多id查询
ids 关键字 : 值为数组类型,用来根据一组id获取多个对应的文档
GET /es_db/_search
{
"query": {
"ids": {
"values": [1,2]
}
}
}
- 模糊查询
在实际的搜索中,我们有时候会打错字,从而导致搜索不到。在Elasticsearch中,我们可以使用fuzziness属性来进行模糊查询,从而达到搜索有错别字的情形。fuzzy 查询会用到两个很重要的参数,fuzziness,prefix_length:
- fuzziness:表示输入的关键字通过几次操作可以转变成为ES库里面的对应field的字段,操作是指:新增一个字符,删除一个字符,修改一个字符,每 次操作可以记做编辑距离为1,如中文集团到中威集团编辑距离就是1,只需要修改一个字符;如果fuzziness值在这里设置成2,会把编辑距离为2的东东集团也查出来。该参数默认值为0,即不开启模糊查询。
- prefix_length:表示限制输入关键字和ES对应查询field的内容开头的第n个字符必须完全匹配,不允许错别字匹配,如这里等于1,则表示开头的字必须匹配,不匹配则不返回,加大prefix_length的值可以提高效率和准确率,默认值也是0。
GET /es_db/_search
{
"query": {
"fuzzy": {
"address": {
"value": "白运山",
"fuzziness": 1
}
}
}
}
注意: fuzzy 模糊查询最大模糊错误 必须在0-2之间
- 搜索关键词长度为 2,不允许存在模糊
- 搜索关键词长度为3-5,允许1次模糊
- 搜索关键词长度大于5,允许最大2次模糊
- 高亮
highlight 关键字: 可以让符合条件的文档中的关键词高亮。highlight相关属性如下:
pre_tags 前缀标签
post_tags 后缀标签
tags_schema 设置为styled可以使用内置高亮样式
require_field_match 多字段高亮需要设置为false
GET /es_db/_search
{
"query": {
"term": {
"address": {
"value": "广州"
}
}
},
"highlight": {
"post_tags": ["</span>"],
"pre_tags": ["<span style='color:red'>"],
"fields": {
"*":{}
}
}
}