ElasticSearch快速入门小记

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体, 这个我觉得现用现查较好,不一一整理了,下面的第三个连接整理的查询例子很多,到时候可以来这里参考。

后面如果有新知识,会继续补充。

参考

猜你喜欢

转载自blog.csdn.net/wuzhongqiang/article/details/125889153
今日推荐