Elasticsearch 核心术语
核心概念
ES -> 数据库
索引 index -> 表
文档 document -> 行(记录)
字段 fields -> 列
映射 mapping -> 表结构
近实时 NRT -> Near real time 近实时的搜索
节点 node -> 每一个服务器
stu_index
{
id: 1001,
name: jason,
age: 19
},
{
id: 1002,
name: tom,
age: 18
},
{
id: 1003,
name: rose,
age: 22
}
集群相关概念
分片(shard):把索引库拆分为多份,分别放在不同的节点上,比如有3个节点,3个节点的所有数据内容加在一起是一个完整的索引库。分别保存到三个节点上水平扩展,提高吞吐量。
备份(replica):每个shard的备份。
shard = primary shard(主分片)
replica = replica shard(备份节点)
倒排索引概念
也常被称为反向索引、置入档案或反向档案,是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。 它是文档检索系统中最常用的数据结构。
根据单词去搜索包含单词的文档,并且显示在文档中的词频(TF)和位置(POS)
安装Elasticsearch
- 上传后解压
- 移动解压后的es文件夹
- ES 目录介绍
bin:可执行文件在里面,运行es的命令就在这个里面,包含了一些脚本文件等
config:配置文件目录
JDK:java环境
lib:依赖的jar,类库
logs:日志文件
modules:es相关的模块
plugins:可以自己开发的插件
data:这个目录没有,自己新建一下,后面要用 -> mkdir data,这个作为索引目录
- 修改核心配置文件 elasticearch.yml
- 修改集群名称,默认是elasticsearch,虽然目前是单机,但是也会有默认的
- 为当前的es节点取个名称,名称随意,如果在集群环境中,都要有相应的名字
环境介绍:目前是虚拟机,单节点。
cluster.name: study-elasticsearch
node.name: es-node1
path.data: /usr/local/elasticsearch-7.10.1/data
path.logs: /usr/local/elasticsearch-7.10.1/logs
network.host: 0.0.0.0 #端口默认9200
cluster.initial_master_nodes: ["es-node1"]
- 修改 jvm.options 文件
-Xms128m
-Xmx128m
- es规定root不能启动es,需创建新用户
useradd esuser //创建用户
chown -R esuser:esuser /usr/local/elasticsearch-7.10.1 // 赋予用户及用户组权限
su esuser // 切换用户
再次查看,操作用户及用户组变成了esuser
进入bin目录下启动。
./elasticsearch
// 报错
ERROR: [3] bootstrap checks failed
[1]: max file descriptors [4096] for elasticsearch process is too low, increase to at least [65535]
解决办法:
切换到root用户修改配置sysctl.conf
vi /etc/sysctl.conf
// 添加如下配置
vm.max_map_count=655360
// 退出执行如下命令
sysctl -p
编辑 /etc/security/limits.conf,追加以下内容;
* soft nofile 65536
* hard nofile 65536
* soft nproc 2048
* hard nproc 4096
es启动错误可以参考这个文章:
www.cnblogs.com/zhi-leaf/p/…
es正常启动后,通过浏览器可以进行访问,ip+端口号,显示如下:
Elasticsearch可视化操作界面es-header插件
插件地址:github.com/mobz/elasti…
安装方式:
这里选择在chrome商店里安装此插件,此方法最为简单。
索引的基本操作
创建索引
通过head插件可以实现对索引创建的操作。
创建索引Restful api
PUT http://121.89.195.81:9200/index_test
{
"settings":{
"index":{
"number_of_shards": "3",
"number_of_replicas": "0"
}
}
}
查看集群健康度
这里引入集群健康度的一个概念
可以通过GET /_cluster/health
进行查看
删除索引
删除索引head插件 可视化操作
删除索引 Restful API
delete + index
查看索引
查看单个索引
查看所有索引
GET _cat/indices?v
映射Mappings设定
创建Mappings
示例:创建一个带两个属性的索引,索引名称为index_mapping
PUT http://121.89.195.81:9200/index_mapping
Body:
{
"mappings": {
"properties":{
"realname":{
"type": "text", // text与keyword本质上都是string类型,
//在新版本将String类型分成了好几种类型,text可分词,keyword不能分词
"index": true
},
"username":{
"type": "keyword",
"index": false // false为索引查询时,不显示的内容
}
}
}
}
查看效果:
查看具体分词效果
示例:查看索引名称为index_mapping里面的某个field。
GET http://121.89.195.81:9200/index_mapping/_analyze
{
"field": "realname",
"text": "study is good" //这里的text不是指文本类型,固定写法
}
类型为text与keyword的属性的分词效果图:
增加mappings属性
这里注意,一旦mapprings设定好了,相应属性的类型无法修改,只能增加额外的属性。
增加属性相应API:
POST http://121.89.195.81:9200/index_mapping/_mapping
{
"properties": {
"id":{
"type": "long"
},
"age":{
"type": "integer"
}
}
}
Mappings设定属性的主要数据类型
- text,keyword
- long,integer,short,byte
- double,float
- boolean
- date
- object
- 数组不能混,类型一致
ES文档基本操作
添加文档
POST /my_doc/_doc/1 -> {索引名}/_doc/{索引ID}(是指索引在es中的id,而不是这条记录的id,比如记录的id可以来自数据库。
POST http://121.89.195.81:9200/my_doc/_doc/1
{
"id": 1001,
"name": "demo1",
"desc": "2020 is bad, 希望2021好起来",
"create_time": "2020-12-31"
}
注意:这里如果在index中没有提前设定好mapping,es会根据新增的数据自动设定属性类型。
注意:如果在新增文档的时候不指定id,es会自动生成id。
POST http://121.89.195.81:9200/my_doc/_doc
{
"id": 1002,
"name": "demo2",
"desc": "2020 is bad, 希望2021好起来",
"create_time": "2020-12-31"
}
删除文档
DELETE /my_doc/_doc/1
注:文档删除不是立即删除,文档还是保存在磁盘上,索引增长越来越多,才会把那些曾经标识过删除的,进行清理,从磁盘上移出去。
修改文档
增量替换
POST http://121.89.195.81:9200/my_doc/_doc/1/_update
{
"doc": {
"name": "demo3"
}
}
全量替换
PUT http://121.89.195.81:9200/my_doc/_doc/1
{
"id": 1001,
"name": "demo4",
"desc": "2021 is very good, 非常牛!",
"create_date": "2020-12-31"
}
注:每次修改后,version会更改
查询文档
查询单个文档
GET http://121.89.195.81:9200/my_doc/_doc/1
_index:文档数据所属那个索引,理解为数据库的某张表即可。
_type:文档数据属于哪个类型,新版本使用_doc 。
_id:文档数据的唯一标识,类似数据库中某张表的主键。可以自动生成或者手动指定。
_score:查询相关度,是否契合用户匹配,分数越高用户的搜索体验越高。
_version:版本号。
_source:文档数据,json格式。
查询所有文档
GET http://121.89.195.81:9200/my_doc/_doc/_search
定制化搜索结果
GET /index_demo/_doc/1?_source=id,name
GET /index_demo/_doc/_search?_source=id,name
判断文档是否存在
执行如下命令,查看返回状态码是200还是404
HEAD /index_demo/_doc/1
文档乐观锁控制 if_seq_no与if_primary_term
插入新数据
POST /my_doc/_doc
{
"id": 1010,
"name": "study-1010",
"desc": "studystudy!",
"create_date": "2019-12-24"
}
# 此时 _version 为 1
修改数据
POST /my_doc/_doc/{_id}/_update
{
"doc": {
"name": "旺财"
}
}
# 此时 _version 为 2
模拟两个客户端操作同一个文档数据,_version都携带为一样的数值
# 操作1
POST /my_doc/_doc/{_id}/_update?if_seq_no={数值}&if_primary_term={数值}
{
"doc": {
"name": "旺财1"
}
}
# 操作2
POST /my_doc/_doc/{_id}/_update?if_seq_no={数值}&if_primary_term={数值}
{
"doc": {
"name": "旺财2"
}
}
注意:if_seq_no以及if_primary_term可以通过GET /my_doc/_doc/id来获取
分词与内置分词器
查看分词效果
POST http://121.89.195.81:9200/_analyze
body:
{
"analyzer": "standard",
"text": "text文本"
}
POST /my_doc/_analyze
body:
{
"analyzer": "standard",
"field": "name",
"text": "text文本"
}
es内置分词器
- standard:默认分词,单词会被拆分,大小会转换为小写。
- simple:按照非字母分词。大写转为小写。
- whitespace:按照空格分词。忽略大小写。
- stop:去除无意义单词,比如the / a / an / is …
- keyword:不做分词。把整个文本作为一个单独的关键词。
ik中文分词器
Github:github.com/medcl/elast…
安装:
这里选择第一种方式进行安装
下载zip安装包:
unzip elasticsearch-analysis-ik-7.10.1.zip -d /usr/local/elasticsearch-7.10.1/plugins/ik
补充下unzip命令(参考:www.cnblogs.com/qa-freeroad…)
unzip elasticsearch-analysis-ik-7.3.2.zip // 直接解压到当前目录
unzip elasticsearch-analysis-ik-7.10.1.zip -d /usr/local/elasticsearch-7.10.1/plugins/ik // 解压到指定目录下
之后重启es, 补充后台重启es命令./elasticsearch -d
测试:
POST http://121.89.195.81:9200/_analyze
body:
{
"analyzer": "ik_max_word",
"text": "上下班车流量很大"
}
ik_max_word: 会将文本做最细粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,中华人民,中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌”,会穷尽各种可能的组合,适合 Term Query;
ik_smart: 会做最粗粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,国歌”,适合 Phrase 查询。
自定义中文词库
- 在{es}/plugins/ik/config下,创建:
vim custom.dic
- 并且添加内容:
骚年
3.配置自定义扩展词典,IKAnalyzer.cfg.xml
<entry key="ext_dict">custom.dic</entry>
- 重启
DSL搜索讲解
数据准备
😊自定义词库
vi custom.dic 并添加如下内容
骚年
慕课网
慕课
课网
慕
课
网
🔑建立索引shop(名字随意)
🤚手动建立mappings
POST http://121.89.195.81:9200/shop/_mapping
{
"properties": {
"id": {
"type": "long"
},
"age": {
"type": "integer"
},
"username": {
"type": "keyword"
},
"nickname": {
"type": "text",
"analyzer": "ik_max_word"
},
"money": {
"type": "float"
},
"desc": {
"type": "text",
"analyzer": "ik_max_word"
},
"sex": {
"type": "byte"
},
"birthday": {
"type": "date"
},
"face": {
"type": "text",
"index": false // 不做任何索引
}
}
}
添加数据
{
"id": 1001,
"age": 18,
"username": "imoocAmazing",
"nickname": "慕课网",
"money": 88.8,
"desc": "我在慕课网学习java和前端,学习到了很多知识",
"sex": 0,
"birthday": "1992-12-24",
"face": "https://www.imooc.com/static/img/index/logo.png"
}
{
"id": 1002,
"age": 19,
"username": "justbuy",
"nickname": "周杰棍",
"money": 77.8,
"desc": "今天上下班都很堵,车流量很大",
"sex": 1,
"birthday": "1993-01-24",
"face": "https://www.imooc.com/static/img/index/logo.png"
}
{
"id": 1003,
"age": 20,
"username": "bigFace",
"nickname": "飞翔的巨鹰",
"money": 66.8,
"desc": "慕课网团队和导游坐飞机去海外旅游,去了新马泰和欧洲",
"sex": 1,
"birthday": "1996-01-14",
"face": "https://www.imooc.com/static/img/index/logo.png"
}
{
"id": 1004,
"age": 22,
"username": "flyfish",
"nickname": "水中鱼",
"money": 55.8,
"desc": "昨天在学校的池塘里,看到有很多鱼在游泳,然后就去慕课网上课了",
"sex": 0,
"birthday": "1988-02-14",
"face": "https://www.imooc.com/static/img/index/logo.png"
}
{
"id": 1005,
"age": 25,
"username": "gotoplay",
"nickname": "ps游戏机",
"money": 155.8,
"desc": "今年生日,女友送了我一台play station游戏机,非常好玩,非常不错",
"sex": 1,
"birthday": "1989-03-14",
"face": "https://www.imooc.com/static/img/index/logo.png"
}
{
"id": 1006,
"age": 19,
"username": "missimooc",
"nickname": "我叫小慕",
"money": 156.8,
"desc": "我叫凌云慕,今年20岁,是一名律师,我在琦䯲星球做演讲",
"sex": 1,
"birthday": "1993-04-14",
"face": "https://www.imooc.com/static/img/index/logo.png"
}
{
"id": 1007,
"age": 19,
"username": "msgame",
"nickname": "gamexbox",
"money": 1056.8,
"desc": "明天去进货,最近微软处理很多游戏机,还要买xbox游戏卡带",
"sex": 1,
"birthday": "1985-05-14",
"face": "https://www.imooc.com/static/img/index/logo.png"
}
{
"id": 1008,
"age": 19,
"username": "muke",
"nickname": "慕学习",
"money": 1056.8,
"desc": "大学毕业后,可以到imooc.com进修",
"sex": 1,
"birthday": "1995-06-14",
"face": "https://www.imooc.com/static/img/index/logo.png"
}
{
"id": 1009,
"age": 22,
"username": "shaonian",
"nickname": "骚年轮",
"money": 96.8,
"desc": "骚年在大学毕业后,考研究生去了",
"sex": 1,
"birthday": "1998-07-14",
"face": "https://www.imooc.com/static/img/index/logo.png"
}
{
"id": 1010,
"age": 30,
"username": "tata",
"nickname": "隔壁老王",
"money": 100.8,
"desc": "隔壁老外去国外出差,带给我很多好吃的",
"sex": 1,
"birthday": "1988-07-14",
"face": "https://www.imooc.com/static/img/index/logo.png"
}
{
"id": 1011,
"age": 31,
"username": "sprder",
"nickname": "皮特帕克",
"money": 180.8,
"desc": "它是一个超级英雄",
"sex": 1,
"birthday": "1989-08-14",
"face": "https://www.imooc.com/static/img/index/logo.png"
}
{
"id": 1012,
"age": 31,
"username": "super hero",
"nickname": "super hero",
"money": 188.8,
"desc": "BatMan, GreenArrow, SpiderMan, IronMan... are all Super Hero",
"sex": 1,
"birthday": "1980-08-14",
"face": "https://www.imooc.com/static/img/index/logo.png"
}
🧑DSL入门语法
🍋请求参数的查询(QueryString)
📃查询[字段]包含[内容]的文档
// q代表query
GET /shop/_doc/_search?q=desc:慕课网
GET /shop/_doc/_search?q=nickname:慕&q=age:25
text与keyword搜索对比测试(keyword不会被倒排索引,不会被分词)
GET /shop/_doc/_search?q=nickname:super
GET /shop/_doc/_search?q=username:super
GET /shop/_doc/_search?q=username:super hero
这种方式称之为QueryString查询方式,参数都是放在url中作为请求参数的。
🍋DSL基本语法
QueryString用的很少,一旦参数复杂就难以构建,所以大多查询都会使用dsl来进行查询更好。
Domain Specific Language
特定领域语言
基于JSON格式的数据查询
查询更灵活,有利于复杂查询
📃DSL格式语法
# 查询
POST /shop/_doc/_search
{
"query": {
"match": {
"desc": "慕课网"
}
}
}
# 判断某个字段是否存在
{
"query": {
"exists": {
"field": "desc"
}
}
}
语法格式为一个json object,内容都是key-value键值对,json可以嵌套。
key可以是一些es的关键字,也可以是某个field字段,后面会遇到
搜索不合法问题定位
DSL查询的时候经常会出现一些错误查询,出现这样的问题大多都是json无法被es解析,他会像java那样报一个异常信息,根据异常信息去推断问题所在,比如json格式不对,关键词不存在未注册等等,甚至有时候不能定位问题直接复制错误信息到百度一搜就能定位问题了。
🏈DSL查询所有与分页
🔍match_all
在索引中查询所有的文档
GET /shop/_doc/_search
或
POST /shop/_doc/_search
{
"query": {
"match_all": {}
},
"_source": ["id", "nickname", "age"]
}
Head 可视化操作
🔍分页查询
默认查询是只有10条记录,可以通过分页来展示
POST /shop/_doc/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 10
}
{
"query": {
"match_all": {}
},
"_source": [
"id",
"nickname",
"age"
],
"from": 5,
"size": 5
}
🏈DSL-term/match
📃term精确搜索与match分词搜索
搜索的时候会把用户搜索内容,比如“慕课网强大”作为一整个关键词去搜索,而不会对其进行分词后再搜索
POST /shop/_doc/_search
{
"query": {
"term": {
"desc": "慕课网"
}
}
}
对比
{
"query": {
"match": {
"desc": "慕课网"
}
}
}
⛑注:match会对慕课网先进行分词(其实就是全文检索),在查询,而term则不会,直接把慕课网作为一个整的词汇去搜索。
📃terms 多个词语匹配检索
相当于是tag标签查询,比如慕课网的一些课程会打上前端/后端/大数据/就业课这样的标签,可以完全匹配做类似标签的查询
POST /shop/_doc/_search
{
"query": {
"terms": {
"desc": ["慕课网", "学习", "骚年"]
}
}
}
🏈DSL-match_phrase
📃match_phrase 短语匹配
match:分词后只要有匹配就返回,match_phrase:分词结果必须在相应的字段分词中都包含,而且顺序必须相同,而且必须都是连续的。(搜索比较严格)
slop:允许词语间跳过的数量
POST /shop/_doc/_search
{
"query": {
"match_phrase": {
"desc": {
"query": "大学 毕业 研究生",
"slop": 2
}
}
}
}
🏈DSL-match(operator)/ids
📃match 扩展
operator
or:搜索内容分词后,只要存在一个词语匹配就展示结果
and:搜索内容分词后,都要满足词语匹配
POST /shop/_doc/_search
{
"query": {
"match": {
"desc": "慕课网"
}
}
}
等同于
{
"query": {
"match": {
"desc": {
"query": "xbox游戏机",
"operator": "or"
}
}
}
}
相当于 select * from shop where desc='xbox' or|and desc='游戏机'
minimum_should_match: 最低匹配精度,至少有[分词后的词语个数]x百分百,得出一个数据值取整。举个例子:当前属性设置为70,若一个用户查询检索内容分词后有10个词语,那么匹配度按照 10x70%=7,则desc中至少需要有7个词语匹配,就展示;若分词后有8个,则 8x70%=5.6,则desc中至少需要有5个词语匹配,就展示。
minimum_should_match 也能设置具体的数字,表示个数
POST /shop/_doc/_search
{
"query": {
"match": {
"desc": {
"query": "女友生日送我好玩的xbox游戏机",
"minimum_should_match": "60%"
}
}
}
}
📃根据文档主键ids搜索
GET /shop/_doc/1001
查询多个
POST /shop/_doc/_search
{
"query": {
"ids": {
"type": "_doc",
"values": ["1001", "1010", "1008"]
}
}
}
🏈DSL搜索-multi_match/boost
📃multi_match
满足使用match在多个字段中进行查询的需求
POST /shop/_doc/_search
{
"query": {
"multi_match": {
"query": "皮特帕克慕课网",
"fields": ["desc", "nickname"]
}
}
}
📃boost
权重,为某个字段设置权重,权重越高,文档相关性得分就越高。通畅来说搜索商品名称要比商品简介的权重更高。
POST /shop/_doc/_search
{
"query": {
"multi_match": {
"query": "皮特帕克慕课网",
"fields": ["desc", "nickname^10"]
}
}
}
nickname^10 代表搜索提升10倍相关性,也就是说用户搜索的时候其实以这个nickname为主,desc为辅,nickname的匹配相关度当然要提高权重比例了。
🏈DSL-布尔查询
可以组合多重查询
must:查询必须匹配搜索条件,譬如 and
should:查询匹配满足1个以上条件,譬如 or
must_not:不匹配搜索条件,一个都不要满足
📃案例1
POST /shop/_doc/_search
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "慕课网",
"fields": ["desc", "nickname"]
}
},
{
"term": {
"sex": 1
}
},
{
"term": {
"birthday": "1996-01-14"
}
}
]
}
}
}
{
"query": {
"bool": {
"should(must_not)": [ // must_not 取反,should为或者,满足一个以上条件即可
{
"multi_match": {
"query": "学习",
"fields": ["desc", "nickname"]
}
},
{
"match": {
"desc": "游戏"
}
},
{
"term": {
"sex": 0
}
}
]
}
}
}
📃案例2
{
"query": {
"bool": {
"must": [
{
"match": {
"desc": "慕"
}
},
{
"match": {
"nickname": "慕"
}
}
],
"should": [
{
"match": {
"sex": "0"
}
}
],
"must_not": [
{
"term": {
"birthday": "1992-12-24"
}
}
]
}
}
}
为指定词语加权
特殊场景下,某些词语可以单独加权,这样可以排得更加靠前。
POST /shop/_doc/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"desc": {
"query": "律师",
"boost": 18
}
}
},
{
"match": {
"desc": {
"query": "进修",
"boost": 2
}
}
}
]
}
}
}
🚿DSL-过滤器
对搜索出来的结果进行数据过滤。不会到es库里去搜,不会去计算文档的相关度分数,所以过滤的性能会比较高,过滤器可以和全文搜索结合在一起使用。 post_filter元素是一个顶层元素,只会对搜索结果进行过滤。不会计算数据的匹配度相关性分数,不会根据分数去排序,query则相反,会计算分数,也会按照分数去排序。 使用场景:
query:根据用户搜索条件检索匹配记录
post_filter:用于查询后,对结果数据的筛选
案例:查询账户金额大于60元,小于1000元的用户。
gte:大于等于
lte:小于等于
gt:大于
lt:小于
(除此以外还能做其他的match等操作也行)
POST /shop/_doc/_search
{
"query": {
"match": {
"desc": "慕课网游戏"
}
},
"post_filter": {
"range": {
"money": {
"gt": 60,
"lt": 1000
}
}
}
}
DSL-排序
es的排序同sql,可以desc也可以asc。也支持组合排序
案例
POST /shop/_doc/_search
{
"query": {
"match": {
"desc": "慕课网游戏"
}
},
"post_filter": {
"range": {
"money": {
"gt": 55.8,
"lte": 155.8
}
}
},
"sort": [
{
"age": "desc"
},
{
"money": "desc"
}
]
}
对文本排序
由于文本会被分词,所以往往要去做排序会报错,通常我们可以为这个字段增加额外的一个附属属性,类型为keyword,用于做排序。
创建新的索引
POST /shop2/_mapping
{
"properties": {
"id": {
"type": "long"
},
"nickname": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
插入数据
POST /shop2/_doc
{
"id": 1001,
"nickname": "美丽的风景"
}
{
"id": 1002,
"nickname": "漂亮的小哥哥"
}
{
"id": 1003,
"nickname": "飞翔的巨鹰"
}
{
"id": 1004,
"nickname": "完美的天空"
}
{
"id": 1005,
"nickname": "广阔的海域"
}
排序
{
"sort": [
{
"nickname.keyword": "desc"
}
]
}
☀DSL-结果高亮显示
POST /shop/_doc/_search
{
"query": {
"match": {
"desc": "慕课网"
}
},
"highlight": {
"pre_tags": ["<tag>"], // 这里的<tag>可以自定义
"post_tags": ["</tag>"],
"fields": {
"desc": {}
}
}
}
深度分页
深度分页其实就是搜索的深浅度,比如第1页,第2页,第10页,第20页,是比较浅的;第10000页,第20000页就是很深了。
使用如下操作:
{
"query": {
"match_all": {}
},
"from": 9990,
"size": 10
}
{
"query": {
"match_all": {}
},
"from": 9999,
"size": 10
}
我们在获取第9999条到10009条数据的时候,其实每个分片都会拿到10009条数据,然后集合在一起,总共是10009*3=30027条数据,针对30027数据再次做排序处理,最终会获取最后10条数据。
如此一来,搜索得太深,就会造成性能问题,会耗费内存和占用cpu。而且es为了性能,他不支持超过一万条数据以上的分页查询。那么如何解决深度分页带来的性能呢?其实我们应该避免深度分页操作(限制分页页数),比如最多只能提供100页的展示,从第101页开始就没了,毕竟用户也不会搜的那么深,我们平时搜索淘宝或者百度,一般也就看个10来页就顶多了。
譬如淘宝搜索限制分页最多100页
📄深度分页-提升搜索量
通过设置index.max_result_window来突破10000数据 GET /shop/_settings
PUT /shop/_settings
{
"index.max_result_window": "20000"
}
🍡scroll 滚动搜索
一次性查询1万+数据,往往会造成性能影响,因为数据量太多了。这个时候可以使用滚动搜索,也就是 scroll。 滚动搜索可以先查询出一些数据,然后再紧接着依次往下查询。在第一次查询的时候会有一个滚动id,相当于一个锚标记,随后再次滚动搜索会需要上一次搜索的锚标记,根据这个进行下一次的搜索请求。每次搜索都是基于一个历史的数据快照,查询数据的期间,如果有数据变更,那么和搜索是没有关系的,搜索的内容还是快照中的数据。
scroll=1m,相当于是一个session会话时间,搜索保持的上下文时间为1分钟。
POST /shop/_search?scroll=1m
{
"query": {
"match_all": {
}
},
"sort" : ["_doc"], // 固定写法
"size": 5 // 每次滚动查询时的记录数
}
// 下次查询
POST /_search/scroll
{
"scroll": "1m", // 间隔时间内
"scroll_id" : "your last scroll_id" // 上次返回的id
}
官文地址:https://www.elastic.co/guide/cn/elasticsearch/guide/current/scroll.html
🚌批量操作bulk
基本语法
bulk操作和以往的普通请求格式有区别。不要格式化json,不然就不在同一行了,这个需要注意。
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
...
{ action: { metadata }}代表批量操作的类型,可以是新增、删除或修改
\n是每行结尾必须填写的一个规范,每一行包括最后一行都要写,用于es的解析
{ request body }是请求body,增加和修改操作需要,删除操作则不需要
批量操作的类型
action 必须是以下选项之一:
create:如果文档不存在,那么就创建它。存在会报错。发生异常报错不会影响其他操作。
index:创建一个新文档或者替换一个现有的文档。
update:部分更新一个文档。
delete:删除一个文档。
metadata 中需要指定要操作的文档的_index 、 _type 和 _id,_index 、 _type也可以在url中指定
案例
create新增文档数据,在metadata中指定index以及type
POST /_bulk
{"create": {"_index": "shop2", "_type": "_doc", "_id": "2001"}}
{"id": "2001", "nickname": "name2001"}
{"create": {"_index": "shop2", "_type": "_doc", "_id": "2002"}}
{"id": "2002", "nickname": "name2002"}
{"create": {"_index": "shop2", "_type": "_doc", "_id": "2003"}}
{"id": "2003", "nickname": "name2003"}
// 最后一行也需要回车,不然会报错
create创建已有id文档,在url中指定index和type
POST /shop/_doc/_bulk
{"create": {"_id": "2003"}}
{"id": "2003", "nickname": "name2003"}
{"create": {"_id": "2004"}}
{"id": "2004", "nickname": "name2004"}
{"create": {"_id": "2005"}}
{"id": "2005", "nickname": "name2005"}
index创建,已有文档id会被覆盖,不存在的id则新增
POST /shop/_doc/_bulk
{"index": {"_id": "2004"}}
{"id": "2004", "nickname": "index2004"}
{"index": {"_id": "2007"}}
{"id": "2007", "nickname": "name2007"}
{"index": {"_id": "2008"}}
{"id": "2008", "nickname": "name2008"}
update跟新部分文档数据
POST /shop/_doc/_bulk
{"update": {"_id": "2004"}}
{"doc":{ "id": "3004"}} // 这里的含义是把后面的属性赋给doc文档的意思
{"update": {"_id": "2007"}}
{"doc":{ "nickname": "nameupdate"}}
delete批量删除
POST /shop/_doc/_bulk
{"delete": {"_id": "2004"}}
{"delete": {"_id": "2007"}}
综合批量各种操作
POST /shop/_doc/_bulk
{"create": {"_id": "8001"}}
{"id": "8001", "nickname": "name8001"}
{"update": {"_id": "2001"}}
{"doc":{ "id": "20010"}}
{"delete": {"_id": "2003"}}
{"delete": {"_id": "2005"}}
官文:https://www.elastic.co/guide/cn/elasticsearch/guide/current/bulk.html
ES集群
单机es可以用,没毛病,但是有一点我们需要去注意,就是高可用是需要关注的,一般我们可以把es搭建成集群,2台以上就能成为es集群了。集群不仅可以实现高可用,也能实现海量数据存储的横向扩展。
分片机制
每个索引可以被分片,就相当于吃披萨的时候被切了好几块,然后分给不同的人吃
- 假设索引shop有3个主分片;
- 每个主分片都包含索引的数据,由于目前是单机,所以副本分片是没有的,这个时候集群健康值显示为黄色。
- 副本分片是主分片的备份,主挂了,备份还是可以访问,这就需要用到集群了。
- 同一个分片的主与副本是不会放在同一个服务器里的,因为一旦宕机,这个分片就没了
前置操作
克隆原来单机的es,当克隆以后,es中的data目录,一定要清空,这里面包含了原先的索引库数据。
进入 /usr/local/elasticsearch-7.10.1
目录下,进入data目录
清除nodes目录及其他
配置集群
原来单节点配置如下
cluster.name: study-elasticsearch
node.name: es-node1
path.data: /usr/local/elasticsearch-7.10.1/data
path.logs: /usr/local/elasticsearch-7.10.1/logs
network.host: 0.0.0.0 #端口默认9200
cluster.initial_master_nodes: ["es-node1"]
在单节点配置的基础上做如下修改,没修改的地方保持不变,比如 path.data及path.logs
配置集群
修改elasticsearch.yml这个配置文件如下:
# 配置集群名称,保证每个节点的名称相同,如此就能都处于一个集群之内了,不同集群,不同的集群名称
cluster.name: es-cluster
# 每一个节点的名称,必须不一样
node.name: es-node1
# http端口(使用默认即可)
http.port: 9200
# 标志为可以被选举为master, 主节点,作用主要是用于来管理整个集群,负责创建或删除索引,
管理其他非master节点(相当于企业老总),所有节点都设置为true
node.master: true
# 数据节点,用于对文档数据的增删改查
node.data: true
# 集群列表
discovery.seed_hosts: ["192.168.3.70", "192.168.3.82", "192.168.3.83"]
# 启动的时候使用一个master节点
cluster.initial_master_nodes: ["es-node1"]
注意:集群配置文件中只有每一个节点名称不一样
more elasticsearch.yml | grep ^[^#] // 查看配置即可
配置好后,分别启动三台es即可,启动成功后,通过浏览器访问不同的ip+9200端口即可查看不同的节点信息,也可以通过head插件查看。 主节点标有✳,其余节点是●标志。
Elasticsearch集群脑裂现象探讨
什么是脑裂
如果发生网络中断或者服务器宕机,那么集群会有可能被划分为两个部分,各自有自己的master来管理,那么这就是脑裂。
脑裂解决方案
master主节点要经过多个master节点共同选举后才能成为新的主节点。就跟班级里选班长一样,并不是你1个人能决定的,需要班里半数以上的人决定。
解决实现原理:半数以上的节点同意选举,节点方可成为新的master。
discovery.zen.minimum_master_nodes=(N/2)+1
N为集群的中master节点的数量,也就是那些 node.master=true 设置的那些服务器节点总数。
ES 7.X
在最新版7.x中,minimum_master_node这个参数已经被移除了,这一块内容完全由es自身去管理,这样就避免了脑裂的问题,选举也会非常快。
es集群的读写原理
每次的协调节点都是不一定的
整合ElastSearch 6.4.3 👨❤️💋👨 SpringBoot 2.2.2
🧊创建SpringBoot工程,引入依赖
SpringBoot2.2.2支持的ElasticSearch的版本为6.4.3,因此需要对之前使用的es进行降低版本,这里降低到6.4.3。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
<!--<version>2.1.5.RELEASE</version>-->
<version>2.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
🩹配置application.yml
spring:
data:
elasticsearch:
cluster-name: study-elasticsearch
cluster-nodes: 192.168.1.187:9300 // 注意是9300端口,客户端使用的端口不是9200是9300
🧊Netty issue fix (解决启动报错)
创建ESConfig配置类
@Configuration
public class ESConfig {
/**
* 解决netty引起的issue
*/
@PostConstruct
void init() {
System.setProperty("es.set.netty.runtime.available.processors", "false");
}
}
编写测试用例
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Document(indexName = "stu", type = "_doc")
public class Stu {
/**
* @Id:作用在成员变量,标记一个字段为id主键;一般id字段或是域不需要存储也不需要分词;
*/
@Id
private Long stuId;
/**
其实不管我们将store值设置为true或false,elasticsearch都会将该字段存储到Field域中;但是他们的区别是什么?
store = false时,默认设置;那么给字段只存储在"_source"的Field域中;
store = true时,该字段的value会存储在一个跟_source平级的独立Field域中;同时也会存储在_source中,所以有两份拷贝。
那么我们在什么样的业务场景下使用store field功能?
*/
@Field(store = true)
private String name;
@Field(store = true)
private Integer age;
@Field(store = true)
private String desc;
@Field(store = true,type = FieldType.Keyword)
private String sign;
@Field(store = true)
private Float money;
public Long getStuId() {
return stuId;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public String getSign() {
return sign;
}
public void setSign(String sign) {
this.sign = sign;
}
public Float getMoney() {
return money;
}
public void setMoney(Float money) {
this.money = money;
}
public void setStuId(Long stuId) {
this.stuId = stuId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Stu{" +
"stuId=" + stuId +
", name='" + name + '\'' +
", age=" + age +
", desc='" + desc + '\'' +
", sign='" + sign + '\'' +
", money=" + money +
'}';
}
}
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class ESTest {
@Autowired
private ElasticsearchTemplate esTemplate;
/**
* 不建议使用 ElasticsearchTemplate 对索引进行管理(创建索引,更新映射,删除索引)
* 索引就像是数据库或者数据库中的表,我们平时是不会是通过java代码频繁的去创建修改删除数据库或者表的
* 我们只会针对数据做CRUD的操作
* 在es中也是同理,我们尽量使用 ElasticsearchTemplate 对文档数据做CRUD的操作
* 1. 属性(FieldType)类型不灵活
* 2. 主分片与副本分片数无法设置
*/
/**
* 新增/修改 索引
* 插入文档数据
*/
@Test
public void createIndexStu() {
Stu stu = new Stu();
stu.setAge(19);
stu.setName("spider Man");
stu.setStuId(1003L);
stu.setMoney(18.8f);
stu.setSign("i am spider man");
stu.setDesc("i am save man");
IndexQuery query = new IndexQueryBuilder().withObject(stu).build();
esTemplate.index(query);
}
/**
* 删除索引
*/
@Test
public void deleteIndexStu() {
esTemplate.deleteIndex(Stu.class);
}
/**
* 搜索文档
*/
@Test
public void searchStuDoc() {
// 分页操作
Pageable pageable = PageRequest.of(0, 2);
SearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("desc", "save man"))
.withPageable(pageable)
.build();
// 这个是包含分页信息的类
AggregatedPage<Stu> pagedStu = esTemplate.queryForPage(query, Stu.class);
// 获取总的分页数目
System.out.println("检索后的总分页数目为:" + pagedStu.getTotalPages());
List<Stu> stuList = pagedStu.getContent();
for (Stu s : stuList) {
System.out.println(s);
}
}
/**
* 高亮显示文本
*/
@Test
public void highlightStuDoc() {
String preTag = "<font color='red'>";
String postTag = "</font>";
Pageable pageable = PageRequest.of(0, 10);
SortBuilder sortBuilder = new FieldSortBuilder("money")
.order(SortOrder.DESC);
SortBuilder sortBuilderAge = new FieldSortBuilder("age")
.order(SortOrder.ASC);
SearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("desc", "save man"))
.withHighlightFields(new HighlightBuilder.Field("desc")
.preTags(preTag)
.postTags(postTag))
.withSort(sortBuilder)
.withSort(sortBuilderAge)
.withPageable(pageable)
.build();
AggregatedPage<Stu> pagedStu = esTemplate.queryForPage(query, Stu.class, new SearchResultMapper() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
List<Stu> stuListHighlight = new ArrayList<>();
SearchHits hits = response.getHits();
for (SearchHit h : hits) {
HighlightField highlightField = h.getHighlightFields().get("desc");
String description = highlightField.getFragments()[0].toString();
Object stuId = (Object)h.getSourceAsMap().get("stuId");
String name = (String)h.getSourceAsMap().get("name");
Integer age = (Integer)h.getSourceAsMap().get("age");
String sign = (String)h.getSourceAsMap().get("sign");
Object money = (Object)h.getSourceAsMap().get("money");
Stu stuHL = new Stu();
stuHL.setDesc(description);
stuHL.setStuId(Long.valueOf(stuId.toString()));
stuHL.setName(name);
stuHL.setAge(age);
stuHL.setSign(sign);
stuHL.setMoney(Float.valueOf(money.toString()));
stuListHighlight.add(stuHL);
}
if (stuListHighlight.size() > 0) {
return new AggregatedPageImpl<>((List<T>)stuListHighlight);
}
return null;
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> aClass) {
return null;
}
});
System.out.println("检索后的总分页数目为:" + pagedStu.getTotalPages());
List<Stu> stuList = pagedStu.getContent();
for (Stu s : stuList) {
System.out.println(s);
}
}
}
注意:一定要保证SpringBoot集成的data es版本和linux上的es版本大版本一致,要么都是6.x要么都用7.x
ElasticSearchRepository和ElasticSearchTemplate
Elasticsearch官方为Java提供了三种客户端API
- TransportClient:这种方式通过TCP与Elasticsearch服务进行交互。
- Java Low Level REST Client: 低级别的REST客户端,通过http与集群交互,用户需自己编组请求JSON串,及解析响应JSON串。兼容所有ES版本。
- Java High Level REST Client: 高级别的REST客户端,基于低级别的REST客户端,增加了编组请求JSON串、解析响应JSON串等相关api。使用的版本需要保持和ES服务端的版本一致,否则会有版本问题。
Spring Data Elasticsearch
Spring Data Elasticsearch作为Spring Data的一个子项目,封装了对EalsticSearch的客户端,现在最新版本是4.1.1,其内核也是由TransportClient变为在后续版本中采用Java High Level REST Client,其支持版本情况如下:
这里注意,在2.2.X的spring-data版本配套的还是6.X的es,2.3.X的spring-data就是7.x的es了
es6.x及es7.x均可以使用ESRepository及ElasticSearchTemplate方式进行搜索,大多数情况下,ESRepository可以满足查询要求,ElasticSearchTemplate更多是对ESRepository的补充,里面提供了一些更底层的方法。详细了解可见以下参考链接, 注意,ElasticsearchTemplate 与 ElasticsearchRestTemplate(Transport Client 替换为 RestHighLevelClient )是两个不同的类,推荐使用ElasticsearchRestTemplate。
在使用2.3.X以上的版本的spring-data时,采用rest-client客户端操作es时,es配置变为如下:
spring:
data:
elasticsearch:
repositories:
enabled: true
# cluster-nodes: 192.168.1.145:9300
# cluster-name: docker-cluster Transport客户端
elasticsearch:
rest:
uris: 121.89.195.81:9200 # rest-client客户端
参考链接:
www.zyiz.net/tech/detail…
cloud.tencent.com/developer/a…
www.jianshu.com/p/4ca5cc624…
blog.csdn.net/weixin_3989…
blog.csdn.net/yang1519784…
blog.csdn.net/yang1519784…
blog.csdn.net/yang1519784…
www.cnblogs.com/jing99/p/13…
blog.csdn.net/Zsigner/art…