Eleaticsearch是什么?
之前了解过的感觉就是像mysql一样可以存储数据,但是他是非结构化数据(NoSql 非结构化数据库,不仅仅可以存储结构化数据,也可以存储非结构化数据)使用的json一样的结构(看上去),他强就强在能够对库中的特定字段进行分词(mysql就做不到),并且根据匹配的相似度获得特定的分数(你搜索数据的相似度)可以根据相似度来排序; 分词是什么呢?就是例如 今天早上吃什么?可以分成 今天 ,早上,吃什么;之后你可以搜索今吃就会匹配到这个数据;如果是数据库呢?肯定做不到吧
看完官方文档之后发现上面说的都是搜索是它的一部分,但是这也是最核心的,它能够收集,聚合,丰富你的数据利用它的Logstash 和 Beats 他还有一个能够可视化数据的平台kibana;总之能够做到数据的收集与解析
DSL
Domain Specified Language :是es的特定语言;
eleaticsearch使用的是curl 的大致请求样式来对数据进行操作
POST:创建数据库 和 新增数据 也可以修改数据
DELETE:删除数据
PUT:建表
GET:查询数据
查看表结构
sql
GET user/_mapping
表的创建(Mapping)
那接下来我们想了解一下表结构 那该如何操作呢?
首先我们要知道,在这边的表结构其实使用mapping来表示的,我们想要创建特定的表结构就需要使用PUT
sql
PUT user
{
"mappings": {
"properties": {
"age": {"type": "integer"},
"name": {"type": "text"},
"ethnic": {"type": "keyword"}
}
}
}
我们创建了一个mapping的基本结构
age:数字类型
name:text 文本类型 可以做分词
ethnic: 关键字类型 不可用分割,不可做分词,必须全部符合才能查询到
如何查询和过滤出自己想要的结果?
首先我们要明确以下几点
1.在elasticsearch中查询的结果返回是会带有一定的分数的(Relevance scores),分数的高低取决于你要查询的结果的匹配程度;匹配度越高,那么分数就越高(分数是一个正浮点数类型) 这里我猜想可以根据分数来最后做一个排序 (每次查询都会将这个分数值放入"_score"这个字段中)
2.elasticsearch中,如果经常使用一个过滤器,那么它会帮你把这个过滤器缓存起来,提高性能
问题一
我们要过滤结果那么就一定要有匹配的条件,mysql中是用where语句后面写过滤条件的那么在这么边应该怎么写呢?
一般的过滤语句都是在bool的字段里面写;bool字段里面有must字段和filter字段
由名字可以判断
must:是必须拥有哪些字段
filter:是过滤哪些
其实俩个都差不多,只不过一个是有加入分数的计算中(must)一个并没有(filter)
如下例句 (如果想要直接查询所有数据可以 GET /文档名/_search)
css
GET /_search
{
"query": {
"bool": {
"must": [
{ "match": { "title": "Search" }},
{ "match": { "content": "Elasticsearch" }}
],
"filter": [
{ "term": { "status": "published" }},
{ "range": { "publish_date": { "gte": "2015-01-01" }}}
]
}
}
}
可以看到我们这边must里面又有一个字段
match:作用是匹配的字段(每个字段里面包含的内容);里面写的都是字段和需要有什么内容;(根据这个字段来进行评分)
filter 里面又有俩个字段(不会影响分数)
term:*???????????????????*暂定为确切百分百就这个单词的
range :作用是查询范围,里面写的都是字段和需要有什么内容
可以大致理一下上面的内容的规律
1.发送一个GET / (根目录) 请求 并且是查询(_search)
2.然后就是我们要开始查询的东西也就是query
3.我们应该怎么查?使用bool方式
4.我们应该使用什么模式来判断? must or filter等
5.我们选择的模式里面使用一般匹配,还是范围查找?
什么时候用匹配什么时候用过滤?
最后要排序的需要分数的(传输过来的 主要查询内容)使用匹配(像mysql中的like)
那些比如说范围,id等于多少啊,就像mysql中where后面的东西(除了like后面的)
注意:分数浮点数只有24位,如果超过这个正浮点数范围就会丢失精度
以上的各个属性的解释纯属 是个人的猜测
问题二
在mysql中有要查询出特定的字段那就需要在前面写字段就可以了那这个里面该如何操作呢?
答:使用"fields"属性
那排序呢?
答:使用"sort"属性
如下
sql
GET logs-my_app-default/_search
{
"query": {
"match_all": { }
},
"fields": [
"@timestamp"
],
"_source": false,
"sort": [
{
"@timestamp": "desc"
}
]
}
就可以查出特定的字段,并且排序好了
细说 bool
上面我们了解到了查询与过滤使用的语句都是在我们的bool中的,我们看看这个bool属性里面含有哪些属性的选择
must 类型:查询的字段的内容必须要存在在我们设置的范围内;然后将其进行计分
filter类型:查询的字段的内容必须要在我们设置的范围内;但是不参与计分(这个filter可能会被进行缓存)
should:查询的内容应该出现在文档中 (也会参与分数运算) 一般配合minimum_should_match 后面将
moust_not:查询到内容不能出现在文档中 (在过滤上下文中执行,直接就是忽略评分,可能被缓存,并且用了之后返回的分数结果为0)
上面产生了一个很严重的问题:should 和 must 有什么区别?
答:should是可以存在,也可以不存在;must是一定要存在
注意事项:当单独使用filter 不会产生分数;分数必须要使用must 或者 should的时候才会产生
如下
css
GET _search
{
"query": {
"bool": {
"filter": {
"term": {
"status": "active"
}
}
}
}
}
当你使用must里面用match_all的时候默认都会给文档分配为1分。
如下
css
GET _search
{
"query": {
"bool": {
"must": {
"match_all": {}
},
"filter": {
"term": {
"status": "active"
}
}
}
}
}
除了bool还有一个选项"constant_score"这个东西的用法和上面这个实例的结果是一样的(都是永远分数为1)
如下
css
GET _search
{
"query": {
"constant_score": {
"filter": {
"term": {
"status": "active"
}
}
}
}
}
注意:如果你要在一个must里面使用term(必须存在):如果要对text这种模糊字段进行精确查询的话,要指定子字段,指定精确的查询的索引(将子字段设置为精确的),例如titile.keyword
如何添加数据呢?
其实添加和创建表都是一样的,因为这个搜索引擎里面的表是很灵活的 ,灵活到什么程度?你直接建一个写一个文档(就是直接post 一个数据进去)就会自动帮你创建好一个这个文档的结构 ;并且如果你添加一个文档中没有的字段的时候,也会给你的这个文档中添加新这个字段
例如
我们新建一个数据,在此之前我们都没有进行创建这个文档结构(文档结构对应mysql中的表)但是他还是可以直接执行 (POST 文档名/类型)
bash
POST user/_doc
{
"name":"lc",
"age": 18
}
结果
bash
{
"_index" : "user",
"_type" : "_doc",
"_id" : "BDXBA4sBbCXNWpYJbPrE",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1, #表示创建成功
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
之后我们再添加一个属性并且添加记录
bash
POST user/_doc
{
"name":"lc",
"age": 18,
"ethnic":"汉"
}
结果:还是成功了并且看到这个文档返回的总数据有俩条(这就印证了我们上面所说的灵活性)
json
{
"_index" : "user",
"_type" : "_doc",
"_id" : "BTXGA4sBbCXNWpYJmPo5",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1, //成功
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
我们看一下表中的所有数据
查询所有的语句
sql
GET user/_search
{
"query": {
"match_all": {}
}
}
结果
json
{
"took" : 456,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "BDXBA4sBbCXNWpYJbPrE",
"_score" : 1.0,
"_source" : {
"name" : "lc",
"age" : 18
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "BTXGA4sBbCXNWpYJmPo5",
"_score" : 1.0,
"_source" : {
"name" : "lc",
"age" : 18,
"ethnic" : "汉"
}
}
]
}
}
如何修改数据呢?
我们可以还是可以使用post来做修改;
我们将name :lc 改为 name:wh
bash
POST user/_doc
{
"name":"wh",
"age": 18,
"ethnic":"汉"
}
最后的结果...好像不是和想象的一样啊(这还是多添加了一条啊)
json
{
"took" : 234,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "BDXBA4sBbCXNWpYJbPrE",
"_score" : 1.0,
"_source" : {
"name" : "lc",
"age" : 18
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "BTXGA4sBbCXNWpYJmPo5",
"_score" : 1.0,
"_source" : {
"name" : "lc",
"age" : 18,
"ethnic" : "汉"
}
},
//这是多添加了一条数据了
{
"_index" : "user",
"_type" : "_doc",
"_id" : "BjXLA4sBbCXNWpYJS_rl",
"_score" : 1.0,
"_source" : {
"name" : "wh",
"age" : 18,
"ethnic" : "汉"
}
}
]
}
}
我们想想数据库修改数据是不是要规定特定的id才能修改那么这里是是否也是一样
修改为如下 (POST 文档吗/文档类型/拿一条数据的id)
bash
POST user/_doc/BTXGA4sBbCXNWpYJmPo5
{
"name":"wh",
"age": 18,
"ethnic":"汉"
}
结果: 果然成功了我们看到第二条lc变成了wh了
json
{
"took" : 349,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "BDXBA4sBbCXNWpYJbPrE",
"_score" : 1.0,
"_source" : {
"name" : "lc",
"age" : 18
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "BjXLA4sBbCXNWpYJS_rl",
"_score" : 1.0,
"_source" : {
"name" : "wh",
"age" : 18,
"ethnic" : "汉"
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "BTXGA4sBbCXNWpYJmPo5",
"_score" : 1.0,
"_source" : {
"name" : "wh",
"age" : 18,
"ethnic" : "汉"
}
}
]
}
}
如何删除数据呢?
使用DELETE 就可以直接删除 如下
sql
DELETE user
返回结果
json
{
"acknowledged" : true
}
可以自行验证一下 使用搜索GET /user/_serch 语句就会发现报错了
如果你想要删除单条数据也是跟上面修改数据其实是差不多的只不过是把POST改成DELETE根据id删除,不在演示
ECS
一些专门用来存储指标类的数据 如果按照官方的操作如下就是创建
有@timestamp 和 event 的
bash
POST logs-my_app-default/_doc
{
"@timestamp": "2099-05-06T16:21:15.000Z",
"event": {
"original": "192.0.2.42 - - [06/May/2099:16:21:15 +0000] "GET /images/bg.jpg HTTP/1.0" 200 24736"
}
}
他不能够做一些正常的根据id查询的操作
只能用EQL语法来查询才可以,其他的不可用
分词器
能够将text的类型的数据转换为一个个单个的词,可以方便搜索
POST _analyze 是测试分词器分词结果的语句
如下
json
POST _analyze
{
"analyzer": "whitespace",
"text": "The quick brown fox."
}
返回的结果
json
{
"tokens": [
{
"token": "The",
"start_offset": 0,
"end_offset": 3,
"type": "word",
"position": 0
},
{
"token": "quick",
"start_offset": 4,
"end_offset": 9,
"type": "word",
"position": 1
},
{
"token": "brown",
"start_offset": 10,
"end_offset": 15,
"type": "word",
"position": 2
},
{
"token": "fox.",
"start_offset": 16,
"end_offset": 20,
"type": "word",
"position": 3
}
]
}
可以看出 他们默认是安空格来进行分词的,并且每个分词玩了之后都会有开始的位置和结束的位置等其他的标记
所以上面的运行的语句是什么意思呢?
其实就是分词的一个分词测试器(测试功能);可以看看最后分出来的词是不是你想要的结果
"analyzer": 使用的是什么分词器 "whitespace"字面意思就是只要有空格的就给他分词
还有一个就是标准的分词 (很少使用,了解即可)
如下
json
POST _analyze
{
"tokenizer": "standard",
"filter": [ "lowercase", "asciifolding" ],
"text": "Is this déja vu?"
}
返回结果
json
{
"tokens" : [
{
"token" : "is",
"start_offset" : 0,
"end_offset" : 2,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "this",
"start_offset" : 3,
"end_offset" : 7,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "deja",
"start_offset" : 8,
"end_offset" : 12,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "vu",
"start_offset" : 13,
"end_offset" : 15,
"type" : "<ALPHANUM>",
"position" : 3
}
]
}
可以看到结果都变成小写了,还有特殊的字符也被取消掉了
关键词分词器
就是不分词 返回的结果还是这个 "Is this déja vu?" 整句话直接当作专业术语
json
POST _analyze
{
"tokenizer": "keyword",
"text": "Is this déja vu?"
}
比如说你的id是不可分的,或者你的标签不可分就可以使用这种关键词分词器
那这些分词器好像对我们国内都不是很友好(中文都分不了词),所以我们可以使用插件
如下
IK分词器(ES插件)
是我们国内的中文的分词器
主要有以下几种分词方式:
Analyzer: ik_smart
, ik_max_word
Tokenizer: ik_smart
, ik_max_word
其实这俩个都是差不多的只是名字叫的不一样
json
POST _analyze
{
"analyzer": "ik_smart",
"text": "抽象的一逼,我真的服了"
}
分词的结果
json
{
"tokens" : [
{
"token" : "抽象",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "的",
"start_offset" : 2,
"end_offset" : 3,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "一",
"start_offset" : 3,
"end_offset" : 4,
"type" : "TYPE_CNUM",
"position" : 2
},
{
"token" : "逼",
"start_offset" : 4,
"end_offset" : 5,
"type" : "CN_CHAR",
"position" : 3
},
{
"token" : "我",
"start_offset" : 6,
"end_offset" : 7,
"type" : "CN_CHAR",
"position" : 4
},
{
"token" : "真的",
"start_offset" : 7,
"end_offset" : 9,
"type" : "CN_WORD",
"position" : 5
},
{
"token" : "服了",
"start_offset" : 9,
"end_offset" : 11,
"type" : "CN_WORD",
"position" : 6
}
]
}
ik_max_word:相比上面的ik_smart分的词的粒度会跟大一些;搜索的概率之后的命中率可以增加
如下
json
{
"tokens" : [
{
"token" : "抽象",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "的",
"start_offset" : 2,
"end_offset" : 3,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "一",
"start_offset" : 3,
"end_offset" : 4,
"type" : "TYPE_CNUM",
"position" : 2
},
{
"token" : "逼",
"start_offset" : 4,
"end_offset" : 5,
"type" : "CN_CHAR",
"position" : 3
},
{
"token" : "我",
"start_offset" : 6,
"end_offset" : 7,
"type" : "CN_CHAR",
"position" : 4
},
{
"token" : "真的",
"start_offset" : 7,
"end_offset" : 9,
"type" : "CN_WORD",
"position" : 5
},
{
"token" : "服了",
"start_offset" : 9,
"end_offset" : 11,
"type" : "CN_WORD",
"position" : 6
}
]
}
差别就在于 如果你输入 小黑子 ik_smart 给你返回的是 "小" "黑子"
ik_max_word: "小黑" "黑子"
所以 ik_max_word 比较适合在于存入的数据进行分词
ik_smart 更适合用户输入的数据进行分词,然后去查询es里面的数据
打分机制
数据库里有如下的操作
今天中午吃么
今天早上吃的不错
今天晚上不吃了
如果我们搜索 今天
那么最高的分数是 第一条 因为 他里面有今天 并且字数更少
第一条是 6字里面匹配2个 第二条是 8字里面匹配2个 第三条是 7字里面匹配2个
如果我们输入今天中午吃什么
操作过程 分词器将他分为 今天 中午 吃 什么 最后是 1 > 3 > 2
自定义词典
TODO
java 操作ES
1.官方的操作方式(官网看,不怎么推荐)
2.spring Data Eleaticsearch(使用方便)
自定义方法:能根据你的方法的名字取查询特定的数据
例如
arduino
List<Person> findByLastnameIgnoreCase(String lastname);//忽略大小写根据lastname查询
就能直接根据你的名字做这种查询,返回的结果就是忽略大小写根据lastname查询
建表(建立索引)
首先要在java中操作es那肯定要先有表
我们可以根据sql来进行设计表;因为一般我们都是为了让他能所处更多的数据的目的去使用es
sql
create table if not exists post
(
id bigint auto_increment comment 'id' primary key,
title varchar(512) null comment '标题',
content text null comment '内容',
tags varchar(1024) null comment '标签列表(json 数组)',
thumbNum int default 0 not null comment '点赞数',
favourNum int default 0 not null comment '收藏数',
userId bigint not null comment '创建用户 id',
createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
isDelete tinyint default 0 not null comment '是否删除',
index idx_userId (userId)
) comment '帖子' collate = utf8mb4_unicode_ci;
id:我们可以不在es中添加
title:我们需要将他设置成text并且指定分词器(因为可能会根据这个搜索)
content:我们也需要将其设置成text指定分词器(同上)
tags:标签可以为keyword因为一般标签都是直接搜索一整个的
点赞数和收藏数可以不放入es中,因为这些数据是频繁变化的,并且也一般情况下也不会根据这个来做搜索
userId:keyword不需要分词 不会根据userId来模糊搜索数据
创建时间,修改时间,是否删除 我们最好也加入到es使用keyword 因为这些可以作为搜索条件有些时候需要这些字段进行搜索
最后的表结构
bash
PUT /post_v1
{
"aliases": {
"post": {}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"content": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"tags": {
"type": "keyword"
},
"userId": {
"type": "keyword"
},
"createTime": {
"type": "date"
},
"updateTime": {
"type": "date"
},
"isDelete": {
"type": "keyword"
}
}
}
}
我们可以看到那些需要分词器的text我们里面都加了额外很多的设置
"analyzer": 我们将数据存入es的时候分词需要分的更散一些,让用户能够更大概率搜索到 所以使用"ik_max_word" "search_analyzer": 用户搜索的时候会使用这个来进行分词,没必要打的更散,而是更贴切用户想要的搜索,所以使用"ik_smart"
其次用户也可能搜索这些text类型的数据会想要关键字搜索 ,像这样的需求应该怎么实现呢?
就是有时候不拆开来,所以可以对text已经指定分词的属性来再加一个属性
如下
json
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
第二个属性就是keyword,关键词 并且 字符数要在256以内(算是对系统的一种保护,字符数太大了es承受的压力就特别大)
{ "aliases": { "post": {} },
这个一定要加上以后说不定数据迁移有用算是一个别名
PUT /post_v1 最好是加上v1算是可以看迁移的次数,像一个标记一样
引入依赖
如果你是ES是7.17的就使用 2.7.2的springboot 然后直接引入(各个版本的依赖可以去官网看)
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
mysql有特定的实体类;那es肯定也是会有特定的实体类如下
typescript
//创建的文档的名字,可以用别名
@Document(indexName = "post")
@Data
public class PostEsDTO implements Serializable {
//设置的时间格式
private static final String DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
/**
* id @Id自动生成id,能够与es里面的对应并且注入其中
*/
@Id
private Long id;
/**
* 标题
*/
private String title;
/**
* 内容
*/
private String content;
/**
* 标签列表
*/
private List<String> tags;
/**
* 创建用户 id
*/
private Long userId;
/**
* 创建时间 index是否将字段作为索引字段 store是否存储 type类型,foramt转换格式,pattern存入的格式
*/
@Field(index = false, store = true, type = FieldType.Date, format = {}, pattern = DATE_TIME_PATTERN)
private Date createTime;
/**
* 更新时间
*/
@Field(index = false, store = true, type = FieldType.Date, format = {}, pattern = DATE_TIME_PATTERN)
private Date updateTime;
/**
* 是否删除
*/
private Integer isDelete;
private static final long serialVersionUID = 1L;
private static final Gson GSON = new Gson();
/**
* 对象转包装类
*
* @param post
* @return
*/
public static PostEsDTO objToDto(Post post) {
if (post == null) {
return null;
}
PostEsDTO postEsDTO = new PostEsDTO();
BeanUtils.copyProperties(post, postEsDTO);
String tagsStr = post.getTags();
if (StringUtils.isNotBlank(tagsStr)) {
postEsDTO.setTags(GSON.fromJson(tagsStr, new TypeToken<List<String>>() {
}.getType()));
}
return postEsDTO;
}
/**
* 包装类转对象
*
* @param postEsDTO
* @return
*/
public static Post dtoToObj(PostEsDTO postEsDTO) {
if (postEsDTO == null) {
return null;
}
Post post = new Post();
BeanUtils.copyProperties(postEsDTO, post);
List<String> tagList = postEsDTO.getTags();
if (tagList.size() > 0) {
post.setTags(GSON.toJson(tagList));
}
return post;
}
}
springboot 配置如下
yaml
spring:
elasticsearch:
uris: http://localhost:9200
username:
password:
只要设置好了就可以进行连接
如何调用?
1.可以创建一个接口实现下面这个类,里面有最简单的增删改查,用来自定义方法 根据名字直接查询 (返回结果直接)
public interface PostEsDao extends ElasticsearchRepository<PostEsDTO, Long> 做一些简单的增删改查,自己定义(返回结果很完整)
2.可以用springboot给我集成好的
java
@Resource
private ElasticsearchRestTemplate elasticsearchRestTemplate;
可以直接调用里面的封装好的增删改查,里面的封装的增上改查更加的完善
使用第一种方式增删改查
创建接口继承 ElasticsearchRepository<PostEsDTO, Long>
增
如果不设置id这种方式获得不了存进去的id是什么
scss
@Test
void testAdd() {
PostEsDTO postEsDTO = new PostEsDTO();
postEsDTO.setId(1L);
postEsDTO.setTitle("测试ES use postEsDao");
postEsDTO.setContent("测试成功");
postEsDTO.setTags(Arrays.asList("java", "python"));//可以直接list存入es,es可以将其分成集合的形式
postEsDTO.setUserId(1L);
postEsDTO.setCreateTime(new Date());
postEsDTO.setUpdateTime(new Date());
postEsDTO.setIsDelete(0);
postEsDao.save(postEsDTO);
System.out.println(postEsDTO.getId());
}
如果设置id
bash
{
"_index" : "post_v1",
"_type" : "_doc",
"_id" : "1", //es 给我们默认设置的id就会是我们存入的id
"_score" : 1.0,
"_source" : {
"_class" : "com.yupi.yuso.model.dto.post.PostEsDTO",
"id" : 1, //并且这里也有一个id
"title" : "测试ES use postEsDao",
"content" : "测试成功",
"tags" : [
"java",
"python"
],
"userId" : 1,
"createTime" : "2023-10-07T06:16:17.497Z",
"updateTime" : "2023-10-07T06:16:17.497Z",
"isDelete" : 0
}
},
如果不设置id
bash
{
"_index" : "post_v1",
"_type" : "_doc",
"_id" : "XXXKCIsBYGkPbzzf0a1J",//并且es给我们随机设置了一个id 还是字符串类型
"_score" : 1.0,
"_source" : { //source里面没有id字段
"_class" : "com.yupi.yuso.model.dto.post.PostEsDTO",
"title" : "测试ES use postEsDao",
"content" : "测试成功",
"tags" : [
"java",
"python"
],
"userId" : 1,
"createTime" : "2023-10-07T06:19:51.443Z",
"updateTime" : "2023-10-07T06:19:51.443Z",
"isDelete" : 0
}
},
注意:es中_开头的字段是系统默认的字段,默认会给你添加进去的,如果不指定就会自动生成
根据ID查询
此时只能传入Long的id 因为我们指定了 <PostEsDTO, Long>是Long
ini
Optional<PostEsDTO> postEsDTO = postEsDao.findById(1L);
下面这俩条是我们自定义的可以实现查询的效果
scss
List<PostEsDTO> findByUserId(Long userId);
List<PostEsDTO> findByTitle(String title);
但是我们发现会出现一个问题
Failed to convert from type [java.lang.String] to type [java.lang.Long] for value 'XXXKCIsBYGkPbzzf0a1J'; nested exception is java.lang.NumberFormatException: For input string: "XXXKCIsBYGkPbzzf0a1J"
原因就是因为之前我们没有给他设置id的时候存入进去了值,默认就变成字符串了这个存入的id 如上面讲的不设置id存入es,这就会导致出现这个问题,id的类型都不一样了(去dev tool 将这些数据删掉)
根据title查询
ini
List<PostEsDTO> postEsDTOS = postEsDao.findByTitle("测试use");
根据用户ID查询
ini
List<PostEsDTO> postEsDaoTestList = postEsDao.findByUserId(1L);
分页查询结果
PageRequest.of(0, 5, Sort.by("createTime"))要传入Pageable 使用PageRequest进行封装
ini
Page<PostEsDTO> PostPage = postEsDao.findAll(
PageRequest.of(0, 5, Sort.by("createTime"))); //当前页的第一个数据,第二个是当前页的数据多少个
List<PostEsDTO> postList = PostPage.getContent();
Optional<PostEsDTO> byId = postEsDao.findById(1L);
System.out.println(byId);
System.out.println(postList);
第二种方式
适合复杂的条件查询
ini
public Page<Post> searchFromEs(PostQueryRequest postQueryRequest) {
Long id = postQueryRequest.getId();
Long notId = postQueryRequest.getNotId();
String searchText = postQueryRequest.getSearchText();
String title = postQueryRequest.getTitle();
String content = postQueryRequest.getContent();
List<String> tagList = postQueryRequest.getTags();
List<String> orTagList = postQueryRequest.getOrTags();
Long userId = postQueryRequest.getUserId();
// es 起始页为 0
long current = postQueryRequest.getCurrent() - 1;
long pageSize = postQueryRequest.getPageSize();
String sortField = postQueryRequest.getSortField();
String sortOrder = postQueryRequest.getSortOrder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 过滤 搜出来的一定要是isDelete是0的
boolQueryBuilder.filter(QueryBuilders.termQuery("isDelete", 0));
if (id != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("id", id));
}
if (notId != null) {
boolQueryBuilder.mustNot(QueryBuilders.termQuery("id", notId));
}
if (userId != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("userId", userId));
}
// 必须包含所有标签
if (CollectionUtils.isNotEmpty(tagList)) {
for (String tag : tagList) {
boolQueryBuilder.filter(QueryBuilders.termQuery("tags", tag));
}
}
// 包含任何一个标签即可
if (CollectionUtils.isNotEmpty(orTagList)) {
BoolQueryBuilder orTagBoolQueryBuilder = QueryBuilders.boolQuery();
for (String tag : orTagList) {
orTagBoolQueryBuilder.should(QueryBuilders.termQuery("tags", tag));
}
orTagBoolQueryBuilder.minimumShouldMatch(1);
boolQueryBuilder.filter(orTagBoolQueryBuilder);
}
// 按关键词检索
if (StringUtils.isNotBlank(searchText)) {
boolQueryBuilder.should(QueryBuilders.matchQuery("title", searchText));
boolQueryBuilder.should(QueryBuilders.matchQuery("content", searchText));
boolQueryBuilder.minimumShouldMatch(1);
}
// 按标题检索
if (StringUtils.isNotBlank(title)) {
boolQueryBuilder.should(QueryBuilders.matchQuery("title", title));
boolQueryBuilder.minimumShouldMatch(1);
}
// 按内容检索
if (StringUtils.isNotBlank(content)) {
boolQueryBuilder.should(QueryBuilders.matchQuery("content", content));
boolQueryBuilder.minimumShouldMatch(1);
}
// 排序
SortBuilder<?> sortBuilder = SortBuilders.scoreSort();
if (StringUtils.isNotBlank(sortField)) {
sortBuilder = SortBuilders.fieldSort(sortField);
sortBuilder.order(CommonConstant.SORT_ORDER_ASC.equals(sortOrder) ? SortOrder.ASC : SortOrder.DESC);
}
// 分页
PageRequest pageRequest = PageRequest.of((int) current, (int) pageSize);
// 构造查询
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
.withPageable(pageRequest).withSorts(sortBuilder).build();
SearchHits<PostEsDTO> searchHits = elasticsearchRestTemplate.search(searchQuery, PostEsDTO.class);
Page<Post> page = new Page<>();
page.setTotal(searchHits.getTotalHits());
List<Post> resourceList = new ArrayList<>();
// 查出结果后,从 db 获取最新动态数据(比如点赞数)
if (searchHits.hasSearchHits()) {
List<SearchHit<PostEsDTO>> searchHitList = searchHits.getSearchHits();
List<Long> postIdList = searchHitList.stream().map(searchHit -> searchHit.getContent().getId())
.collect(Collectors.toList());
// 从数据库中取出更完整的数据
List<Post> postList = baseMapper.selectBatchIds(postIdList);
if (postList != null) {
Map<Long, List<Post>> idPostMap = postList.stream().collect(Collectors.groupingBy(Post::getId));
postIdList.forEach(postId -> {
if (idPostMap.containsKey(postId)) {
resourceList.add(idPostMap.get(postId).get(0));
} else {
// 从 es 清空 db 已物理删除的数据
String delete = elasticsearchRestTemplate.delete(String.valueOf(postId), PostEsDTO.class);
log.info("delete post {}", delete);
}
});
}
}
page.setRecords(resourceList);
return page;
}
主要讲一下第二步BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); 构建查询的语句
scss
boolQueryBuilder.filter(QueryBuilders.termQuery("isDelete", 0));
if (id != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("id", id));
}
if (notId != null) {
boolQueryBuilder.mustNot(QueryBuilders.termQuery("id", notId));
}
if (userId != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("userId", userId));
}
// 必须包含所有标签
if (CollectionUtils.isNotEmpty(tagList)) {
for (String tag : tagList) {
boolQueryBuilder.filter(QueryBuilders.termQuery("tags", tag));
}
}
// 包含任何一个标签即可
if (CollectionUtils.isNotEmpty(orTagList)) {
BoolQueryBuilder orTagBoolQueryBuilder = QueryBuilders.boolQuery();
for (String tag : orTagList) {
orTagBoolQueryBuilder.should(QueryBuilders.termQuery("tags", tag));
}
orTagBoolQueryBuilder.minimumShouldMatch(1);
boolQueryBuilder.filter(orTagBoolQueryBuilder);
}
// 按关键词检索
if (StringUtils.isNotBlank(searchText)) {
boolQueryBuilder.should(QueryBuilders.matchQuery("title", searchText));
boolQueryBuilder.should(QueryBuilders.matchQuery("content", searchText));
boolQueryBuilder.minimumShouldMatch(1);
}
// 按标题检索
if (StringUtils.isNotBlank(title)) {
boolQueryBuilder.should(QueryBuilders.matchQuery("title", title));
boolQueryBuilder.minimumShouldMatch(1);
}
// 按内容检索
if (StringUtils.isNotBlank(content)) {
boolQueryBuilder.should(QueryBuilders.matchQuery("content", content));
boolQueryBuilder.minimumShouldMatch(1);
}
// 排序
SortBuilder<?> sortBuilder = SortBuilders.scoreSort();
if (StringUtils.isNotBlank(sortField)) {
sortBuilder = SortBuilders.fieldSort(sortField);
sortBuilder.order(CommonConstant.SORT_ORDER_ASC.equals(sortOrder) ? SortOrder.ASC : SortOrder.DESC);
}
// 分页
PageRequest pageRequest = PageRequest.of((int) current, (int) pageSize);
// 构造查询
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
.withPageable(pageRequest).withSorts(sortBuilder).build();
SearchHits<PostEsDTO> searchHits = elasticsearchRestTemplate.search(searchQuery, PostEsDTO.class);
1.当我们要使用不许包含哪些数据的适合使用
filter(为什么不用must?因为must会改变你要查数据的评分)
less
boolQueryBuilder.filter(QueryBuilders.termQuery("tags", tag));
2.当我们要让其中有部分标签就行的话我们就可以使用should
less
orTagBoolQueryBuilder.should(QueryBuilders.termQuery("tags", tag));
我们发现有一个这个我们好像没有接触过
boolQueryBuilder.minimumShouldMatch(1);
他的作用就是如果上面这些只要满足其中一个(可能有context,title,tags)匹配上就可以返回
最全面的查询语句
建议在java中写es查询的时候,现在es把查询语句构建好,然后用java语句翻译出来
最后我们查出的所有数据做解析
ini
SearchHits<PostEsDTO> searchHits = elasticsearchRestTemplate.search(searchQuery, PostEsDTO.class);
Page<Post> page = new Page<>();
page.setTotal(searchHits.getTotalHits());
List<Post> resourceList = new ArrayList<>();
// 查出结果后,从 db 获取最新动态数据(比如点赞数)
if (searchHits.hasSearchHits()) {
List<SearchHit<PostEsDTO>> searchHitList = searchHits.getSearchHits();
List<Long> postIdList = searchHitList.stream().map(searchHit -> searchHit.getContent().getId())
.collect(Collectors.toList());
// 从数据库中取出更完整的数据
List<Post> postList = baseMapper.selectBatchIds(postIdList);
if (postList != null) {
Map<Long, List<Post>> idPostMap = postList.stream().collect(Collectors.groupingBy(Post::getId));
postIdList.forEach(postId -> {
if (idPostMap.containsKey(postId)) {
resourceList.add(idPostMap.get(postId).get(0));
} else {
// 从 es 清空 db 已物理删除的数据
String delete = elasticsearchRestTemplate.delete(String.valueOf(postId), PostEsDTO.class);
log.info("delete post {}", delete);
}
});
}
}
page.setRecords(resourceList);
return page;
}
一般将静态需要查找的数据放入es,动态的数据再去重mysql中查(所有的id查询),进行俩次查询操作,最后返回给前端
数据同步
一般我们做一些存储数据,需要用到俩种第三方软件的时候就需要考虑数据的一致性,例如数据库与redis,还有我们的现在的ES,如果在程序运行过程中,总会有出现一些特殊的事情,是的我们俩个存数据的软件中出现数据存储不同的情况,就可能导致很严重的后果,所以我们就需要做数据同步,数据的同步的方案有很多种,没有完美的方案,每个项目都有每个项目适合方案,要看情况分析。
一.定时任务
1.定时任务,使用springboot的@Scheduled()可以完成,定时去数据库中查看是否有更新了数据,然后将更新的数据存入ES,因为ES存入的适合如果id是相同的话,数据不同就会修改数据,而不是新增了所以没事
2.或者可以查出数据库中的所有数据,然后直接全部插入ES
优点:不需要第三方资源,操作简单
缺点:数据可能会一定时间内不一致, 一段时间内数据不一致不影响程序
二.双写
在写入mysql的时候同时写入es,删除修改的时候也是同理, 但是这个过程中在写入ES的时候很有可能会数据丢失,所以需要配合定时任务 + 日志 + 告警系统
三.Logstash数据同步管道
一般配合kafka消息队列 + beats采集器
四.Canal
可以订阅数据库流水的同步方式
优点:实时同步,实时性非常强
alibaba/canal: 阿里巴巴 MySQL binlog 增量订阅&消费组件 (github.com)
MySQL主备复制原理
- MySQL master 将数据变更写入二进制日志( binary log,每次增删改查都会往里面写一些相关的数据,其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看)
- MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)
- MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据
canal 工作原理
- canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
- MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
- canal 解析 binary log 对象(原始为 byte 流)
脚本启动失败
可以修改脚本里面的配置
如下设置javahome的位置
启动后去官方找java的maven和客户端的实例代码
最后的效果就是每次数据库更新cannel都会知道修改前和修改后的信息,我们只需要在修改后的信息插入到es中就行