1. 写在前面
工作中用到了ElasticSearch,这是一个全文搜索引擎,可以快速的储存搜索和分析海量数据,这个东西非常重要,各大公司也都在用。这篇文章是快速入门ElasticSearch的笔记记录,我的想法是先通过一些资料学习下这东西怎么使用,先用起来,后面如果需要补理论的话再去补就快了。
下面分别从安装,基本概念,以及postman和通过Python API使用ElasticSearch进行介绍。
2. Elastic Stack的核心
Elastic Stack, 包括ElasticSearch,Kibana, Beats和Logstash(ELK Stack), 能安全可靠获取任何来源,任何形式的数据,然后实时对数据进行搜素,分析和可视化。
ElasticSearch(ES)是一个开源高扩展的分布式全文搜索引擎, 整个Elastic Stack技术栈的核心。 可以近乎实时的存储、检索数据,本身扩展性很好,可扩展到上百台服务器,处理PB级别的数据。
ElasticSearch的官方地址:https://www.elastic.co/cn/
3. ElasticSearch安装
关于Elastic的安装, 这里不多整理,可以参考网上的教程,我看有的还需要配置环境啥的,由于我这块是小白,所以想先用起来,于是乎,直接docker拉了个Elastic镜像,跑了个容器,就能玩了,这里是docker安装Elastic的代码:
docker pull elasticsearch:7.6.2 存储和检索数据
docker pull kibana:7.6.2 可视化检索数据
# 创建自己的目录 /home/wuzhongqiang下面
mkdir -p ./mydata/elasticsearch/config
mkdir -p ./mydata/elasticsearch/data
echo "http.host: 0.0.0.0" >> ./mydata/elasticsearch/config/elasticsearch.yml
# 新建一个网络 方便后面与kaibana在一个网络通信
docker cereate network es_net
# /wuzhongqiang/mydata/elasticsearch/config
# -p 9200:9200 容器内部端口映射到linux的端口 9200是后端发送请求restAPI使用的
# -p 9300:9300 9300是es在分布式集群下节点间的通信端口
# -e "discovery.type = single-node" 指定单节点模式运行
# -e ES_JAVA_OPTS="-Xms64m -Xmx128m" 如果不指定会将整个内存全部占用 初始64m最大占用128 上线一般32G
docker run --name elasticsearch --network es_net -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx128m" \
-v /home/wuzhongqiang/mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /home/wuzhongqiang/mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /home/wuzhongqiang/mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.6.2
下载Postman,这是一款强大的网页调试工具,提供功能强大的WebAPI和HTTP请求调试。www.getpostman.com
能够发送任何类型的HTTP请求(GET, HEAD, POST, PUT,…),不仅能表单提交,且可以附带任意类型请求体
4. ElasticSearch数据格式
ElasticSearch是面向文档型数据库,一条数据就是一个文档。Elasticsearch存储文档数据和关系型数据库MySQL存储数据概念对比:
ES里面的索引可以看做一个库, 而Types相当于表,Documents相当于表的行。
ElasticSearch中的基本概念:
- Node和Cluster: ElasticSearch本质上是一个分布式数据库,允许多台服务器协同工作,每个服务器可以运行多个ElasticSearch实例。 单个ElasticSearch实例称为一个节点(Node)。一组节点构成一个集群(Cluster)
- Index(索引): ElasticSearch数据管理的顶层单位叫做Index, 相当于MySQL、MongoDB等里面的数据库概念。 ES会索引所有字段,经过处理好写入一个反向索引(倒排),查找数据的时候,直接查找该索引。 注意: 每个index的名字要小写
- Document(文档):Index里面单条记录称为Document,许多条Document构成了一个Index。 Document使用JSON格式表示,同一个Index的Document,不要求有相同的结构(Scheme), 但最好保持相同,这样有利于提高搜索效率。
- Type:Document可以分组,比如weather这个Index, 可以按照城市分组(北京和上海),也可以按照气候分组(晴天和雨天),这种分组叫做Type,是虚拟的逻辑分组,用来过滤Document,类似MySQL中的数据表,MongoDB中的Collection。 不同的Type应该有相似的结构(Scheme),举例来说,id 字段不能在这个组是字符串,在另一个组是数值。这是与关系型数据库的表的一个区别。性质完全不同的数据(比如 products 和 logs)应该存成两个 Index,而不是一个 Index 里面的两个 Type(虽然可以做到)。注意: 这里的Types的概念逐渐弱化, 6.x版本中,一个index下只包含一个type, 但7.x版本中,type概念删除。
- Fields(字段): 每个Document都类似一个JSON结构,包含了许多字段,每个字段都有对应的值,多个字段组成了一个Document,这个类比于MySQL数据表中的字段
理解正排索引和倒排索引,这个我之前写过一篇文章
正排索引: 文章id -> 文章内容 -> 文章关键字
倒排索引: 文章关键字 -> 文章id -> 文章内容
5. ES - 基础操作(Postman演示)
5.1 ES - 索引操作
5.1.1 索引创建
这里可以用postman去发送请求, 在ElasticSearch中创建索引。
情景说明:
- 如果此时再点击send发送一次put请求,由于put具有幂等性, 再去创建,会显示shopping索引已经存在
- 如果换一个请求,比如把put换成POST, 由于POST没有幂等性,产生的索引结果可能不一样,这种情况也是不允许的
5.1.2 索引查询和删除
获取某个索引下的详细信息, 请求方式改成GET
列出所有索引,这个GET请求方式不变, 修改URL
删除索引,选定某个索引的URL,然后发送DELETE请求
5.2 ES - 文档操作
5.2.1 文档创建
索引创建好, 接下来创建文档,并添加数据。 这里文档可以类比为关系型数据库的表数据, 添加的数据格式为JSON。
在Postman中,向ES发送POST请求:
这里的下划线_doc
表示文档数据的意思。
注意,这里只能用POST,不能用PUT。这是因为,我们点击send之后, 会发现下面结果中会产生一个id, 这个是数据的唯一性表示。 But,如果我们多次点击这个send, 会发现下面数据里面的id会随机生成,都是不一样的,这也就是说, 这个插入数据操作的请求不是幂等性的, 但PUT请求要求幂等性。
上面还有个问题,既然这里面的id是数据的唯一性标识,后面我们就可以用这个id来操作数据,但是随机性生成的这个id太长太难记,那么有没有简单的方式自定义数据id呢? http://xxx.xxx.xxx.xx:9200/shopping/_doc/1001
后面的1001就是自定义的id号,一旦定义,后面就不变了,可以用这个id号去访问数据。这时候,由于多次send都是发送这一个id号,符合幂等性了,此时就可以用PUT请求了。
5.2.2 文档查询
URL是某个具体的文档id, 换成GET请求
1001类似于主键, 结果就类似于主键查询的结果。
指定某个数据id, 然后GET就能在ElsticSearch数据库中查询结果,那么如果我想查某个index下面的所有数据呢? 这样:
5.2.3 文档修改
数据创建完之后,如果想修改应该怎么修改呢?
上面这是一种完全覆盖的操作,不管发出多少次请求, 数据都是完全被覆盖,这是一种全量数据的更新。本质上其实是一种覆盖的操作。
如果不想全部覆盖,而是想局部修改某个字段呢? 此时就可以用局部更新的方式,由于每次更新结果都不一定相同,所以这就不是幂等操作了,所以此时用POST。
如果是删除数据呢? 修改成DELETE请求:
5.3 复杂的查询操作
5.3.1 属性值筛选
按照具体的属性值筛选:
但在URL中写查询条件不优雅,所以一般推荐使用第二种方式, 也就是请求体中写条件查询
这里可以通过修改body里面的写法,来满足不同的需求:
{
"query":{
# 条件过滤筛选 如果想查全部数据,"match_all": {}
"match":{
"category": "小米"
},
# 分页显示
"from": 0, # 起始页 查询任意页公式: (页码-1) * size
"size": 2, # 每页多少个
# 只想要某些字段,不需要全部显示, 只想看title
"_source": ["title"],
# 对字段排序
"sort": {
# 按照哪个字段排?
"price": {
# 升降序?
"order": "desc"
}
}
}
}
根据自己的需求,可以进行相应的设置。
5.3.2 多条件查询
多个条件一块查询, 和sql里面的and很像
{
"query": {
"bool": {
# 多个条件同时满足and 如果
"must": [
{
"match": {
"category": "小米"
}
},
{
"match": {
"price": 1999
}
},
{
"match"
},
.....
]
}
}
}
如果想实现sql里面的or
{
"query": {
"bool": {
# or
"should": [
{
"match": {
"category": "小米"
}
},
{
"match": {
"category": "华为"
}
},
.....
],
# 如果想再进行范围的查询
"filter": {
"range": {
"price": {
"gt" : 5000, # price > 5000
}
}
}
}
}
}
5.3.3 文档聚合查询
聚合函数都可以用,比如对某个字段求平均值
{
"aggs": {
# 聚合操作
"price_avg": {
# 聚合后的列名
"avg": {
# 聚合函数
"field": "price" # 分组字段
}
}
}
}
分组聚合
{
"aggs": {
# 聚合操作
"price_groups": {
# 分组后的列名
"terms": {
# 分组,类似groupby
"field": "price" # 分组字段
}
}
}
}
上面这个代码,分组之后,每组的数量进行显示。
5.4全文检索 & 完全匹配
ES在给文档建立倒排索引的时候, 是按照拆字进行建立的, 这时候如果进行查询,其实是做的一个全文检索,啥意思?
{
"query":{
# 条件过滤筛选 如果想查全部数据,"match_all": {}
"match":{
"category": "小" # 这里会查出带小字的所有数据来 "小华" 会查出带小字和华字的所有数据来
}
}
那不行啊, 如果我想完全匹配查询呢?
{
"query":{
# 条件过滤筛选 如果想查全部数据,"match_all": {}
"match_phrase":{
"category": "小" # 精确查询 category="小"的数据会回来
}
},
# 对查询的字段高亮显示
"highlight": {
"fields": {
"category": {
}
}
}
}
5.5 ES - 映射关系
映射关系,就类似于数据库建表时的表结构信息,表示是在索引下建立数据的时候,要遵循的一些字段查询规则,比如上面有的字段能支持全文检索,有的字段只支持完全匹配, 有的字段还可以被索引,有的不能被索引等,这个就可以事先通过映射关系说明。
{
"properties":{
"name": {
"type": "text", # 文本类型,支持全文检索
"index": true # 可以通过索引给找到
},
"sex": {
"type": "keyword", # 关键字类型,此时不能拆开,只能完全匹配
"index": true # 创建索引
},
"tel": {
"type": "keyword", # 完全匹配
"index": false # 这个不创建索引,所以不能通过这个字段查询
}
}
}
可以具体看个例子, 首先创建一个索引叫做user
然后,给这个索引创建映射关系,把上面代码复制到body中
此时换成GET请求,就能查询当前索引的映射关系。接下来,插入一条数据:
此时,我们执行查询,
此时,就发现name这个字段支持全文检索,如果我们通过sex字段查询
如果想通过tel做查询,会报错失败,因为tel没有被索引。
6. Python对接ElasticSearch
ElasticSearch提供了pythonAPI, 这样我们可以通过写python代码完成上面的索引的创建或者数据的增删改查操作。官方文档, 这里整理常用的命令。
虚拟环境中安装:
# 这里最好是指定版本,和安装的ES的版本匹配起来,否则可能会报错
pip install elasticsearch==7.6
6.1 初始化ES
from elasticsearch import Elasticsearch
es = Elasticsearch([{
"host": xx.xxx.xxx.72,
"port": 9200
}], timeout=3600)
注意,这个操作如果报
TypeError: NodeConfig.__init__() missing 1 required positional argument: 'scheme'
说明,装的Elasticsearch包版本是8.x,而安装的ES是7.x,我这里是先看了下安装的ES的版本,然后装报的时候指定了版本。
6.2 index操作
mappings = {
"mappings": {
"properties": {
"id": {
"type": "long",
"index": "false"
},
"serial": {
"type": "text", # keyword不会进行分词,text会分词
"index": "false" # 不建索引
},
# tags可以存json格式,访问tags.content
"tags": {
"type": "object",
"properties": {
"content": {
"type": "keyword", "index": True},
"dominant_color_name": {
"type": "keyword", "index": True},
"skill": {
"type": "keyword", "index": True},
}
},
"hasTag": {
"type": "long",
"index": True
},
"status": {
"type": "long",
"index": True
},
"createTime": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
},
"updateTime": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
}
}
}
}
es.indices.create(index = 'test',body=mappings, ignore=[400, 404]) # 创建索引,如果创建不成功,会返回错误码,这里指定了ignore参数之后, 就忽略对应的错误码保证程序往后执行,而不是抛异常
删除索引:
result = es.indices.delete(index='test', ignore=[400, 404])
6.3 数据操作
6.3.1数据插入
插入单条数据
action = {
"id": "111",
"serial": "版本",
# 以下tags.content是错误的写法
# "tags.content" :"标签2",
# "tags.dominant_color_name": "域名的颜色黄色",
# 正确的写法如下:
"tags": {
"content": "标签3", "dominant_color_name": "域名的颜色黄色"},
# 按照字典的格式写入,如果用上面的那种写法,会直接写成一个tags.content字段。
# 而不是在tags中content添加数据,这点需要注意
"tags.skill": "分类信息",
"hasTag": "123",
"status": "11",
"createTime": "2018-02-02",
"updateTime": "2018-02-03",
}
es.index(index="test", doc_type="_doc", body=action, id="111") # 如果不指定id,则会自动生成一个id,
# 注意根据测试发现,action里面那个id并不是文档的id,这个会当成文档其中的一个field,也就是字段
# 这里创建也可以用create函数,但是这个函数,需要指定id字段来唯一标识该条数据
es.create(index="test", doc_type="_doc", body=action, id="111")
插入多条数据
doc = [
{
"_index": {
"test"},
"_source":
{
"id": "111",
"serial": "版本",
"tags": {
"content": "标签3", "dominant_color_name": "域名的颜色黄色"},
"tags.skill": "分类信息",
"hasTag": "123",
"status": "11",
"createTime": "2018-2-2",
"updateTime": "2018-2-3",
}
},
{
"_index": {
"test"},
"_source":
{
"id": "222",
"serial": "版本",
"tags": {
"content": "标签3", "dominant_color_name": "域名的颜色黄色"},
"tags.skill": "分类信息",
"hasTag": "123",
"status": "11",
"createTime": "2018-2-2",
"updateTime": "2018-2-3",
}
},
...
]
a = es.bulk(index='test', doc_type='_doc', body=doc)
# 下面这种写法也行
doc = [
{
"index": {
}},
{
"id": "111",
"serial": "版本1",
"tags": {
"content": "标签3", "dominant_color_name": "域名的颜色黄色"},
"tags.skill": "分类信息",
"hasTag": "123",
"status": "11",
"createTime": "2018-2-2",
"updateTime": "2018-2-3",
},
{
"index": {
}},
{
"id": "222",
"serial": "版本2",
"tags": {
"content": "标签3", "dominant_color_name": "域名的颜色黄色"},
"tags.skill": "分类信息",
"hasTag": "123",
"status": "11",
"createTime": "2018-2-2",
"updateTime": "2018-2-3",
},]
6.3.2 更新数据
modify_data = {
"tags": {
"content": "标签5"}
}
response = es.update(index="test", doc_type="_doc", id="222", body=modify_data)
# 或者也可以用index,这个可以代替我们完成两个操作,如果数据不存在,那就插入,如果存在,就更新
response = es.index(index="test", doc_type="_doc", id="222", body=modify_data)
6.4 删除数据
如果删除一条数据,调用delete方法, 指定要删除的数据id即可。
response = es.delete(index="test", doc_type="_doc", id="222")
6.5 查询数据
6.5.1 id查询
response = es.get(index="test", id="111")
6.5.2 根据特定字段查询
这个就需要和上面postman那样,需要写JSON的body了,比如
query = {
"query": {
"bool": {
"must": [
{
"term": {
"serial": {
"value": "版本1"
}
}
}
]
}
}
}
response = es.search(index="test", size=1, body=query)
ES提供查询的功能也非常强大,比如fillter查询, 聚合查询,分组查询等等。
这个都是使用search函数, 需要写不同的body体, 这个我觉得现用现查较好,不一一整理了,下面的第三个连接整理的查询例子很多,到时候可以来这里参考。
后面如果有新知识,会继续补充。
参考:
-
Elasticsearch 基本介绍及其与 Python 的对接实现 - 提供了很多学习参考资料
-
elasticsearch 8.0 python使用API操作 – 提供了比较全的数据操作