Elasticsearch学习笔记(二)Elasticsearch入门

时间:2022-10-03 20:00:44

elasticsearch术语简介

Elasticsearch是一个分布式的文档(document)存储引擎。它可以实时存储并检索复杂数据结构——序列化的JSON文档
当然,我们不仅需要存储数据,还要快速的批量查询。虽然已经有很多NoSQL的解决方案允许我们以文档的形式存储对象,但它们依旧需要考虑如何查询这些数据,以及哪些字段需要被索引以便检索时更加快速。
在Elasticsearch中,每一个字段的数据都是默认被索引的。也就是说,每个字段专门有一个反向索引用于快速检索。而且,与其它数据库不同,它可以在同一个查询中利用所有的这些反向索引,以惊人的速度返回结果。
本文我们将探讨如何使用API来创建、检索、更新和删除文档

索引

在Elasticsearch中存储数据的行为就叫做索引(indexing)
在Elasticsearch中,文档归属于一种类型(type),而这些类型存在于索引(index)中,我们可以画一些简单的对比图来类比传统关系型数据库:

Relational DB -> Databases -> Tables -> Rows      -> Columns
Elasticsearch -> Indices -> Types -> Documents -> Fields

Elasticsearch集群可以包含多个索引(indices)(数据库),每一个索引可以包含多个类型(types)(表),每一个类型包含多个文档(documents)(行),然后每个文档包含多个字段(Fields)(列)。

倒排索引

传统数据库为特定列增加一个索引,例如B-Tree索引来加速检索。Elasticsearch和Lucene使用一种叫做倒排索引(inverted index)的数据结构来达到相同目的。具体倒排索引后续再讲。

文档

什么是文档?简单来讲,一个文档就对应于关系型数据库中的一张表,field对应于数据库中的一个字段。可以参考《Elasticsearch学习笔记(二)Elasticsearch入门》中的对应关系。

  • 文档元数据

一个文档不只有数据。它还包含了元数据(metadata)——关于文档的信息。三个必须的元数据节点是:

节点 说明
_index 文档存储的地方
_type 文档代表的对象的类
_id 文档的唯一标识

来看一下之前存入数据库中的数据,在我们存入数据的基础上,Elasticsearch额外添加了这三个字段:

  1. 索引名字必须是全部小写,不能以下划线开头,不能包含逗号。事实上,我们的数据被存储和索引在分片(shards)中,索引只是一个把一个或多个分片分组在一起的逻辑空间。
  2. _type的名字可以是大写或小写,不能包含下划线或逗号
  3. 当创建一个文档,你可以自定义_id,也可以让Elasticsearch帮你自动生成。
  4. 元数据不仅仅只有3个,还有很多,后续再讲

创建索引

自定义ID

我们创建一个员工目录,我们将进行如下操作:

  • 为每个员工的文档(document)建立索引,每个文档包含了相应员工的所有信息。
  • 每个文档的类型为employee
  • employee类型归属于索引megacorp
  • megacorp索引存储在Elasticsearch集群中。

其实一条curl命令即可完成新建索引的工作:

curl -XPUT 'http://10.104.29.19:9211/megacorp/employee/1' -d '
{
"
first_name" : "John",
"
last_name" : "Smith",
"
age" : 25,
"
about" : "I love to go rock climbing",
"
interests": [ "sports", "music" ]
}'

但是直接这样请求报错:index_not_found_exception,那我们就先创建一个index:
curl -XPUT 'http://10.104.29.19:9211/megacorp/'
结果:

[root@vm-29-19-pro01-bgp config]# curl -XPUT 'http://10.104.29.19:9211/megacorp/'
{"acknowledged":true}

然后在执行上面的创建索引命令即可。
当然除了直接用curl命令,也可以使用head插件:

Elasticsearch学习笔记(二)Elasticsearch入门

让我们来看下生成的索引:
Elasticsearch学习笔记(二)Elasticsearch入门
可以看到elasticsearch自动添加了一个_score,score是评分相关的,是搜索引擎中很重要的一个参数。关于score后续再讲。
还有一个_version,Elasticsearch中每个文档都有版本号,每当文档变化(包括删除)都会使_version增加。后续我们将探讨如何使用_version号确保你程序的一部分不会覆盖掉另一部分所做的更改(多版本并发控制,线程安全)。

让ES自动生成ID

想让ES自动生成ID简单,只需要使用POST即可:
Elasticsearch学习笔记(二)Elasticsearch入门

自动生成的ID有22个字符长,URL-safe, Base64-encoded string universally unique identifiers, 或者叫 UUIDs

查询索引

简单查询

可以使用curl命令直接查询

[root@vm-29-19-pro01-bgp config]# curl -i -XGET '10.104.29.19:9211/megacorp/employee/1'
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Content-Length: 205

{"_index":"megacorp","_type":"employee","_id":"1","_version":1,"found":true,"_source":{"first_name":"John","last_name":"Smith","age":25,"about":"I love to go rock climbing","interests":["sports","music"]}}

也可以使用head插件来查:
Elasticsearch学习笔记(二)Elasticsearch入门

GET请求返回的响应内容包括{"found": true}。这意味着文档已经找到。如果我们请求一个不存在的文档,依旧会得到一个JSON,不过found值变成了false

我们通过HTTP方法GET来检索文档,同样的,我们可以使用DELETE方法删除文档,使用HEAD方法检查某文档是否存在。如果想更新已存在的文档,我们只需再PUT一次。

检查索引是否存在

一种方式是通过上面的查询返回结果中的found字段判断,另一种是通过查看curl的返回头部信息(其实当found=false时就会返回404错误码):

[root@vm-29-19-pro01-bgp whatslive-api]# curl -i -XHEAD http://10.104.29.19:9211/website/blog/124
HTTP/1.1 404 Not Found
es.resource.id: website
es.resource.type: index_expression
es.index: website
Content-Type: text/plain; charset=UTF-8
Content-Length: 0

带参数查询

/megacorp/employee/_search

接下来除非特殊情况,就不截图了,还是直接列出请求和结果比较好。

请求curl -i -XGET '10.104.29.19:9211/megacorp/employee/_search'
结果:

{
"took": 9,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
}
,
"hits": {
"total": 3,
"max_score": 1.0,
"hits": [{
"_index": "megacorp",
"_type": "employee",
"_id": "2",
"_score": 1.0,
"_source": {
"first_name": "Jane",
"last_name": "Smith",
"age": 32,
"about": "I like to collect rock albums",
"interests": ["music"]
}
},
{
"_index": "megacorp",
"_type": "employee",
"_id": "1",
"_score": 1.0,
"_source": {
"first_name": "John",
"last_name": "Smith",
"age": 25,
"about": "I love to go rock climbing",
"interests": ["sports",
"music"]
}
},
{
"_index": "megacorp",
"_type": "employee",
"_id": "3",
"_score": 1.0,
"_source": {
"first_name": "Douglas",
"last_name": "Fir",
"age": 35,
"about": "I like to build cabinets",
"interests": ["forestry"]
}
}]
}
}

查询结果中有集群的信息shards和命中数,也就是查询结果数。

  • 搜索姓氏中包含“Smith”的员工
    curl -XGET '10.104.29.19:9211/megacorp/employee/_search?q=last_name:Smith'

head插件中:

Elasticsearch学习笔记(二)Elasticsearch入门

DSL(Domain Specific Language特定领域语言)

DSL查询(Query DSL),它允许你构建更加复杂、强大的查询。

DSL(Domain Specific Language特定领域语言)以JSON请求体的形式出现。其实上面使用head插件截图的查询方式就是DSL。

复杂查询

复杂查询一般使用DSL来构建

我们查询“last_name=Smith并且30岁以上的员工

{
"query": {
"filtered": {
"filter": {
"range": {
"age": {
"gt": 30
}
}
}
,
"query": {
"match": {
"last_name": "Smith"
}
}
}
}
}
  • 这部分查询属于区间过滤器(range filter),它用于查找所有年龄大于30岁的数据——gt为”greater than”的缩写。
  • 这部分查询与之前的match语句(query)一致。

结果:

{
"took": 8,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
}
,
"hits": {
"total": 1,
"max_score": 0.30685282,
"hits": [{
"_index": "megacorp",
"_type": "employee",
"_id": "2",
"_score": 0.30685282,
"_source": {
"first_name": "Jane",
"last_name": "Smith",
"age": 32,
"about": "I like to collect rock albums",
"interests": ["music"]
}
}]
}
}

只查询一部分字段

  • 只显示数据,不显示其他额外信息megacorp/employee/1/_source
{
"first_name": "John",
"last_name": "Smith",
"age": 25,
"about": "I love to go rock climbing",
"interests": ["sports",
"music"]
}
  • 只查询一部分字段megacorp/employee/1?_source=about,age
{
"_index": "megacorp",
"_type": "employee",
"_id": "1",
"_version": 1,
"found": true,
"_source": {
"age": 25,
"about": "I love to go rock climbing"
}
}

全文搜索

在关系型数据库中想要进行全文搜索比较困难,当然现在MySQL最新版本已经支持全文索引了,不过性能还有待考究。

示例:搜索所有喜欢“rock climbing”的员工

{
"query": {
"match": {
"about": "rock climbing"
}
}
}

查询出两条结果:

{
"took": 25,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
}
,
"hits": {
"total": 2,
"max_score": 0.16273327,
"hits": [{
"_index": "megacorp",
"_type": "employee",
"_id": "1",
"_score": 0.16273327,
"_source": {
"first_name": "John",
"last_name": "Smith",
"age": 25,
"about": "I love to go rock climbing",
"interests": ["sports",
"music"]
}
},
{
"_index": "megacorp",
"_type": "employee",
"_id": "2",
"_score": 0.016878016,
"_source": {
"first_name": "Jane",
"last_name": "Smith",
"age": 32,
"about": "I like to collect rock albums",
"interests": ["music"]
}
}]
}
}

结果分析:
默认情况下,Elasticsearch根据结果相关性评分来对结果集进行排序,所谓的「结果相关性评分」就是文档与查询条件的匹配程度[上面搜索结果中的_score字段]。很显然,排名第一的John Smithabout字段明确的写到“rock climbing”
但是为什么Jane Smith也会出现在结果里呢?原因是“rock”在她的abuot字段中被提及了。因为只有“rock”被提及而“climbing”没有,所以她的_score要低于John。

短语搜索

短语搜索的意思就是要求要搜索的短语完全匹配。上面的查询结果中id=2的员工就没有完全匹配,因为其about字段中并没有包含climbing。要想全部匹配只需要使用match_phrase即可

{
"query": {
"match_phrase": {
"about": "rock climbing"
}
}
}

搜索结果中只剩下了id=1的记录.

高亮我们的搜索

如果直接使用lucene进行高亮搜索的话,还要写一段代码来实现(当然这样做*度更高),使用elasticsearch则只需要简单的命令即可

{
"query": {
"match_phrase": {
"about": "rock climbing"
}
}
,
"highlight": {
"fields": {
"about": {}
}
}
}

在查询的时候添加highlight参数,再返回结果中会增加一个highlight字段,里面的内容是高亮的数据:增加了<em>标识。

{
"took": 145,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
}
,
"hits": {
"total": 1,
"max_score": 0.23013961,
"hits": [{
"_index": "megacorp",
"_type": "employee",
"_id": "1",
"_score": 0.23013961,
"_source": {
"first_name": "John",
"last_name": "Smith",
"age": 25,
"about": "I love to go rock climbing",
"interests": ["sports",
"music"]
}
,
"highlight": {
"about": ["I love to go <em>rock</em> <em>climbing</em>"]
}
}]
}
}

聚合aggregations

先忽略语法,简单看看输出结果,语法后续再讲

查询员工中相同共同点及人数

{
"aggs": {
"all_interests": {
"terms": {
"field": "interests"
}
}
}
}

会在原有结果基础上添加一个aggregations字段:

"aggregations": {
"all_interests": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [{
"key": "music",
"doc_count": 2
},
{
"key": "forestry",
"doc_count": 1
},
{
"key": "sports",
"doc_count": 1
}]
}
}

从上面结果可以看出,喜欢music的有2人,喜欢sports的有1人,喜欢forestry的有1人。

当然我们也可以添加其他查询条件,比如统计”last_name=smith”的员工中相同爱好及人数

{
"query": {
"match": {
"last_name": "smith"
}
}
,
"aggs": {
"all_interests": {
"terms": {
"field": "interests"
}
}
}
}

此时查询结果中就只剩下last_name=smith的数据了。

"aggregations": {
"all_interests": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [{
"key": "music",
"doc_count": 2
},
{
"key": "sports",
"doc_count": 1
}]
}
}

聚合也允许分级汇总。例如,让我们统计每种兴趣下职员的平均年龄:

{
"query": {
"match": {
"last_name": "smith"
}
}
,
"aggs": {
"all_interests": {
"terms": {
"field": "interests"
}
,
"aggs": {
"avg_age": {
"avg": {
"field": "age"
}
}
}
}
}
}

结果:

"aggregations": {
"all_interests": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [{
"key": "music",
"doc_count": 2,
"avg_age": {
"value": 28.5
}
},
{
"key": "sports",
"doc_count": 1,
"avg_age": {
"value": 25
}
}]
}
}

可以看到返回结果中多了“avg_age”平均年龄字段。

请求参数中的avg_age是我们自己定义的

更新文档

记住lucene中文档是不可以被修改的,修改文档的过程其实是一个新建一个文档并且version+1,并将旧的文档标记为删除,之后会被清理掉。

我们把之前创建的文档id=1的年龄改为33岁:
Elasticsearch学习笔记(二)Elasticsearch入门

从上面的结果中可以看得version变为了2,并且created=false,因为之前应存在id=1的文档了

删除文档

Elasticsearch学习笔记(二)Elasticsearch入门

这里我们把id=1的文档删除,注意返回结果中version又加了1,这是因为这里的删除和更新一样,并没有立即删除,而是做了个标记,之后才会真正被删除。