Elastic Stack

125 阅读12分钟

问题:搜索不够灵活,搜索优化!!!
比如:‘’鱼皮rapper‘’无法搜到“ 鱼皮是rapper ”,因为数据库中的like是包含查询
需要分词

官网:www.elastic.co/cn/
包含了数据的整合 = > 提取 => 存储 => 使用,一整套!
beats:从各种不同类型的文件/应用来 采集数据 a, b, c, d, e, aa, bb, cc
Logstash:从多个采集器或数据源来抽取/转换数据,向es输送, aa, bb, cc
elasticsearch:存储,查询数据
kibana:可视化es的数据

安装ES

elasticsearch:www.elastic.co/guide/en/el…
kibana:www.elastic.co/guide/en/ki…

只要是一套技术,所有版本必须一致!!!此处用7.17

安装:
www.elastic.co/guide/en/el…
www.elastic.co/guide/en/ki…
点击下载压缩包
artifacts.elastic.co/downloads/e…
artifacts.elastic.co/downloads/k…

Elasticsearch概念

你就把当MySQL一样的数据库。
Index索引 => MySQL里的表(table)
建表、增删改查(查询需要花费的时间最多)
用客户端去调用ElasticSearch(3种)
语法:SQL、代码的方法(4种语法)

ES相比于MySQL,能够自动帮我们做分词,能够非常高效、灵活的查询内容。

索引(倒排索引)

正向索引:理解为书籍的目录,可以快速帮你找到对应的内容(怎么根据页码找到对应的文章)
倒排索引:

文章A:你好,我是rapper
文章B:鱼皮你好,我是coder
怎么根据内容找到文章,可以构建倒排索引。

进行分词
你好,我是, rapper
鱼皮,你好,我是coder

构建倒排索引:

内容id
你好文章A,B
我是文章A,B
rapper文章A
鱼皮文章B
coder文章B

用户搜:“鱼皮rapper”
ES先切词:鱼皮,rapper
去倒排索引表找对应的文章

ES的几种调用方式

1)restful api调用(http请求)

GET 请求:http://localhost:9200/
curl 可以模拟发送请求:curl -X GET "localhost:9200/?pretty"
image.png
image.png
ES的启动端口:
1)9200 :给外部用户(客户端)的端口
2)9300 :给内部集群通信的(外部调用不了的)

2)kibana devtools

自由的对ES进行操作(本质也是restful api)

3)客户端调用

Java客户端、go客户端

ES的语法

Mapping

理解为数据的表结构,有哪些字段,字段类型
ES支持动态mapping,表结构可以动态改变,而不像MySQL一样必须手动建表,没有的字段就不能插入
显示创建mapping:

GET user/_mapping

PUT user
{
	"mappings":{
  "properties":{
  "age":   { "type": "integer" },
  "email": { "type": "keyword" },
  "name":  { "type": "text" }
   }
  }
}

DSL

json格式好理解,和http请求最兼容,应用最广,也是我个人比较推荐的

建表、插入数据
POST 建表插入数据、post指表名(文档名字)、_doc指类型(索引/文档)
POST post/_doc
{
  "title":"鱼皮",
  "desc": "鱼皮的描述"
}

查询

DSL:语法:www.elastic.co/guide/en/el…(忘了就查,不用背)

GET logs-my_app-default/_search
{
  "query": {
    "match_all": { }
  },
  "sort": [
    {
      "@timestamp": "desc"
    }
  ]
}

根据id查询

GET post/_doc/n38dFIcBbRgQsiHiiyuH

修改

POST post/_doc/n38dFIcBbRgQsiHiiyuH
{
  "title":"鱼ssss皮",
  "desc": "鱼皮的描述"
}

删除

DELETE _data_stream/logs-my_app-default

EQL

专门ESC文档(标准指标文档)的数据的语法,更加规范,但只适用于特定场景
www.elastic.co/guide/en/el…

POST my_event/_doc
{
  "title":"鱼皮",
  "@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"
  }
}

GET /my_event/_eql/search
{
  "query": """
    any where title == "鱼皮"
  """
}

SQL

www.elastic.co/guide/en/el…
学习成本低,但是可能需要额外插件支持、性能较差

POST /_sql?format=txt
{
  "query": "SELECT * FROM post where title like '%鱼%'"
}

Painless Scripting languge

编程式取值,更加灵活,但是学习成本高

第四期

1.继续讲ElasticStack的概念
2.学习用Java来调用ElasticSearch
3.使用ES来优化聚合搜索接口
4.已有的DB的数据和ES数据同步(增量,全量;实时,非实时)
5.jemeter压力测试
6.保障接口稳定性
7.其他的扩展思路

ElasticStack概念

ES索引(Index) => 表
ES field(字段)=> 列
倒排索引
调用语法(DSL、EQL、SQL等)
Mapping

  • 自动生成mapping
  • 手动指定mapping

分词器

分词的一种规则

内置分词器:www.elastic.co/guide/en/el…
空格分词器:whitespace,结果:The、 quick、 brown、 fox.

POST _analyze
{
  "analyzer": "whitespace",
  "text":     "The quick brown fox."
}

标准分词器:filter 过滤条件 结果:is this deja vu

POST _analyze
{
  "tokenizer": "standard",
  "filter":  [ "lowercase", "asciifolding" ],
  "text":      "Is this déja vu?"
}

关键词分析器:整句话当作分词,就是不分词

POST _analyze
{
  "tokenizer": "keyword",
  "text":      "The quick brown fox."
}

IK分词器(ES插件)

中文友好:github.com/medcl/elast…
(注意版本一致,不一致的话就下载最近的一个版本去修改他的properties文件为相同版本即可)
下载地址:github.com/medcl/elast…

思考:怎么让ik分出自己想要的词?
回答:自定义词典

ik_smart和ik_max_word的区别?举例:“小黑子”
ik_smart是只能分词,尽量选择最像一个词的拆分方式,比如:“小”,“黑子”
ik_max_word尽可能的分词,可以包括组合词,比如:“小黑”,“黑子”

打分机制

有3条内容:
1.鱼皮是狗
2.鱼皮是小黑子
3.我是小黑子

用户搜索:
1.鱼皮,第一条分数最高,因为第一条匹配了关键词,而且更短(匹配比例更大)
2.鱼皮小黑子 => 鱼皮、小、黑子 => 2>3>1

打分机制原理:
参考文章:blog.csdn.net/weixin_4170…
官方参考文档:www.elastic.co/guide/en/el…

ES调用方式

3种:
1.HTTP Restful调用
2.kibana操作(dev tools)
3.客户端调用(Java操作)

Java 操作 ES

3种方式:
1)ES官方的 Java API
www.elastic.co/guide/en/el…
快速开始:www.elastic.co/guide/en/el…

2)ES以前的官方 Java API, HighLeveIRestClient(已废弃,不建议用)

3)Spring Data Elasticsearch(推荐)

spring-data系列:spring提供的操作数据的框架
spring-data-redis:操作redis的一套方法
spring-data-mongodb:操作mongodb的一套方法
spring-data-elasticsearch:操作elastsearch的一套方法

官方文档:docs.spring.io/spring-data…

自定义方法:用户可以指定接口的方法名称,框架帮你自动生成查询

用ES实现搜索接口

1)建表(建立索引)

数据库表结构:

-- 帖子表
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;

ES Mapping:
id(可以不放到字段设置里)
ES中,尽量存放用户需要筛选(搜索)的数据

aliases:别名(为了后续方便数据迁移)
字段类型是text,这个字段是可被分词的、可模糊查询的;而如果是keyword,只能完全匹配、精确查询。

analyzer(存储时生效的分词器):ik_max_word,拆的更碎、索引更多,更有可能被搜出来
search_analyzer(查询时生效的分词器):用ik_smart,跟偏向于用户想搜的分词
如果想要 text 类型的分词字段也支持精确查询,可以创建keyword类型的子字段:

"fields": {
	"keyword": {
    "type": "keyword",
    "ignore_above": 256 //超过字符数则忽略查询
  }
}

建表结构:

 

2)增删改查

第一种方式:ElasticsearchRepository<PostEsDTO, Long>, 默认提供了简单的增删改查,多用于可预期的、相对没那么复杂的查询、自定义查询,返回结果相对简单直接。
接口代码:

public interface CrudRepository<T, ID> extends Repository<T, ID> {
    <S extends T> S save(S entity);

    <S extends T> Iterable<S> saveAll(Iterable<S> entities);

    Optional<T> findById(ID id);

    boolean existsById(ID id);

    Iterable<T> findAll();

    Iterable<T> findAllById(Iterable<ID> ids);

    long count();

    void deleteById(ID id);

    void delete(T entity);

    void deleteAllById(Iterable<? extends ID> ids);

    void deleteAll(Iterable<? extends T> entities);

    void deleteAll();
}

ES中,_开头的字段表示系统默认字段, 比如_id,如果系统不指定,会自动生成。但是不会在_source字段中补充id的值,所以建议大家手动指定。

支持根据方法名自动生成方法,比如:

List<PostEsDTO> findByTitle(String title);

第二种方式:Spring默认给我们提供的操作 es 的客户端对象 ElastsearchRestTemplate,也提供了增删改查,它的增删改查更灵活,适用于更复杂的操作,返回结果更完整,但需要自己解析。
对于复杂的查询,建议用第二种方式。
三个步骤:
1.取参数
2.把参数组合为ES支持的搜索条件
3.从返回值中取结果

查询DSL:
参考文档:www.elastic.co/guide/en/el…
www.elastic.co/guide/en/el…

GET post/_search
{
  "query": { 
    "bool": { 	// 组合条件
      "must": [	// 必须都满足
        { "match": { "title":   "鱼皮"        }},	// match 模糊查询
        { "match": { "content": "知识星球" }}
      ],
      "filter": [
        { "term":  { "status": "published" }},	// term 精确查询
        { "range": { "publish_date": { "gte": "2015-01-01" }}}	// range 范围查询
      ]
    }
  }
}

wildcard 模糊查询
regexp 正则匹配查询

查询结果中,score代表匹配分数
对于复杂的查询建议先测试DSL,再翻译为Java代码

{
  "query": {
    "bool": { 
      "must_not": [ 
        { 
          "match": { 
            "title": "" 
          } 
        }, 
      ] 
      "should": [ 	// should 条件满足一部分即可
        { 
          "match": {
            "title": "" 
          } 
        }, 
        { 
          "match": {
            "desc": "" 
          } 
        } 
      ],
      "filter": [ 	// filter 过滤
        {
        	"term": { 	// term 精确
            "isDelete": 0 
          } 
        }, 
        { 
          "term": { 
            "id": 1 
          } 
        }, 
        { 
          "term": {
            "tags": "java" 
          } 
        }, 
        { 
          "term": { 
            "tags": "框架" 
          } 
        } 
      ],
      "minimum_should_match": 1  // 通常与布尔查询(bool query)中的should子句一起使用,以确定满足搜索条件的文档的最低匹配要求。您可以控制在返回的文档中至少需要多少个should子句匹配。
    }
  },
  "from": 0, // 分⻚
  "size": 5, // 分⻚
  "_source": ["title", "_createTime", "content", "updateTime", "tags"], // 要查的字段
  "sort": [ // 排序
    {
      "_createTime": {
      	"order": "asc"
      }
    },
    {
      "_score": {
      	"order": "desc"
      }
    },
    {
      "updateTime": {
      	"order": "desc"
      }
    }
  ]
}

翻译为 Java:

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();
        // 过滤
        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("description", 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);

先模糊筛选静态数据,查出数据后,再根据查到的内容 id 去数据库查找到动态数据

数据同步

一般情况下,如果做查询搜索功能,使用 ES 来模糊搜索,但是数据是存放在数据库 MySQL 里的,所以我们需要把 MySQL 中的数据和 ES 进行同步,保证数据一致(以 MySQL 为主)。

MySQL=>ES(单项)

首次安装完 ES,把 MySQL 数据全量同步到 ES里,写一个单次脚本
4 种方式,全量同步(首次)——增量同步(新数据):
1.定时任务,比如 1 分钟 1 次,找到 MySQL 中过去几分钟内(至少是定时周期的 2 倍)发生改变的数据,然后更新到 ES。
优点:简单易懂、占用资源少、不引入第三方中间件
缺点:有时间差
应用场景:数据时间内不同步影响不大、或者数据几乎不发生修改
2.双写:写入数据的时候,必须也去写ES;更新删除数据库同理。(事务:建议先保证MySQL写入成功,如果ES
写入失败了,可以通过定时任务 + 日志 + 告警进行检测和修复(补偿))
3.用Logstash数据同步管道(一般会配合kafka消息队列 + beats 采集器):
4.Canal 监听 MySQL Binlog,实时同步

Logstash

**传输 **和 处理数据的管道
www.elastic.co/guide/en/lo…
artifacts.elastic.co/downloads/l…

好处:用起来翻遍,插件多
缺点:成本更大、一般要配合其他组件使用(比如 kafka)
image.png

事件 Demo:

cd logstash-7.17.9
.\bin\logstash.bat -e "input { stdin { } } output { stdout {} }"

快速开始文档:www.elastic.co/guide/en/lo…
坚挺 upd 并输出:

# Sample Logstash configuration for receiving
# UDP syslog messages over port 514

input {
  udp {
    port => 514
      type => "syslog"
  }
}

output {
  stdout { codec => rubydebug }
}

要把 MySQL数据 同步给 Elasticsearch。
问题1:找不到 mysql 的包
Error: unable to load mysql-connector-java-5.1.36-bin.jar from :jdbc_driver_library, file not readable (please check user and group permissions for the path)
Exception: LogStash::PluginLoadingError

解决:修改 Logstash 任务配置中的 jdbc_driver_library 委屈东宝的绝对路径(启动包可以从 maven 仓库中拷贝)

增量配置:是不是可以只查最新更新的?可以记录上次更新的数据时间,只查出>该更新时间的数据

小知识:预编译 SQL 的优点?
1.灵活
2.模版好懂
3.快(有缓存)
4.部分防 SQL 注入

sql_last_value 是取上次查到的数据的最后一昂的指定的字段,如果要全量更新,只要删除掉E:\software\ElasticStack\logstash-7.17.9\data\plugins\inputs\jdbc\logstash_jdbc_last_run 文件即可(这个文件存储了上次同步到的数据)

input {
  jdbc {
    jdbc_driver_library => "E:\software\ElasticStack\logstash-7.17.9\config\mysql-connector-java-8.0.29.jar"
    jdbc_driver_class => "com.mysql.jdbc.Driver"
    jdbc_connection_string => "jdbc:mysql://localhost:3306/my_db"
    jdbc_user => "root"
    jdbc_password => "123456"
    statement => "SELECT * from post where updateTime > :sql_last_value"
    tracking_column => "updatetime"
    tracking_column_type => "timestamp"
    use_column_value => true
    parameters => { "favorite_artist" => "Beethoven" }
    schedule => "*/5 * * * * *"
    jdbc_default_timezone => "Asia/Shanghai"
  }
}

output {
  stdout { codec => rubydebug }
}

注意查询语句重要安 updateTime 排序,保证最后一条是最大的:

input {
  jdbc {
    jdbc_driver_library => "E:\software\ElasticStack\logstash-7.17.9\config\mysql-connector-java-8.0.29.jar"
    jdbc_driver_class => "com.mysql.jdbc.Driver"
    jdbc_connection_string => "jdbc:mysql://localhost:3306/my_db"
    jdbc_user => "root"
    jdbc_password => "123456"
    statement => "SELECT * from post where updateTime > :sql_last_value and updateTime < now() order by updateTime desc"
    tracking_column => "updatetime"
    tracking_column_type => "timestamp"
    use_column_value => true
    parameters => { "favorite_artist" => "Beethoven" }
    schedule => "*/5 * * * * *"
    jdbc_default_timezone => "Asia/Shanghai"
  }
}

output {
  stdout { codec => rubydebug }

  elasticsearch {
    hosts => "http://localhost:9200"
    index => "post_v1"
    document_id => "%{id}"
  }
}

存在的两个问:
1.字段全变成小写了
2.多了一些我们不想同步的字段

可以编写过滤:

input {
  jdbc {
    jdbc_driver_library => "E:\software\ElasticStack\logstash-7.17.9\config\mysql-connector-java-8.0.29.jar"
    jdbc_driver_class => "com.mysql.jdbc.Driver"
    jdbc_connection_string => "jdbc:mysql://localhost:3306/my_db"
    jdbc_user => "root"
    jdbc_password => "123456"
    statement => "SELECT * from post where updateTime > :sql_last_value and updateTime < now() order by updateTime desc"
    tracking_column => "updatetime"
    tracking_column_type => "timestamp"
    use_column_value => true
    parameters => { "favorite_artist" => "Beethoven" }
    schedule => "*/5 * * * * *"
    jdbc_default_timezone => "Asia/Shanghai"
  }
}

filter {
    mutate {
        rename => {
          "updatetime" => "updateTime"
          "userid" => "userId"
          "createtime" => "createTime"
          "isdelete" => "isDelete"
        }
        remove_field => ["thumbnum", "favournum"]
    }
}

output {
  stdout { codec => rubydebug }

  elasticsearch {
    hosts => "127.0.0.1:9200"
    index => "post_v1"
    document_id => "%{id}"
  }
}

订阅数据库的同步方式 Canal

github.com/alibaba/can…
优点:实时同步,实时性非常强
原理:数据每次修改时,会修改 binlog 文件,只要监听该文件的修改,就能第一时间得到消息并处理
canal:帮你监听 binlog,并解析 binlog 为你可以理解的内容。
他伪装成了 MySQL 的从节点,获取主节点给的 binlog,如图:
image.png

快速开始:github.com/alibaba/can…
window 系统,找到你本地的 mysql 安装目录,在根目录下新建 my.ini 文件:

[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复

如果 Java 找不到,就修改startup.bat 脚本为你自己的 java home:


set JAVA_HOME=C:\Users\59278\.jdks\corretto-1.8.0_302
echo %JAVA_HOME%
set PATH=%JAVA_HOME%\bin;%PATH%
echo %PATH%

问题:mysql 无法链接,Caused by: java.io.IOException: caching_sha2_password Auth failed
解决方案:
github.com/alibaba/can…
ALTER USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'canal';
ALTER USER 'canal'@'%' IDENTIFIED BY 'canal' PASSWORD EXPIRE NEVER;
FLUSH PRIVILEGES;