我们在入门(1)中详细介绍了ES的安装、基本概念和一些基本的REST Api
请求,在这篇入门(2)中,我们继续介绍ES的高级查询功能。
为了说明ES强大的搜索功能,我们还以上篇文章中的customer索引为例,但对其中的文档字段进行了一定的补充,补充后一个文档的内容大致如下所示:
{
"firstname": "zhang",
"lastname": "san",
"age": 29,
"gender": "F",
"address": "某某区某某街某某小区某号楼某单元某零几",
"email": "[email protected]",
"city": "北京"
}
在ES中有两种方式可以进行高级查询,一种是通过在REST request URI
中传递参数,另一种是通过REST request body
来传递查询参数。因为第二种方式更富有表现力、不受URI长度的限制并且使用了更加易读的JSON格式来表示,因此实际应用中多数以request body
的形式来查询,我们这里也不再对第一种方式进行过多的介绍。
如果想通过
REST API
来使用查询功能,则必须要在URI
的最后添加_search
关键字,并且不需要再指定类型(type)。
match_all
首先来看下怎么查询customer索引下的所有文档:
curl -XGET 'localhost:9200/customer/_search?pretty' -H 'Content-Type: application/json' -d'
{
"query": { "match_all": {} }
}
'
query
关键字说明了我们希望进行的是查询操作,它的值就是要查询的条件。上面的请求返回的数据我们摘录一部分内容,如下所示:
{
"took" : 4,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"failed" : 0
},
"hits" : {
"total" : 3,
"max_score" : 1.0,
"hits" : []
}
}
其中,每个字段的含义是:
- took -- ES执行查询操作用时,单位为milliseconds
- timed_out -- 查询是否超时
- _shards -- 查询了几个分片,每个分配查询的结果是什么
- hits -- 查询的结果
- hits.total -- 匹配我们搜索条件的文档数量
- hits.hits -- 查询结果的数组,默认是取前10个文档
- hits.sort -- 对结果进行排序的字段,如果没有排序这个字段为空
- hits._score 和 max_score -- 文档与指定查询的相关性,越高说明相关性越大
sort
如果我们想对结果进行排序,可以使用sort
关键字:
curl -XGET 'localhost:9200/customer/_search?pretty' -H 'Content-Type: application/json' -d'
{
"query": { "match_all": {} },
"sort": [
{ "account_number": "asc" }
]
}
'
因为可以根据多个字段进行排序,因此sort
关键字对应的是一个数组,允许我们指定多个排序策略。
size、from
我们在介绍返回结果字段含义的时候说过,默认是取前10个文档,如果想修改这个值可以指定size
参数,比如只取一个文档:
curl -XGET 'localhost:9200/customer/_search?pretty' -H 'Content-Type: application/json' -d'
{
"query": { "match_all": {} },
"size": 1
}
'
默认情况下,上面的请求返回的是从第0个文档算起的,同样,我们也可以修改这个值,比如取第11个到第20个文档,就要指定另外一个值from
:
curl -XGET 'localhost:9200/customer/_search?pretty' -H 'Content-Type: application/json' -d'
{
"query": { "match_all": {} },
"from": 10,
"size": 10
}
'
有一点需要强调的是,from
这个参数是从0开始的,它表明了希望从哪里开始截取数据,size
参数指定了要返回多少文档,因此,通过利用from
和size
,我们可以方便的实现分页操作。
_source
跟其他的数据库操作一样,有的时候我们并不需要返回一个文档的所有字段,返回部分字段,可以极大的减少数据量的传输,比如,对于customer中的文档,我们只希望返回fristname
和lastname
两个字段,这个时候就可以使用_source
关键字了:
curl -XGET 'localhost:9200/customer/_search?pretty' -H 'Content-Type: application/json' -d'
{
"query": { "match_all": {} },
"_source": ["firstname", "lastname"]
}
'
目前为止,我们已经介绍了关于查询的几个关键字,在继续往下进行之前,先来总结一下:
-
query
:指定查询的条件 -
match_all
:匹配所有文档 -
sort
:对结果进行排序 -
from
:查询结果起始位置,从0开始 -
size
:查询结果大小 -
_source
:指定返回的字段
match
match_all
查询可以匹配所有的文档,但大部分时候这个查询是没啥意义的,如果只需要查询所有文档,就没有非用ES不可的理由了。当我们需要根据某个字段进行查找,这个时候match
就派上用场了,先来看一下用法:
curl -XGET 'localhost:9200/customer/_search?pretty' -H 'Content-Type: application/json' -d'
{
"query": { "match": { "age": 20 } }
}
'
上面的请求会查询年龄为20的文档。除了数字类型,match
还可以接受文本和日期类型的查询条件,看下一个例子:
curl -XGET 'localhost:9200/customer/_search?pretty' -H 'Content-Type: application/json' -d'
{
"query": { "match": { "address": "朝阳区和平街" } }
}
'
在这个例子中,我们的查询条件是一个字符串,ES会返回给我们什么样的数据呢?在其他数据库系统中,只有address字段的内容跟查询条件完全一致才能被匹配,而在ES中则大不同。
ES首先会对朝阳区和平街
进行分析,假如分析的结果是将这个字符串拆分成了朝阳区
和和平街
,然后,根据倒排索引,会找到所有包含朝阳区
或者包含和平街
的文档。
如何对查询文本进行分析,并不是一成不变的,我们可以指定分析器,来告诉ES怎么对文本进行拆分,上面的这种拆分需要用到一个中文的分词器叫做
ik_smart
,ES默认是不支持中文分词的,需要安装第三方的工具。
这里之所以说是或者包含
,是因为我们没有指定match
的行为,通过operator
关键字,我们可以指定是or
还是and
。比如我们希望找到既包含朝阳区
又包含和平街
的文档,就可以改写上面的语句如下:
curl -XGET 'localhost:9200/customer/_search?pretty' -H 'Content-Type: application/json' -d'
{
"query": {
"match" : {
"address" : {
"query" : "朝阳区和平街",
"operator" : "and"
}
}
}
}
'
bool
之所以在使用match
的时候可以指定operator
,是因为match
其实是一种布尔类型的查询。在ES中,我们也可以单独的使用这种类型的查询,布尔查询的关键字是bool
,bool
查询将许多个小的查询利用一定的布尔逻辑综合成一个较大的查询。比如,上面查询既包含朝阳区
又包含和平街
的语句就可以利用bool
改写成下面这样:
curl -XGET 'localhost:9200/customer/_search?pretty' -H 'Content-Type: application/json' -d'
{
"query": {
"bool": {
"must": [
{ "match": { "address": "朝阳区" } },
{ "match": { "address": "和平街" } }
]
}
}
}
'
改成bool
查询后,逻辑变得更清晰了。同时,我们看到了一个新的关键字must
,在bool
查询中,并不是用or
、and
来声明逻辑关系的,must
表明所有的查询条件都返回True的时候才能匹配,作为对比,用should
来表明or
的逻辑关系。
should
的行为并不像我们通常理解的那样:只要有一个条件返回True就匹配成功。事实上,我们在使用bool
的时候,还会涉及到另外一个参数:minimum_should_match
,如果不指定这个参数,则默认当所有条件都返回False的时候,也会匹配成功。
除此之外,在一个bool
查询中还可以同时指定must
、should
、must_not
。还是以上面的例子说明,除了希望address
至少包含朝阳区
或和平街
外,还希望age
等于20:
curl -XGET 'localhost:9200/customer/_search?pretty' -H 'Content-Type: application/json' -d'
{
"query": {
"bool": {
"should": [
{ "match": { "address": "朝阳区" } },
{ "match": { "address": "和平街" } }
],
"must": {"match": {"age": 20}},
"minimum_should_match": 1
}
}
}
'
term
除了match
查询外,还有一个term
查询,term
查询跟match
查询唯一不同的一点是:term
查询不会对查询文本进行分析,而是直接去倒排索引中去看都有哪些文档包含要查询的条件;match
是首先对要查询的文本进行分析,划分为多个子文本,然后将一个大查询拆分成多个小查询,最后进行汇总处理。因此,如果match
查询不能对查询文本进行再划分,那么它与term
查询的效果是一样的。
还是上面的查询,将match
换成term
:
curl -XGET 'localhost:9200/customer/_search?pretty' -H 'Content-Type: application/json' -d'
{
"query": { "term": { "address": "朝阳区和平街" } }
}
'
这个查询的含义就变成了查询在address中包含朝阳区和平街
这个字符串的文档。
query与filter
最后,我们来说下filter
,对于很多初学者来说,有的时候很难区分query
查询和filter
查询,尤其是遇到两种方法都能正确得到数据的情况下,更是难决断。所以,在这节我们来看下两者的区别。
首先来说query
查询,对于query
语句,它要回答的是:某个文档跟查询语句的匹配程度如何?除了决定一个文档是否匹配查询语句外,还要计算一个_score
值,这个值就代表了文档的相关性,值越大说明相关性越高。但是,对于这个值,我们大多数时候并不关心,如果不再计算这个值势必会在一定程度上提高ES查询的效率,因此,引入了filter
查询。
对于filter
语句,只考虑某个文档是否匹配,也就是Yes or No的问题,并不计算相关性,这种情况下可以类比于一般数据库的select语句。filter
另外一个跟query
不同的地方是,ES会对经常使用的filter进行缓存,以此来提供查询效率,而query
不会使用缓存。
如此一来,我们的结论就是,在构造查询语句的时候,能使用filter
的地方绝不使用query
。
我们在上面的内容中介绍了bool
查询,它除了支持must
、should
、must_not
外,还可以支持filter
语句,例如:
curl -XGET 'localhost:9200/customer/_search?pretty' -H 'Content-Type: application/json' -d'
{
"query": {
"bool": {
"must": { "match_all": {} },
"filter": {
"range": {
"age": {"gte": 20, "lte": 30}
}
}
}
}
}
'
上面的语句查询了所有年龄在20到30之间的人,它包含一个match_all
语句和一个range
语句,其中range
是放在filter
中的,当然如果不使用filter
也是可以的,像下面这样:
curl -XGET 'localhost:9200/customer/_search?pretty' -H 'Content-Type: application/json' -d'
{
"query": {
"bool": {
"must": {
"range": {
"age": {"gte": 20, "lte": 30}
}
}
}
}
}
'
不过,还是那句话,能使用filter
的地方绝不使用query
。
到这里,查询语句的介绍就到这里,在入门(3)中,会继续介绍ES中的几种特殊的数据类型:列表类型和嵌套类型。