• 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    下载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"
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    根据自己的需求,可以进行相应的设置。

    5.3.2 多条件查询

    多个条件一块查询, 和sql里面的and很像

    {
    	"query": {
    		"bool": {
    			# 多个条件同时满足and   如果
    			"must": [
    				{
    					"match": {
    						"category": "小米"
    					}
    				},
                    {
                        "match": {
                            "price": 1999
                        }
                    }, 
                    {
                        "match"
                    }, 
                    .....
    			]
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    如果想实现sql里面的or

    {
    	"query": {
    		"bool": {
    			# or
    			"should": [
    				{
    					"match": {
    						"category": "小米"
    					}
    				},
                    {
                        "match": {
                            "category": "华为"
                        }
                    }, 
                    .....
    			], 
                
                # 如果想再进行范围的查询
                "filter": {
                    "range": {
                        "price": {
                            "gt" : 5000,    # price > 5000
                        }
                    }
                }
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    5.3.3 文档聚合查询

    聚合函数都可以用,比如对某个字段求平均值

    {
    	"aggs": {  # 聚合操作
    		"price_avg": {  # 聚合后的列名
    			"avg": {   # 聚合函数
    				"field": "price"  # 分组字段
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    分组聚合

    {
    	"aggs": {  # 聚合操作
    		"price_groups": {  # 分组后的列名
    			"terms": {   # 分组,类似groupby
    				"field": "price"  # 分组字段
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上面这个代码,分组之后,每组的数量进行显示。

    5.4全文检索 & 完全匹配

    ES在给文档建立倒排索引的时候, 是按照拆字进行建立的, 这时候如果进行查询,其实是做的一个全文检索,啥意思?

    {
    	"query":{
    		# 条件过滤筛选  如果想查全部数据,"match_all": {}
    		"match":{
    			"category": "小"  # 这里会查出带小字的所有数据来  "小华" 会查出带小字和华字的所有数据来
    		}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那不行啊, 如果我想完全匹配查询呢?

    {
    	"query":{
    		# 条件过滤筛选  如果想查全部数据,"match_all": {}
    		"match_phrase":{
    			"category": "小"  # 精确查询  category="小"的数据会回来
    		}
        },
        
        # 对查询的字段高亮显示
        "highlight": {
            "fields": {
                "category": {}
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    5.5 ES - 映射关系

    映射关系,就类似于数据库建表时的表结构信息,表示是在索引下建立数据的时候,要遵循的一些字段查询规则,比如上面有的字段能支持全文检索,有的字段只支持完全匹配, 有的字段还可以被索引,有的不能被索引等,这个就可以事先通过映射关系说明。

    {
        "properties":{
            "name": {
                "type": "text",  # 文本类型,支持全文检索
                "index": true   # 可以通过索引给找到
            },
            "sex": {
                "type": "keyword",  # 关键字类型,此时不能拆开,只能完全匹配
                "index": true  # 创建索引
            },
            "tel": {
                "type": "keyword", # 完全匹配
                "index": false # 这个不创建索引,所以不能通过这个字段查询
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    可以具体看个例子, 首先创建一个索引叫做user
    在这里插入图片描述

    然后,给这个索引创建映射关系,把上面代码复制到body中

    在这里插入图片描述

    此时换成GET请求,就能查询当前索引的映射关系。接下来,插入一条数据:
    在这里插入图片描述

    此时,我们执行查询,
    在这里插入图片描述

    此时,就发现name这个字段支持全文检索,如果我们通过sex字段查询

    在这里插入图片描述
    如果想通过tel做查询,会报错失败,因为tel没有被索引。

    6. Python对接ElasticSearch

    ElasticSearch提供了pythonAPI, 这样我们可以通过写python代码完成上面的索引的创建或者数据的增删改查操作。官方文档, 这里整理常用的命令。

    虚拟环境中安装:

    # 这里最好是指定版本,和安装的ES的版本匹配起来,否则可能会报错
    pip install elasticsearch==7.6
    
    • 1
    • 2

    6.1 初始化ES

    from elasticsearch import Elasticsearch
    
    es = Elasticsearch([{
    	"host": xx.xxx.xxx.72,
    	"port": 9200
    }], timeout=3600)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    注意,这个操作如果报

    TypeError: NodeConfig.__init__() missing 1 required positional argument: 'scheme'
    
    • 1

    说明,装的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参数之后, 就忽略对应的错误码保证程序往后执行,而不是抛异常
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    删除索引:

    result = es.indices.delete(index='test', ignore=[400, 404])
    
    • 1

    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")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    插入多条数据

    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",
        },]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    6.4 删除数据

    如果删除一条数据,调用delete方法, 指定要删除的数据id即可。

    response = es.delete(index="test", doc_type="_doc", id="222")
    
    • 1

    6.5 查询数据

    6.5.1 id查询

    response = es.get(index="test", id="111")
    
    • 1

    6.5.2 根据特定字段查询

    这个就需要和上面postman那样,需要写JSON的body了,比如

    query = {
    	"query": {
    		"bool": {
    			"must": [
    				{
    					"term": {
    						"serial": {
    							"value": "版本1"
    						}
    					}
    				}
    			]
    		}
    	}	
    }
    response = es.search(index="test", size=1, body=query)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    ES提供查询的功能也非常强大,比如fillter查询, 聚合查询,分组查询等等。

    这个都是使用search函数, 需要写不同的body体, 这个我觉得现用现查较好,不一一整理了,下面的第三个连接整理的查询例子很多,到时候可以来这里参考。

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

    参考

  • 相关阅读:
    Codeforces Round 848 (Div. 2)C
    图书管理系统C语言课程设计
    一个c程序的内存分布
    Linux下的基本指令(1)
    python实现命令tree的效果
    云安虚拟化应用性能监测系统—应用异常检测
    记录:Address already in use: JVM_Bind 端口被占用
    《网络协议》08. 概念补充
    RPA的命令库与子程序是什么?
    论文阅读《Sylph: A Hypernetwork Framework for Incremental Few-shot Object Detection》
  • 原文地址:https://blog.csdn.net/wuzhongqiang/article/details/125889153