## 什么是Elasticsearch
你知道的,为了搜索
首先,它是一个全文搜索和分析引擎,有以下三个特点。
- 高度可扩展的
- 开源的
- 基于Lucene的
ELK的版本问题
现在所说的ELK其实就是Elastic Stack(官方称呼),包含ElasticSearch, logstash(服务器端的数据处理管道), kibana(ElasticSearch可视化)和beats(日志收集工具)。
这个网站可以查询所有有关ELK的历史版本https://www.elastic.co/cn/downloads/past-releases,当使用elk成员配合使用时,起码要保证大版本的统一,不能说使用的ElasticSearch是7.x版本的,而kibana却是6.x版本的,会产生版本不兼容问题。
环境搭建
安装 Elasticsearch
在安装地址中选择你想要安装的版本,解压之后即可使用。
如果是集群形式的,可在 …\config\elasticsearch.yml中配置一些你的集群信息:
cluster.name: my-learn # 集群名称
# path.data: /path/to/data # ES数据存储路径
# path.logs: /path/to/logs # ES日志存储路径
node.name: node-1 # 当前节点的名称
# network.host: 0.0.0.0 # 配置当前结点绑定的IP地址
http.port: 9200 # 设置对外服务的HTTP端口,默认为9200
运行Elasticsearch
Linux
./bin/elasticsearch
Windows
...\bin\elasticsearch.bat
当启动日志中出现starting …,标志后可以访问 http://localhost:9200/?pretty 。
{
"name" : "LAPTOP-E0AJGK48",
"cluster_name" : "my-application",
"cluster_uuid" : "y5dYZ3tdQCqOuWeeSkkOhA",
"version" : {
"number" : "7.17.0",
"build_flavor" : "default",
"build_type" : "zip",
"build_hash" : "bee86328705acaa9a6daede7140defd4d9ec56bd",
"build_date" : "2022-01-28T08:36:04.875279988Z",
"build_snapshot" : false,
"lucene_version" : "8.11.1",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
当出现如上信息后则代表启动成功。
注意:在Elasticsearch v5.0版本之后需要JVAV8的环境。
安装Web界面(可视化处理)
为了方便我们使用,有两个可视化界面可供选择,elasticsearch-head和kibana。
安装elasticsearch-head插件
安装es-head插件需要有的依赖
- node
- 检查是否有node,在控制台输入node -v,如果有版本号弹出就是有了。
- 没有的可以去下载,下载地址。
- 再次测试是否安装成功。
- grunt
- 在控制台中执行 npm install -g grunt -cli 等待安装成功即可。
- 控制台输入:grunt -version 命令检查验证安装是否成功。
下载es-head
1.github下载:https://github.com/mobz/elasticsearch-head
2.百度云下载:https://pan.baidu.com/s/119GZF4E1NeEgctRxRp88Ng 提取码:oz9q
下载好后进行解压。
进入文件夹修改 Gruntfile.js
找到connect中添加hostname: ‘*’, 如下所示。
connect: {
server: {
options: {
hostname: '*',
port: 9100,
base: '.',
keepalive: true
}
}
}
接着在该文件下打开控制台,输入 npm install,等待安装完成。
接着输入 npm run start 启动es-head插件。
验证:http://localhost:9100/,如果发现无连接可能是
- es 并未启动,启动即可。
- es启动
- 解决方法,在es文件中 …\config\elasticsearch.yml中添加以下配置:
http.cors.enabled: true # 配置跨域资源共享
http.cors.allow-origin: "*"
安装kibana
在下载地址中选择和你下载的es同一个版本的kibana。
解压后在…/config/kibana.yml文件中找到 elasticsearch.hosts: [“http://localhost:9200”],释放。
linux运行
./bin/kibana
windows
bin/kibana.bat
# 或是双击kibana.bat文件
验证,访问http://localhost:5601/app/dev_tools#/console出现如下界面,就代表成功了:
基本概念
了解基本概念有助于我们的进一步学习。
集群(Cluster)
集群(cluster)是一组具有相同cluster.name
的节点集合,他们协同工作,共享数据并提供故障转移和扩展功能,当然一个节点也可以组成一个集群。
集群由唯一名称标识,默认情况下为“elasticsearch”。
但建议修改集群名称,使用默认名称可能会导致节点加入错误集群造成不必要的麻烦。
集群名称修改在 elasticsearch.yam 文件中,由 cluster.name 字段控制。
cluster.name: my-learn # 集群名称
要确保不同的环境中使用不同的集群名称,否则最终会导致节点加入错误的集群。
集群的健康值
有三种状态,可以在es-head中查看。
也可以在kibana的控制台中使用 GET /_cluster/health
命令来查看状态。
{
"cluster_name" : "my-learn",
"status" : "green",
"timed_out" : false,
"number_of_nodes" : 1,
"number_of_data_nodes" : 1,
"active_primary_shards" : 8,
"active_shards" : 8,
"relocating_shards" : 0,
"initializing_shards" : 0,
"unassigned_shards" : 0,
"delayed_unassigned_shards" : 0,
"number_of_pending_tasks" : 0,
"number_of_in_flight_fetch" : 0,
"task_max_waiting_in_queue_millis" : 0,
"active_shards_percent_as_number" : 100.0
}
绿色 - 一切都很好(集群功能齐全)。
黄色 - 所有数据均可用,但尚未分配一些副本(集群功能齐全)。
红色 - 某些数据由于某种原因不可用(集群部分功能)。
注意:当群集为红色时,它将继续提供来自可用分片的搜索请求,但您可能需要尽快修复它,因为存在未分配的分片。
节点(Node)
一个正在运行的ES实例就是一个节点,节点存储数据并参与集群的索引和搜索功能。
和集群一样,节点也是由名称进行标识的,默认的名称是随机分配的,这对管理项目以及查看日志非常的不利,所以尽量确定节点的名称。
建议给每个节点设置一个有意义的、清楚的、描述性的名字,同样也是在 elasticsearch.yml 中配置
node.name: elasticsearch_learn
可以将节点配置为按集群名称加入特定集群。默认情况下,每个节点都设置为加入一个名为 elasticsearch 的集群,这意味着如果您在网络上启动了许多节点并且假设它们可以相互发现 - 它们将自动形成并加入一个名为 elasticsearch 的集群。
索引(Index)
索引是具有某些类似特征的文档集合。例如,您可以拥有店铺数据的索引,商品的一个索引以及订单数据的一个索引。
类似于关系型数据库中的表。
索引由名称标识(必须全部小写),此名称用于在对其中的文档执行索引,搜索,更新和删除操作时引用索引。
类型(Type)
在v6.x及以上版本中已弃用。
是索引的逻辑类别/分区,允许您在同一索引中存储不同类型的文档,例如,一种类型用于用户,另一种类型用于博客帖子。
文档(Document)
文档是可以建立索引的基本信息单元。例如,您可以为单个客户提供文档,为单个产品提供一个文档,为单个订单提供一个文档。该文档以JSON(JavaScript Object Notation)格式表示。
类似于关系型数据库中的一行记录。
文档的元数据
es每一个文档,除了保存我们写入进行的文档原始数据外,也有文档自己的元数据,这些元数据,用于标识文档的相关信息。
下面是一个普通的es文档:
{
"_index" : "test_logs2",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"uid" : 1,
"username" : "test"
}
}
从上面的文档中,我们可以看文档的元数据字段如下:
_index:文档所在索引名称
_source:原始json数据
_type:文档所属类型,es7.0以后只有为 _doc
_version:文档版本,如果对文档进行修改,则该字段会增加
_score:相关性打分
id:文档唯一id
分片(Shards)
索引存储量可能超过单个节点的硬件限制的数据量。例如,占用1TB磁盘空间的十亿个文档的单个索引可能不适合单个节点的磁盘,或者可能太慢而无法单独从单个节点提供搜索请求。
为了解决这个问题,Elasticsearch提供将索引细分为多个分布在不同的片区,这种功能被称之为分片,也是数据的最小单元块。
类似于关系型数据库的表分区概念。
设置分片的目的及原因主要是:
- 它允许您水平拆分/缩放内容量
- 它允许您跨分片(可能在多个节点上)分布和并行化操作,从而提高性能/吞吐量
分片的分布方式以及如何将其文档聚合回搜索请求的机制完全由 Elasticsearch 管理,对用户而言是透明的。也就是说用户在使用时是感觉不到分片的。
在可能随时发生故障的网络/云环境中,分片非常有用,建议使用故障转移机制,以防分片/节点以某种方式脱机或因任何原因消失。为此,Elasticsearch 允许您将索引的分片的一个或多个制作成所谓的副本分片或简称副本。
副本(Replicasedit)
副本,是对分片的复制。目的是为了当分片/节点发生故障时提供高可用性,它允许您扩展搜索量/吞吐量,因为可以在所有副本上并行执行搜索。
类似于关系型数据库中,担心一个表中数据量过大,新建了一个表。
总而言之,每个索引可以拆分为多个分片。索引也可以复制为零次(表示没有副本)或更多次。复制之后,每个索引将具有主分片(从原始分片复制而来的)和复制分片(主分片的副本)。
可以在创建索引时为每个索引定义分片和副本的数量。创建索引后,您也可以随时动态更改副本数。您可以使用_shrink 和 _splitAPI 更改现有索引的分片数,但这不是一项轻松的任务,所以预先计划正确数量的分片是最佳方法。
相同分片的副本不会放在同一节点。
副本是乘法,越多越浪费,但也越保险。分片是除法,分片越多,单分片数据就越少也越分散。
elasticsearch和关系型数据库的对比
关系型数据库 -> Tables(表) -> Rows(行) -> Columns(列)。
Elasticsearch -> Indeces(索引) -> Documents(文档) -> Fields(属性)。
由于elasticsearch中索引的特殊性,我们有必要将它做一个区分。
「索引」含义的区分
索引(名词) 如上文所述,一个索引(index)就像是传统关系数据库中的数据库,它是相关文档存储的地方,index的复数是 indices 或 indexes。
索引(动词) 「索引一个文档」表示把一个文档存储到索引(名词)里,以便它可以被检索或者查询。这很像SQL中的INSERT关键字,差别是,如果文档已经存在,新的文档将覆盖旧的文档。
倒排索引 传统数据库为特定列增加一个索引,例如B-Tree索引来加速检索。Elasticsearch和Lucene使用一种叫做倒排索引(inverted index)的数据结构来达到相同目的。
交互
elasticsearch交互方式主要取决于你是否使用Java。
Java API
如果你正在使用 Java,在代码中你可以使用 Elasticsearch 内置的两个客户端:
-
节点客户端(Node client)
节点客户端作为一个非数据节点加入到本地集群中。换句话说,它本身不保存任何数据,但是它知道数据在集群中的哪个节点中,并且可以把请求转发到正确的节点。
-
传输客户端(Transport client)
轻量级的传输客户端可以将请求发送到远程集群。它本身不加入集群,但是它可以将请求转发到集群中的一个节点上。
两个 Java 客户端都是通过 9300 端口并使用 Elasticsearch 的原生 传输 协议和集群交互。集群中的节点通过端口 9300 彼此通信。如果这个端口没有打开,节点将无法形成一个集群。
更多的信息可以在Elasticserch客户端找到。
RESTful API with JSON over HTTP
除java外的语言可以使用RESTful API 通过端口 9200 和 Elasticsearch 进行通信,你可以用你最喜爱的 web 客户端访问 Elasticsearch 。事实上,正如你所看到的,你甚至可以使用 curl 命令来和 Elasticsearch 交互。
curlcurl -X<VERB> '<PROTOCOL>://<HOST>:<PORT>/<PATH>?<QUERY_STRING>' -d '<BODY>'
curl的缩写格式
一个完整的curl请求为
curl -XGET 'localhost:9200/_count?pretty' -d '
{
"query": {
"match_all": {}
}
}'
curl的缩写格式就是省略请求中所有相同的部分,例如主机名、端口号以及 curl 命令本身。下面是他的缩写格式。
GET /_count
{
"query": {
"match_all": {}
}
}
CRUD
可以在kibana控制台进行命令的执行操作。
插入文档信息
插入单条文档信息
PUT /learn/user/1
{
"first_name" : "三",
"last_name" : "张",
"age" : 25,
"about" : "法外狂徒",
"interests": [ "偷盗", "抢劫","嘿嘿" ]
}
有关路径 /learn/user/1 包含了三部分信息:
- learn 索引名称
- user 类型名称
- 1 ID
当出现如下信息时代表插入成功
{
"_index" : "learn",
"_type" : "user",
"_id" : "1",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 3,
"_primary_term" : 1
}
在这里我们指定了文档的id,如果不指定id而是使用如下命令,那么es将会自动给其分配一个id。这个id是URL-safe、 基于 Base64 编码且长度为20个字符的 GUID 字符串,产生冲突的几率几乎为0。
POST /learn/user/
{
"first_name" : "三",
"last_name" : "张",
"age" : 25,
"about" : "法外狂徒",
"interests": [ "偷盗", "抢劫","嘿嘿" ]
}
我又插入两条信息
PUT /learn/user/2
{
"first_name" : "xk",
"last_name" : "蔡",
"age" : 99,
"about" : "基尼太美",
"interests": [ "唱", "跳","rap","篮球" ]
}
PUT /learn/user/3
{
"first_name" : "倍",
"last_name" : "安",
"age" : 10,
"about" : "散弹枪狂热者",
"interests": [ "盗窃","篮球" ]
}
我们下面将讲到更改文档,如果使用PUT插入文档的话很容易造成文档的覆盖,这显然不是我们想要看到的。我们想要的是当文档不存在时再新增文档。有如下方法
# 新增文档但不更改,1
PUT /learn/user/5/_create
{
"first_name" : "先森",
"last_name" : "双口",
"age" : 10,
"about" : "散弹批发商",
"interests": [ "嘿嘿","游戏" ]
}
# 新增文档但不更改,2
PUT /learn/user/6?op_type=create
{
"first_name" : "先森",
"last_name" : "双口",
"age" : 10,
"about" : "散弹批发商",
"interests": [ "嘿嘿","游戏" ]
}
如果文档已近存在返回409状态码,如果成功添加返回201状态码。
更改文档信息
使用PUT请求
当一个文档已经存在是,再次使用PUT插入文档的方法就可以做到文档的更改。
PUT /learn/user/3
{
"first_name" : "小日本鬼子",
"last_name" : "安",
"age" : 10,
"about" : "散弹枪狂热者",
"interests": [ "盗窃","篮球" ]
}
更改完成后version会改变。
在内部,Elasticsearch 已将旧文档标记为已删除,并增加一个全新的文档。 尽管你不能再对旧版本的文档进行访问,但它并不会立即消失。当继续索引更多的数据,Elasticsearch 会在后台清理这些已删除文档。
文档的部分更改
我们对文档进行更改时使用的是 update
API。
文档是不可变的:他们不能被修改,只能被替换,所以我们对文档进行更改时其实时拿了个新的文档将老的文档给替换掉了
update
请求最简单的一种形式是接收文档的一部分作为 doc
的参数, 它只是与现有的文档进行合并。对象被合并到一起,覆盖现有的字段,增加新的字段。
我们将user中的安倍的first_name
改为鬼子就可以进行如下操作。
POST /learn/user/3/_update
{
"doc" : {
"first_name" : "鬼子"
}
}
结果
{
"_index" : "learn",
"_type" : "user",
"_id" : "3",
"_version" : 4,
"_seq_no" : 21,
"_primary_term" : 2,
"found" : true,
"_source" : {
"first_name" : "鬼子",
"last_name" : "安",
"age" : 10,
"about" : "散弹枪狂热者",
"interests" : [
"盗窃",
"篮球"
]
}
}
我们也可以使用脚本对部分文档进行更新。
脚本可以在 update
API中用来改变 _source
的字段内容, 它在更新脚本中称为 ctx._source
。
下面使用脚本对安鬼子的年龄进行修改。
POST /learn/user/3/_update
{
"script" : "ctx._source.age=0"
}
结果
{
"_index" : "learn",
"_type" : "user",
"_id" : "3",
"_version" : 5,
"_seq_no" : 22,
"_primary_term" : 2,
"found" : true,
"_source" : {
"first_name" : "鬼子",
"last_name" : "安",
"age" : 0,
"about" : "散弹枪狂热者",
"interests" : [
"盗窃",
"篮球"
]
}
}
删除文档信息
想要删除文档信息其实很简单,只是使用 DELETE
方法即可。如下
查询文档信息
检索单条信息
使用HTTP GET 请求,并指定索引、类型和ID进行查询。如下
GET /learn/user/1
_primary_term_seq_no{
"_index" : "learn",
"_type" : "user",
"_id" : "1",
"_version" : 2,
"_seq_no" : 3,
"_primary_term" : 1,
"found" : true,
"_source" : {
"first_name" : "三",
"last_name" : "张",
"age" : 25,
"about" : "法外狂徒",
"interests" : [
"偷盗",
"抢劫",
"嘿嘿"
]
}
}
几个字段说明
- _version: 文档版本号,文档进行更新就会自增1
- _seq_no:序列号
- _primary_term:每当主分片发生重新分配时,比如重启,主分片的选举等,_primary_term会递增1。
_primary_term主要是用来恢复数据时处理当多个文档的_seq_no一样时的冲突,比如当一个shard宕机了,raplica需要用到最新的数据,就会根据_primary_term和_seq_no这两个值来拿到最新的document
有的喜欢在请求的查询串上加上pretty参数,如 GET /learn/user/1?pretty,这样只是为了让响应的信息更加可读。
当我们检索一个不存在的文档时,也会给我们返回一个结构体,但是该结构体中的found字段变为了false,同时HTTP响应也会变为 404 Not Found,而不是200 OK,如下所示
{ "_index" : "learn", "_type" : "user", "_id" : "5", "found" : false }
只检索_source字段
只需在请求后加上 _source 限制符即可。
GET /learn/user/1/_source
如我们只想要_source 字段中的某些字段,我们可以将请求改为
GET /learn/user/1/_source/?_source=age,about
检测文档是否存在
从上面我们已经知道,当查询不存在的文档时,HTTP响应会变为404。我们就可以根据这个返回的状态码来确定文档是否存在。
检索多条信息
使用mget
API来完成。
mget
API 要求有一个 docs
数组作为参数,每个元素包含需要检索文档的元数据, 包括 _index
、 _type
和 _id
。同时如果想要指定字段也可以使用source
来指定。
如我们取出learn索引下user的id为1和2的文档,可以写为
GET /_mget
{
"docs" : [
{
"_index" : "learn",
"_type" : "user",
"_id" : 1
},
{
"_index" : "learn",
"_type" : "user",
"_id" : 2,
"_source": "about"
}
]
}
结果为
{
"docs" : [
{
"_index" : "learn",
"_type" : "user",
"_id" : "1",
"_version" : 2,
"_seq_no" : 3,
"_primary_term" : 1,
"found" : true,
"_source" : {
"first_name" : "三",
"last_name" : "张",
"age" : 25,
"about" : "法外狂徒",
"interests" : [
"偷盗",
"抢劫",
"嘿嘿"
]
}
},
{
"_index" : "learn",
"_type" : "user",
"_id" : "2",
"_version" : 1,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
"about" : "基尼太美"
}
}
]
}
也可以将索引和类型放在请求URL上,做到简写的目的,如下
GET /learn/user/_mget
{
"docs": [
{
"_id": 1
},
{
"_id": 2,
"_source": "about"
}
]
}
结果同上面相同。
当然会出现多个文档中有部分文档不存在的情况,这时文档正常返回,状态码仍为200,但是查询不到的文档的found
字段显示为false
。
匹配查询 match,match_all
全搜索
最简单的搜索全部用户的请求:
GET /learn/user/_search
{
"query": {
"match_all": {}
}
}
结果
{
"took" : 7,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "learn",
"_type" : "user",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"first_name" : "xk",
"last_name" : "蔡",
"age" : 99,
"about" : "基尼太美",
"interests" : [
"唱",
"跳",
"rap",
"篮球"
]
}
},
{
"_index" : "learn",
"_type" : "user",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"first_name" : "倍",
"last_name" : "安",
"age" : 10,
"about" : "散弹枪狂热者",
"interests" : [
"盗窃",
"篮球"
]
}
},
{
"_index" : "learn",
"_type" : "user",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"first_name" : "三",
"last_name" : "张",
"age" : 25,
"about" : "法外狂徒",
"interests" : [
"偷盗",
"抢劫",
"嘿嘿"
]
}
}
]
}
}
第一部分为:分片副本信息,第二部分 hits 包装的为查询的数据集。
参数解析:
-
took
该命令请求花费了多长时间,单位:毫秒。 -
timed_out
搜索是否超时。 -
shards
搜索分片信息。 -
total
搜索分片总数。 -
successful
搜索成功的分片数量。 -
skipped
没有搜索的分片,跳过的分片。 -
failed
搜索失败的分片数量。 -
hits
搜索结果集。项目中,我们需要的一切数据都是从hits中获取。 -
total
返回多少条数据。 -
max_score
返回结果中,最大的匹配度分值。 -
hits
默认查询前十条数据,根据分值降序排序。 -
_index
索引库名称。 -
_type
类型名称。 -
_id
该条数据的id。 -
_score
关键字与该条数据的匹配度分值。匹配度越高分值就越高。 -
_source
索引库中类型,返回结果字段,不指定的话,默认全部显示出来。
根据字段信息搜索
查询爱好中包含篮球的用户
GET /learn/user/_search
{
"query": {
"match": {
"interests.keyword": "篮球"
}
}
}
结果
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 0.646255,
"hits" : [
{
"_index" : "learn",
"_type" : "user",
"_id" : "2",
"_score" : 0.646255,
"_source" : {
"first_name" : "xk",
"last_name" : "蔡",
"age" : 99,
"about" : "基尼太美",
"interests" : [
"唱",
"跳",
"rap",
"篮球"
]
}
},
{
"_index" : "learn",
"_type" : "user",
"_id" : "3",
"_score" : 0.646255,
"_source" : {
"first_name" : "倍",
"last_name" : "安",
"age" : 10,
"about" : "散弹枪狂热者",
"interests" : [
"盗窃",
"篮球"
]
}
}
]
}
}
也可以不使用match匹配查询,而是进行查询字符串 (query-string) 搜索,通过一个URL参数来传递查询信息给搜索接口,如下
GET /learn/user/_search?q=interests:篮球
过滤查询 Filter
假如我们同样要找喜欢打篮球的,但是年龄大于50岁的。我们就可以利用 filter 将查询语句进行一些改变。如下
GET /learn/user/_search
{
"query": {
"bool": {
"filter": {
"range": {
"age": {
"gt": 50
}
}
},
"must": {
"match": {
"interests.keyword": "篮球"
}
}
}
}
}
这部分是一个 range 过滤器 , 它能找到年龄大于 30 的文档,其中 gt 表示_大于_(great than)。
结果为
{
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.646255,
"hits" : [
{
"_index" : "learn",
"_type" : "user",
"_id" : "2",
"_score" : 0.646255,
"_source" : {
"first_name" : "xk",
"last_name" : "蔡",
"age" : 99,
"about" : "基尼太美",
"interests" : [
"唱",
"跳",
"rap",
"篮球"
]
}
}
]
}
}
短语搜索
我们再为learn索引添一条user文档。
PUT /learn/user/4
{
"first_name" : "先森",
"last_name" : "双口",
"age" : 10,
"about" : "散弹批发商",
"interests": [ "嘿嘿","游戏" ]
}
再进行匹配查询。查询条件为关于中包含散弹枪的。根据上面所说匹配查询,查询应该这么写
GET /learn/user/_search
{
"query" : {
"match" : {
"about" : "散弹枪"
}
}
}
结果
_score{
"took" : 444,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 2.3385136,
"hits" : [
{
"_index" : "learn",
"_type" : "user",
"_id" : "3",
"_score" : 2.3385136,
"_source" : {
"first_name" : "倍",
"last_name" : "安",
"age" : 10,
"about" : "散弹枪狂热者",
"interests" : [
"盗窃",
"篮球"
]
}
},
{
"_index" : "learn",
"_type" : "user",
"_id" : "4",
"_score" : 1.357075,
"_source" : {
"first_name" : "先森",
"last_name" : "双口",
"age" : 10,
"about" : "散弹批发商",
"interests" : [
"嘿嘿",
"游戏"
]
}
}
]
}
}
这里查询到两个文档,一条是安的,一条是双口的。
但是我们发现两个文档中 _score 字段数字是不同的,_score 是Elasticsearch给查询出来的文档进行的相关性分析得出来的分数,这个分数越高说明匹配程度越高。我们搜索的是“散弹枪”,安的about中就含有“散弹枪”,但双口的about中包含的是“散弹”得分就比安低了。
我们有时候单纯的就只是想要搜索“散弹枪”这个词而不是“散”、“弹”、“枪”,这时候就要用到短语搜索了。
对 match 查询稍作调整,使用一个叫做 match_phrase 的查询,如下
GET /learn/user/_search
{
"query" : {
"match_phrase" : {
"about" : "散弹枪"
}
}
}
结果
{
"took" : 19,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 2.3385136,
"hits" : [
{
"_index" : "learn",
"_type" : "user",
"_id" : "3",
"_score" : 2.3385136,
"_source" : {
"first_name" : "倍",
"last_name" : "安",
"age" : 10,
"about" : "散弹枪狂热者",
"interests" : [
"盗窃",
"篮球"
]
}
}
]
}
}
就只查询到含有“散弹枪”的文档了。
高亮搜索
许多应用都倾向于在每个搜索结果中 高亮 部分文本片段,以便让用户知道为何该文档符合查询条件。我们可以使用highlight 参数来实现,如下
GET /learn/user/_search
{
"query" : {
"match" : {
"about" : "散弹枪"
}
},
"highlight": {
"fields" : {
"about" : {}
}
}
}
结果
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 2.3385136,
"hits" : [
{
"_index" : "learn",
"_type" : "user",
"_id" : "3",
"_score" : 2.3385136,
"_source" : {
"first_name" : "倍",
"last_name" : "安",
"age" : 10,
"about" : "散弹枪狂热者",
"interests" : [
"盗窃",
"篮球"
]
},
"highlight" : {
"about" : [
"<em>散</em><em>弹</em><em>枪</em>狂热者"
]
}
},
{
"_index" : "learn",
"_type" : "user",
"_id" : "4",
"_score" : 1.357075,
"_source" : {
"first_name" : "先森",
"last_name" : "双口",
"age" : 10,
"about" : "散弹批发商",
"interests" : [
"嘿嘿",
"游戏"
]
},
"highlight" : {
"about" : [
"<em>散</em><em>弹</em>批发商"
]
}
}
]
}
}
几种重要的搜素
空搜索
简单的使用 _search ,不提供索引和类型,返回所有文档。
GET /_search
在搜索路径中使用通配符
在 gb
和 us
索引中搜索所有的文档
/gb,us/_search
在任何以 g
或者 u
开头的索引中搜索所有的类型
/g*,u*/_search
检索所有索引下的user
和tweet
类型的所有文档
/_all/user,tweet/_search
分页
主要是使用如下两个关键字。
size
显示应该返回的结果数量,默认是 10
from
显示应该跳过的初始结果数量,默认是 0
类似于关系型数据库中的Limit。
我们规定每页两条数据,则一到三页的数据可写为
GET /learn/user/_search?size=2
GET /learn/user/_search?size=2&from=2
GET /learn/user/_search?size=2&from=4
也可以写为请求体格式。如
GET /learn/user/_search
{
"size": 2,
"from": 2
}
执行过后我们发现他会出现不是按照id分页的,这就涉及到了排序问题。后面会讲到。
exists查询和missing查询
类似于SQL中的IS_NULL(missing)和NOT IS_NULL(exists)。
这种查询经常用于某个字段有值的情况和某个字段缺值的情况。
{
"exists": {
"field": "title"
}
}
区间查询
range
查询找出那些落在指定区间内的数字或者时间:
{
"range": {
"age": {
"gte": 20,
"lt": 30
}
}
}
被允许的操作符如下:
-
gt
大于
-
gte
大于等于
-
lt
小于
-
lte
小于等于
如果要实现等于或不等于的操作需要将两个操作符联合使用。
代价较小的批量操作
使用bulk
API 允许在单个步骤中进行多次 create
、 index
、 update
或 delete
请求。
bulk
与其他的请求体格式稍有不同,如下所示:
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
...
这种格式类似一个有效的单行 JSON 文档 流 ,它通过换行符(\n
)连接到一起。注意两个要点:
- 每行一定要以换行符(
\n
)结尾, 包括最后一行 。这些换行符被用作一个标记,可以有效分隔行。 - 这些行不能包含未转义的换行符,因为他们将会对解析造成干扰。这意味着这个 JSON 不 能使用 pretty 参数打印。
其中参数的含义
action
必须是以下选项之一:
-
**
create
**如果文档不存在,那么就创建它。 -
**
index
**创建一个新文档或者替换一个现有的文档。 -
**
update
**部分更新一个文档。 -
**
delete
**删除一个文档。
metadata
应该指定被索引、创建、更新或者删除的文档的 _index
、 _type
和 _id
。
request body
行由文档的 _source
本身组成—文档包含的字段和值。它是 index
和 create
操作所必需的,这是有道理的:必须为请求提供结构体。只有删除操作不需要 request body
行。
现在我们就进行实践,在learn索引的user类型下
- 删除id为5的文档
- 创建一个新的文档,id为1
- 创建一个新的文档,不指定id
- 更改id为2的的兴趣爱好为“唱跳”、“RAP”、“你干嘛~”
POST /_bulk
{"delete": {"_index": "learn","_type": "user","_id": "5"}}
{"create":{"_index": "learn","_type": "user","_id": "1"}}
{ "first_name" : "四", "last_name" : "李","age" : 25,"about" : "法外狂徒的弟弟","interests": [ "偷盗", "抢劫","嘿嘿" ]}
{"create":{"_index": "learn","_type": "user"}}
{ "first_name" : "四", "last_name" : "李","age" : 25,"about" : "法外狂徒的弟弟","interests": [ "偷盗", "抢劫","嘿嘿" ]}
{"update": { "_index": "learn", "_type": "user", "_id": "2"}}
{"doc": {"interests": ["唱跳","RAP","你干嘛~"]}}
结果
{
"took" : 7,
"errors" : true,
"items" : [
{
"delete" : {
"_index" : "learn",
"_type" : "user",
"_id" : "5",
"_version" : 1,
"result" : "not_found",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 53,
"_primary_term" : 2,
"status" : 404
}
},
{
"create" : {
"_index" : "learn",
"_type" : "user",
"_id" : "1",
"status" : 409,
"error" : {
"type" : "version_conflict_engine_exception",
"reason" : "[1]: version conflict, document already exists (current version [2])",
"index_uuid" : "AyqqcNFHSgyE2tgDurtkLA",
"shard" : "0",
"index" : "learn"
}
}
},
{
"create" : {
"_index" : "learn",
"_type" : "user",
"_id" : "TBfyY4IB_561K7UQqFc7",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 54,
"_primary_term" : 2,
"status" : 201
}
},
{
"update" : {
"_index" : "learn",
"_type" : "user",
"_id" : "2",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 55,
"_primary_term" : 2,
"status" : 200
}
}
]
}
注意:1.delete动作后不要有请求体 2.最后一行也需要换行符,不要忘记
参数介绍:
-
errors:改errors为true并不表示所有操作失败,而是一个或者多个请求失败。
-
error:解释为什么请求失败的错误信息。
这意味着bulk
请求不是原子的: 不能用它来实现事务控制。每个请求是单独处理的,因此一个请求的成功或失败不会影响其他的请求。
注意:在使用bulk API进行批量的插入操作时,当指定的文档已经存在,那么则会返回一个错误,这个错误不是致命的
我们使用批量操作原因一般是有着大量文档要进行操作,那么操作的文档量多大是太大了呢?这是官方给出的大小。
一个好的批量大小在开始处理后所占用的物理大小约为 5-15 MB。
排序与相关性问题
一般来说,默认的排序是按照相关性打分来排的。但是也会出现随机排序的情况,比如我们进行空搜索时就会出现相关性评分都是零的情况,这时候就会出现随机排序的情况。
在elasticsearch中我们可以使用sort
指定字段来进行排序。
比如根据_id
进行的排序如下
GET /learn/user/_search
{
"sort": [{"_id": {"order": "asc"}}]
}
GET /learn/user/_search
{
"sort": [{"_id": {"order": "desc"}}]
}
Query-string 搜索 也支持自定义排序,可以写为
GET /learn/user/_search?sort=_id:desc
多级排序
满足第一个排序条件后再进行第二个排序条件的排序,一次类推
我们先根据相关性得分排序然后根据id排序,就可以写为
GET /learn/user/_search
{
"sort": [
{"_score": {"order": "desc"}},
{"_id": {"order": "desc"}}
]
}
当然所给出的例子是无效的。因为没有条件时,相关性得分为null
多值字段排序
什么时多值字段?就类似于时间2022-08-04
之类的数与数的组合。
可以将多值字段减为单值,这可以通过使用
min
、max
、avg
或是sum
排序模式 。例如你可以按照每个date
字段中的最早日期进行排序,通过以下方法:
"sort": {
"dates": {
"order": "asc",
"mode": "min"
}
}
管理索引
创建索引
再创建文档时,我们就已经通过索引文档创建过一个文档learn,这个索引采用的是默认的配置,新的字段通过动态映射的方式被添加到类型映射。
如果你想禁止自动创建索引,你 可以通过在 config/elasticsearch.yml
的每个节点下添加下面的配置:
action.auto_create_index: false
现在我们想要确保这个索引有数量适中的主分片,并且在我们索引任何数据 之前 ,分析器和映射已经被建立好。为了达到这个目的,我们需要手动创建索引。
创建索引的格式如下
PUT /my_index
{
"settings": { ... any settings ... },
"mappings": {
"type_one": { ... any mappings ... },
"type_two": { ... any mappings ... },
...
}
}
再v7.0以后的版本不在支持指定类型,所有的类型都为doc。所以格式变为
PUT /my_index
{
"settings": { ... any settings ... },
"mappings": {
... any mappings ...
}
}
我们创建一个名为test索引。
PUT /test
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 2
},
"mappings": {
"properties":{
"id":{"type": "long"},
"name":{"type": "text"},
"age":{"type": "integer"},
"sex":{"type": "text"}
}
}
}
参数介绍:
- number_of_shards:主分片数
- number_of_replicas:每个主分片的副本数
- properties:属性设置
属性可有
- 字符串类型:string;
- 数值类型:字节(byte)、2字节(short)、4字节(integer)、8字节(long)、float、double;
- 布尔类型:boolean,值是true或false;
- 时间/日期类型:date,用于存储日期和时间;
- 二进制类型:binary;
- IP地址类型:ip,以字符串形式存储IPv4地址;
- 特殊数据类型:token_count,用于存储索引的字数信息
注意再v5.0版本后string
属性由text
和keyword
代替。
删除索引
用以下的请求来 删除索引:
DELETE /my_index
你也可以这样删除多个索引:
DELETE /index_one,index_two
DELETE /index_*
你甚至可以这样删除 全部 索引:
DELETE /_all
DELETE /*
处理冲突
我们一般会将数据存放在关系型数据库中同时复制到es上使其可以被搜索到,但是当两个人同时进行操作时,可能会造成业务变更的丢失,即使这个几率是很小的,我们仍然要予以重视。
一个官网上的例子
有一天,管理层决定做一次促销。突然地,我们一秒要卖好几个商品。 假设有两个 web 程序并行运行,每一个都同时处理所有商品的销售,如图 Figure 7, “Consequence of no concurrency control” 所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ebWC55dZ-1659605536079)(https://www.elastic.co/guide/cn/elasticsearch/guide/current/images/elas_0301.png)]
Figure 7. Consequence of no concurrency control
web_1
对stock_count
所做的更改已经丢失,因为web_2
不知道它的stock_count
的拷贝已经过期。 结果我们会认为有超过商品的实际数量的库存,因为卖给顾客的库存商品并不存在,我们将让他们非常失望。
常用到的处理方式有两种
悲观并发控制
这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。 一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程能够对这行数据进行修改。
乐观并发控制
Elasticsearch 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。 然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。 例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。
我们可以利用 _version
号来确保 应用中相互冲突的变更不会导致数据丢失。我们通过指定想要修改文档的 version
号来达到这个目的。 如果该版本不是当前版本号,我们的请求将会失败。
一般来说应该是这样的,新版本已近不在支持version,但是我们可以使用_seq_no和 _primary_term来对文档进行版本控制。
首先我们要先知道两点:
- 删除或更新文档会使_seq_no自增+1。
- _primary_term主分片的编号不会轻易改变。
在进行乐观并发控制操作前,我们先来介绍下_seq_no 和 version的区别。
- _seq_no 是针对索引的,而version是针对文档的。
- _seq_no 当索引中有文档发生变更(更新、删除、增加)都会引起 _seq_no 加一
- version只有当前文档发生变更才会加一。
我们首先创建一个文档
PUT /learn/user/5/_create
{
"first_name" : "先森",
"last_name" : "双口",
"age" : 10,
"about" : "散弹批发商",
"interests": [ "嘿嘿","游戏" ]
}
查询结果
GET /learn/user/5
{
"_index" : "learn",
"_type" : "user",
"_id" : "5",
"_version" : 7,
"_seq_no" : 19,
"_primary_term" : 2,
"found" : true,
"_source" : {
"first_name" : "先森",
"last_name" : "双口",
"age" : 10,
"about" : "散弹批发商",
"interests" : [
"嘿嘿",
"游戏"
]
}
}
然后我们在根据上面所查到的序列号和主分片号对文档进行操作。
PUT /learn/user/5?if_seq_no=19&if_primary_term=2
{
"first_name" : "先森",
"last_name" : "双口",
"age" : 10,
"about" : "散弹批发商",
"interests": [ "嘿嘿","游戏" ]
}
当序列号会主分片号不对时会返回409状态码。
交互所有代码
# 插入信息
PUT /learn/user/1
{
"first_name" : "三",
"last_name" : "张",
"age" : 25,
"about" : "法外狂徒",
"interests": [ "偷盗", "抢劫","嘿嘿" ]
}
PUT /learn/user/2
{
"first_name" : "xk",
"last_name" : "蔡",
"age" : 99,
"about" : "基尼太美",
"interests": [ "唱", "跳","rap","篮球" ]
}
PUT /learn/user/3
{
"first_name" : "倍",
"last_name" : "安",
"age" : 10,
"about" : "散弹枪狂热者",
"interests": [ "盗窃","篮球" ]
}
# 对文档信息进行更改
PUT /learn/user/3
{
"first_name" : "小日本鬼子",
"last_name" : "安",
"age" : 10,
"about" : "散弹枪狂热者",
"interests": [ "盗窃","篮球" ]
}
# 不指定ID,随机生成
POST /learn/user/
{
"first_name" : "三",
"last_name" : "张",
"age" : 25,
"about" : "法外狂徒",
"interests": [ "偷盗", "抢劫","嘿嘿" ]
}
# 新增文档但不更改,1
PUT /learn/user/5/_create
{
"first_name" : "先森",
"last_name" : "双口",
"age" : 10,
"about" : "散弹批发商",
"interests": [ "嘿嘿","游戏" ]
}
# 新增文档但不更改,2
PUT /learn/user/6?op_type=create
{
"first_name" : "先森",
"last_name" : "双口",
"age" : 10,
"about" : "散弹批发商",
"interests": [ "嘿嘿","游戏" ]
}
# 删除
DELETE /learn/user/5
DELETE /learn/user/6
PUT /learn/user/4
{
"first_name" : "先森",
"last_name" : "双口",
"age" : 10,
"about" : "散弹批发商",
"interests": [ "嘿嘿","游戏" ]
}
# 获取指定id的信息
GET /learn/user/3
# 查询不存在的文档
GET /learn/user/5
# 获取指定id的信息
GET /learn/user/1?pretty
# 获取全部搜索信息
GET /learn/user/_search
{
"query": {
"match_all": {}
}
}
# 搜索兴趣为篮球的文档
GET /learn/user/_search
{
"query": {
"match": {
"interests.keyword": "篮球"
}
}
}
GET /learn/user/_search?q=interests:篮球
# 使用filter进行请求体搜索
GET /learn/user/_search
{
"query": {
"bool": {
"filter": {
"range": {
"age": {
"gt": 50
}
}
},
"must": {
"match": {
"interests.keyword": "篮球"
}
}
}
}
}
GET /learn/user/_search
{
"query" : {
"match" : {
"about" : "散弹枪"
}
}
}
# 短语搜索
GET /learn/user/_search
{
"query" : {
"match_phrase" : {
"about" : "散弹枪"
}
}
}
# 高亮搜索
GET /learn/user/_search
{
"query" : {
"match" : {
"about" : "散弹枪"
}
},
"highlight": {
"fields" : {
"about" : {}
}
}
}
# 分析,不能用,可能是已被版本淘汰
GET /learn/user/_search
{
"aggs": {
"all_interests": {
"terms": { "field": "interests" }
}
}
}
# 只得到source字段
GET /learn/user/1/_source
# 得到指定字段
GET /learn/user/1/_source/?_source=age,about
# 指定_seq_no和 _primary_term
PUT /learn/user/5?if_seq_no=18&if_primary_term=2
{
"first_name" : "先森",
"last_name" : "双口",
"age" : 10,
"about" : "散弹批发商",
"interests": [ "嘿嘿","游戏" ]
}
# 更改部分文档
POST /learn/user/3/_update
{
"doc" : {
"first_name" : "鬼子"
}
}
# 使用脚本对部分文档进行修改
POST /learn/user/3/_update
{
"script" : "ctx._source.age=0"
}
# 给数组新增元素,也不可用,看报错因该是数组越界,暂不清楚时那个版本进行的更新。
POST /learn/user/3/_update
{
"script" : "ctx._source.interests+=new_interest",
"params" : {
"new_interest" : "new_interest"
}
}
# 检索多条文档信息
GET /_mget
{
"docs" : [
{
"_index" : "learn",
"_type" : "user",
"_id" : 1
},
{
"_index" : "learn",
"_type" : "user",
"_id" : 2,
"_source": "about"
}
]
}
# 简写
GET /learn/user/_mget
{
"docs": [
{
"_id": 1
},
{
"_id": 2,
"_source": "about"
}
]
}
GET /learn/user/2
# 使用代价较小的批量操作
POST /_bulk
{"delete": {"_index": "learn","_type": "user","_id": "5"}}
{"create":{"_index": "learn","_type": "user","_id": "1"}}
{ "first_name" : "四", "last_name" : "李","age" : 25,"about" : "法外狂徒的弟弟","interests": [ "偷盗", "抢劫","嘿嘿" ]}
{"create":{"_index": "learn","_type": "user"}}
{ "first_name" : "四", "last_name" : "李","age" : 25,"about" : "法外狂徒的弟弟","interests": [ "偷盗", "抢劫","嘿嘿" ]}
{"update": { "_index": "learn", "_type": "user", "_id": "2"}}
{"doc": {"interests": ["唱跳","RAP","你干嘛~"]}}
# 分页查询
GET /learn/user/_search
POST /learn/user/_bulk
{"delete": {"_id": "TBfyY4IB_561K7UQqFc7"}}
{"delete": {"_id": "ShfxY4IB_561K7UQBVey"}}
{"delete": {"_id": "SRftY4IB_561K7UQHVeY"}}
{"delete": {"_id": "SBftY4IB_561K7UQHFes"}}
{"delete": {"_id": "RxftY4IB_561K7UQG1c7"}}
{"delete": {"_id": "RhftY4IB_561K7UQGVex"}}
GET /learn/user/_search?size=2
GET /learn/user/_search?size=2&from=2
GET /learn/user/_search?size=2&from=4
# 请求体格式
GET /learn/user/_search
{
"size": 2,
"from": 2
}
# 排序
GET /learn/user/_search
{
"sort": [{"_id": {"order": "asc"}}]
}
GET /learn/user/_search
{
"sort": [{"_id": {"order": "desc"}}]
}
GET /learn/user/_search?sort=_id:desc
# 多级排序
GET /learn/user/_search
{
"sort": [
{"_score": {"order": "desc"}},
{"_id": {"order": "desc"}}
]
}
# 创建一个索引
PUT /test
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 2
},
"mappings": {
"properties":{
"id":{"type": "long"},
"name":{"type": "text"},
"age":{"type": "integer"},
"sex":{"type": "text"}
}
}
}
# 删除索引
DELETE /test
如果觉的这篇文章有用点个赞吧,赞是免费的但能让我开心一整天,拜托了。
参考资料: