更适合中国宝宝的ES(Elasticsearch)学习笔记

86 阅读11分钟

初识ES

什么是ES(Elasticsearch)

  • 一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能

什么是ELK(Elastic stack)

  • 是以elasticsearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch。

倒排索引

文档与词条

  • 文档: 每一条数据就是一个文档
  • 词条: 对文档中的内容分词,得到的词语就是词条

正向索引

  • 基于文档id创建索引。查询词条时必须先找到文档,而后判断是否包含词条

倒排索引

  • 对文档内容分词,对词条创建索引,并记录词条所在文档的信息。查询时先根据词条查询到文档id,而后获取到文档

ES与MySql概念对比

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

索引库操作

mapping属性

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

常见的mapping属性:

  • type 数据类型,常见的类型有:
    • 字符串:text(可分词的文本)、keyword(精确值,如:国家、ip地址)
    • 数字:long integer short byte double float
    • 布尔:boolean
    • 日期:date
    • 对象:object
    • (对于type的声明都默认作用于相应数组)
  • index 是否创建索引,默认为true
  • analyzer 使用哪种分词器
  • properties 子字段

创建索引库

ES中通过Restful请求操作索引库和文档。请求内容用DSL语句表示。

  • 创建索引库和mapping的DSL语法如下:
PUT /索引库名称
{
    "mappings": {
        "properties": {
            "字段名1": {
                "type": "text",
                "analyzer": "ik_smart"
            },
            "字段名2": {
                "type": "keyword",
                "index": "false"
            },
            "字段名3": {
                "properties": {
                    "子字段名": {
                        "type": "keyword"
                    }
                }
            }
        }
    }
}
  • 查看索引库

    • GET /索引库名
  • 删除索引库

    • DELETE /索引库名
  • 修改索引库

索引库和mapping一旦创建无法修改,只能添加新的字段

语法:PUT /索引库名/_mapping

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

文档操作

  • 新增文档的DSL语法:
POST /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    "字段3": {
        "子属性1": "值3",
        "子属性2": "值4"
    }
}
  • 查看文档

    • GET /索引库名/_doc/文档id
  • 删除文档

    • DELETE /索引库名/_doc/文档id
  • 修改文档

    • 全量修改:删除旧文档,添加新文档。若文档id不存在,则执行新增操作
    • 局部修改:修改指定字段值
PUT /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    "字段3": {
        "子属性1": "值3",
        "子属性2": "值4"
    }
}
POST /索引库名/_update/文档id
{
    "doc": {
        "字段名": "新值"
    }
}

RestClient操作数据库

初始化JavaRestClient

  1. 引入es的RestHighLevelClient依赖
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
  1. 初始化RestHighLevelClient
@SpringBootTest
class HotelDemoApplicationTests {

    private RestHighLevelClient client;

    @BeforeEach
    void setUp(){
    // 43.143.xxx.xxx 是云服务器中的ip
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://43.143.xxx.xxx:9200")
        ));
    }

    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }
}

  1. 创建索引库
@Test
void testCreateHotelIndex() throws IOException {
    // 1.创建Request对象
    CreateIndexRequest request = new CreateIndexRequest("hotel");
    // 2.请求参数,MAPPING_TEMPLATE是静态常量字符串,内容是创建索引库的DSL语句
    request.source(MAPPING_TEMPLATE, XContentType.JSON);
    // 3.发起请求
    client.indices().create(request, RequestOptions.DEFAULT);
}
  1. 删除索引库
@Test
void testDeleteHotelIndex() throws IOException {
    // 1。创建Request对象
    DeleteIndexRequest request = new DeleteIndexRequest("hotel");
    // 2. 发起请求
    client.indices().delete(request,RequestOptions.DEFAULT);
}
  1. 判断索引库是否存在
@Test
void testExistsHotelIndex() throws IOException {
    // 1。创建Request对象
    GetIndexRequest request = new GetIndexRequest("hotel");
    // 2.发起请求
    boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
    // 3.输出结果
    System.out.println(exists);
}

文档操作

新增文档

从MySql数据库查询数据,批量导入到es

@Test
void testAddDocument() throws IOException {
    // 0.创建批量导入Request
    BulkRequest request = new BulkRequest();
    // 1.从MySQL中查询数据
    List<Hotel> hotelList = hotelService.list();
    for (Hotel hotel : hotelList){
        // 2.转换文档类型
        HotelDoc hotelDoc = new HotelDoc(hotel);
        // 3.Request中添加数据
        request.add(new IndexRequest("hotel")
                .id(hotelDoc.getId().toString())
                .source(JSON.toJSONString(hotelDoc),XContentType.JSON));
        // 4.发送请求
        client.bulk(request,RequestOptions.DEFAULT);
    }
}

查询文档

@Test
void testGetDocumentById() throws IOException {
    // 1.准备Request
    GetRequest request = new GetRequest("hotel", "36934");
    // 2.发送请求,得到响应
    GetResponse response = client.get(request, RequestOptions.DEFAULT);
    // 3.解析相应结果
    String json = response.getSourceAsString();
    HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
    // 4.打印查看结果
    System.out.println(hotelDoc);
}

更新文档

  • 全量更新:再次写入id一样的文档,就会删除旧文档,添加新文档
  • 局部更新:只更新部分字段
@Test
void testUUpdateDocumentById() throws IOException {
    // 1.创建Request对象
    UpdateRequest request = new UpdateRequest("hotel", "36934");
    // 2.准备参数,每两个参数为一对k v
    request.doc(
            "price",347,
            "starName","三钻"
    );
    // 3.更新文档
    client.update(request,RequestOptions.DEFAULT);
}

删除文档

@Test
void testDeleteDocument() throws IOException {
    // 1.准备Request
    DeleteRequest request = new DeleteRequest("hotel", "36943");
    // 2.发送请求
    client.delete(request,RequestOptions.DEFAULT);
}

ES搜索功能

DSL查询文档

基本语法:

GET /索引库名/_search{
    "query":{
        "查询类型":{
            "FIELD":"TEXT"
        }
    }
}

DSL查询分类

  • 查询所有:查询出所有数据,一般测试使用
  • 全文检索查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配
  • 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段
  • 地理查询:根据经纬度查询
  • 复合查询:将上述查询条件组合起来,合并查询条件

全文检索查询

match根据一个字段查询

muti_match根据多个字段查询,参与查询字段越多,查询性能越差

GET /hotel/_search
{
  "query": {
    "match": {
      "FIELD": "TEXT"
    }
  }
}
GET /hotel/_search
{
  "query": {
    "multi_match": {
      "query": "TEXT"
      "fiekds": ["FIELD1","FIELD2"]
    }
  }
}

精准查询

精确查询是根据id、数值、keyword类型或bool字段来查询

  • term查询
GET /indexName/_search
{
  "query":{
    "term": {
      "FIELD": {
        "value": "VALUE"
      }
    }
  }
}
  • range查询
GET /indexName/_search
{
  "query":{
    "range": {
      "FIELD": {
        "gte":10,
        "lte": 20
      }
    }
  }
}

地理坐标查询

  • geo_bounding_box: 查询geo_point值落在某个矩形范围内的所有文档
GET /indexName/_search
{
  "query": {
    "geo_bounding_box":{
      "FIELD":{
        "top_left":{ // 左上角
          "lat":33.3,
          "lon":122.2
        },
        "bottom_right":{ // 右下角
          "lat":30.9,
          "lon":121.7
        }
      }
    }
  }
}

  • geo_distance: 查询到指定中心点小于某个距离值的所有文档
GET /indexName/_search
{
  "query": {
    "geo_distance":{
      "distance": "15km",
      "FIELD": "31.21,121.5"
    }
  }
}

复合查询

复合查询可以将其它简单查询组合起来,实现复杂的搜索逻辑,如:fuction score算分函数查询,可以控制文档相关性得分,控制文档排名。

GET /hote;/_search{
  "query":{
    "function_score":{
      "query":{
        "match":{
          "all":"外滩"
        }
      },
      "functions"[{
        "filter":{
          "term":{
            "brand":"如家"
          }
        },
        "weight":10
      }],
      "boost_mode":"mltiply"
    }
  }
}

参数说明:

  • query: 原始查询条件,搜索文档并根据相关性打分(query score)
  • filter:过滤条件,符合条件的文档才会被重新算分
  • weight:算分函数,算分函数的结果称为function score,将来会与query score运算,得到新算分,常见的算分函数有:
    • weight:给一个常量值,作为函数结果
    • field_value_factor:用文档中的某个字段值作为函数结果
    • random_score:随机生成一个值,作为函数结果
    • script_score:自定义计算公式,公式结果作为函数结果
  • boost_mode: 加权模式,定义function score和query score的运算方式,包括:
    • multiply:两者相乘。默认
    • replace:用function score替换query score
    • 其它:sum、avg、max、min

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

  • must:必须匹配每个子查询,与
  • shoud:选择性匹配子查询,或
  • must_not:必须不匹配,不参与算分,非
  • filter:必须匹配,不参与算分

bool查询示例:

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类型、数值类型、地理坐标类型、日期类型等

按照score降序,score相同,再按照price升序

GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "score": {
        "order": "desc"
      },
      "price": {
        "order": "asc"
      }
    }
  ]
}

分页

ES默认情况下只返回top10的数据,如需查询更多数据,需要修改分页参数。

ES是分布式的,会面临深度分页问题。如果搜索页数过深,或者结果集(from + size)越大,对内存和cpu的消耗也越高,因此ES设定结果集查询的上限是10000。

GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "from": 10, // 分页开始的位置,默认为0
  "size": 20, // 期望获取的文档总数
  "sort": [
    {"price": "asc"}
  ]
}

高亮

高亮原理:

  • 将搜索结果中的关键字用标签标记出来
  • 在页面中给标签添加css样式
GET /hotel/_search
{
  "query": {
    "match": {
      "FIELD": "TEXT"
    }
  },
  "highlight": {
    "fields": {
      "FIELD":{ // 指定要高亮的字段
        "pre_tags": "<em>", // 标记高亮字段的前置标签
        "post_tags": "</em>" // 标记高亮字段的后置标签
      }
    }
  }
}

默认情况下,ES搜索字段必须与高亮字段一致,但是可以通过 require_field_match:false 更改

GET /hotel/_search
{
  "query": {
    "match": {
      "all": "如家"
    }
  },
  "highlight": {
    "fields": {
      "name":{
        "pre_tags": "<em>",
        "post_tags": "</em>",
        "require_field_match": "false"
      }
    }
  }
}

RestClient查询文档

查询基本步骤

  1. 创建SearchRequest对象
  2. 准备Request.source(),也就是DSL
    • REQUEST.source()中 包含查询、排序、分页、高亮等所有功能
    • QuerBuilders中包含各种查询方法
  3. 发送请求,得到结果
  4. 结果解析(参照JSON结果,从外到内,逐层解析)
@Test
void testMatchAll() throws IOException {
    // 1.准备Request
    SearchRequest searchRequest = new SearchRequest("hotel");
    // 2.准备DSL,不同的查询类型只需要更改query()中的查询方法,注意使用QueryBuilders
    searchRequest.source().query(QueryBuilders.matchAllQuery());
    // 3.发送请求
    SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
    // 4.解析结果
    System.out.println(response);
}

分页和排序

@Test
void testPageAndSort() throws IOException {
    // 0.模拟前端传过来的数据
    int page = 2,size = 15;
    // 1.准备Request
    SearchRequest searchRequest = new SearchRequest("hotel");
    // 2.准备DSL
    // 2.1 query
    searchRequest.source().query(QueryBuilders.matchAllQuery());
    // 2.2 排序
    searchRequest.source().sort("price", SortOrder.ASC);
    // 2.3 分页
    searchRequest.source().from((page - 1) * size).size(size);
    // 3.发送请求
    SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
    // 4.解析结果
    System.out.println(response);
}

高亮

@Test
void testHighlight() throws IOException {
    // 1.准备Request
    SearchRequest searchRequest = new SearchRequest("hotel");
    // 2.准备DSL
    // 2.1 query
    searchRequest.source().query(QueryBuilders.matchQuery("all","如家"));
    // 2.2.高亮
    searchRequest.source().highlighter(new HighlightBuilder()
            // 高亮字段
            .field("name")
            // 是否需要与查询字段匹配
            .requireFieldMatch(false)
    );
    // 3.发送请求
    SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
    // 4.对高亮结果的处理
    // 4.1 解析响应
    SearchHits searchHits = response.getHits();
    // 4.2 获取文档数组
    SearchHit[] hits = searchHits.getHits();
    for (SearchHit hit:hits){
        // 4.3 获取文档source并反序列化为对象
        HotelDoc hotelDoc = JSON.parseObject(hit.getSourceAsString(), HotelDoc.class);
        // 4.4 处理高亮
        Map<String, HighlightField> highlightFields = hit.getHighlightFields();
        if (!CollectionUtils.isEmpty(highlightFields)){
            // 4.5 获取高亮字段结果
            HighlightField highlightField = highlightFields.get("name");
            if (highlightField != null){
                // 高亮结果数组中的第一个,即为酒店名称
                String name = highlightField.getFragments()[0].string();
                // 覆盖非高亮结果
                hotelDoc.setName(name);
            }
        }
        System.out.println(hotelDoc);
    }

}

数据聚合

聚合(aggregations)可以实现对文档数据的统计、分析、运算。参与聚合的字段类型必须是:keyword、数值、日期、布尔

常见的聚合有三类:

  • 桶(Bucket)聚合:对文档分组
    • TermAggregation:按照文档字段值分组
    • DataHistogram:按日期阶梯分组,例如一周一组、一月一组
  • 度量(Metric)聚合:用来计算一些值,比如最大值、最小值、平均值等
    • Avg:求平均值
    • Max:求最大值
    • Min:求最小值
    • Stats:同时求max、min、avg、sum等
  • 管道(pipeline)聚合: 以其它聚合的结果为基础做聚合

DSL实现Bucket聚合

默认情况下,Bucket聚合是对索引库中所有文档做聚合,一般需要使用query条件限定聚合文档范围

GET /hotel/_search
{
  "query": {
    "range": {
      "price": {
        "lte": 200  // 只对price<=200的文档做聚合
      }
    }
  },
  "size": 0, // 设置size为0,结果中不包含文档,只包含聚合结果
  "aggs": { // 定义聚合
    "brandAgg": { // 给聚合起名字
      "terms": { // 聚合的类型,按照品牌值聚合,所以选term
        "field": "brand",  // 参与聚合的字段
        "size": 10  // 获取的聚合结果的数量
      }
    }
  }
}

DSL实现Metrics聚合

GET /hotel/_search
{
  "size": 0,
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand",
        "size": 10,
        "order": { // 对bucket中的数据按照scoreAgg.avg字段排序
          "scoreAgg.avg": "desc"
        }
      },
      "aggs": { // brandAgg的子聚合,即分组后对每组分别计算
        "scoreAgg": { // 聚合名称
          "stats": { // 聚合类型
            "field": "score"  // 聚合字段
          }
        }
      }
    }
  }
}

RestClient实现聚合

@Test
void testAggregation() throws IOException {
    // 1.准备Request
    SearchRequest searchRequest = new SearchRequest("hotel");
    // 2.准备DDL
    // 2.1设置size,丢弃文档结果,只显示聚合结果
    searchRequest.source().size(0);
    // 2.2 聚合
    searchRequest.source().aggregation(AggregationBuilders
            .terms("brandAgg")
            .field("brand")
            .size(10)
    );
    // 3.发出请求
    SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
    // 4. 解析结果
    System.out.println(searchResponse);
}

数据同步

  • 同步调用
    • 优点:实现简单、粗暴
    • 缺点:业务耦合度高 image.png
  • 异步通知
    • 优点:低耦合,实现难度一般
    • 缺点:依赖mq的可靠性 image.png
  • 监听binlog
    • 优点:完全解除服务间耦合
    • 缺点:开启binlog增加数据库负担、实现复杂度高 image.png