es基础学习

文章目录

ES

ES概述

ES和LUCENS

一个很好的搜索库Lucene,但是很复杂,ES就封装了Lucene,支持了很多东西

  • 分布式

ES核心概念

  • 近实时
    • 两方面
      • 写入数据时,过1秒才会被搜索到,因为内部再分词和录入索引
      • es搜索时,搜索和分析数据需要秒级出结果
  • Cluster集群
    • 包含一个或多个启动这es实例的机器群时,通常一个机器起es实例,同一网络下,多个es自动集群,负载均衡
  • Node节点
    • 每个es实例
  • Document文档
    • es的最小数据单元
    • 格式为json格式
    • 多个document存储于一个索引中
  • index索引
    • 包含一堆相似结构的文档数据
    • 和数据库的表一样
  • Field字段
    • 相当于mysql的列
  • type
    • 是index的逻辑分类,将要删除
  • shard分片
    • index过大时,就是表数据量过大时,可以分为多个表
    • 减轻单节点压力
    • 充分利用性能
    • 方便集群拓展
  • 副本replica shard
    • 为了容错,就是为了备份,像redis的slave主从
    • 高可用,吞吐量 0

es和mysql对比

mysqles
database库index索引
tableindex索引(原来为type)
数据行Row文档document
数据列column字段field
约束schema映射mapping

ES目录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n3sDyQo7-1666522442625)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221009095533897.png)]

  • bin
    • 启动脚本
  • config
    • 配置文件
  • jdk
    • 就是一些jar包
  • logs
    • 日志
  • modules
    • 模块包

主要配置文件

  • elasticesearch.yml

    • 配置格式yaml格式

    •   cluster.name: my-application #集群名称
       基本都是默认设置 node.name: node-1  #节点名字
      node.attr.rack: r1 #机架
      
    • 两个端口

      • 9200 对外端口
      • 9300 集群节点之间通信端口
  • jvm.options

    • jvm设置
    • -Xms2g 启动时多少g
    • -Xmx2g 最大运行
  • log4j

    • 日志设置

es在windos要配一点东西才可以

node.name: node-1  
cluster.initial_master_nodes: ["node-1"]  
xpack.ml.enabled: false 
http.cors.enabled: true
http.cors.allow-origin: /.*/

启动

/bin/elasticsearch.bat

kibana下载

默认5601端口

  • i18n.locale:
    • 语言设置
  • 启动
    • /bin/kibana.bat

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HkGDCNO4-1666522442625)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221009105611242.png)]

Es快速入门

文档documnt

json格式

案例图书管理案例

  • 增删改查crud

ES中Api

  • GET /_cat/health?v
    • 节点信息
  • GET /_cat/indices?v
    • 索引,也就是表
  • PUT /demo_index?pretty
    • 创建索引
  • DELETE /demo_index
    • 删除索引

Crud操作

创建book索引

插入数据
  • PUT /索引库/type/id
PUT /book

// /索引库/表/id
PUT /book/_doc/1

{
"name": "Bootstrap开发",
"description": "Bootstrap是由Twitter推出的一个前台页面开发css框架,是一个非常流行的开发框架,此框架集成了多种页面效果。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长css页面开发的程序人员)轻松的实现一个css,不受浏览器限制的精美界面css效果。",
"studymodel": "201002",
"price":38.6,
"timestamp":"2019-08-25 19:11:35",
"pic":"group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags": [ "bootstrap", "dev"]
}


PUT /book/_doc/2

{
"name": "java编程思想",
"description": "java语言是世界第一编程语言,在软件开发领域使用人数最多。",
"studymodel": "201001",
"price":68.6,
"timestamp":"2019-08-25 19:11:35",
"pic":"group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags": [ "java", "dev"]
}



PUT /book/_doc/3

{
"name": "spring开发基础",
"description": "spring 在java领域非常流行,java程序员都在用。",
"studymodel": "201001",
"price":88.6,
"timestamp":"2019-08-24 19:11:35",
"pic":"group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags": [ "spring", "java"]
}
获取数据
  • GET /索引库/type/id

    GET /book/_doc/1
    
    GET /book/_doc/2?_source_includes=name,price
    
修改数据

修改全部

  • PUT /book/_doc/1

    {

    ​ 全部数据

    }

PUT /book/_doc/1
{
    "name": "Bootstrap开发教程1",
    "description": "Bootstrap是由Twitter推出的一个前台页面开发css框架,是一个非常流行的开发框架,此框架集成了多种页面效果。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长css页面开发的程序人员)轻松的实现一个css,不受浏览器限制的精美界面css效果。",
    "studymodel": "201002",
    "price":38.6,
    "timestamp":"2019-08-25 19:11:35",
    "pic":"group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
    "tags": [ "bootstrap", "开发"]
}
修改部分
POST /索引库/type/id/_update
{
	"doc":{
		"name": "bootstrap开发教程高级"
	}
}


POST /book/_update/1/ 
{
  "doc": {
   "name": " Bootstrap开发教程高级"
  }
}
删除
DELETE /索引库/type/id

默认自带字段解析

_index

_type

_id

id主键,可以自动生成

创建索引时,不同数据放入不同索引。

生成文档ID

直接POST

POST /index/_doc


PUT /test_index/_doc/1
{
  "test_field": "test"
}
  • 自动ID
    • 长度为20,URL安全,base64,GUID,分布式生成不冲突

_source

原始数据

定制获取相应字段,不用返回全部字段

GET /book/_doc/2?_source_includes=name,price

全量替换

就是我们刚刚的修改全部的哪里

PUT /index/type/1
{
	
}

内部实现

并不会一次次地去删除,比如全局修改时,他会在整个数据上面加上一个字段delete标志为删除,version为1

新的文档version为2。当删除的文档很多时,也就是是delete很多时,es会自动批量删除

旧文档的内容不会立即删除,只是标记为deleted。适当的时机,集群会将这些文档删除。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9wOkvz3N-1666522442625)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221009150514705.png)]

java全局替换会先让es返回这条数据的对象,然后把要修改的值修改了再插入进去

强制创建

为防止覆盖原有数据,我们在新增时,设置为强制创建,不会覆盖原有文档

PUT /test_index/_doc/1/_create
{
	"test_field": "test"
}

就是已经存在的话,就不会覆盖

删除

不会立刻删除,只是delete,es批量删除

lazy delete

局部替换

POST /index/_doc/id/_update
{
	要修改的字段
}

内部原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mLkCAn9A-1666522442625)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221009151629408.png)]

优点

  • 减少网络请求次数
  • 减少网络开销
  • 减少并发冲突

使用脚本更新

内置脚本

es6后就不支持了,因为耗内存,不安全远程注入漏洞

脚本操作

ctx就是context上下文

_source

num+=1

POST /test_index/_doc/6/_update
{
   "script" : "ctx._source.num+=1"
}

这样的话就可以一次请求就加一了。不用先获取看看他多少再加一,减少了网络传输

下面这个是比较复杂的内置脚本

GET /test_index/_search
{
  "script_fields": {
    "my_doubled_field": {
      "script": {
       "lang": "expression",
        "source": "doc['num'] * multiplier",
        "params": {
          "multiplier": 2
        }
      }
    }
  }
}

es并发问题

在高并发情况下,仍会有并发

  • 悲观锁
    • 锁线程
  • 乐观锁
    • 版本管理

es的锁是乐观锁,他本身就有一个版本号。

每次修改都会version+1,删除也是在version+1的版本里,此时新增的话就在delete的version+1

比如

version = 3,修改 version = 4 ,删除 version = 5 ,加上delete标识,添加version = 6,没有delete标识

es的后台主从同步时异步多线程,所以说,多个请求时是乱序的 ,就是他是打乱发送的,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zvfReC7e-1666522442626)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221009155403919.png)]

javaAPI

es都是rest,很方便

es的rest api很多

在maven工程中

注意这个版本一定要对应到你的es版本

 <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
        <version>7.17.6</version>
        <exclusions>
            <exclusion>
                <groupId>org.elasticsearch</groupId>
                <artifactId>elasticsearch</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.elasticsearch</groupId>
        <artifactId>elasticsearch</artifactId>
        <version>7.17.6</version>
    </dependency>

测试的demo

public class TestDemo {
    public static void main(String[] args) throws IOException {
        //1,获取连接客户端
        RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
                new HttpHost("localhost",9000,"http")
        ));

        //2,构建请求
        GetRequest getRequest = new GetRequest("book", "2");

        //3,执行
        GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);

        System.out.println(response.getId());
        System.out.println(response.getVersion());
        System.out.println(response.getSourceAsString());

    }
}

springboot工程实现

配置类注入restClient

@Component
public class EsConfig {
    @Value("${heima.elasticsearch.hostlist}")
    private String hostList;

    //es自带的close方法
    @Bean(destroyMethod = "close")
    public RestHighLevelClient restHighLevelClient(){
        String[] split = hostList.split(",");
        HttpHost[] httpHosts = new HttpHost[split.length];
        for (int i = 0; i < httpHosts.length; i++) {
            String item = split[i];
            httpHosts[i] = new HttpHost(item.split(":")[0],Integer.parseInt(item.split(":")[1]),"http");
        }
        return new RestHighLevelClient(RestClient.builder(
            httpHosts
        ));
    }
}

下面直接引入就可以了

获取信息的API
@Autowired
private RestHighLevelClient restHighLevelClient;

@Test
void contextLoads() throws IOException {
    //1,构建请求
    GetRequest book = new GetRequest("book", "2");

    //2,执行
    GetResponse response = restHighLevelClient.get(book, RequestOptions.DEFAULT);
    System.out.println(response.getSourceAsString());
}

上面这种方式是查询所有的字段的,查询全字段效率低,我们可以查询自己需要的字段

透过两个String和FetchSourceContext来进行辨别

 @Test
    void test2() throws IOException {
        GetRequest getRequest = new GetRequest("book", "2");
        
        //想要的字段
        String[] includes = new String[]{"name", "price"};
        //不想要的字段
        String[] excludes = Strings.EMPTY_ARRAY;

        FetchSourceContext fetchSourceContext = new FetchSourceContext(true, includes, excludes);
        getRequest.fetchSourceContext(fetchSourceContext);

        GetResponse response = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
        System.out.println(response.getSourceAsString());
    }

使用异步查询

			ActionListener<GetResponse> listener = new ActionListener<GetResponse>() {
            //成功
            @Override
            public void onResponse(GetResponse getResponse) {
                System.out.println(getResponse.getSourceAsString());
            }

            //失败
            @Override
            public void onFailure(Exception e) {
                e.printStackTrace();
            }
        };

多种返回String,Map,byte数组

    @Test
    void contextLoads() throws IOException {
        //1,构建请求
        GetRequest book = new GetRequest("book", "3");

        //2,执行
        GetResponse response = restHighLevelClient.get(book, RequestOptions.DEFAULT);

        if (response.isExists()){
            System.out.println(response.getSourceAsMap());
        }else {
            System.out.println("不存在");
        }
    }
文档新增操作

PUT

这里只学3种方式

 @Test
    void testAdd() throws IOException {
//        PUT /test_index/_doc/3/_create
//        {
//            "test_field": "test"
//        }

        IndexRequest request = new IndexRequest("test_index", "_doc");
//        request.id("1");

        //4种构建文档数据的方式

        //方法1 使用json字符串
        String jsonString = " {\n" +
                "            \"test_field\": \"test\"\n" +
                "        }";

        request.source(jsonString, XContentType.JSON);

        //方法2 使用map
//        HashMap<String, Object> map = new HashMap<>();
//        map.put("test_field","test");
//        request.source(map);

        //方法3 不说了优点麻烦

        //方法4
//        request.source("test_index","test");


        //设置超时时间
        request.timeout("1s");

        //手动维护版本号
//        request.version(2);
//        request.versionType(VersionType.EXTERNAL);

        IndexResponse index = restHighLevelClient.index(request, RequestOptions.DEFAULT);

        System.out.println(index.getId());
    }
  • Json格式
  • Map格式
  • 直接source一个个Json

插入成功的判断

   		IndexResponse response = restHighLevelClient.index(request, RequestOptions.DEFAULT);
        //如果创建成功
        if (response.getResult() == DocWriteResponse.Result.CREATED){
            DocWriteResponse.Result result = response.getResult();
            System.out.println(result);
        }else if (response.getResult() == DocWriteResponse.Result.UPDATED){
            //全文覆盖的情况
        }else {
            //失败
        }

分片管理

   ReplicationResponse.ShardInfo shardInfo = response.getShardInfo();
        if (shardInfo.getTotal() != shardInfo.getSuccessful()){
            System.out.println("处理成功的分片数少于总分片数");
        }

        if (shardInfo.getFailed() > 0){
            for (ReplicationResponse.ShardInfo.Failure failure:
                 shardInfo.getFailures()) {
                System.out.println(failure.reason());
            }
        }

异步新增
 ActionListener<IndexResponse> listener = new ActionListener<IndexResponse>() {
            @Override
            public void onResponse(IndexResponse response) {
                System.out.println(response.getIndex());
            }

            @Override
            public void onFailure(Exception e) {
                System.out.println("新增失败");
            }
        };
        restHighLevelClient.indexAsync(request,RequestOptions.DEFAULT,listener);
更新文档
  @Test
    void testPost() throws IOException {
//        POST /test_index/_doc/3/_update
//        {
//            "doc":{
//            "test_field":"test2"
//        }
//        }

        UpdateRequest request = new UpdateRequest("test_index", "_doc", "3");
        //和新增的3种方式一样
        request.doc("test_field","test2");
        request.timeout("1s");
        //重试次数
        request.retryOnConflict(3);

        UpdateResponse update = restHighLevelClient.update(request, RequestOptions.DEFAULT);
        System.out.println(update.getResult());
        
             //如果创建成功
        if (response.getResult() == DocWriteResponse.Result.CREATED){
            DocWriteResponse.Result result = response.getResult();
            System.out.println(result);
        }else if (response.getResult() == DocWriteResponse.Result.UPDATED){
            //全文覆盖的情况
            System.out.println("update");
        }else if (response.getResult() == DocWriteResponse.Result.DELETED){
            System.out.println("delete");
        }else if (response.getResult() == DocWriteResponse.Result.NOOP){
            System.out.println("没有操作");
        }
    }

   }

也可以异步,弄个监听器就行

批量插入
@Test
    void testBulk() throws IOException {
        BulkRequest request = new BulkRequest("test_index", "_doc");
        request.add(new IndexRequest("post").id("6").source(XContentType.JSON,"test_field","test3"));
        request.add(new IndexRequest("post").id("7").source(XContentType.JSON,"test_field","test3"));
        request.add(new IndexRequest("post").id("8").source(XContentType.JSON,"test_field","test3"));
        request.add(new DeleteRequest("post").id("7"));
        request.add(new DeleteRequest("post").id("8"));
        BulkResponse bulk = restHighLevelClient.bulk(request, RequestOptions.DEFAULT);

        //成功条数
        System.out.println("成功条数" + bulk.getIngestTook());

        //获取每个插入的Response
        for (BulkItemResponse res:
             bulk) {
            DocWriteResponse response = res.getResponse();
            switch (res.getOpType()){
                case INDEX:
                    IndexResponse indexResponse = (IndexResponse)response;
                    System.out.println("put普通插入或者全局替换");
                    break;
                case CREATE:
                    IndexResponse indexResponse1 = (IndexResponse)response;
                    System.out.println("强制创建");
                    break;
                case UPDATE:
                    UpdateResponse updateResponse = (UpdateResponse)response;
                    System.out.println("修改");
                    break;
                case DELETE:
                    DeleteResponse deleteResponse = (DeleteResponse)response;
                    System.out.println("删除");
                    break;
            }
        }
    }

POST /_bulk

功能总结

  • delete
    • 删除
  • create
    • 强制创建
  • index
    • 普通put,也可以全量替换
  • update
    • 局部更新

ES的分布式架构

  • 1,复杂分布式分片
    • 负载,分片,副本,平均分配分片
  • 2, 新的es实例,自动加入集群
    • 自动加入es集群,自动副本
  • 3,扩容方案
    • 垂直扩容
      • 比如1T数据到8T数据,换机器,买个大的
    • 水平扩容
      • 直接新加入,原来的还继续用
    • 一般情况下使用水平扩容
  • 4,rebalence
    • 有时有些服务器负载重或者轻,es集群会将某些分片会自动转移获取移走
  • 5,master节点
    • 管理es集群的元数据,创建删除索引,维护索引的元数据
    • 默认下,es会自动选一台机器作为master
  • 6,节点对等分布式
    • 请求的数据在各个分片的情况 都有可能

图解shard,副本机制

  • 每个shard节点,其实都封装了Lucene实例,完整的建立索引
  • 每个index包含一个或这个shard
  • shard自动负载均衡
  • shard和其副本,数据同步
  • 副本负责容错,代替shard的备胎,同时也可以负责读的一些操作,因为它是和其shard是数据同步的
  • 主分片创建索引就固定了,副本则可以改
  • 默认共有2个分片,1个主分片,1个副本分片
  • 主分片和副本分片不可以在同一节点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3r9z9XSH-1666522442626)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221010100052367.png)]

单一个节点下的机制图解

一个node就是一台机器

可以自行设置分片数和副本数,比如先新创一个index,3个主分片一个副本分片

但是副本是使用不了的,因为上面说到副本和分片不能再一个机器节点,所以副本失效,状态为yellow,一旦宕机全部失效

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8iCefXaN-1666522442626)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221010100720016.png)]

两个节点机制

两个节点下,创建分片数3,副本数3

因为副本和主分片不在同一机器,所以有可能一个节点放主分片,一个节点放副本

读取时,负载均衡,在主和副读都可以

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o4rzYXK6-1666522442627)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221010101022227.png)]

横向扩容

这样的话,可以分担写的请求,不用都在一台机器上操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hy8xdj9y-1666522442627)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221010101445330.png)]

若再加一个台机器,横向会自动分布主分片和节点数量,使各个机器均衡分布

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PBIlwN1O-1666522442628)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221010101633989.png)]

这样,同时也是让读是3台机器,写也是3台

如果机器足够多的话,会自动给副本再加副本,读的话9台,写的话3台

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J8UVwXHF-1666522442628)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221010101921717.png)]

Master选举

这个和redis的是基本一样的,有一个节点挂了,会去找另一个节点作为master,当宕机的重启会作为子节点,新master的副本会代替原来master的主分片

  • 情况1
    • node1宕机,P0shard没有了,所有主分片不是全active,集群状态red
    • 容错第一步
      • 重新选master节点
    • 容错第二步
      • 新master会将丢失的主分片的某个副本提升为主分片,集群状态为yellow(副本分片丢失)
    • 容错的第三步
      • 宕机的机器重启,作为子节点
      • 将确实的副本分片copy一份到新机器上(部分替换)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BG7tR1tT-1666522442628)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221010131652570.png)]

文档的存储机制

一个文档只会再一个分片上面

数据路由

一个文档会只落在一个分片上

路由算法

路由公式

shard = harh(routing) % number_of_primary_shards

1%3 = 1放在1号分片,取得时候也很简单,获取到这个位置直接取这个分片找

手动指定
PUT /test_index/_doc/15?routing=tom
{
	"username":"zs"
}

这种一般用于同种属性得数据放于同一分片,方便管理

但是容易数据倾斜,所以不同文档尽量放到不同的索引,剩下得交给es

主分片数量不可变

因为路由的情况,在路由时已经确定好,如果改变会破坏路由的规则,导致寻找数据失败。

比如

存储id = 8

​ 主分片数 = 3

​ hash(8) = 8 % 3 = 2 放到2号分片,如果现在分片数变了 + 1 主分片数为4

​ hash(8) = 8 % 4 = 0 到0号库找这条数据,但是插入的时候在2号库所以找不到

增删改机制

分为4步走

比如现在3分片1副本

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mr3E8d9T-1666522442629)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221010134213787.png)]

新增id = 1的数据

  • 1,java客户端发送请求,选择一个node作为协调节点
  • 2,协调节点进行路由计算,然后把请求转发到对应node
  • 3,实际node上的主分片进行数据处理,然后该节点的主分片会发一个请求到其副本的节点进行数据同步
  • 4,协调节点如果发现分片和副本都成功,则返回结构

查询机制

查询id = 1数据

  • 1,java客户端发送请求,选择一个node作为协调节点
  • 2,协调节点进行路由计算,然后把请求转发到对应node,此时会使用round-robin轮询随机算法,在主分片及其所有的副本中随机选一个进行负载均衡
    • 实现减轻查询负担
  • 3,接收的node返回document给协调节点
  • 4,协调节点返回客户端
  • 5,特殊情况:
    • document在建立索引过程,主分片已建立,但是副本还没有,此时导致可能无法读取到数据,当建立索引完毕后,就一定查询到。

bulk api的json格式

传统json格式节点需要把不同操作分割为json数组,然后进行路由分配,再将其序列化,这样会消耗更多内存,jvm gc的开销

现在的格式,天然形成的分配,插入和修改会占用两个元素

POST /_bulk
{ "delete": { "_index": "test_index",  "_id": "5" }} \n
{ "create": { "_index": "test_index",  "_id": "14" }}\n
{ "test_field": "test14" }\n
{ "update": { "_index": "test_index",  "_id": "2"} }\n
{ "doc" : {"test_field" : "bulk test"} }\n

Mapping映射入门

mapping映射概念

自动或手动为index中的_doc建立的一种数据结构的相关配置,坚持mapping映射

先插入几条数据

PUT /website/_doc/1
{
  "post_date": "2019-01-01",
  "title": "my first article",
  "content": "this is my first article in this website",
  "author_id": 11400
}

PUT /website/_doc/2
{
  "post_date": "2019-01-02",
  "title": "my second article",
  "content": "this is my second article in this website",
  "author_id": 11400
}
 
PUT /website/_doc/3
{
  "post_date": "2019-01-03",
  "title": "my third article",
  "content": "this is my third article in this website",
  "author_id": 11400
}

对于上面条数据,我们并没有创建index就可以直接对website进行插入,因为系统帮我们创建了。额对于mysql则是必须创建表才可以插入。

比如在mysql中需要指定特定的数据类型

create table website(
     post_date date,
     title varchar(50),     
     content varchar(100),
     author_id int(11) 
 );

在es中:

​ 会进行动态映射,根据自动创建索引index,以及对于mapping,mapping中包含了每个field的数据类型,以及分词设置

这个就是系统自动创建的动态映射

{
  "website" : {
    "mappings" : { 
      "properties" : {
        "author_id" : {
          "type" : "long"
        },
        "content" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "post_date" : {
          "type" : "date"
        },
        "title" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        }
      }
    }
  }
}

搜全部

GET index/_search
GET index/_search?q=查询条件
GET index/_search?q=third
#这样是在全部属性都查

GET index/_search?q=属性:值
GET index/_search?q=title:third

在不同的data type的分词和搜索行为不一样,有可能搜不到,比如2019搜不了,2019-01-01能搜到。

精确匹配和全文检索的对比分析

exact value精确匹配

就是全部输入2019-01-01才能搜到,2019搜不出

full text全文搜索

用户只输入关键词输入2019就可以,如笔记电脑,内部会进行分词笔记 电脑两个词

2019-01-01 对于2019 01 01分为3个词

  • 缩写自动全称
    • cn vs china
  • 格式转化
    • like vs likes
  • 大小写
    • tom vs Tom
  • 同义词
    • like vs love

NPL自然语言处理底层。

全文检索下的倒排索引

doc1:I really liked my small dogs, and I think my mom also liked them.

doc2:He never liked any dogs, so I hope that my mom will not expect me to liked him

分词,建立倒排索引

termdoc1doc2
I**
really*
liked**
my**
small*
dogs*
and*
think*
mom**
also*
them*
He*
never*
any*
so*
hope*
that*
will*
not*
expect*
me*
to*
him*

mother like little dog,不可能有任何结果

mother

like

little

dog

这不是我们想要的结果。同义词mom\mother在我们人类看来是一样。想进行标准化操作,没有进行同义词转化

重建倒排索引

就是上面时态,单复数,同义词,大小写转化

mom ―> mother

liked ―> like

small ―> little

dogs ―> dog

重新建立倒排索引,加入normalization,再次用mother liked little dog搜索,就可以搜索到了

worddoc1doc2normalization
I**
really*
like**liked ―> like
my**
little*small ―> little
dog*dogs ―> dog
and*
think*
mother**mom ―> mother
also*
them*
He*
never*
any*
so*
hope*
that*
will*
not*
expect*
me*
to*
him*

这样的话就能搜索到了

分词器

作用:切分词语在进行正规化操作

就是把句子分词,然后再进行语态同义词等进行添加。

分词过程
  • 1,预处理,把表签去掉,比如过滤html标签
  • 2,进行分词
  • 3,正规化处理,比如同义词,大小写转换,同时把禁用词去掉,无语义的词也去掉

通过上面,才会进行建立倒排索引。

分词器分类
  • 标准分词
  • 简单分词器
  • 空格分词器,更简单的分词器
  • 特定语言分词器,比如中文,英文进行分词
  • 等等
对于字段的分词策略

对于不同的数据类型分词策略也不一样,日期data 的话一般是精确匹配,对于文本text一般是全文检索

我们可以指定分词器进行分词

GET /_analyze
{
  "analyzer": "standard",
  "text": "Text to analyze 80"
}
动态自动映射

回顾

  • 1,插入数据,自动建立索引
  • 2,自动定义数据类型
  • 3,不同数据类型,匹配规则不一样,精确匹配,全文检索
  • 4,在全文检索建立倒排索引前,会预处理,分词,正规化
  • 5,精确匹配是直接将整个放入倒排索引
核心数据类型
  • text,keyword
    • string
  • 地理位置信息
  • ip
  • 数组
  • 基本类型
  • 多个域
  • 等等
动态推测

根据值推测

true 就是 boolean

2019-01-01 date

查看索引映射
GET /index/_mapping
手动管理映射

手动创建映射,动态映射可能不太好,我们就需要自己去定义映射

PUT book


PUT book/_mapping
{
	"properties": {
           "name": {
                  "type": "text"
            },
           "description": {
              "type": "text",
              "analyzer":"english",
              "search_analyzer":"english"
           },
           "pic":{
             "type":"text",
             "index":false
           },
           "studymodel":{
             "type":"text"
           }
    }
}



PUT /book/_doc/1
{
  "name":"Bootstrap开发框架",
  "description":"Bootstrap是由Twitter推出的一个前台页面开发框架,在行业之中使用较为广泛。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现一个不受浏览器限制的精美界面效果。",
  "pic":"group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg",
  "studymodel":"201002"
}


Get /book/_search?q=name:开发

Get /book/_search?q=description:开发

Get /book/_search?q=pic:group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg

Get /book/_search?q=studymodel:201002

通过测试发现:name和description都支持全文检索,pic不可作为查询条件。
  • properties{}
    • 属性 :{}
      • 属性的类型
      • 属性的分词器
      • 属性搜索的分词器
      • index是否进行索引,就是整个倒排索引,比如URL就不用建立倒排索引
      • 如果是日期还可以指定format格式
keyword关键词

创建映射时不需要倒排索引,一般用户身份证,手机号码等等,不需要分词的

date日期

这个也默认不建立倒排索引

选用基本数据类

尽量选用小的提高效率

浮点数

尽量使用比例因子,比如单位元,按分,按百

不可以修改映射,只可以新增

PUT /book/_mapping/
{
  "properties" : {
    "new_field" : {
      "type" :    "text",
     "index":    "false"
    }
  }
}
一些复杂的数据类型

multivalue field

{"tag":["tag1","tag2"]}

empty field

null , [] , [null]

对象

像这个地址就是一个对象

PUT /company/_doc/1
{
  "address": {
    "country": "china",
    "province": "guangdong",
    "city": "guangzhou"
  },
  "name": "jack",
  "age": 27,
  "join_date": "2019-01-01"
}
{
  "address": {
    "country": "china",
    "province": "guangdong",
    "city": "guangzhou"
  },
  "name": "jack",
  "age": 27,
  "join_date": "2017-01-01"
}
{
    "name":            [jack],
    "age":          [27],
    "join_date":      [2017-01-01],
    "address.country":         [china],
    "address.province":   [guangdong],
    "address.city":  [guangzhou]
}

对象数组

{
    "authors": [
        { "age": 26, "name": "Jack White"},
        { "age": 55, "name": "Tom Jones"},
        { "age": 39, "name": "Kitty Smith"}
    ]
}
{
    "authors.age":    [26, 55, 39],
    "authors.name":   [jack, white, tom, jones, kitty, smith]
}

索引

我们之前都是直接PUT index/_doc/1创建索引的,这种动态映射有可能出错

所以,我们需要自己去定制

自定义创建索引

创建索引的时候把映射上

PUT /index
{
    "settings": { ... any settings ... },
    "mappings": {
       "properties" : {
            "field1" : { "type" : "text" }
        }
    },
    "aliases": {
    	"default_index": {}
  } 
}
  • settings
    • 配置分片数
  • mappings
    • properties
      • 属性
        • type,分词器等等
  • aliases别名
PUT /my_index
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1
  },
  "mappings": {
    "properties": {
      "field1":{
        "type": "text"
      },
      "field2":{
        "type": "text"
      }
    }
  },
  "aliases": {
    "default_index": {}
  } 
}
put /my_index/_doc/1
{
	"field1":"java",
	"field2":"js"
}

GET my_index/_doc/1

#获取索引信息
GET my_index


GET my_index/_mapping
GET my_index/_settings

DELETE my_index

之前以及说过映射不可以修改,只可以添加,同时分片数也不可修改

定制分词器

默认分词器standard

standard,其中分词的三个组件,

  • character filter

  • tokenizer

  • token filter

  • standard tokenizer:以单词边界进行切分

  • standard token filter:什么都不做

  • lowercase token filter:将所有字母转换为小写

  • stop token filer(默认被禁用):移除停用词,比如a the it等等

PUT /my_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "es_std":{
          "type":"standard",
          "stopwords":"_english_"
        }
      }
    }
  }
}

定制化自己的分词器

PUT /my_index
{
  "settings": {
    "analysis": {
      #& 转化为and 
      "char_filter": {
        "&_to_and": {
          "type": "mapping",
          "mappings": ["&=> and"]
        }
      },
      #定义声明停用词
      "filter": {
        "my_stopwords": {
          "type": "stop",
          "stopwords": ["the", "a"]
        }
      },
      "analyzer": {
        "my_analyzer": {
          "type": "custom",
          #使用上面的和html标签过滤
          "char_filter": ["html_strip", "&_to_and"],
          "tokenizer": "standard",
          #使用上面的去掉停用词
          "filter": ["lowercase", "my_stopwords"]
        }
      }
    }
  }
}

使用我们自己的分词器进行测试分词

GET /my_index2/_analyze
{
  "analyzer": "my_analyzer"
  , "text": "tom & jerry are a friend in the house <a>,HAHA!!"
}

就是使用自己的分词器制定分词策略

type弃用的原因

PUT my_index/_doc/1

底层都是没有type类型

es储存不同type的机制,就是把它们都合起来

{
   "goods": {
      "mappings": {
         "electronic_goods": {
            "properties": {
               "name": {
                  "type": "string",
               },
               "price": {
                  "type": "double"
               },
               "service_period": {
                  "type": "string"
                   }			
                }
         },
         "fresh_goods": {
            "properties": {
               "name": {
                  "type": "string",
               },
               "price": {
                  "type": "double"
               },
               "eat_period": {
              		"type": "string"
               }
                }
         }
      }
   }
}

变成下面这样

{
   "goods": {
      "mappings": {
        "_type": {
          "type": "string",
          "index": "false"
        },
        "name": {
          "type": "string"
        }
        "price": {
          "type": "double"
        }
        "service_period": {
          "type": "string"
        },
        "eat_period": {
          "type": "string"
        }
      }
   }
}

没有得那些属性就为""

如果不弃用得话,就会还记录type,会浪费内存

定制动态映射

true:遇到陌生字段,就进行dynamic mapping

false:新检测到得字段将被忽略。这些字段将不会被索引,因此将无法搜索,但仍将出现在返回点击的源字段中。这些字段不会添加到映射中,

​ 必须显式添加新字段

strict:遇陌生字段,就报错

主要有一些

  • 陌生字段映射
  • 类型探测
  • 进行自定义匹配规则dynamic_templates

创建mapping

PUT my_index3
{
  "mappings": {
    "dynamic": true
    , "properties": {
          "title":{
            "type": "text"
          },
          "address":{
            "type": "object",
            "dynamic":true
          }
    }
  }
}

这个address就是一个对象,里面有可能有未知道的属性

PUT /my_index
{
    "mappings": {
      "dynamic": "false",
       "properties": {
        "title": {
          "type": "text"
        },
        "address": {
          "type": "object",
          "dynamic": "true"
        }
	    }
    }
}

插入title,content,address数据,content为未知数据,不分词放入倒排索引,

PUT /my_index
{
    "mappings": {
      "dynamic": "strict",
       "properties": {
        "title": {
          "type": "text"
        },
        "address": {
          "type": "object",
          "dynamic": "true"
        }
	    }
    }
}

如果为strict就直接报错了

日期探测

比如现在有一个属性是2019-01-01如果直接插入的话,动态映射会是一个data类型,如果我们希望他是一个字符=串类型,

就需要把date_detection设置为false

PUT /my_index
{
    "mappings": {
      "date_detection": false,
       "properties": {
        "title": {
          "type": "text"
        },
        "address": {
          "type": "object",
          "dynamic": "true"
        }
	    }
    }
}

自定义日期格式

PUT my_index
{
  "mappings": {
    "dynamic_date_formats": ["MM/dd/yyyy"]
  }
}
数字探测

numeric_detection

PUT my_index
{
  "mappings": {
    "numeric_detection": true
  }
}
PUT my_index/_doc/1
{
  "my_float":   "1.0", 
  "my_integer": "1" 
}
定制动态模板
PUT /my_index
{
    "mappings": {
            "dynamic_templates": [
                { 
                  "en": {
                      "match":              "*_en", 
                      "match_mapping_type": "string",
                      "mapping": {
                          "type":           "text",
                          "analyzer":       "english"
                      }
                }                  
            }
        ]
	}
}

定制en动态模板

  • match 是匹配 _en结尾的

  • match_mapping_type 匹配字符串String

  • 如果满足上面两个,则自动映射为text 分词器为英语

  • mappings

    • dynamic_templates
      • 名字
        • 匹配规则match
          • 映射为什么mapping
PUT /my_index/_doc/1
{
  "title": "this is my first article"
}

PUT /my_index/_doc/2
{
  "title_en": "this is my first article"
}


GET my_index/_search?q=first

#这个被模板检测到的话,因为英语的分词器is去掉,所以搜不到2文档
GET my_index/_search?q=is
PUT my_index
{
  "mappings": {
    "dynamic_templates": [
      {
        "integers": {
          "match_mapping_type": "long",
          "mapping": {
            "type": "integer"
          }
        }
      },
      {
        "strings": {
          "match_mapping_type": "string",
          "mapping": {
            "type": "text",
            "fields": {
              "raw": {
                "type":  "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }
    ]
  }
}
  • 看见long类型的就匹配,转为integer
  • 看见string就转为text,还有另一个field

所以

  • match

    • long_*
  • unmatch

    • 不匹配这样的
  • match_mapping_type

    • 类型
  • path_match 路劲

    • name.*
  • path_unmatch

  • *.middle

  • "match_pattern": "regex"
    "match": "正则表达式"
    
使用场景
1,结构化搜索

映射为关键字,就只搜那个关键字

{
        "strings_as_keywords": {
          "match_mapping_type": "string",
          "mapping": {
            "type": "keyword"
          }
        }
      }
2,仅搜索

将内容全文检索搜索

{
        "strings_as_text": {
          "match_mapping_type": "string",
          "mapping": {
            "type": "text"
          }
        }
      }
3,不关心评分norms

norms是指时间的评分因素,如不关心评分,例如不对文档评分进行排序,则可以在索引中禁用评分因子储存来节省空间。

{
        "strings_as_keywords": {
          "match_mapping_type": "string",
          "mapping": {
            "type": "text",
            "norms": false,
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }

“norms”: false

零停机重建索引

当插入一条数据,类型对不上你的索引库,不可能修改field类型,那么一个重新按照新的mapping,建立一个index

在系统建立索引过程中,是停机状态,影响生产。

PUT /my_index/_doc/1
{
  "title": "2019-09-10"
}

PUT /my_index/_doc/2
{
  "title": "2019-09-11"
}

上面会自动动态生成mapping

PUT /my_index/_doc/3
{
  "title": "my first article"
}

上面属性对不上自动创建的index会报错

同时对于索引属性的类型不可改

我们需要定制一个新的mapping

PUT /my_index_new
{
  "mappings": {
    "properties": {
		"title": {
         "type": "text"
        }
    }
  }
}

但是这样做会停机。

停机后,我们这样把旧的数据全部查出,然后批量插入新的索引

PUT /my_index/_alias/prod_index

#取出所有数据
GET /my_index/_search?scroll=1m
{
    "query": {
        "match_all": {}
    },    
    "size":  1
}


#批量插入
POST /_bulk
{ "index":  { "_index": "my_index_new", "_id": "1" }}
{ "title":    "2019-09-10" }


#别名切换
POST /_aliases
{
    "actions": [
        { "remove": { "index": "my_index", "alias": "prod_index" }},
        { "add":    { "index": "my_index_new", "alias": "prod_index" }}
    ]
}


#重新执行插入到新的
PUT /my_index_new/_doc/3
{
  "title": "my first article"
}

#通过使用索引别名访问
GET /prod_index/_doc/3

这样的话我们就可以无缝切入到my_index_new

过程

  • 当旧的index出现类型出错
  • 先新创建一个符合类型的索引
  • 查询旧的索引的全部数据,批量插入到新索引中
  • 把旧索引的别名指向新索引
  • 把出错的数据插入到新索引

所以在生产中,我们都是对索引起别名,因为出现类型错误时要0停机创建索引。然后别名指向新索引,使用别名查询代码依旧可以进行使用

  • 陌生字段动态映射
  • 日期和数字探测
  • 动态模板

IK分词器

因为es的标志分词器不对中文支持,我们需要按照IK按中文分词

中文分词器IK

基础知识
  • ik_max_word
    • 最细粒度区别
  • ik_smart
    • 最粗粒度区别
GET /_analyze
{
  "analyzer": "ik_max_word"
  ,"text": "中华人民共和国大会堂"
}

我们可以创建索引的时候指定分词器

PUT /my_index_new
{
  "mappings": {
    "properties": {
      "text":{
        "type": "text"
        , "analyzer": "ik_max_word"
      ,  "search_analyzer": "ik_smart"
      }
    }
  }
}

存储用ik_max_word,搜索用ik_smart,这是固定的

PUT /my_index_new/_doc/1
{
  "text":"中华人民共和国大会堂"
}

GET /my_index_new/_search?q=text:会堂
ik分词的配置文件
  • ik配置文件地址:es/plugins/ik/config目录

  • IKAnalyzer.cfg.xml

  • 用来配置自定义词库

  • main.dic

  • ik原生内置的中文词库,总共有27万多条,只要是这些单词,都会被分在一起

  • preposition.dic: 介词

  • quantifier.dic:放了一些单位相关的词,量词

  • suffix.dic:放了一些后缀

  • surname.dic:中国的姓氏

  • stopword.dic:英文停用词 ,ik原生最重要的两个配置文件

  • stopword.dic:包含了英文的停用词

    ​ 停用词,stopword a the and at but一般,像停用词,会在分词的时候,直接被干掉,不会建立在倒排索引中

自定义词组

因为在每年都有会一些流行词,所以我们需要自定义词汇,把一些新词放入main.dic

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DMmVrAdk-1666522442629)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221011104955818.png)]

然后到配置文件把文件设置好就行

使用mysql热更新词库

为什么我们要热更新?

虽然在拓展dic中添加拓展词汇,但是需要重启es,很不方便,不可能使用人工更新

热更新方案

请求远程接口

就是在配置文件中的

	<!--用户可以在这里配置远程扩展字典 -->
	<!-- <entry key="remote_ext_dict">words_location</entry> -->
	<!--用户可以在这里配置远程扩展停止词字典-->
	<!-- <entry key="remote_ext_stopwords">words_location</entry> -->

我们可以直接修改源码调用,手动支持从mysql中热更新

修改源代码

1、下载源码

https://github.com/medcl/elasticsearch-analysis-ik/releases

ik分词器,是个标准的java maven工程,直接导入eclipse就可以看到源码

2、修改源

org.wltea.analyzer.dic.Dictionary类,160行Dictionary单例类的初始化方法,在这里需要创建一个我们自定义的线程,并且启动它

org.wltea.analyzer.dic.HotDictReloadThread类:就是死循环,不断调用Dictionary.getSingleton().reLoadMainDict(),去重新加载词典

Dictionary类,399行:this.loadMySQLExtDict(); 加载mymsql字典。

Dictionary类,609行:this.loadMySQLStopwordDict();加载mysql停用词

config下jdbc-reload.properties。mysql配置文件

3、mvn package打包代码

target\releases\elasticsearch-analysis-ik-7.3.0.zip

4、解压缩ik压缩包

将mysql驱动jar,放入ik的目录下

5、修改jdbc相关配置

6、重启es

观察日志,日志中就会显示我们打印的那些东西,比如加载了什么配置,加载了什么词语,什么停用词

7、在mysql中添加词库与停用词

8、分词实验,验证热更新生效

JAVAAPI实现索引操作

新增

  @Test
    public void testCreateIndex() throws IOException {

        //1,创建请求
        CreateIndexRequest request = new CreateIndexRequest("user_index_new1");
        //2,执行操作
        //2.1,settings操作
        request.settings(Settings.builder()
                .put("number_of_shards","1")
                .put("number_of_replicas","1")
        );

        //2.2 添加mapping
        request.mapping("{\n" +
                "    \"properties\": {\n" +
                "      \"field1\":{\n" +
                "        \"type\": \"text\"\n" +
                "      },\n" +
                "      \"field2\":{\n" +
                "        \"type\": \"text\"\n" +
                "      }\n" +
                "    }\n" +
                "  }", XContentType.JSON);

        //3,设置别名
        request.alias(new Alias("my_alias_new"));

        //4,额外参数
        //4.1 超时时间为两分钟
        request.setTimeout(TimeValue.timeValueMinutes(2));
        //设置主节点超时时间
        request.setMasterTimeout(TimeValue.timeValueMinutes(1));
        //在创建索引API返回响应之前等待的活动分片副本的数量,以int形式表示
        request.waitForActiveShards(ActiveShardCount.from(2));
        request.waitForActiveShards(ActiveShardCount.DEFAULT);


        IndicesClient indices = restHighLevelClient.indices();
        CreateIndexResponse response = indices.create(request, RequestOptions.DEFAULT);

        //是否成功
        boolean isSuccess = response.isAcknowledged();
        //得到响应 指示是否在超时前为索引中的每个分片启动了所需数量的碎片副本
        boolean isShardsAcknowledged = response.isShardsAcknowledged();

        System.out.println(isSuccess);
        System.out.println(isShardsAcknowledged);
    }


	//异步
    @Test
    public void testAysnc() throws IOException {
        //1,创建请求
        CreateIndexRequest request = new CreateIndexRequest("user_index_new2");
        //2,执行操作
        //2.1,settings操作
        request.settings(Settings.builder()
                .put("number_of_shards","1")
                .put("number_of_replicas","1")
        );

        //2.2 添加mapping
        request.mapping("{\n" +
                "    \"properties\": {\n" +
                "      \"field1\":{\n" +
                "        \"type\": \"text\"\n" +
                "      },\n" +
                "      \"field2\":{\n" +
                "        \"type\": \"text\"\n" +
                "      }\n" +
                "    }\n" +
                "  }", XContentType.JSON);

        //3,设置别名
        request.alias(new Alias("my_alias_new"));

        //4,额外参数
        //4.1 超时时间为两分钟
        request.setTimeout(TimeValue.timeValueMinutes(2));
        //设置主节点超时时间
        request.setMasterTimeout(TimeValue.timeValueMinutes(1));
        //在创建索引API返回响应之前等待的活动分片副本的数量,以int形式表示
        request.waitForActiveShards(ActiveShardCount.from(2));
        request.waitForActiveShards(ActiveShardCount.DEFAULT);


        ActionListener<CreateIndexResponse> listener = new ActionListener<CreateIndexResponse>() {

            @Override
            public void onResponse(CreateIndexResponse response) {
                //是否成功
                boolean isSuccess = response.isAcknowledged();
                //得到响应 指示是否在超时前为索引中的每个分片启动了所需数量的碎片副本
                boolean isShardsAcknowledged = response.isShardsAcknowledged();

                System.out.println(isSuccess);
                System.out.println(isShardsAcknowledged);
            }

            @Override
            public void onFailure(Exception e) {

            }
        };
        IndicesClient indices = restHighLevelClient.indices();
        indices.createAsync(request, RequestOptions.DEFAULT,listener);
        
    }

删除索引

//删除索引库
@Test
public void testDeleteIndex() throws IOException {
    //删除索引对象
    DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("ydlclass_book2");
    //操作索引的客户端
    IndicesClient indices = restHighLevelClient.indices();
    //执行删除索引
    AcknowledgedResponse delete = indices.delete(deleteIndexRequest, RequestOptions.DEFAULT);
    //得到响应
    boolean acknowledged = delete.isAcknowledged();
    System.out.println(acknowledged);

}

//异步删除索引库
@Test
public void testDeleteIndexAsync() throws IOException {
    //删除索引对象
    DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("ydlclass_book2");
    //操作索引的客户端
    IndicesClient indices = restHighLevelClient.indices();

    //监听方法
    ActionListener<AcknowledgedResponse> listener =
            new ActionListener<AcknowledgedResponse>() {
                @Override
                public void onResponse(AcknowledgedResponse deleteIndexResponse) {
                    System.out.println("!!!!!!!!删除索引成功");
                    System.out.println(deleteIndexResponse.toString());
                }

                @Override
                public void onFailure(Exception e) {
                    System.out.println("!!!!!!!!删除索引失败");
                    e.printStackTrace();
                }
            };
    //执行删除索引
    indices.deleteAsync(deleteIndexRequest, RequestOptions.DEFAULT, listener);

    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

}

查看索引

// Indices Exists API
    @Test
    public void testExistIndex() throws IOException {
        GetIndexRequest request = new GetIndexRequest("ydlclass_book");
        request.local(false);//从主节点返回本地信息或检索状态
        request.humanReadable(true);//以适合人类的格式返回结果
        request.includeDefaults(false);//是否返回每个索引的所有默认设置

        boolean exists = restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
        System.out.println(exists);
    }

开启和关闭索引

// Indices Open API
@Test
public void testOpenIndex() throws IOException {
    OpenIndexRequest request = new OpenIndexRequest("ydlclass_book");

    OpenIndexResponse openIndexResponse = restHighLevelClient.indices().open(request, RequestOptions.DEFAULT);
    boolean acknowledged = openIndexResponse.isAcknowledged();
    System.out.println("!!!!!!!!!"+acknowledged);
}

// Indices Close API
@Test
public void testCloseIndex() throws IOException {
    CloseIndexRequest request = new CloseIndexRequest("index");
    AcknowledgedResponse closeIndexResponse = restHighLevelClient.indices().close(request, RequestOptions.DEFAULT);
    boolean acknowledged = closeIndexResponse.isAcknowledged();
    System.out.println("!!!!!!!!!"+acknowledged);

}

搜素入门

无条件搜素

查全部

GET book/_search
{
  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "name" : "java编程思想",
          "description" : "java语言是世界第一编程语言,在软件开发领域使用人数最多。",
          "studymodel" : "201001",
          "price" : 68.6,
          "timestamp" : "2019-08-25 19:11:35",
          "pic" : "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
          "tags" : [
            "java",
            "dev"
          ]
        }
      },
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "name" : "spring开发基础",
          "description" : "spring 在java领域非常流行,java程序员都在用。",
          "studymodel" : "201001",
          "price" : 88.6,
          "timestamp" : "2019-08-24 19:11:35",
          "pic" : "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
          "tags" : [
            "spring",
            "java"
          ]
        }
      }
    ]
  }
}

字段信息

  • took
    • 花费时间
  • timeout
    • 超时
  • shard
    • 分片信息
  • hits
    • 数据信息
  • max_source
    • 匹配度分数

传参

查名字

GET book/_search?q=name:java

按名字查并且按金额排序

GET book/_search?q=name:java&sort=price

图解timeout机制

超时就把这个时间内查到的数据返回,比如有100条数据,10ms内只查到20条,就把20条返回

  • 丢失超时部分,设置timeout时间

    • GET book/_search?q=name:java&sort=price&timeout=10ms
      

      夜可以设置全局

多索引搜索

相当于多表

GET book,book1/_search
GET book*/_search

有时需要*,因为有时日期分表分库,根据日期创建索引

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J5gyuZ44-1666522442630)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221011171838244.png)]

每个分片都有一部分数据,每个主分片都搜索,有副本可以到副本去搜索,

分页搜素

size,from进行分页

从0开始,拿3条数据

GET book/_search?from=0&size=3

深度分页

根据相关度评分倒排序,所以分页过深,协调节点会将大量数据聚合分析。

  • 1消耗网络带宽,因为所搜过深的话,各 shard 要把数据传递给 coordinate node,这个过程是有大量数据传递的,消耗网络。

  • 2消耗内存,各 shard 要把数据传送给 coordinate node,这个传递回来的数据,是被 coordinate node 保存在内存中的,这样会大量消耗内存。

  • 3消耗cup,coordinate node 要把传回来的数据进行排序,这个排序过程很消耗cpu。 所以:鉴于deep paging的性能问题,所有应尽量减少使用

es如何进行分页

搜9990开始要10条数据,那么就是到10000,然后3个节点共3万条数据,进行大到小排序,然后在内存中的三万数据中的9990到10000取出这10条数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CgPV0Ke6-1666522442630)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221011174259419.png)]

query string

name中不包含java的

GET book/_search?q=-name:java

包含java的

GET book/_search?q=+name:java

只要有java都搜出来

GET book/_search?q=java

它会将进行一次全量分词,就是一次全部放进去,放到all field中,没有指定时,就在_all搜索。

DSL入门

query string后面参数越来越多,搜索越来越复杂,不能满足需求,就是带上请求体。

query string基础语法
GET /book/_search?q=name:java

GET /book/_search?q=+name:java

GET /book/_search?q=-name:java
_all metadata的原理和作用

分词的时候就把一个完整的进去倒排索引,这样所有字段都能覆盖搜索

GET /book/_search?q=java
query DSL
简单固定语法
GET /test_index/_search 
{
  "query": {
    "match": {
      "test_field": "test"
    }
  }
}
  • query
    • match
      • field
    • sort
      • 排序
    • from size
      • 分页
    • _source
      • 返回具体字段

查全部

GET /book/_search
{
  "query": { "match_all": {} }
}

查name和对priice排序

GET /book/_search 
{
    "query" : {
        "match" : {
            "name" : " java"
        }
    },
    "sort": [
        { "price": "desc" }
    ]
}

分页

GET  /book/_search 
{
  "query": { "match_all": {} },
  "from": 0,
  "size": 1
}

返回具体字段

GET /book/_search 
{
  "query": { "match_all": {} },
  "_source": ["name", "studymodel"]
}
复杂完整语法
GET /website/_doc/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "title": "elasticsearch"
          }
        }
      ],
      "should": [
        {
          "match": {
            "content": "elasticsearch"
          }
        }
      ],
      "must_not": [
        {
          "match": {
            "author_id": 111
          }
        }
      ]
    }
  }
}
  • query
    • bool
      • must 必须的
        • match
      • should 可有的
        • match
      • must_not 不等于
        • match
select * from test_index where name='tom' or (hired =true and (personality ='good' and rude != true ))
GET /test_index/_search
{
    "query": {
            "bool": {
                "must": { "match":{ "name": "tom" }},
                "should": [
                    { "match":{ "hired": true }},
                    { "bool": {
                        "must":{ "match": { "personality": "good" }},
                        "must_not": { "match": { "rude": true }}
                    }}
                ],
                "minimum_should_match": 1
            }
    }
}
_source

结果是按照匹配分数排序的,所以,我们来看看这个分数是怎么得出来得

其实很简单,你的输入条件,它会根据分词策略进行分词,看看文档里对于搜索条件分词的频率

1、建立索引时, description字段 term倒排索引

java 2,3

程序员 3

2、搜索时,直接找description中含有java的文档 2,3,并且3号文档含有两个java字段,一个程序员,所以得分高,排在前面。2号文档含有一个java,排在后面。

匹配规则
match_all

匹配全部

GET /book/_search
{
    "query": {
        "match_all": {}
    }
}
match

对字段全文检索

GET /book/_search
{
	"query": { 
		"match": { 
			"description": "java程序员"
		}
	}
}
multi_match

匹配多字段

GET /book/_search
{
  "query": {
    "multi_match": {
      "query": "java程序员",
      "fields": ["name", "description"]
    }
  }
}
range query 范围查询

范围查询

GET /book/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 80,
		"lte": 90
      }
    }
  }
}
term query

字段为key word时搜索和储存不进行分词

GET /book/_search
{
  "query": {
    "term": {
      "description": "java程序员"
    }
  }
}
terms query

和上面一样就是多个字段,直接查,不用分词

GET /book/_search
{
    "query": { "terms": { "tag": [ "search", "full_text", "nosql" ] }}
}
exist query

查有该字段的文档

GET /_search
{
    "query": {
        "exists": {
            "field": "join_date"
        }
    }
}
Fuzzy query

智能搜索

GET /book/_search
{
    "query": {
        "fuzzy": {
            "description": {
                "value": "jave"
            }
        }
    }
}
IDS

根据id查

GET /book/_search
{
    "query": {
        "ids" : {
            "values" : ["1", "4", "100"]
        }
    }
}
前缀
GET /book/_search
{
    "query": {
        "prefix": {
            "description": {
                "value": "spring"
            }
        }
    }
}

正则查

GET /book/_search
{
    "query": {
        "regexp": {
            "description": {
                "value": "j.*a",
                "flags" : "ALL",
                "max_determinized_states": 10000,
                "rewrite": "constant_score"
            }
        }
    }
}

Filter过滤器

query和filter的例子

查询despcrition中有java程序员的,并且加个大于50小于90的

query的

GET /book/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "description": "java程序员"
          }
        },
        {
          "range": {
            "price": {
              "gte": 80,
		      "lte": 90
            }
          }
        }
      ]
    }
  }
}

使用filter

GET /book/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "description": "java程序员"
          }
        }
      ],
      "filter": {
        "range": {
          "price": {
            "gte": 80,
		     "lte": 90
          }
        }
      }
    }
  }
}

对比

  • query对文档分数排序,所以要处理文档的分数评分
  • filter只根据条件过滤,不处理分数

性能

filter不参与分数评分排序,同时内置缓存,更快

定位错误语法

GET /book/_validate/query?explain
{
  "query": {
    "mach": {
      "description": "java程序员"
    }
  }
}

错误提示

{
  "valid" : false,
  "error" : "org.elasticsearch.common.ParsingException: no [query] registered for [mach]"
}

写得多的时候可以用这种方法验证

语法,_validate/query?explain

定制排序规则

两种

  • 默认使用query,分数排序
  • fliter,然后定制

使用过滤器的

GET /book/_search 
{
  "query": {
    "constant_score": {
      "filter" : {
            "term" : {
                "studymodel" : "201001"
            }
        }
    }
  },
  "sort": [
    {
      "price": {
        "order": "asc"
      }
    }
  ]
}

正常的

GET book/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "description": "java程序员"
          }
        }
      ]
    }
  }
}

对text排序

如果对一个text field进行排序,结果往往不准确,因为分词后是多个单词,再排序就不是我们想要的结果了。

通常解决方案是,将一个text field建立两次索引,一个分词,用来进行搜索;一个不分词,用来进行排序。、

索引给加多一个属性,就是fields加一个keyword,查的时候直接field.keyword

PUT /website 
{
  "mappings": {
  "properties": {
    "title": {
      "type": "text",
      "fields": {
        "keyword": {
          "type": "keyword"
        }        
      }      
    },
    "content": {
      "type": "text"
    },
    "post_date": {
      "type": "date"
    },
    "author_id": {
      "type": "long"
    }
  }
 }
}
PUT /website/_doc/1
{
  "title": "first article",
  "content": "this is my second article",
  "post_date": "2019-01-01",
  "author_id": 110
}

PUT /website/_doc/2
{
    "title": "second article",
    "content": "this is my second article",
     "post_date": "2019-01-01",
    "author_id": 110
}

PUT /website/_doc/3
{
     "title": "third article",
     "content": "this is my third article",
     "post_date": "2019-01-02",
     "author_id": 110
}

搜索字段的不分词哪里keyword

GET /website/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "title.keyword": {
        "order": "desc"
      }
    }
  ]
}

Scroll分批查询

由于数据量过大,不能一次查出,内部分多次查,查到全部一起返回。

GET /book/_search?scroll=1m
{
  "query": {
    "match_all": {}
  },
  "size": 3
}
{
  "_scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAMOkWTURBNDUtcjZTVUdKMFp5cXloVElOQQ==",
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
     
    ]
  }
}

但是不这么做,一般定制mapping

用于系统内部操作,0停机改变索引,数据转移

JAVA搜索API

简单搜素

@Test
    public void testGetAll() throws IOException {
        //1,构建请求
        SearchRequest request = new SearchRequest("book");

        SearchSourceBuilder builder = new SearchSourceBuilder();

        //匹配全部
        builder.query(QueryBuilders.matchAllQuery());

        //获取某些字段 第一个数组是想要,第二个数组是排除
        builder.fetchSource(new String[]{"name"}, new String[]{});
        request.source(builder);

        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);

        //所有数据
        SearchHits hits = response.getHits();
        SearchHit[] hits1 = hits.getHits();
        for (SearchHit hit:
             hits1) {
            Map<String, Object> map = hit.getSourceAsMap();
            System.out.println(map.get("name"));
            System.out.println(map.get("price"));
            System.out.println(map.get("description"));
            System.out.println(hit);
        }
    }

分页搜素

from size

 @Test
    public void testPage() throws IOException {
//        GET book/_search
//    {
//        "query": {
//          "match_all": {}
//       },
//        "from": 0,
//        "size": 2
//    }
        SearchRequest request = new SearchRequest("book");
        SearchSourceBuilder builder = new SearchSourceBuilder();
        builder.query(QueryBuilders.matchAllQuery());

        builder.from(0);
        builder.size(1);

        request.source(builder);

        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);

        SearchHits hits = response.getHits();

        SearchHit[] hits1 = hits.getHits();

        for (SearchHit hit:
             hits1) {
            System.out.println(hit);
        }

    }

根据ID搜

@Test
    public void testIDS() throws IOException {
        SearchRequest request = new SearchRequest("book");
        SearchSourceBuilder builder = new SearchSourceBuilder();
        builder.query(QueryBuilders.idsQuery().addIds("2"));

        request.source(builder);

        SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);

        SearchHits hits = search.getHits();
        SearchHit[] hits1 = hits.getHits();
        for (SearchHit hit:
                hits1) {
            System.out.println(hit);
        }
    }

全文检索

 @Test
    public void testMatch() throws IOException {
        SearchRequest request = new SearchRequest("book");
        SearchSourceBuilder builder = new SearchSourceBuilder();
        builder.query(QueryBuilders.matchQuery("description", "java"));
        request.source(builder);

        SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        SearchHits hits = search.getHits();
        SearchHit[] hits1 = hits.getHits();
        for (SearchHit hit :
                hits1) {
            System.out.println(hit);
        }
    }

多条件全文检索

 @Test
    public void multiMatch() throws IOException {
        SearchRequest request = new SearchRequest("book");
        SearchSourceBuilder builder = new SearchSourceBuilder();
        builder.query(QueryBuilders.multiMatchQuery("java", "description","name"));
        request.source(builder);

        SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        SearchHits hits = search.getHits();
        SearchHit[] hits1 = hits.getHits();
        for (SearchHit hit :
                hits1) {
            System.out.println(hit);
        }
    }

使用bool查询

 @Test
    public void testBool() throws IOException {

        //    GET /book/_search
//    {
//        "query": {
//          "bool": {
//            "must": [
//            {
//                "multi_match": {
//                  "query": "java程序员",
//                  "fields": ["name","description"]
//            }
//            }
//      ],
//            "should": [
//            {
//                "match": {
//                "studymodel": "201001"
//            }
//            }
//      ]
//        }
//    }
//    }
        //bool复杂搜索
        SearchRequest request = new SearchRequest("book");
        SearchSourceBuilder builder = new SearchSourceBuilder();

        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("java程序", "name", "description");

        MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("studymodel", "201001");

        boolQueryBuilder.must(multiMatchQueryBuilder);
        boolQueryBuilder.should(matchQueryBuilder);

        builder.query(boolQueryBuilder);

        request.source(builder);

        SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        SearchHits hits = search.getHits();
        SearchHit[] hits1 = hits.getHits();
        for (SearchHit hit :
                hits1) {
            System.out.println(hit);
        }
    }

使用过滤

 @Test
    public void testBoolAndFitler() throws IOException {

        //    GET /book/_search
//    {
//        "query": {
//          "bool": {
//            "must": [
//            {
//                "multi_match": {
//                "query": "java程序员",
//                        "fields": ["name","description"]
//            }
//            }
//      ],
//            "should": [
//            {
//                "match": {
//                "studymodel": "201001"
//            }
//            }
//      ],
//            "filter": {
//                "range": {
//                    "price": {
//                        "gte": 50,
//                                "lte": 90
//                    }
//                }
//
//            }
//        }
//    }
//    }
        //bool加上filter复杂搜索
        SearchRequest request = new SearchRequest("book");
        SearchSourceBuilder builder = new SearchSourceBuilder();

        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("java程序", "name", "description");

        MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("studymodel", "201001");

        RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price").gte(50).lte(90);

        boolQueryBuilder.must(multiMatchQueryBuilder);
        boolQueryBuilder.should(matchQueryBuilder);
        boolQueryBuilder.filter(rangeQueryBuilder);

        builder.query(boolQueryBuilder);

        request.source(builder);

        SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        SearchHits hits = search.getHits();
        SearchHit[] hits1 = hits.getHits();
        for (SearchHit hit :
                hits1) {
            System.out.println(hit);
        }
    }

排序

@Test
    public void testSort() throws IOException {
//        GET /book/_search
//    {
//        "query": {
//        "bool": {
//            "must": [
//            {
//                "multi_match": {
//                "query": "java程序员",
//                        "fields": ["name","description"]
//            }
//            }
//      ],
//            "should": [
//            {
//                "match": {
//                "studymodel": "201001"
//            }
//            }
//      ],
//            "filter": {
//                "range": {
//                    "price": {
//                        "gte": 50,
//                                "lte": 90
//                    }
//                }
//
//            }
//        }
//    },
//        "sort": [
//        {
//            "price": {
//            "order": "asc"
//        }
//        }
//  ]
//    }

        //bool加上filter复杂搜索
        SearchRequest request = new SearchRequest("book");
        SearchSourceBuilder builder = new SearchSourceBuilder();

        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("java程序", "name", "description");

        MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("studymodel", "201001");

        RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price").gte(50).lte(90);

        boolQueryBuilder.must(multiMatchQueryBuilder);
        boolQueryBuilder.should(matchQueryBuilder);
        boolQueryBuilder.filter(rangeQueryBuilder);


        //排序
        builder.sort("price", SortOrder.DESC);
        builder.query(boolQueryBuilder);

        request.source(builder);

        SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);

        SearchHits hits = search.getHits();
        SearchHit[] hits1 = hits.getHits();
        for (SearchHit hit :
                hits1) {
            System.out.println(hit);
        }
    }

TFIDF算法

评分机制

算法介绍

分析要素

  • 出现频率,
    • 多的分数高
  • 逆序出现频率
  • 整个文档中越多出现的分数越低
  • 长度
    • field长度越长,相关度越弱

score怎么算出来

  • 1,对用户输入进行分词
  • 2,对每个分词计算tf和idf值
  • 3,综合每个分词的tf和idf值,利用公式计算每个文档评分

DOC value

搜索时使用倒排索引,排序时使用正排索引

DOC value就是正排索引

比如对price进行排序,或者对字符串进行排序

对text field排序,不准确,因为分词后是多个单词,再排序就不是我们的结果

  • 方案一
    • fielddata:true
  • 方案二
    • 使用正排索引,和数据库差不多,一条一条的存

query phase

在返回数据前的阶段,就是把查到的数据先只返回两个id和score字段到协调节点

fetch phase

获取数据阶段,按照上面阶段获取到的id,多次批量获取

preference

跳越结果情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-liiSu8fl-1666522442630)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221012143111772.png)]

  • 情况一
    • 3号文档为同步到R0,查P0和P1则拿到132
    • 到R0和P1拿则拿到12
  • 情况二
    • P0和P1的读取顺序,P0快时,13 然后2
    • P1快时,2 然后13

决定了哪些shard会被用来执行搜索操作

_primary, _primary_first, _local, _only_node:xyz, _prefer_node:xyz, _shards:2,3

解决方案就是将preference设置为一个字符串,比如说user_id,让每个user每次搜索的时候,都使用同一个replica shard去执行,就不会看到bouncing results

ES的聚合

像mysql的聚合函数,求总数,最大值,最小值,平均值等等。

注意,只有不建立分词倒排的才可以进行聚合操作

PUT /book/_mapping/
{
  "properties": {
      "tags": {
          "type": "text",
          "fielddata":true
      }
  }
}

可以通过上述操作,或者在创建mapping的时候加多个field弄个keyword

aggs其实就是聚合

terms求个数

1、需求:计算每个studymodel下的商品数量

select studymodel,count(*) from book group by studymodel

size0是为了跳过具体的数据

GET /book/_search
{
  "size": 0, 
  "query": {
    "match_all": {}
  }, 
  "aggs": {
    "group_by_model": {
      "terms": { "field": "studymodel" }
    }
  }
}

2、需求:计算每个tags下的商品数量

GET /book/_search
{
  "size": 0, 
  "query": {
    "match_all": {}
  }, 
  "aggs": {
    "group_by_tags": {
      "terms": { "field": "tags" }
    }
  }
}

3,需求:加上搜索条件,计算每个tags下的商品数量

GET /book/_search
{
  "size": 0, 
  "query": {
      "match": {
        "description": "java程序员"
      }
  }
  , "aggs": {
      "group_by_tags": {
        "terms": {
          "field": "tags"
        }
      }
    }
}

4,需求:先分组,再算每组的平均值,计算每个tag下的商品的平均价格

GET /book/_search
{
  "size": 0, 

  "aggs": {
      "group_by_tags": {
        "terms": {
          "field": "tags"
        }
        , "aggs": {
          "avg_price": {
            "avg": {
              "field": "price"
            }
          }
        }
      }
    }
}

5,需求:计算每个tag下的商品的平均价格,并且按照平均价格降序排序

es的语法

聚合里可以直接调用聚合的结果进行排序

GET /book/_search
{
  "size": 0, 

  "aggs": {
      "group_by_tags": {
        "terms": {
          "field": "tags",
          "order": {
            "avg_price": "desc"
          }
        }
        , "aggs": {
          "avg_price": {
            "avg": {
              "field": "price"
            }
          }
        }
      }
    }
}

6,需求:1,按照指定的价格范围区间进行分组,2,然后在每组内再按照tag进行分组,3最后再计算每组的平均价格

GET /book/_search
{
  "size": 0,
  "aggs": {
    "group_by_price": {
      "range": {
        "field": "price",
        "ranges": [
          {
            "from": 0,
            "to": 40
          },
          {
            "from": 40,
            "to": 60
          },
          {
            "from": 60,
            "to": 80
          }
        ]
      },
      "aggs": {
        "group_by_tags": {
          "terms": {
            "field": "tags"
            , "order": {
              "average_price": "desc"
            }
          },
          "aggs": {
            "average_price": {
              "avg": {
                "field": "price"
              }
            }
          }
        }
      }
    }
  }
}}

关于两个概率

bucket

一个数据分组。按照城市分桶,

select city,count(*) from table group by city
metric

聚合函数操作,比如求平均值,最小值,最大值

聚合电视的案例

建立index

PUT /tvs
PUT /tvs/_mapping
{			
			"properties": {
				"price": {
					"type": "long"
				},
				"color": {
					"type": "keyword"
				},
				"brand": {
					"type": "keyword"
				},
				"sold_date": {
					"type": "date"
				}
			}
}

4个属性

  • price
  • 价格
  • color颜色
  • brand品牌
  • sold_data 销售日期

插入数据

POST /tvs/_bulk
{ "index": {}}
{ "price" : 1000, "color" : "红色", "brand" : "长虹", "sold_date" : "2019-10-28" }
{ "index": {}}
{ "price" : 2000, "color" : "红色", "brand" : "长虹", "sold_date" : "2019-11-05" }
{ "index": {}}
{ "price" : 3000, "color" : "绿色", "brand" : "小米", "sold_date" : "2019-05-18" }
{ "index": {}}
{ "price" : 1500, "color" : "蓝色", "brand" : "TCL", "sold_date" : "2019-07-02" }
{ "index": {}}
{ "price" : 1200, "color" : "绿色", "brand" : "TCL", "sold_date" : "2019-08-19" }
{ "index": {}}
{ "price" : 2000, "color" : "红色", "brand" : "长虹", "sold_date" : "2019-11-05" }
{ "index": {}}
{ "price" : 8000, "color" : "红色", "brand" : "三星", "sold_date" : "2020-01-01" }
{ "index": {}}
{ "price" : 2500, "color" : "蓝色", "brand" : "小米", "sold_date" : "2020-02-12" }

需求

需求1 统计哪种颜色的电视销量最高

桶的第一个就是

GET /tvs/_search
{
  "size": 0,
  "aggs": {
    "popular_color": {
      "terms": {
        "field": "color"
      }
    }
  }
}
 

统计每种颜色电视平均价格

GET /tvs/_search
{
   "size" : 0,
   "aggs": {
      "colors": {
         "terms": {
            "field": "color"
         },
         "aggs": { 
            "avg_price": { 
               "avg": {
                  "field": "price" 
               }
            }
         }
      }
   }
}

每个颜色下,平均价格及每个颜色下,每个品牌的平均价格

GET /tvs/_search 
{
  "size": 0,
  "aggs": {
    "group_by_color": {
      "terms": {
        "field": "color"
      },
      "aggs": {
        "color_avg_price": {
          "avg": {
            "field": "price"
          }
        },
        "group_by_brand": {
          "terms": {
            "field": "brand"
          },
          "aggs": {
            "brand_avg_price": {
              "avg": {
                "field": "price"
              }
            }
          }
        }
      }
    }
  }
}

#各种颜色的最大,最小,平均,总价格

#各种颜色的最大,最小,平均,总价格
GET /tvs/_search
{
  "size": 0,
  "aggs": {
    "all_colors": {
      "terms": {
        "field": "color"
      },
      "aggs": {
        "avg_price": {"avg":{"field": "price"}},
        "sum_price":{ "sum":{"field": "price"}},
        "min_price":{ "min":{"field": "price"}},
        "max_price":{ "max":{"field": "price"}}
      }
    }
  }
}

#划分范围 histogram

#划分范围 histogram
GET /tvs/_search
{
  "aggs": {
    "price_score": {
      "histogram": {
        "field": "price",
        "interval": 2000
      },
      "aggs": {
        "sum_price": {
          "sum": {
            "field": "price"
          }
        }
      }
    }
  }
}

#按照日期分组聚合

#按照日期分组聚合
GET /tvs/_search
{
  "size": 0,
  "aggs": {
    "data_scope": {
      "date_histogram": {
        "field": "sold_date",
        "interval": "month",
        "format": "yyyy-MM-dd",
        "extended_bounds": {
           "min" : "2019-01-01",
            "max" : "2020-12-31"
        }
      }
    }
  }
}

#需求7 统计每季度每个品牌的销售额

#需求7 统计每季度每个品牌的销售额
GET /tvs/_search
{
   "size": 0,
  "aggs": {
    "data_scope": {
      "date_histogram": {
        "field": "sold_date",
        "interval": "quarter",
        "min_doc_count": 0,
        "format": "yyyy-MM-dd",
        "extended_bounds": {
           "min" : "2019-01-01",
            "max" : "2020-12-31"
        }
      },
      "aggs": {
        "all_brands": {
          "terms": {
            "field": "brand"
          },
          "aggs": {
            "price_3mon": {
              "sum": {
                "field": "price"
              }
            }
          }
        },
        
        "total_sum_price": {
          "sum": {
            "field": "price"
          }
        }
      }
    }
  }
}

#搜索与聚合结合,查询某个品牌按颜色销量

#搜索与聚合结合,查询某个品牌按颜色销量
GET /tvs/_search
{
  "size": 0,
  "query": {
    "term": {
      "brand": {
        "value": "小米"
      }
    }
  },
  "aggs": {
    "group_by_color": {
      "terms": {
        "field": "color"
      }
    }
  }
}

#global bucket:单个品牌与所有品牌销量对比 global为全局桶

#global bucket:单个品牌与所有品牌销量对比 global为全局桶
GET /tvs/_search
{
  "size": 0,
  "query": {
    "term": {
      "brand": {
        "value": "小米"
      }
    }
  },
  "aggs": {
    "one_brand": {
      "avg": {
        "field": "price"
      }
    },
    "all_brand":{
      "global": {}, 
      "aggs": {
        "all_avg_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}

#过滤+聚合:统计价格大于1200的电视平均价格

#过滤+聚合:统计价格大于1200的电视平均价格
GET /tvs/_search 
{
  "size": 0,
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "price": {
            "gte": 1200
          }
        }
      }
    }
  },
  "aggs": {
    "avg_price": {
      "avg": {
        "field": "price"
      }
    }
  }
}

#bucket filter:统计品牌最近一个月的平均价格


GET /tvs/_search 
{
  "size": 0,
  "query": {
    "term": {
      "brand": {
        "value": "小米"
      }
    }
  },
  "aggs": {
    "recent_150d": {
      "filter": {
        "range": {
          "sold_date": {
            "gte": "now-150d"
          }
        }
      },
      "aggs": {
        "recent_150d_avg_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    },
    "recent_140d": {
      "filter": {
        "range": {
          "sold_date": {
            "gte": "now-140d"
          }
        }
      },
      "aggs": {
        "recent_140d_avg_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    },
    "recent_130d": {
      "filter": {
        "range": {
          "sold_date": {
            "gte": "now-130d"
          }
        }
      },
      "aggs": {
        "recent_130d_avg_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}

#排序:按每种颜色的平均销售额降序排序

#排序:按每种颜色的平均销售额降序排序
GET /tvs/_search 
{
  "size": 0,
  "aggs": {
    "group_by_color": {
      "terms": {
        "field": "color",
        "order": {
          "avg_price": "asc"
        }
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}

#排序:按每种颜色的每种品牌平均销售额降序排序

#排序:按每种颜色的每种品牌平均销售额降序排序
GET /tvs/_search  
{
  "size": 0,
  "aggs": {
    "group_by_color": {
      "terms": {
        "field": "color"
      },
      "aggs": {
        "group_by_brand": {
          "terms": {
            "field": "brand",
            "order": {
              "avg_price": "desc"
            }
          },
          "aggs": {
            "avg_price": {
              "avg": {
                "field": "price"
              }
            }
          }
        }
      }
    }
  }
}

javaAPI实现聚合

//需求一:按照颜色分组,计算每个颜色卖出的个数

  //需求一:按照颜色分组,计算每个颜色卖出的个数
        // GET /tvs/_search
        // {
        //     "size": 0,
        //     "query": {"match_all": {}},
        //     "aggs": {
        //       "group_by_color": {
        //         "terms": {
        //             "field": "color"
        //         }
        //     }
        // }
        // }
    @Test
    public void test1() throws IOException {
        SearchRequest request = new SearchRequest("tvs");
        SearchSourceBuilder builder = new SearchSourceBuilder();
        builder.size(0);
        builder.query(QueryBuilders.matchAllQuery());

        TermsAggregationBuilder aggsBuild = AggregationBuilders.terms("group_by_color").field("color");
        builder.aggregation(aggsBuild);

        request.source(builder);
        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        
        //获取aggs
        Aggregations aggregations = response.getAggregations();
        //分组
        Terms group_by_color = aggregations.get("group_by_color");
        
        List<? extends Terms.Bucket> buckets = group_by_color.getBuckets();
        
        for (Terms.Bucket bucket : buckets) {
            
            String key = bucket.getKeyAsString();
            System.out.println("key:"+key);

            long docCount = bucket.getDocCount();
            System.out.println("docCount:"+docCount);

            System.out.println("=================================");
        }
    }

    // #需求二:按照颜色分组,计算每个颜色卖出的个数,每个颜色卖出的平均价格
    @Test
    public void testAggsAndAvg() throws IOException {
        // GET /tvs/_search
        // {
        //     "size": 0,
        //      "query": {"match_all": {}},
        //     "aggs": {
        //     "group_by_color": {
        //         "terms": {
        //             "field": "color"
        //         },
        //         "aggs": {
        //             "avg_price": {
        //                 "avg": {
        //                     "field": "price"
        //                 }
        //             }
        //         }
        //     }
        // }
        // }

        //1 构建请求
        SearchRequest searchRequest=new SearchRequest("tvs");

        //请求体
        SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
        searchSourceBuilder.size(0);
        searchSourceBuilder.query(QueryBuilders.matchAllQuery());

        TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms("group_by_color").field("color");

        //terms聚合下填充一个子聚合
        AvgAggregationBuilder avgAggregationBuilder = AggregationBuilders.avg("avg_price").field("price");
        termsAggregationBuilder.subAggregation(avgAggregationBuilder);

        searchSourceBuilder.aggregation(termsAggregationBuilder);

        //请求体放入请求头
        searchRequest.source(searchSourceBuilder);

        //2 执行
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

        //3 获取结果
        // {
        //     "key" : "红色",
        //      "doc_count" : 4,
        //      "avg_price" : {
        //        "value" : 3250.0
        //       }
        // }
        Aggregations aggregations = searchResponse.getAggregations();
        Terms group_by_color = aggregations.get("group_by_color");
        List<? extends Terms.Bucket> buckets = group_by_color.getBuckets();
        for (Terms.Bucket bucket : buckets) {
            String key = bucket.getKeyAsString();
            System.out.println("key:"+key);

            long docCount = bucket.getDocCount();
            System.out.println("docCount:"+docCount);

            Aggregations aggregations1 = bucket.getAggregations();
            Avg avg_price = aggregations1.get("avg_price");
            double value = avg_price.getValue();
            System.out.println("value:"+value);

            System.out.println("=================================");
        }
    }

  
 // #需求四:按照售价每2000价格划分范围,算出每个区间的销售总额 histogram
    @Test
    public void testAggsAndHistogram() throws IOException {
        // GET /tvs/_search
        // {
        //     "size" : 0,
        //     "aggs":{
        //      "by_histogram":{
        //         "histogram":{
        //             "field": "price",
        //             "interval": 2000
        //         },
        //         "aggs":{
        //             "income": {
        //                 "sum": {
        //                     "field" : "price"
        //                 }
        //             }
        //         }
        //     }
        // }
        // }

        //1 构建请求
        SearchRequest searchRequest=new SearchRequest("tvs");

        //请求体
        SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
        searchSourceBuilder.size(0);
        searchSourceBuilder.query(QueryBuilders.matchAllQuery());

        HistogramAggregationBuilder histogramAggregationBuilder = AggregationBuilders.histogram("by_histogram").field("price").interval(2000);

        SumAggregationBuilder sumAggregationBuilder = AggregationBuilders.sum("income").field("price");
        histogramAggregationBuilder.subAggregation(sumAggregationBuilder);
        searchSourceBuilder.aggregation(histogramAggregationBuilder);

        //请求体放入请求头
        searchRequest.source(searchSourceBuilder);

        //2 执行
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

        //3 获取结果
        // {
        //     "key" : 0.0,
        //     "doc_count" : 3,
        //      income" : {
        //          "value" : 3700.0
        //       }
        // }
        Aggregations aggregations = searchResponse.getAggregations();
        Histogram group_by_color = aggregations.get("by_histogram");
        List<? extends Histogram.Bucket> buckets = group_by_color.getBuckets();
        for (Histogram.Bucket bucket : buckets) {
            String keyAsString = bucket.getKeyAsString();
            System.out.println("keyAsString:"+keyAsString);
            long docCount = bucket.getDocCount();
            System.out.println("docCount:"+docCount);

            Aggregations aggregations1 = bucket.getAggregations();
            Sum income = aggregations1.get("income");
            double value = income.getValue();
            System.out.println("value:"+value);

            System.out.println("=================================");

        }
    }

 
  // #需求五:计算每个季度的销售总额
    @Test
    public void testAggsAndDateHistogram() throws IOException {
        // GET /tvs/_search
        // {
        //     "size" : 0,
        //     "aggs": {
        //     "sales": {
        //         "date_histogram": {
        //                      "field": "sold_date",
        //                     "interval": "quarter",
        //                     "format": "yyyy-MM-dd",
        //                     "min_doc_count" : 0,
        //                     "extended_bounds" : {
        //                         "min" : "2019-01-01",
        //                         "max" : "2020-12-31"
        //             }
        //         },
        //         "aggs": {
        //             "income": {
        //                 "sum": {
        //                     "field": "price"
        //                 }
        //             }
        //         }
        //     }
        // }
        // }

        //1 构建请求
        SearchRequest searchRequest=new SearchRequest("tvs");

        //请求体
        SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
        searchSourceBuilder.size(0);
        searchSourceBuilder.query(QueryBuilders.matchAllQuery());

        DateHistogramAggregationBuilder dateHistogramAggregationBuilder = AggregationBuilders.dateHistogram("date_histogram").field("sold_date").calendarInterval(DateHistogramInterval.QUARTER)
                .format("yyyy-MM-dd").minDocCount(0).extendedBounds(new ExtendedBounds("2019-01-01", "2020-12-31"));
        SumAggregationBuilder sumAggregationBuilder = AggregationBuilders.sum("income").field("price");
        dateHistogramAggregationBuilder.subAggregation(sumAggregationBuilder);

        searchSourceBuilder.aggregation(dateHistogramAggregationBuilder);
        //请求体放入请求头
        searchRequest.source(searchSourceBuilder);

        //2 执行
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

        //3 获取结果
        // {
        //     "key_as_string" : "2019-01-01",
        //      "key" : 1546300800000,
        //      "doc_count" : 0,
        //      "income" : {
        //         "value" : 0.0
        //      }
        // }
        Aggregations aggregations = searchResponse.getAggregations();
        ParsedDateHistogram date_histogram = aggregations.get("date_histogram");
        List<? extends Histogram.Bucket> buckets = date_histogram.getBuckets();
        for (Histogram.Bucket bucket : buckets) {
            String keyAsString = bucket.getKeyAsString();
            System.out.println("keyAsString:"+keyAsString);
            long docCount = bucket.getDocCount();
            System.out.println("docCount:"+docCount);

            Aggregations aggregations1 = bucket.getAggregations();
            Sum income = aggregations1.get("income");
            double value = income.getValue();
            System.out.println("value:"+value);

            System.out.println("====================");
        }

    }
 // #需求三:按照颜色分组,计算每个颜色卖出的个数,以及每个颜色卖出的平均值、最大值、最小值、总和。
    @Test
    public void testAggsAndMore() throws IOException {
        // GET /tvs/_search
        // {
        //     "size" : 0,
        //     "aggs": {
        //      "group_by_color": {
        //         "terms": {
        //             "field": "color"
        //         },
        //         "aggs": {
        //             "avg_price": { "avg": { "field": "price" } },
        //             "min_price" : { "min": { "field": "price"} },
        //             "max_price" : { "max": { "field": "price"} },
        //             "sum_price" : { "sum": { "field": "price" } }
        //         }
        //     }
        // }
        // }

        //1 构建请求
        SearchRequest searchRequest=new SearchRequest("tvs");

        //请求体
        SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
        searchSourceBuilder.size(0);
        searchSourceBuilder.query(QueryBuilders.matchAllQuery());

        TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms("group_by_color").field("color");


        //termsAggregationBuilder里放入多个子聚合
        AvgAggregationBuilder avgAggregationBuilder = AggregationBuilders.avg("avg_price").field("price");
        MinAggregationBuilder minAggregationBuilder = AggregationBuilders.min("min_price").field("price");
        MaxAggregationBuilder maxAggregationBuilder = AggregationBuilders.max("max_price").field("price");
        SumAggregationBuilder sumAggregationBuilder = AggregationBuilders.sum("sum_price").field("price");

        termsAggregationBuilder.subAggregation(avgAggregationBuilder);
        termsAggregationBuilder.subAggregation(minAggregationBuilder);
        termsAggregationBuilder.subAggregation(maxAggregationBuilder);
        termsAggregationBuilder.subAggregation(sumAggregationBuilder);


        searchSourceBuilder.aggregation(termsAggregationBuilder);

        //请求体放入请求头
        searchRequest.source(searchSourceBuilder);

        //2 执行
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

        //3 获取结果
        // {
        //     "key" : "红色",
        //     "doc_count" : 4,
        //     "max_price" : {
        //          "value" : 8000.0
        //     },
        //     "min_price" : {
        //          "value" : 1000.0
        // },
        //     "avg_price" : {
        //         "value" : 3250.0
        // },
        //     "sum_price" : {
        //         "value" : 13000.0
        // }
        // }
        Aggregations aggregations = searchResponse.getAggregations();
        Terms group_by_color = aggregations.get("group_by_color");
        List<? extends Terms.Bucket> buckets = group_by_color.getBuckets();
        for (Terms.Bucket bucket : buckets) {
            String key = bucket.getKeyAsString();
            System.out.println("key:"+key);

            long docCount = bucket.getDocCount();
            System.out.println("docCount:"+docCount);

            Aggregations aggregations1 = bucket.getAggregations();

            Max max_price = aggregations1.get("max_price");
            double maxPriceValue = max_price.getValue();
            System.out.println("maxPriceValue:"+maxPriceValue);

            Min min_price = aggregations1.get("min_price");
            double minPriceValue = min_price.getValue();
            System.out.println("minPriceValue:"+minPriceValue);

            Avg avg_price = aggregations1.get("avg_price");
            double avgPriceValue = avg_price.getValue();
            System.out.println("avgPriceValue:"+avgPriceValue);

            Sum sum_price = aggregations1.get("sum_price");
            double sumPriceValue = sum_price.getValue();
            System.out.println("sumPriceValue:"+sumPriceValue);

            System.out.println("=================================");
        }
    }

ES7新特性

可以使用sql的语法进行查询

get /_sql?format=txt
{
  "query": "select color,avg(price),min(price) from tvs group by color"

}

使用javaapi实现sql

  • 1,必要有白金会员的功能

  • 2,导入坐标

    导入依赖
    
        <dependency>
            <groupId>org.elasticsearch.plugin</groupId>
            <artifactId>x-pack-sql-jdbc</artifactId>
            <version>7.3.0</version>
        </dependency>
        
    
    
    远程创库
        <repositories>
            <repository>
                <id>elastic.co</id>
                <url>https://artifacts.elastic.co/maven</url>
            </repository>
        </repositories>
    
    

3,api

    @Test
    public void sqlTestJDBC(){
        //1,创建连接
        try {
            Connection connection = DriverManager.getConnection("jdbc:es://http://localhost:9000");
            //2,创建statement
            Statement statement = connection.createStatement();
            //3,执行sql
            ResultSet resultSet = statement.executeQuery("select * from tvs");
            //4,获取结果
            while (resultSet.next()){
                System.out.println(resultSet.getString(1));
                System.out.println(resultSet.getString(2));
                System.out.println(resultSet.getString(3));
                System.out.println(resultSet.getString(4));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

Logstash

什么是logstash?

它是一个数据抽取工具,转移数据。

1573291947262

  • input

  • output

  • filter

下载

https://www.elastic.co/cn/downloads/logstash

配置文件

input {
    #输入插件
}
filter {
    #过滤匹配插件
}
output {
    #输出插件
}

启动

logstash.bat -e 'input{stdin{}} output{stdout{}}'

在生产中input{stdin{}},这里是输入环境,也就是你的数据,比如数据库中的表或者某个日志文件

output{stdout{}}输出环境,也就是我们的es

我们可将它物化到一个文件中

我们在一个配置文件中这样写下这些配置文件

输入在控制台输入,输出也是在控制台,

然后启动

logstash.bat -f …/config/test1.conf

input{
	stdin{
	}
}

filter{
	#过滤匹配插件
}

output{
	stdout{
		codec=>rubydebug
	}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WhtM3uiq-1666522442631)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221013114000571.png)]

输入插件

https://www.elastic.co/guide/en/logstash/current/input-plugins.html

可以从各个地方中获取数据

比如这些,还有很多很多

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VcCpvStB-1666522442631)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221013114430807.png)]

读取文件信息
input {
    file {
        path => ["/var/*/*"]
        start_position => "beginning"
    }
}
output {
    stdout{
        codec=>rubydebug    
    }
}   

这样的话,我们可以实现分布式日志读到一个文件的实现

读取TCP网络信息
input {
  tcp {
    port => "1234"
  }
}

filter {
  grok {
    match => { "message" => "%{SYSLOGLINE}" }
  }
}

output {
    stdout{
        codec=>rubydebug
    }
}

所以我们也可以取读jdbc,也就是mysql的,

过滤插件

https://www.elastic.co/guide/en/logstash/current/filter-plugins.html

Grok 正则捕获

grok是一个十分强大的logstash filter插件,他可以通过正则解析任意文本,将非结构化日志数据弄成结构化和方便查询的结构。他是目前logstash 中解析非结构化日志数据最好的方式。

Grok 的语法规则是:

%{语法: 语义}

例如输入的内容为:

172.16.213.132 [07/Feb/2019:16:24:19 +0800] "GET / HTTP/1.1" 403 5039

%{IP:clientip}匹配模式将获得的结果为:clientip: 172.16.213.132 %{HTTPDATE:timestamp}匹配模式将获得的结果为:timestamp: 07/Feb/2018:16:24:19 +0800 而%{QS:referrer}匹配模式将获得的结果为:referrer: “GET / HTTP/1.1”

下面是一个组合匹配模式,它可以获取上面输入的所有内容:

%{IP:clientip}\ \[%{HTTPDATE:timestamp}\]\ %{QS:referrer}\ %{NUMBER:response}\ %{NUMBER:bytes}

通过上面这个组合匹配模式,我们将输入的内容分成了五个部分,即五个字段,将输入内容分割为不同的数据字段,这对于日后解析和查询日志数据非常有用,这正是使用grok的目的。

例子:

input{
    stdin{}
}
filter{
    grok{
        match => ["message","%{IP:clientip}\ \[%{HTTPDATE:timestamp}\]\ %{QS:referrer}\ %{NUMBER:response}\ %{NUMBER:bytes}"]
    }
}
output{
    stdout{
        codec => "rubydebug"
    }
}

输入内容:

172.16.213.132 [07/Feb/2019:16:24:19 +0800] "GET / HTTP/1.1" 403 5039
# 2、时间处理(Date)

date插件是对于排序事件和回填旧数据尤其重要,它可以用来转换日志记录中的时间字段,变成LogStash::Timestamp对象,然后转存到@timestamp字段里,这在之前已经做过简单的介绍。 下面是date插件的一个配置示例(这里仅仅列出filter部分):

filter {
    grok {
        match => ["message", "%{HTTPDATE:timestamp}"]
    }
    date {
        match => ["timestamp", "dd/MMM/yyyy:HH:mm:ss Z"]
    }
}
# 3、数据修改(Mutate)
# (1)正则表达式替换匹配字段

gsub可以通过正则表达式替换字段中匹配到的值,只对字符串字段有效,下面是一个关于mutate插件中gsub的示例(仅列出filter部分):

filter {
    mutate {
        gsub => ["filed_name_1", "/" , "_"]
    }
}

这个示例表示将filed_name_1字段中所有"/“字符替换为”_"。

# (2)分隔符分割字符串为数组

split可以通过指定的分隔符分割字段中的字符串为数组,下面是一个关于mutate插件中split的示例(仅列出filter部分):

filter {
    mutate {
        split => ["filed_name_2", "|"]
    }
}

这个示例表示将filed_name_2字段以"|"为区间分隔为数组。

# (3)重命名字段

rename可以实现重命名某个字段的功能,下面是一个关于mutate插件中rename的示例(仅列出filter部分):

filter {
    mutate {
        rename => { "old_field" => "new_field" }
    }
}

这个示例表示将字段old_field重命名为new_field。

# (4)删除字段

remove_field可以实现删除某个字段的功能,下面是一个关于mutate插件中remove_field的示例(仅列出filter部分):

filter {
    mutate {
        remove_field  =>  ["timestamp"]
    }
}

这个示例表示将字段timestamp删除。

# (5)GeoIP 地址查询归类
filter {
    geoip {
        source => "ip_field"
    }
}
# 综合例子:
  • 输入
    • 控制台输入
  • 过滤
    • grok正则插件
    • 删除message
    • 将timestamp格式
    • response状态码改为float类型
    • 在referrer中的\改为空
    • clientip以.分割为数组
  • 输出
    • 控制台输出
input {
    stdin {}
}
filter {
    grok {
        match => { "message" => "%{IP:clientip}\ \[%{HTTPDATE:timestamp}\]\ %{QS:referrer}\ %{NUMBER:response}\ %{NUMBER:bytes}" }
        remove_field => [ "message" ]
   }
date {
        match => ["timestamp", "dd/MMM/yyyy:HH:mm:ss Z"]
    }
mutate {
          convert => [ "response","float" ]
           rename => { "response" => "response_new" }   
           gsub => ["referrer","\"",""]          
           split => ["clientip", "."]
        }
}
output {
    stdout {
        codec => "rubydebug"
    }

四、Logstash输出插件(output)

https://www.elastic.co/guide/en/logstash/current/output-plugins.html

output是Logstash的最后阶段,一个事件可以经过多个输出,而一旦所有输出处理完成,整个事件就执行完成。 一些常用的输出包括:

  • file: 表示将日志数据写入磁盘上的文件。
  • elasticsearch:表示将日志数据发送给Elasticsearch。Elasticsearch可以高效方便和易于查询的保存数据。

1、输出到标准输出(stdout)

output {
    stdout {
        codec => rubydebug
    }
}

2、保存为文件(file)

统一输出实现分布式集中管理日志

output {
    file {
        path => "/data/log/%{+yyyy-MM-dd}/%{host}_%{+HH}.log"
    }
}

3、输出到elasticsearch

output {
    elasticsearch {
        host => ["192.168.1.1:9200","172.16.213.77:9200"]
        index => "logstash-%{+YYYY.MM.dd}"       
    }
}
  • host:是一个数组类型的值,后面跟的值是elasticsearch节点的地址与端口,默认端口是9200。可添加多个地址。
  • index:写入elasticsearch的索引的名称,这里可以使用变量。Logstash提供了%{+YYYY.MM.dd}这种写法。在语法解析的时候,看到以+ 号开头的,就会自动认为后面是时间格式,尝试用时间格式来解析后续字符串。这种以天为单位分割的写法,可以很容易的删除老的数据或者搜索指定时间范围内的数据。此外,注意索引名中不能有大写字母。
  • manage_template:用来设置是否开启logstash自动管理模板功能,如果设置为false将关闭自动管理模板功能。如果我们自定义了模板,那么应该设置为false。
  • template_name:这个配置项用来设置在Elasticsearch中模板的名称。

# 五、综合案例

  • 输入 监听ngix日志文件
  • 过滤和上面的一样
  • 输出到es保存
    • 往那个索引输入,接上年月日,更方便按时间看日志
input {
    file {
        path => ["D:/ES/logstash-7.3.0/nginx.log"]        
        start_position => "beginning"
    }
}

filter {
    grok {
        match => { "message" => "%{IP:clientip}\ \[%{HTTPDATE:timestamp}\]\ %{QS:referrer}\ %{NUMBER:response}\ %{NUMBER:bytes}" }
        remove_field => [ "message" ]
   }
	date {
        match => ["timestamp", "dd/MMM/yyyy:HH:mm:ss Z"]
    }
	mutate {
           rename => { "response" => "response_new" }
           convert => [ "response","float" ]
           gsub => ["referrer","\"",""]
           remove_field => ["timestamp"]
           split => ["clientip", "."]
        }
}

output {
    stdout {
        codec => "rubydebug"
    }

elasticsearch {
    host => ["localhost:9200"]
    index => "logstash-%{+YYYY.MM.dd}"       
}

}

select content from message_1 where content like “%今天不开心%”;

select * from message_1 where content in
(select content from message_1 where content like “%今天不开心%”);

集群部署

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-esRYaYag-1666522442632)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221013152652923.png)]

结点的三个角色

主结点:master节点主要用于集群的管理及索引 比如新增结点、分片分配、索引的新增和删除等。 数据结点:data 节点上保存了数据分片,它负责索引和搜索操作。 客户端结点:client 节点仅作为请求客户端存在,client的作用也作为负载均衡器,client 节点不存数据,只是将请求均衡转发到其它结点。

通过下边两项参数来配置结点的功能:

node.master: #是否允许为主结点
node.data: #允许存储数据作为数据结点
node.ingest: #是否允许成为协调节点

四种组合方式:

master=true,data=true:即是主结点又是数据结点
master=false,data=true:仅是数据结点
master=true,data=false:仅是主结点,不存储数据
master=false,data=false:即不是主结点也不是数据结点,此时可设置ingest为true表示它是一个客户端。

协调节点仅仅接收请求,不存数据。

打印日志的收集实例

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/base.xml" />
    <logger name="org.springframework.web" level="INFO"/>
    <logger name="org.springboot.sample" level="TRACE" />
    <!-- 日志输出格式:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
    <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
    <!-- 控制台输出 -->
<!--
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
    </appender>
-->

    <!-- 开发、测试环境 -->
    <springProfile name="dev,test">
        <!--日志存放路径-->
        <property name="log.path" value="C:/tjScience/logs" />
        <!--日志文件错误日志保存位置-->
        <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>${log.path}/sys-error.log</file>
            <!-- 循环政策:基于时间创建日志文件 -->
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <!-- 日志文件名格式 -->
                <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
                <!-- 日志最大的历史 30天 -->
                <maxHistory>30</maxHistory>
            </rollingPolicy>
            <encoder>
                <pattern>${log.pattern}</pattern>
            </encoder>
            <filter class="ch.qos.logback.classic.filter.LevelFilter">
                <!-- 过滤的级别 -->
                <level>ERROR</level>
                <!-- 匹配时的操作:接收指定级别的(记录) -->
                <onMatch>ACCEPT</onMatch>
                <!-- 不匹配时的操作:拒绝(不记录) -->
                <onMismatch>DENY</onMismatch>
            </filter>
        </appender>
        <root lever="INFO">
            <appender-ref ref="file_error" />
        </root>
        <logger name="org.springframework.web" level="INFO"/>
        <logger name="org.springboot.sample" level="INFO" />
        <logger name="cn.tj.controller" level="DEBUG" />
        <logger name="cn.tj.service" level="DEBUG" />
    </springProfile>

    <!-- 生产环境 -->
    <springProfile name="pro">
        <!--日志存放路径-->
        <property name="log.path" value="/usr/local/tjScience/logs" />
        <!--日志文件错误日志保存位置-->
        <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>${log.path}/sys-error.log</file>
            <!-- 循环政策:基于时间创建日志文件 -->
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <!-- 日志文件名格式 -->
                <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
                <!-- 日志最大的历史 30天 -->
                <maxHistory>30</maxHistory>
            </rollingPolicy>
            <encoder>
                <pattern>${log.pattern}</pattern>
            </encoder>
            <filter class="ch.qos.logback.classic.filter.LevelFilter">
                <!-- 过滤的级别 -->
                <level>ERROR</level>
                <!-- 匹配时的操作:接收指定级别的(记录) -->
                <onMatch>ACCEPT</onMatch>
                <!-- 不匹配时的操作:拒绝(不记录) -->
                <onMismatch>DENY</onMismatch>
            </filter>
        </appender>
        <root lever="ERROR">
            <appender-ref ref="file_error" />
        </root>
        <logger name="org.springframework.web" level="ERROR"/>
        <logger name="org.springboot.sample" level="ERROR" />
        <logger name="cn.tj" level="ERROR" />
    </springProfile>

</configuration>
/**
 * creste by ydlclass.ydl
 */
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestLog {
    private static final Logger LOGGER= LoggerFactory.getLogger(TestLog.class);

    @Test
    public void testLog(){
        Random random =new Random();

        while (true){
            int userid=random.nextInt(10);
            LOGGER.info("userId:{},send:{}",userid,"hello world.I am "+userid);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

然后我们可以使用logstash到这个日志文件去收集日志到es

写这个logstash很难,我们可以去ibana开启grok debugger模式,解析失败就过滤掉

实现搜索引擎

1、mysql导入course_pub表
# 2、创建索引xc_course
# 3、创建映射
PUT /xc_course
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "properties": {
      "description" : {
                "analyzer" : "ik_max_word",
                "search_analyzer": "ik_smart",
               "type" : "text"
            },
            "grade" : {
               "type" : "keyword"
            },
            "id" : {
               "type" : "keyword"
            },
            "mt" : {
               "type" : "keyword"
            },
            "name" : {
                "analyzer" : "ik_max_word",
           "search_analyzer": "ik_smart",
               "type" : "text"
            },
            "users" : {
               "index" : false,
               "type" : "text"
            },
            "charge" : {
               "type" : "keyword"
            },
            "valid" : {
               "type" : "keyword"
            },
            "pic" : {
               "index" : false,
               "type" : "keyword"
            },
            "qq" : {
               "index" : false,
               "type" : "keyword"
            },
            "price" : {
               "type" : "float"
            },
            "price_old" : {
               "type" : "float"
            },
            "st" : {
               "type" : "keyword"
            },
            "status" : {
               "type" : "keyword"
            },
            "studymodel" : {
               "type" : "keyword"
            },
            "teachmode" : {
               "type" : "keyword"
            },
            "teachplan" : {
                "analyzer" : "ik_max_word",
           "search_analyzer": "ik_smart",
               "type" : "text"
            },
           "expires" : {
               "type" : "date",
            "format": "yyyy-MM-dd HH:mm:ss"
            },
            "pub_time" : {
               "type" : "date",
             "format": "yyyy-MM-dd HH:mm:ss"
            },
            "start_time" : {
               "type" : "date",
           "format": "yyyy-MM-dd HH:mm:ss"
            },
          "end_time" : {
                 "type" : "date",
           "format": "yyyy-MM-dd HH:mm:ss"
            }
    }
  } 
}
# 4、logstash创建模板文件

Logstash的工作是从MySQL中读取数据,向ES中创建索引,这里需要提前创建mapping的模板文件以便logstash使用。

在logstach的config目录创建xc_course_template.json,内容如下:

{
   "mappings" : {
      "doc" : {
         "properties" : {
            "charge" : {
               "type" : "keyword"
            },
            "description" : {
               "analyzer" : "ik_max_word",
               "search_analyzer" : "ik_smart",
               "type" : "text"
            },
            "end_time" : {
               "format" : "yyyy-MM-dd HH:mm:ss",
               "type" : "date"
            },
            "expires" : {
               "format" : "yyyy-MM-dd HH:mm:ss",
               "type" : "date"
            },
            "grade" : {
               "type" : "keyword"
            },
            "id" : {
               "type" : "keyword"
            },
            "mt" : {
               "type" : "keyword"
            },
            "name" : {
               "analyzer" : "ik_max_word",
               "search_analyzer" : "ik_smart",
               "type" : "text"
            },
            "pic" : {
               "index" : false,
               "type" : "keyword"
            },
            "price" : {
               "type" : "float"
            },
            "price_old" : {
               "type" : "float"
            },
            "pub_time" : {
               "format" : "yyyy-MM-dd HH:mm:ss",
               "type" : "date"
            },
            "qq" : {
               "index" : false,
               "type" : "keyword"
            },
            "st" : {
               "type" : "keyword"
            },
            "start_time" : {
               "format" : "yyyy-MM-dd HH:mm:ss",
               "type" : "date"
            },
            "status" : {
               "type" : "keyword"
            },
            "studymodel" : {
               "type" : "keyword"
            },
            "teachmode" : {
               "type" : "keyword"
            },
            "teachplan" : {
               "analyzer" : "ik_max_word",
               "search_analyzer" : "ik_smart",
               "type" : "text"
            },
            "users" : {
               "index" : false,
               "type" : "text"
            },
            "valid" : {
               "type" : "keyword"
            }
         }
      }
   },
   "template" : "xc_course"
}
# 5、logstash配置mysql.conf

1、ES采用UTC时区问题

ES采用UTC 时区,比北京时间早8小时,所以ES读取数据时让最后更新时间加8小时

where timestamp > date_add(:sql_last_value,INTERVAL 8 HOUR)

2、logstash每个执行完成会在/config/logstash_metadata记录执行时间下次以此时间为基准进行增量同步数据到索引库。

# 6、启动
.\logstash.bat -f ..\config\mysql.conf
# 7、后端代码

(1)Controller

@RestController
@RequestMapping("/search/course")
public class EsCourseController  {
    @Autowired
    EsCourseService esCourseService;

    @GetMapping(value="/list/{page}/{size}")
    public QueryResponseResult<CoursePub> list(@PathVariable("page") int page, @PathVariable("size") int size, CourseSearchParam courseSearchParam) {
        return esCourseService.list(page,size,courseSearchParam);
    }

}

(2)

@Service
public class EsCourseService {
    @Value("${heima.course.source_field}")
    private String source_field;

    @Autowired
    RestHighLevelClient restHighLevelClient;

    //课程搜索
    public QueryResponseResult<CoursePub> list(int page, int size, CourseSearchParam courseSearchParam) {
        if (courseSearchParam == null) {
            courseSearchParam = new CourseSearchParam();
        }
        //1创建搜索请求对象
        SearchRequest searchRequest = new SearchRequest("xc_course");

        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //过虑源字段
        String[] source_field_array = source_field.split(",");
        searchSourceBuilder.fetchSource(source_field_array, new String[]{});
        //创建布尔查询对象
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //搜索条件
        //根据关键字搜索
        if (StringUtils.isNotEmpty(courseSearchParam.getKeyword())) {
            MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(courseSearchParam.getKeyword(), "name", "description", "teachplan")
                    .minimumShouldMatch("70%")
                    .field("name", 10);
            boolQueryBuilder.must(multiMatchQueryBuilder);
        }
        if (StringUtils.isNotEmpty(courseSearchParam.getMt())) {
            //根据一级分类
            boolQueryBuilder.filter(QueryBuilders.termQuery("mt", courseSearchParam.getMt()));
        }
        if (StringUtils.isNotEmpty(courseSearchParam.getSt())) {
            //根据二级分类
            boolQueryBuilder.filter(QueryBuilders.termQuery("st", courseSearchParam.getSt()));
        }
        if (StringUtils.isNotEmpty(courseSearchParam.getGrade())) {
            //根据难度等级
            boolQueryBuilder.filter(QueryBuilders.termQuery("grade", courseSearchParam.getGrade()));
        }

        //设置boolQueryBuilder到searchSourceBuilder
        searchSourceBuilder.query(boolQueryBuilder);
        //设置分页参数
        if (page <= 0) {
            page = 1;
        }
        if (size <= 0) {
            size = 12;
        }
        //起始记录下标
        int from = (page - 1) * size;
        searchSourceBuilder.from(from);
        searchSourceBuilder.size(size);

        //设置高亮
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.preTags("<font class='eslight'>");
        highlightBuilder.postTags("</font>");
        //设置高亮字段
//        <font class='eslight'>node</font>学习
        highlightBuilder.fields().add(new HighlightBuilder.Field("name"));
        searchSourceBuilder.highlighter(highlightBuilder);

        searchRequest.source(searchSourceBuilder);

        QueryResult<CoursePub> queryResult = new QueryResult();
        List<CoursePub> list = new ArrayList<CoursePub>();
        try {
            //2执行搜索
            SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            //3获取响应结果
            SearchHits hits = searchResponse.getHits();
            long totalHits=hits.getTotalHits().value;
            //匹配的总记录数
//            long totalHits = hits.totalHits;
            queryResult.setTotal(totalHits);
            SearchHit[] searchHits = hits.getHits();
            for (SearchHit hit : searchHits) {
                CoursePub coursePub = new CoursePub();
                //源文档
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                //取出id
                String id = (String) sourceAsMap.get("id");
                coursePub.setId(id);
                //取出name
                String name = (String) sourceAsMap.get("name");
                //取出高亮字段name
                Map<String, HighlightField> highlightFields = hit.getHighlightFields();
                if (highlightFields != null) {
                    HighlightField highlightFieldName = highlightFields.get("name");
                    if (highlightFieldName != null) {
                        Text[] fragments = highlightFieldName.fragments();
                        StringBuffer stringBuffer = new StringBuffer();
                        for (Text text : fragments) {
                            stringBuffer.append(text);
                        }
                        name = stringBuffer.toString();
                    }
                }
                coursePub.setName(name);
                //图片
                String pic = (String) sourceAsMap.get("pic");
                coursePub.setPic(pic);
                //价格
                Double price = null;
                try {
                    if (sourceAsMap.get("price") != null) {
                        price = (Double) sourceAsMap.get("price");
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                }
                coursePub.setPrice(price);
                //旧价格
                Double price_old = null;
                try {
                    if (sourceAsMap.get("price_old") != null) {
                        price_old = (Double) sourceAsMap.get("price_old");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                coursePub.setPrice_old(price_old);
                //将coursePub对象放入list
                list.add(coursePub);
            }


        } catch (IOException e) {
            e.printStackTrace();
        }

        queryResult.setList(list);
        QueryResponseResult<CoursePub> queryResponseResult = new QueryResponseResult<CoursePub>(CommonCode.SUCCESS, queryResult);

        return queryResponseResult;
    }


}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值