ELK 学习笔记(二)—— 关于 Elasticsearch 检索


概要

基于上一篇 Elasticsearch 录入测试数据后,抱着先能用再说的想法,选择率先学习查询,本文用于整理查询相关语法,用于备忘,也加深记忆

环境

数据准备

如果已经看过上一篇文章《ELK学习笔记(一)——服务部署与Elasticsearch基础知识》,那么补充一条曹天元的《上帝掷骰子吗》即可,如果没添加数据,可将如下命令保存到 load_data.sh 脚本执行录入数据

curl -XPOST http://127.0.0.1:9200/books_idx/_doc/9787111606420?pretty -H 'Content-Type: application/json' -d '{"name": "深入浅出Rust", "author": "范长春", "publish": "机械工业出版社", "isbn": "9787111606420", "pubdate": "2018"}'

curl -XPOST http://127.0.0.1:9200/books_idx/_doc/9787506365437?pretty -H 'Content-Type: application/json' -d '{"name": "活着", "author": "余华", "publish": "作家出版社", "isbn": "9787506365437", "pubdate": "2017"}'

curl -XPOST http://127.0.0.1:9200/books_idx/_doc/9787115373557?pretty -H 'Content-Type: application/json' -d '{"name": "数学之美", "author": "吴军", "publish": "人民邮电出版社", "isbn": "9787115373557", "pubdate": "2014"}'

curl -XPOST http://127.0.0.1:9200/books_idx/_doc/9787121202087?pretty -H 'Content-Type: application/json' -d '{"name": "LaTeX入门", "author": "刘海洋", "publish": "电子工业出版社", "isbn": "9787121202087", "pubdate": "2013"}'

curl -XPOST http://127.0.0.1:9200/books_idx/_doc/9787121354854?pretty -H 'Content-Type: application/json' -d '{"name": "Rust编程之道", "author": "张汉东", "publish": "电子工业出版社", "isbn": "9787121354854", "pubdate": "2019"}'

curl -XPOST http://127.0.0.1:9200/books_idx/_doc/9787550218895?pretty -H 'Content-Type: application/json' -d '{"name": "上帝掷骰子吗", "author": "曹天元", "publish": "北京联合出版公司", "isbn": "9787550218895", "pubdate": "2013"}'

知识点

  • 在上一篇博文中记录过一些查询语句,通过URL参数传递的,另一种更好的方式是通过Body进行传递,功能更丰富且易于编辑
  • Elasticsearch 接口可以使用 Post 查询数据,尽管语义上 Get 更加符合,详见 <一个带请求体的 GET 请求?>

一个带请求体的 GET 请求?

某些特定语言(特别是 JavaScript)的 HTTP 库是不允许 GET 请求带有请求体的。事实上,一些使用者对于 GET 请求可以带请求体感到非常的吃惊。

而事实是这个RFC文档 RFC 7231— 一个专门负责处理 HTTP 语义和内容的文档 — 并没有规定一个带有请求体的 GET 请求应该如何处理!结果是,一些 HTTP 服务器允许这样子,而有一些 — 特别是一些用于缓存和代理的服务器 — 则不允许。

对于一个查询请求,Elasticsearch 的工程师偏向于使用 GET 方式,因为他们觉得它比 POST 能更好的描述信息检索(retrieving information)的行为。然而,因为带请求体的 GET 请求并不被广泛支持,所以 search API同时支持 POST 请求

在指定字段内搜索

查询 name 字段内包含 “Rust” 字符串的记录

curl -X GET "localhost:9200/books_idx/_doc/_search?pretty" -H 'Content-Type: application/json' -d'
{
    "query" : {
        "match" : {
            "name" : "Rust"
        }
    }
}
'

短语搜索

让我们调整一下关键词,之前用 “Rust” 修改为 “Rust编程”

curl -X GET "localhost:9200/books_idx/_doc/_search" -H 'Content-Type: application/json' -d'
{
    "query" : {
        "match" : {
            "name" : "Rust编程"
        }
    }
}
'

查询结果

搜索 “Rust” 一样,有两条记录,包括《深入浅出Rust》这条记录也会搜索出来,查看结果中有一个 _score 字段,表示这条记录与搜索关键词的相关性,《Rust编程之道》的分值为 3.2306948,深入浅出Rust分值为 0.77530915

PS: 为了节省空间,查询结果的 pretty 参数去掉了

{"took":0,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":2,"relation":"eq"},"max_score":3.7996306,"hits":[{"_index":"books_idx","_type":"_doc","_id":"9787121354854","_score":3.7996306,"_source":{"name":"Rust编程之道","author":"张汉东","publish":"电子工业出版社","isbn":"9787121354854","pubdate":"2019"}},{"_index":"books_idx","_type":"_doc","_id":"9787111606420","_score":0.9517491,"_source":{"name":"深入浅出Rust","author":"范长春","publish":"机械工业出版社","isbn":"9787111606420","pubdate":"2018"}}]}}

这里就需要提及一下 Elasticsearch 与传统数据库的区别了

传统数据库搜索记录,结果是二项的,有或者没有,Elasticsearch 则更关心数据与查询字符串的相关性,数值 1 代表中立,数值越大则相关性越高,这在全文监所,搜索建议这样的场景下,是非常方便的,而传统数据库则很难实现类似的需求

精确匹配搜索

如果我想要精确的搜索包含 “Rust编程” 语句的记录呢,使用 match_phrase 就可以了,结果就只会包含《Rust编程之道》

curl -X GET "localhost:9200/books_idx/_doc/_search?pretty" -H 'Content-Type: application/json' -d'
{
    "query" : {
        "match_phrase" : {
            "name" : "Rust编程"
        }
    }
}
'

获取多条记录

curl -X GET "localhost:9200/books_idx/_mget?pretty" -H 'Content-Type: application/json' -d'
{
    "ids": ["9787111606420", "9787121354854"]
}
'

如果指定文档的 id 值不存在,则返回值的 found 字段值为 false

分页

可以说这个功能非常的有用

URL 参数方式

curl -X GET "localhost:9200/books_idx/_search?size=5&pretty"
curl -X GET "localhost:9200/books_idx/_search?size=5&from=5&pretty"
curl -X GET "localhost:9200/books_idx/_search?size=5&from=10&pretty"

Body 方式

curl -X POST "localhost:9200/books_idx/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "from": 0,
  "size": 5
}
'

应该限制分页的深度

在分布式系统中深度分页

理解为什么深度分页是有问题的,我们可以假设在一个有 5 个主分片的索引中搜索。 当我们请求结果的第一页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 50 个结果排序得到全部结果的前 10 个。

现在假设我们请求第 1000 页—结果从 10001 到 10010 。所有都以相同的方式工作除了每个分片不得不产生前10010个结果以外。 然后协调节点对全部 50050 个结果排序最后丢弃掉这些结果中的 50040 个结果。

可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。这就是 web 搜索引擎对任何查询都不要返回超过 1000 个结果的原因。

数据过滤

查询发行年份大于等于 2019 年的书籍

curl -X GET "localhost:9200/books_idx/_doc/_search?pretty " -H 'Content-Type: application/json' -d'
{
    "query" : {
        "bool": {
            "must": {
                "match" : {
                    "name" : "Rust" 
                }
            },
            "filter": {
                "range" : {
                    "pubdate" : { "gte" : 2019 } 
                }
            }
        }
    }
}
'

bool 是一个复合语句(Compound),可以包含叶子子句(Leaf clauses),上边搜索命令表示必须包含 Rust 字符串,filter 语句搭配 range 语句,以及 gte 可以检索 pubdate 字段大于等于 2019 的记录

用于比较的可选参数

关键字 描述
gt Greater than. (大于)
gte Greater than or equal to. (大于等于)
lt Less than. (小于)
lte Less than or equal to. (小于等于)
format Date format used to convert date values in the query. (日期格式化)

更多可选参数参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html

高亮搜索

网页上很多模糊搜索,在结果页上会高亮显示搜索关键字,方便用户判断这条记录和搜索内容相关性

curl -X GET "localhost:9200/books_idx/_doc/_search" -H 'Content-Type: application/json' -d'
{
    "query" : {
        "match_phrase" : {
            "name" : "Rust编程"
        }
    },
    "highlight": {
        "fields" : {
            "name" : {}
        }
    }
}
'

搜索结果

{"took":2,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":1,"relation":"eq"},"max_score":3.7996309,"hits":[{"_index":"books_idx","_type":"_doc","_id":"9787121354854","_score":3.7996309,"_source":{"name":"Rust编程之道","author":"张汉东","publish":"电子工业出版社","isbn":"9787121354854","pubdate":"2019"},"highlight":{"name":["<em>Rust</em><em>编</em><em>程</em>之道"]}}]}}

聚合分析

使用 eggs 可以进行聚合分析统计,如下查询了数据库中每年出版书籍的数量

curl -X GET "localhost:9200/books_idx/_doc/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "aggs": {
    "years_count": {
      "terms": { "field": "pubdate" }
    }
  }
}
'

搜索结果(部分 / 数据部分已隐藏)

"aggregations" : {
  "years_count" : {
    "doc_count_error_upper_bound" : 0,
    "sum_other_doc_count" : 0,
    "buckets" : [
      {
        "key" : "2013",
        "doc_count" : 2
      },
      {
        "key" : "2014",
        "doc_count" : 1
      },
      {
        "key" : "2017",
        "doc_count" : 1
      },
      {
        "key" : "2018",
        "doc_count" : 1
      },
      {
        "key" : "2019",
        "doc_count" : 1
      }
    ]
  }
}

可以说非常的方便,不过这里有个自己埋的问题,我没有自己创建映射,映射是在提交数据时自己生成的,根据年份聚合的时候hi报错

Fielddata is disabled on text fields by default. Set fielddata=true on [publish] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.

我按照提示修改索引参数属性

curl -X PUT "localhost:9200/books_idx/_mapping?pretty" -H 'Content-Type: application/json' -d'
{
  "properties": {
    "pubdate": { 
      "type": "text",
      "fielddata": true
    }
  }
}
'

这样解决了问题,但是如果想要使用 “出版社” 来聚合统计,那么搜索结果就炸裂了,默认的 type: text 是告诉这个字段用于全文检索,所以它会按照规则对内容进行拆分创建倒排索引等,并且不能用于聚合搜索,解决方法是为这个字段创建其它类型,已有的索引类型是不能修改的

遗留的小问题下篇文章 Fix,那么关于搜索,先到这里

参考

  1. Elasticsearch:权威指南