Elasticsearch笔记

ES

ES是一个开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容
ES结合kibana、Logstash、Beats,也就是Elastic技术栈,被广泛应用在日志数据分析、实时监控等领域
ES是Elastic技术栈的核心,负责存储、搜索、处理数据

Lucene

一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发

Lucene优缺点
  • 优势
    • 易扩展
    • 高性能(基于倒排索引)
  • 缺点
    • 只限于Java语言开发
    • 学习曲线陡峭
    • 不支持水平扩展

ES相对于Lucene的优点

  • 支持分布式,可水平扩展
  • 提供Restful接口,可被任何语言调用

ES相关概念

文档和词条

ES是面向文档(Document)存储的,可以是数据库中的一条商品信息,一个订单信息。

文档数据会被序列化为json格式后存储到ES中

json文档中往往包含很多字段(Field),类似于数据库中的列

  • 文档: 每条数据就是一个文档
  • 词条: 文档按照语义分成的词语
索引和映射

索引(Index)就是相同类型的文档的集合

数据库中的表会有约束信息用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),这是索引文档的字段约束信息,类似表的结构约束

例:

  • 所有的用户文档,如果组织在一起,就称为用户的索引
  • 所有的商品文档,如果组织在一起,就是商品的索引

在这里插入图片描述

ES和MySQL
MySQLElasticsearch说明
TableIndex索引(index),就是文档的集合,类似数据库的表(table)
RowDocument文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
ColumnField字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)
SchemaMappingMapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
SQLDSLDSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD
  • MySQL擅长事务类型操作,可以确保数据的安全和一致性
  • Elasticsearch擅长海量数据的搜索、分析、计算
  • MySQL和Elasticsearch不是相互替代的关系、是互补的关系

所以,两者在实际使用中是结合使用的,对安全性要求较高的使用MySQL实现、对查询性能要求较高的,使用ES实现,两者再基于某种方式,实现数据的同步,保证一致性。

倒排索引

倒排索引是基于MySQL这样的正向索引而言的

倒排索引流程
  • 将每一个文档的数据利用算法分词,得到一个个词条
  • 创建表,每行数据包括词条、词条所在文档id、位置等信息
  • 因为词条唯一性,可以给词条创建索引,例如hash表结构索引

在这里插入图片描述

在这里插入图片描述

部署单点ES

  1. 创建网络
docker network create es-net
  1. 下载镜像/加载镜像
# 因为镜像比较大,可以选择从本地上传
# 下载elasticsearch
docker pull elasticsearch:所需tag
# 下载kibana
docker pull kibana:所需tag
  1. 运行镜像
docker run -d \
    --name es \
    -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
    -e "discovery.type=single-node" \
    -v es-data:/usr/share/elasticsearch/data \
    -v es-plugins:/usr/share/elasticsearch/plugins \
    --privileged \
    --network es-net \
    -p 9200:9200 \
    -p 9300:9300 \
elasticsearch:7.12.1

# 命令解释
# -e "cluster.name=es-docker-cluster":设置集群名称
# -e "http.host=0.0.0.0":监听的地址,可以外网访问
# -e "ES_JAVA_OPTS=-Xms256m -Xmx256m":内存大小
# -e "discovery.type=single-node":非集群模式
# -v es-data:/usr/share/elasticsearch/data:挂载逻辑卷,绑定es的数据目录
# -v es-logs:/usr/share/elasticsearch/logs:挂载逻辑卷,绑定es的日志目录
# -v es-plugins:/usr/share/elasticsearch/plugins:挂载逻辑卷,绑定es的插件目录
# --privileged:授予逻辑卷访问权
# --network es-net:加入一个名为es-net的网络中
# -p 9200:9200:端口映射配置
  1. 浏览器查看是否启动(记得开放9200端口)
  2. 启动Kibana(ES可视化)
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601  \
kibana:7.12.1
  • 注: 内存不足时ES容器会自动停止

安装IK分词器

  1. 在线安装IK插件
# 进入容器内部
docker exec -it es /bin/bash

# 在线下载并安装(下载很容易失败,如果多次尝试均失败,请自行下载现成压缩包,解压并命名文件夹为ik并上传至插件目录
# 具体目录可以通过docker volume inspect es-plugins命令查看)
./bin/elasticsearch-plugin  install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip

#退出
exit
#重启容器
docker restart elasticsearch
  1. 推出容器
exit
  1. 重启容器
docker restart es
  1. 测试
GET /_analyze
{
  "analyzer": "ik_max_word",
  "text": "ES真是太好了,太好用了了"
}
分析策略
  • 细粒度分析: "analyzer": "ik_max_word"搜索出来的词多,但可能不是我们想要的分词加过
  • 粗粒度分析: "analyzer": "ik_smart"搜索出来的词少,更可能分出我们想要的词
扩展词词典
  1. 打开IK分词器config目录
# 具体位置看个人,一般是这个
cd /var/lib/docker/volumes/es-plugins/_data/ik/config
  1. IKAnalyzer.cfg.xml配置文件内容添加
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
        <comment>IK Analyzer 扩展配置</comment>
        <!--用户可以在这里配置自己的扩展字典 *** 添加扩展词典-->
        <entry key="ext_dict">ext.dic</entry>
</properties>
  1. 创建一个ext.dic并写入扩展词(此文件必须是UTF-8格式)
阿根廷冠军
  1. 重启ES
docker restart es
停用词词库
  1. 和扩展词词库类似,打开IKAnalyzer.cfg.xml并添加
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
        <comment>IK Analyzer 扩展配置</comment>
        <!--用户可以在这里配置自己的扩展字典-->
        <entry key="ext_dict">ext.dic</entry>
         <!--用户可以在这里配置自己的扩展停止词字典  *** 添加停用词词典-->
        <entry key="ext_stopwords">stopword.dic</entry>
</properties>
  1. stopword.dic添加停用词
答辩
  1. 重启ES
docker restart elasticsearch

索引库操作

mapping映射属性

mapping是对索引库中文档的约束

常见的mapping属性
  • type: 字段数据类型,常见的简单类型有
    • 字符串: test(可分词的文本)、keyword(精确值 例如: 国家、IP)
    • 数值: long、integer、short、byte、double、float
    • 布尔: boolean
    • 日期: date
    • 对象: object
  • index: 是否创建索引,默认为true(没有倒排索引就不会参与搜索)
  • analyzer: 使用哪种分词器
  • properties: 该字段的子字段
创建索引库

语法:

// dsl创建的索引库,默认是1个分片、1个副本
PUT /索引库名称
{
  "settings": {
    // 可修改分片和副本的数目
    "number_of_shards": 3,
    "number_of_replicas": 0	// 可以集群时再设置副本
  },
  "mappings": {
    "properties": {
      "字段名":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "字段名2":{
        "type": "keyword",
        "index": "false"
      },
      "字段名3":{
        "properties": {
          "子字段": {
            "type": "keyword"
          }
        }
      },
      // ...
    }
  }
}
  • 将当前字段拷贝到指定字段,以达到可以同时搜索多个字段的实现
PUT /索引名
{
  "mappings": {
    "properties": {
      "字段名":{
        "type": "keyword",
        "copy_to": "all"
      }
    }
  }
}
查询索引库

语法:

GET /索引库名
修改索引库

因为索引库创建时json结构已经固定,所以不可修改,但是可以添加新字段

语法:

PUT /索引库名/_mapping
{
  "properties": {
    "新字段名":{
      "type": "integer"
    }
  }
}
删除索引库

语法:

DELETE /索引库名

文档操作

创建文档

语法:

POST /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    "字段3": {
        "子属性1": "值3",
        "子属性2": "值4"
    },
    // ...
}
查询文档

语法:

GET /{索引库名称}/_doc/{id}
删除文档

语法:

DELETE /{索引库名}/_doc/id值
修改文档
  • 全量修改: 直接覆盖原来的文档(先根据id删除文档、再创建一个新文档,如果该id文档不存在,直接创建一个新文档)
PUT /{索引库名}/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    // ... 略
}
  • 增量修改: 修改文档中的部分文档
POST /{索引库名}/_update/文档id
{
    "doc": {
         "字段名": "新的值",
    }
}

先创建索引库和映射,再添加文档

RestAPI

基本操作步骤
  1. 引入elasticsearch依赖
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>版本和自己的ES版本一致</version>
</dependency>
  1. 因为SpringBoot有默认ES版本,所以需要指定我们需要的版本
<properties>
    <java.version>1.8</java.version>
    <elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
  1. 初始化RestHighLevelClient
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
	HttpHost.create("http://ip:9200")
));
RestClient操作索引库
创建索引库

DSL语句:

private static final String MAPPING_TEMPLATE = "{\n" +
            "  \"mappings\": {\n" +
            "    \"properties\": {\n" +
            "      \"id\": {\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"name\":{\n" +
            "        \"type\": \"text\",\n" +
            "        \"analyzer\": \"ik_max_word\",\n" +
            "        \"copy_to\": \"all\"\n" +
            "      },\n" +
            "      \"address\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": false\n" +
            "      },\n" +
            "      \"price\":{\n" +
            "        \"type\": \"integer\"\n" +
            "      },\n" +
            "      \"score\":{\n" +
            "        \"type\": \"integer\"\n" +
            "      },\n" +
            "      \"brand\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"copy_to\": \"all\"\n" +
            "      },\n" +
            "      \"city\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"copy_to\": \"all\"\n" +
            "      },\n" +
            "      \"starName\":{\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"business\":{\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"location\":{\n" +
            "        \"type\": \"geo_point\"\n" +
            "      },\n" +
            "      \"pic\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": false\n" +
            "      },\n" +
            "      \"all\":{\n" +
            "        \"type\": \"text\",\n" +
            "        \"analyzer\": \"ik_max_word\"\n" +
            "      }\n" +
            "    }\n" +
            "  }\n" +
            "}";

操作步骤:

private RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
            HttpHost.create("http://ip :9200")));

// 1. 创建Request对象
CreateIndexRequest request = new CreateIndexRequest("hotel");

// 2, 准备请求的参数,DSL语句
request.source(MAPPING_TEMPLATE, XContentType.JSON);

// 3. 发送请求
client.indices().create(request, RequestOptions.DEFAULT);
判断索引库是否存在
GetIndexRequest request = new GetIndexRequest("hotel");

boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);

System.out.println(exists);
删除索引库
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
        
client.indices().delete(request, RequestOptions.DEFAULT);
RestClient操作文档
基本步骤
  1. 初始化RestClient
  2. 进行操作
新增文档
// 取到数据
数据

// 1. 准备Request对象
IndexRequest request = new IndexRequest("索引名").id(文档id);

// 2. 准备JSON文档
request.source(JSON.toJSONString(数据), XContentType.JSON);

// 3. 发送请求
client.index(request, RequestOptions.DEFAULT);
查询文档
// 1. 创建request对象
GetRequest request = new GetRequest("索引名", "文档id");

// 2. 发送请求
GetResponse response = client.get(request, RequestOptions.DEFAULT);

// 3. 解析请求
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, 实体类);
删除文档
// 1. 准备Request
DeleteRequest request = new DeleteRequest("索引名", "文档id");

// 2. 发送请求
client.delete(request, RequestOptions.DEFAULT);
修改文档
  • 全量修改: 和新增一样
  • 增量新增: 只修改部分字段
// 1. 准备Request
UpdateRequest request = new UpdateRequest("索引名", "文档id");

// 2. 准备请求参数
request.doc(
    "字段", "新值"
);

// 3. 发送请求
client.update(request, RequestOptions.DEFAULT);
批量导入文档
// 1. 创建Request
BulkRequest request = new BulkRequest();

// 2. 准备参数,添加多个新增的Request
for (集合) {
    request.add(new IndexRequest("索引名")
                .id("文档id")
                .source(JSON数据, XContentType.JSON));
}

// 3. 发送请求
client.bulk(request, RequestOptions.DEFAULT);

DSL查询文档

slasticsearch的查询是基于JSON风格的DSL来实现的

DSL查询分类
  • 查询所有: 查询出所有数据,一般测试用。例如: match_all
  • 全文检索查询: 利用分词器对用户输入内容分词,然后去倒叙索引库中匹配。例如:
    • match_query
    • multi_match_query
  • 精确查询: 根据精确词条值查找数据,一般是查找keyword数值日期boolean等类型字段。例如:
    • ids
    • range
    • term
  • 地理查询: 根据经纬度查询。例如:
    • geo_distance
    • geo_bounding_box
  • 复合查询: 复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
    • bool
    • function_score
语法
  • match查询:
GET /indexName/_search
{
  "query": {
    "match": {
      "FIELD": "TEXT"
    }
  }
}
  • mulit_match查询:
GET /indexName/_search
{
  "query": {
    "multi_match": {
      "query": "TEXT",
      "fields": ["FIELD1", "FIELD2"]
    }
  }
}
全文线索查询
  • match查询: 全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索,语法:
GET /indexName/_search
{
  "query": {
    "match": {
      "FIELD": "TEXT"
    }
  }
}
  • multi_match: 与marth查询类似,但是允许同时查询多个字段,语法:
GET /indexName/_search
{
  "query": {
    "multi_match": {
      "query": "TEXT",
      "FIELDS": ["FIELD1", "FIELD2"]
    }
  }
}
精确查询

精确查询一般是查找keyword、数值、日期、boolean等类型字段,该字段不会对搜索条件分词

  • term: 根据词条精确值查询,语法:
GET /indexName/_search
{
  "query": {
    "term": {
      "FIELD": {
          "value": "VALUE"
      }
    }
  }
}
  • range: 根据值得范围查询,语法:
GET /indexName/_search
{
  "query": {
    "range": {
      "FIELD": {
          // 大于等于,不要等于就去掉e
          "gte": VALUE1,
          "lte": VALUE2
      }
    }
  }
}
  • 地理查询

    • 根据经纬度查询
    GET /indexName/_search
    {
      "query": {
        "geo_bounding_box": {
          "FIELD": {
            "top_left": {
              "lat": 39.92,
              "lon": 116.39
            },
            "bottom_right": {
              "lat": 39.91,
              "lon": 116.40
            }
          }
        }
      }
    }
    
    • 根据距离查询
    GET /indexName/_search
    {
      "query": {
        "geo_distance": {
          "distance": "15km",
          "FIELD": "39.92,116.4"
        }
      }
    }
    
复合查询

复合查询可以将其他的简单查询组合起来,实现更复杂的搜索逻辑

当我们利用match查询时,文档会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列

算分函数查询(dunction_score): 可以控制文档相关性算分,控制文档排名
  • ES在5.0之前使用TF-IDF算法,5.0之后使用BM25算法
Function score query

可以修改文档的相关性算分(query score),根据新得到的算分排序

例:

GET /city/_search
{
  "query": {
    "function_score": {
      "query": {"match": {"all": "北京"}},
      "function": [
        {
          "filter": {"term": {"id": "1"}},
          "weight": 10
        }  
      ],
      "boost_mode": "multiply"
    }
  }
}
  • "query": {"match": {"all": "北京"}},: 原始查询条件,搜索文档并根据相关性打分
  • "filter": {"term": {"id": "1"}},: 过滤条件,符合条件的文档才会被重新打分
  • "weight": 10: 算分函数,算分函数的结果称为function score,将来会与query score运算,得到新算分,常见的算分函数:
  • weight: 给一个常量值,作为函数结果
  • field_value_factor: 用文档中的某个字段值作为函数结果
  • random_score: 随机生成一个值,作为函数结果
  • script_score: 自定义计算公式,公式结果作为函数结果
  • "boost_mode": "multiply": 加权模式,定义function scorequery score的运算方式,包括:
    • multiply: 两者相乘,默认
    • replace: 用function score替换query score
    • 其他: sumavgmaxmin
Boolean query

布尔查询是一个或多个查询子句的组合,组合方式有:

  • must: 必须匹配每个子查询,类似”与“
  • should: 选择性匹配子查询,类似”或“
  • must_not: 必须不匹配,不参与算分,性能较高,类似”非“
  • filter: 必须匹配,不参与算分,性能较高
GET /hotel/_search
{
  "query": {
    "bool": {
      "must": [
        {"term": {"city": "上海" }}
      ],
      "should": [
        {"term": {"brand": "皇冠假日" }},
        {"term": {"brand": "华美达" }}
      ],
      "must_not": [
        { "range": { "price": { "lte": 500 } }}
      ],
      "filter": [
        { "range": {"score": { "gte": 45 } }}
      ]
    }
  }
}
搜索结果处理
排序

ES支持对搜索结果排序,默认是根据相关度算分来排序,可以根据keyword、数值类型、日期类型、地理坐标类型等

排序之后就不再进行分数计算了

GET /indexName/_search
{
  "query": {
      "match_all": {}
  },
  "sort": [
    {
      "FIELD": "desc"
    }
  ]
}
分页

ES默认情况下只返回前十条数据,如果需要查询更多,则需要修改分页参数from和size

ES是倒排索引的,所以分页的实现方式是先查出所有的,再截取所需的部分

GET /indexName/_search
{
  "query": {
    "match_all": {}
  },
  "form": 990,	// 分页开始的位置,默认为0
  "size": 10,	// 期望获取的文档总数
  "sort": [
    {"price": "asc"}
  ]
}
深度分页问题

ES是分布式的,所以会面临深度分页的问题

比如获取form=990,size=10:

  • 首先在每个数据分片上都排序并查询前1000条文档
  • 然后把所有节点的结果聚合,在内存中重新排序选出前1000条文档
  • 最后从这1000条中,选取从990开始的10条数据

在这里插入图片描述

深度分页解决思路
  • search after: 分页时需要排序,原理是从上一次的排序值开始,查询下一页数据(官方推荐)
  • scroll: 原理是将排序数据形成快照,保存在内存(官方不推荐)
高亮

进行搜索时,关键字会标红

实现思路
  1. 将搜索结果中的关键字用标签标记出来
  2. 在页面中给标签添加css样式
GET /indexName/_search
{
  "query": {
    "match": {
      "FIELD": "TEXT"
    }
  },
  "highlight": {
    "fields": {	// 指定要高亮的字段
      "FIELD": {	// 字段名
        "require_field_match": "true",	// 该字段是否在搜索字段中
        "pre_tags": "<em>",	// 用来标记高亮字段的前置标签
        "post_tags": "</em>"	// 用来标记高亮字段的后置标签
      }
    }
  }
}

RestClient查询文档

快速入门(对比DSL来写Java语句即可)
// 1. 准备Request
SearchRequest request = new SearchRequest("hotel");

// 2. 准备DSL
request.source().query(QueryBuilders.matchAllQuery());
// request.source().query(QueryBuilders.matchQuery("all", "如家"));
// request.source().query(QueryBuilders.multiMatchQuery("如家", "name", "brand"));


// 3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

// 4. 解析数据
SearchHits searchHits = response.getHits();

// 4.1 查询的总条数
long total = searchHits.getTotalHits().value;
System.out.println("共获取到: " + total + " 条");

// 4.2 查询的结果数组
SearchHit[] hits = searchHits.getHits();

for (SearchHit hit : hits) {
    // 4.3 得到source
    String json = hit.getSourceAsString();

    // 4.4 打印
    System.out.println(json);
}
多条件查询
// 1. 准备Request
SearchRequest request = new SearchRequest("hotel");

// 2. 准备DSL
// 2.1 准备BooleanQuery
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

// 2.2 添加term
boolQuery.must(QueryBuilders.termQuery("city", "上海"));

// 2.3 添加range
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));

request.source().query(boolQuery);

// 或者直接链式编程
// request.source().query(QueryBuilders.boolQuery()
//         .must(QueryBuilders.termQuery("city", "上海"))
//         .filter(QueryBuilders.rangeQuery("price").lte(300)));

// 3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

// 4. 解析数据
// ...
排序、分页
// 1. 准备Request
SearchRequest request = new SearchRequest("hotel");

// 2. 准备DSL
request.source().query(QueryBuilders.matchQuery("city", "北京"));

// 3. 分页、排序
request.source().sort("price", SortOrder.ASC);
request.source().from(0).size(3);

// 4. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

// 5. 解析数据
// ...
高亮
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");

// 2.准备DSL
// 2.1.query
request.source().query(QueryBuilders.matchQuery("all", "如家"));

// 2.2.高亮
request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));

// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

// 4. 解析数据
SearchHits searchHits = response.getHits();

// 4.1 查询的总条数
long total = searchHits.getTotalHits().value;
System.out.println("共获取到: " + total + " 条");

// 4.2 查询的结果数组
SearchHit[] hits = searchHits.getHits();

for (SearchHit hit : hits) {
    // 4.3 得到source
    String json = hit.getSourceAsString();

    // 反序列化
    HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);

    // 获取高亮结果
    Map<String, HighlightField> highlightFields = hit.getHighlightFields();

    if (!CollectionUtils.isEmpty(highlightFields)) {
        // 根据字段名获取高亮结果
        HighlightField highlightField = highlightFields.get("name");
        if (highlightField != null) {
            // 获取高亮值
            String name = highlightField.getFragments()[0].string();

            // 覆盖非高亮结果
            hotelDoc.setName(name);
        }
    }

    // 4.4 打印
    System.out.println(hotelDoc);
}

数据聚合

可以实现对文档数据的统计、分析、运算

参与聚合的必须使keyword、日期、数值、布尔类型等

聚合的种类
  • 桶(Bucket)聚合: 用来对文档做分组
    • TermAggregation: 按照文档字段值分组,例如按照品牌值分组、按照国家分组
    • Date Histogram: 按照日期阶梯分组,例如一周为一组,或者一月为一组
  • 度量(Metric)聚合: 用以计算一些值,比如: 最大值、最小值、平均值等
    • Avg: 求平均值
    • Max: 求最大值
    • Min: 求最小值
    • Stats: 同时求max、min、avg、sum等
  • 管道(Pipeline)聚合: 其他聚合的结果为基础做聚合
DSL实现聚合
  • 桶聚合
// 默认情况下,桶聚合会统计桶内文档数量,记为_count,并且按照_count降序排序
GET /indexName/_search
{
  // "query": {	// 限定聚合范围
  // },
  "size": 0,	// 设置size为0,结果中不包含文档,只包含聚合的结果
  "aggs": {	// 实现聚合
    "brandAgg": {	// 给聚合起个名字
      "terms": {	// 聚合的类型,按照品牌值聚合
        "field": "brand",	// 参与聚合的字段
        // "order": {	// 修改默认排序方式
        //   "_count": "asc"
        // },
        "size": 20	// 希望获取的聚合结果数量
      }
    }
  }
}
  • Metrics聚合
GET /indexName/_search
{
  // "query": {	// 限定聚合范围
  // },
  "size": 0,	// 设置size为0,结果中不包含文档,只包含聚合的结果
  "aggs": {	// 实现聚合
    "brandAgg": {	// 给聚合起个名字
      "terms": {	// 聚合的类型,按照品牌值聚合
        "field": "brand",	// 参与聚合的字段
        // "order": {	// 修改默认排序方式
        //   "_count": "asc"
        // },
          "order": {
            "score_stats.avg": "asc"	// 对聚合结果排序
          }
        "size": 20	// 希望获取的聚合结果数量
      },
      "aggs": {	// 是brands聚合的子聚合,也就是分组后对每组分别计算
        "score_stats": {	// 聚合名称
          "stats": {	// 聚合类型,这里stats可以计算min、max、avg等
            "field": "score"	// 聚合字段,这里是score
          }
        }
      }
    }
  }
}
RestAPI实现聚合

在这里插入图片描述

在这里插入图片描述

// 1. 准备Request
SearchRequest request = new SearchRequest("hotel");	// GET /hotel/_search

// 2. 准备DSL
// 2.1 设置size
request.source().size(0);	// "size": 0

// 2.2 聚合
request.source().aggregation(AggregationBuilders	
                             .terms("brandAgg")	// "brandAgg": { "term": {
                             .field("brand")	// "field": "brand"
                             .size(10)	// "size": 10
                             .subAggregation(AggregationBuilders.stats("scoreAgg")	// "scoreAgg": {
                        		.field("score"))	"field": "score" 
                            );

// 3. 发出请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);	

// 4. 解析结果
Aggregations aggregations = response.getAggregations();		// 结果中的"aggregations": {

// 根据名称获取聚合结果
Terms terms = aggregations.get("brandAgg");	// "brandAgg": {

// 获取Buckets
List<? extends Terms.Bucket> buckets = terms.getBuckets();	// "buckets": {

for (Terms.Bucket bucket : buckets) {
    System.out.println(bucket.getKeyAsString());
    System.out.println(bucket.getDocCount());

    Stats stats = bucket.getAggregations().get("scoreAgg");

    System.out.println(stats.getCount());
    System.out.println(stats.getMax());
    System.out.println(stats.getMin());
    System.out.println(stats.getSum());
    System.out.println(stats.getAvg());
}

自动补全查询

安装拼音分词器

拼音分词器适合在创建倒排索引时使用,不适合在搜索时使用(会根据拼音生成同音词)

安装步骤和ik分词器类似,参考官方文档: elasticsearch-analysis-pinyin (注意和自己的ES版本对应)

测试:

POST /_analyze
{
  "text": ["不要来学计算机,快跑!"],
  "analyzer": "pinyin"
}
自定义分词器

ES分词器的组成包含三部分:

  • character filters: 在tokenizer之前对文本进行处理(如删除字符、替换字符)
  • tokenizer: 将文本按照一定的规则切割成词条(term),例如: keyword,就是不分词;ik_smart
  • tokenizer filter: 将tokenizer输出的词条做进一步处理,例如: 大小写转换、同义词处理、拼音处理

在这里插入图片描述

我们可以再创建索引库时,通过settings来配置自定义的analyzer分词器

PUT /test
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {	// 自定义分词器
          "tokenizer": "ik_max_word",	// 分词器名称
          "filter": "py"
        }
      },
      "filter": {	// 自定义tokenizer filter
        "py": {	// 过滤器名称
          "type": "pinyin",	// 过滤器类型,这里是pinyin
          "keep_full_pinyin": false,	// 具体参数可以参考官网
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "my_analyzer"
      }
    }
  }
}

由于可能会发生两个字读音相同、意义不同的问题,所以推荐创建索引时使用自定义分词器,搜索时应该使用ik_smart分词器,例:

PUT /test
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {
          "tokenizer": "ik_max_word",
          "filter": "py"
        }
      },
      "filter": {
        "py": {
          ...
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "my_analyzer",
        "search_analyzer": "ik_smart"
      }
    }
  }
}
自动补全

ES提供了completion suggester查询来实现自动补全功能,这个查询会匹配用户输入内容开头的词条并返回

要求:

  • 参与补全查询的字段必须是completion类型
  • 字段的内容一般是用来补全的多个词条形成的数组

创建索引例:

// 创建索引
PUT /test 
{
  "mappings": {
    "properties": {
      "title": {
        "type": "completion"
      }
    }
  }
}

// 示例数据
POST /test/_doc
{
  "title": ["Sony", "WH-1000XM3"]
}
POST /test/_doc
{
  "title": ["XiaoMi", "XiaoMi-13"]
}
POST /test/_doc
{
  "title": ["Apple", "MacBookAir2020"]
}

查询例:

// 自动补全查询
GET /test/_search
{
  "suggest": {
    "title_suggest": {
      "test": "s",	// 关键字
      "completion": {
        "field": "title",	// 补全查询的字段
        "skip_duplicates": true,	// 跳过重复的
        "size": 10	// 获取前10条结果
      }
    }
  }
}
RestAPI实现自动补全
// 1. 准备请求
SearchRequest request = new SearchRequest("hotel");

// 2. 请求参数
request.source()
    .suggest(new SuggestBuilder().addSuggestion(
    	"mySuggestion",
        SuggestBuilders
        	.completionSuggestion("title")
        	.prefix("s")
        	.skipDuplicates(true)
        	.size(10)
    ));

// 3. 请求参数
client.search(request, RequestOptions.DEFAULT);

结果解析:

// 4. 处理结果
Suggest suggest = response.getSuggest();

// 4.1 根据名称获取补全结果
CompletionSuggestion suggestion = suggest.getSuggestion("hotelSuggestion");

// 4.2 获取options并遍历
for (CompletionSuggestion.Entry.Option option : suggestion.getOptions()) {
    // 4.3 获取一个Option中的text,也就是补全的词条
    String text = option.getText().string();
    
}

数据同步

ES数据来自于MySQL中时,如果MySQL数据发生改变,那么ES的数据也应该及时发生改变

  • 方法1: 同步调用,数据库发生更新时,调用ES的更新方法,实现简单,但是耦合较高
  • 方法2: 异步通知,数据库发生改变后,将信息使用类似于MQ的技术发送给注册服务,实现简单,耦合低,但是依赖于mq的可靠性
  • 方法3: 监听binlog,使用类似于canal的方法,完全解决服务间的耦合,但是开启binlog,增加MySQL压力,实现难度高
ES集群中的职责划分

在这里插入图片描述

脑裂问题

当一个集群中的主节点于其他节点失联时,其余节点会重新选出个新节点,当连接恢复后,两个主节点产生的信息可能会发生不一致,出现数据差异问题

ES7.0之前解决脑裂的方案是,要求选票超过 ( eligible节点数量 + 1 )/ 2 才能当选为主,因此eligible节点数量最好是奇数。对应配置项是discovery.zen.minimum_master_nodes

在es7.0以后,已经成为默认配置,因此一般不会发生脑裂问题

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值