Elasticsearch 学习笔记

目录

ElasticSearch的作用

ELK技术栈

倒排索引

正向和倒排

MySQL与ElasticSearch

索引库操作

mapping映射属性

创建索引库和映射

查询索引库

修改索引库

删除索引库

文档操作

新增文档

查询文档

删除文档

修改文档

全量修改

增量修改

RestClient操作索引库

初始化JavaRestClient

创建索引库

DSL语句

代码示例:

删除索引库

DSL语句

代码示例:

判断索引库是否存在

DSL语句

代码示例:

总结

RestClient操作文档

前置工作:

新增文档

DSL语句

代码示例:

查询文档

DSL语句

代码示例:

修改文档

增量修改

DSL语句

删除文档

DSL语句

代码示例:

批量导入文档

总结

elasticsearch搜索功能

DSL查询分类

DSL查询语法

全文检索查询

精确查询

term查询

range查询

地理坐标查询

矩形范围查询(geo_bounding_box查询)

附近查询(geo_distance查询)

复合查询

相关性算分

算分函数查询

布尔查询

搜索结果处理

排序

分页

深度分页问题

解决方案

总结

高亮

原理:

[RestClient 查询文档](#RestClient 查询文档)

全文检索查询

精确查询

复合查询

排序和分页

高亮

结果解析处理



Elaticsearch,简称为 ES,ES 是一个开源的高扩展的分布式全文搜索引擎,是整个 Elastic Stack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。


ElasticSearch的作用

  • **ElasticSearch**是一款非常强大的开源搜素引擎,具备非常强大的功能,可以帮助我们从海量数据中快速找到需要的内容

ELK技术栈

  • ElasticSearch结合kibanaLogstashBeats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域

倒排索引

倒排索引中有两个非常重要的概念

  • 文档(Document):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息
  • 词条(Term):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我最喜欢的FPS游戏是Apex,就可以分为我、我最喜欢、FPS游戏、最喜欢的FPS、Apex这样的几个词条

创建倒排索引是对正向索引的一种特殊处理,流程如下

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

正向和倒排

那么为什么一个叫做正向索引,一个叫做倒排索引呢?

  • 正向索引是最传统的,根据id索引的方式。但是根据词条查询是,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档查找词条的过程
  • 而倒排索引则相反,是先找到用户要搜索的词条,然后根据词条得到包含词条的文档id,然后根据文档id获取文档,是根据词条查找文档的过程

那么二者的优缺点各是什么呢?

正向索引

  • 优点:可以给多个字段创建索引,根据索引字段搜索、排序速度非常快
  • 缺点:根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描

倒排索引

  • 优点:根据词条搜索、模糊搜索时,速度非常快
  • 缺点:只能给词条创建索引,而不是字段,无法根据字段做排序

MySQL与ElasticSearch

ES 里的 Index 可以看做一个库,而 Types 相当于表,Documents 则相当于表的行

索引库操作

  • 索引库就类似于数据库表,mapping映射就类似表的结构
  • 我们要向es中存储数据,必须先创建

mapping映射属性

  • mapping是对索引库中文档的约束,常见的mapping属性包括
  • type:字段数据类型,常见的简单类型有
  • 字符串:text(可分词文本)、keyword(精确值,例如:品牌、国家、ip地址;因为这些词,分词之后毫无意义)
  • 数值:long、integer、short、byte、double、float
  • 布尔:boolean
  • 日期:date
  • 对象:object
  • index:是否创建索引,默认为true,默认情况下会对所有字段创建倒排索引,即每个字段都可以被搜索。但是某些字段是不存在搜索的意义的,例如邮箱,图片(存储的只是图片url),搜索邮箱或图片url的片段,没有任何意义。因此我们在创建字段映射时,一定要判断一下这个字段是否参与搜索,如果不参与搜索,则将其设置为false
  • analyzer:使用哪种分词器
  • properties:该字段的子字段

创建索引库和映射

  • 语法

    PUT /{索引库名}
    {
    "mappings": {
    "properties": {
    "字段名1": {
    "type": "text ",
    "analyzer": "standard"
    },
    "字段名2": {
    "type": "text",
    "index": true
    },
    "字段名3": {
    "type": "text",
    "properties": {
    "子字段1": {
    "type": "keyword"
    },
    "子字段2": {
    "type": "keyword"
    }
    }
    }
    }
    }
    }

查询索引库

  • 语法

    GET /{索引库名}

修改索引库

  • 语法

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

  • 倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,就无法修改mapping
  • 虽然无法修改mapping中已有的字段,但是却允许添加新字段到mapping中,因为不会对倒排索引产生影响

删除索引库

  • 语法

    DELETE /{索引库名}

文档操作

这里的文档可以类比为关系型数据库中的表数据,添加的数据格式为 JSON 格式。

新增文档

  • 语法

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

查询文档

  • 语法

    GET /{索引库名}/_doc/{id}

删除文档

  • 语法

    DELETE /{索引库名}/_doc/{id}

修改文档

  • 修改有两种方式
    1. 全量修改:直接覆盖原来的文档
    2. 增量修改:修改文档中的部分字段
全量修改
  • 全量修改是覆盖原来的文档,其本质是

    • 根据指定的id删除文档
    • 新增一个相同id的文档
      {% note warning no-icon %}
      注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了
      {% endnote %}
  • 语法

    PUT /{索引库名}/_doc/{文档id}
    {
    "字段1": "值1",
    "字段2": "值2",
    // ... 略
    }

增量修改
  • 增量修改只修改指定id匹配文档中的部分字段

  • 语法

    POST /{索引库名}/_update/{文档id}
    {
    "doc": {
    "字段名": "新的值",
    ...
    }
    }

RestClient操作索引库

初始化JavaRestClient

1.引入ES的RestHighLevelClient的依赖

XML 复制代码
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

2.因为SpringBoot管理的ES默认版本为7.6.2,所以我们需要覆盖默认的ES版本

XML 复制代码
<properties>
    <java.version>1.8</java.version>
    <elasticsearch.version>7.12.1</elasticsearch.version>
</properties>

3.初始化RestHighLevelClient

java 复制代码
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
        HttpHost.create("http://192.168.150.101:9200")
));

创建索引库

DSL语句

PUT /hotel

{

"mappings": {

"properties": {

"id": {

"type": "keyword"

},

"name": {

"type": "text",

"analyzer": "ik_max_word",

"copy_to": "all"

},

"address": {

"type": "keyword",

"index": false

},

"price": {

"type": "integer"

},

"score": {

"type": "integer"

},

"brand": {

"type": "keyword",

"copy_to": "all"

},

"city": {

"type": "keyword"

},

"starName": {

"type": "keyword"

},

"business": {

"type": "keyword",

"copy_to": "all"

},

"location": {

"type": "geo_point"

},

"pic": {

"type": "keyword",

"index": false

},

"all": {

"type": "text",

"analyzer": "ik_max_word"

}

}

}

}

代码示例:

java 复制代码
    @Test
    void testCreateHotelIndex() throws IOException {
        CreateIndexRequest request = new CreateIndexRequest("hotel");
        request.source(MAPPING_TEMPLATE, XContentType.JSON);
        client.indices().create(request, RequestOptions.DEFAULT);
    }

代码解析:

定义了静态常量MAPPING_TEMPLATE

java 复制代码
public 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" +
        "      },\n" +
        "      \"starName\": {\n" +
        "        \"type\": \"keyword\"\n" +
        "      },\n" +
        "      \"business\": {\n" +
        "        \"type\": \"keyword\"\n" +
        "        , \"copy_to\": \"all\"\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" +
        "}";

client.indics()方法的返回值是IndicesClient类型,封装了所有与索引库有关的方法

删除索引库

DSL语句

DELETE /hotel

代码示例:

java 复制代码
    @Test
    void testDeleteHotelIndex() throws IOException {
        DeleteIndexRequest request = new DeleteIndexRequest("hotel");
        client.indices().delete(request, RequestOptions.DEFAULT);
    }

判断索引库是否存在

DSL语句

GET /hotel

代码示例:

java 复制代码
    @Test
    void testGetHotelIndex() throws IOException {
        GetIndexRequest request = new GetIndexRequest("hotel");
        boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
        System.out.println(exists ? "索引库已存在" : "索引库不存在");
    }
总结

JavaRestClient对索引库操作的流程计本类似,核心就是client.indices()方法来获取索引库的操作对象

索引库操作基本步骤

  1. 初始化RestHighLevelClient
  2. 创建XxxIndexRequest。Xxx是Create、Get、Delete
  3. 准备DSL(Create时需要,其它是无参)
  4. 发送请求,调用ReseHighLevelClient.indices().xxx()方法,xxx是create、exists、delete

RestClient操作文档

前置工作:

注入IHotelService接口

java 复制代码
    @Autowired
    private IHotelService hotelService;

索引库实体类

java 复制代码
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class HotelDoc {
    private Long id;
    private String name;
    private String address;
    private Integer price;
    private Integer score;
    private String brand;
    private String city;
    private String starName;
    private String business;
    private String location;
    private String pic;

    public HotelDoc(Hotel hotel) {
        this.id = hotel.getId();
        this.name = hotel.getName();
        this.address = hotel.getAddress();
        this.price = hotel.getPrice();
        this.score = hotel.getScore();
        this.brand = hotel.getBrand();
        this.city = hotel.getCity();
        this.starName = hotel.getStarName();
        this.business = hotel.getBusiness();
        this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
        this.pic = hotel.getPic();
    }
}

新增文档

我们要把数据库中的酒店数据查询出来,写入ES中

DSL语句

POST /{索引库名}/_doc/{id}

{

"name": "Jack",

"age": 21

}

代码示例:

java 复制代码
@Test
void testAddDocument() throws IOException {
    // 1. 根据id查询酒店数据
    Hotel hotel = hotelService.getById(61083L);
    // 2. 转换为文档类型
    HotelDoc hotelDoc = new HotelDoc(hotel);
    // 3. 转换为Json字符串
    String jsonString = JSON.toJSONString(hotelDoc);
    // 4. 准备request对象
    IndexRequest request = new IndexRequest();
    // 5. 准备json文档
    request.source(jsonString, XContentType.JSON);
    // 6. 发送请求
    client.index(request, RequestOptions.DEFAULT);
}

查询文档

DSL语句

GET /hotel/_doc/{id}

代码示例:

java 复制代码
@Test
void testGetDocumentById() throws IOException {
    // 1. 准备request对象
    GetRequest request = new GetRequest("hotel").id("61083");
    // 2. 发送请求,得到结果
    GetResponse response = client.get(request, RequestOptions.DEFAULT);
    // 3. 解析结果
    String jsonStr = response.getSourceAsString();
    HotelDoc hotelDoc = JSON.parseObject(jsonStr, HotelDoc.class);
    System.out.println(hotelDoc);
}

修改文档

  • 修改依旧是两种方式
    1. 全量修改:本质是先根据id删除,再新增
    2. 增量修改:修改文档中的指定字段值

增量修改

DSL语句

POST /test001/_update/1

{

"doc":{

"city":"北京",

"price":"1888"

}

}

代码示例:

java 复制代码
@Test
void testUpdateDocumentById() throws IOException {
    // 1. 准备request对象
    UpdateRequest request = new UpdateRequest("hotel","61083");
    // 2. 准备参数
    request.doc(
            "city","北京",
            "price",1888);
    // 3. 发送请求
    client.update(request,RequestOptions.DEFAULT);
}

删除文档

DSL语句

DELETE /hotel/_doc/{id}

代码示例:

java 复制代码
    @Test
    void testDeleteDocumentById() throws IOException {
        // 1. 准备request对象
        DeleteRequest request = new DeleteRequest("hotel","61083");
        // 2. 发送请求
        client.delete(request,RequestOptions.DEFAULT);
    }

批量导入文档

代码示例:

java 复制代码
@Test
void testBulkAddDoc() throws IOException {
    BulkRequest request = new BulkRequest();
    List<Hotel> hotels = hotelService.list();
    for (Hotel hotel : hotels) {
        HotelDoc hotelDoc = new HotelDoc(hotel);
        request.add(new IndexRequest("hotel").
                id(hotelDoc.getId().toString()).
                source(JSON.toJSONString(hotelDoc), XContentType.JSON));
    }
    client.bulk(request, RequestOptions.DEFAULT);
}
总结

文档初始化的基本步骤

  1. 初始化RestHighLevelClient
  2. 创建XxxRequest对象,Xxx是Index、Get、Update、Delete
  3. 准备参数(Index和Update时需要)
  4. 发送请求,调用RestHighLevelClient.xxx方法,xxx是index、get、update、delete
  5. 解析结果(Get时需要)

elasticsearch搜索功能

DSL查询分类

elasticsearch提供了基于JSON的DSL来定义查询。

常见的查询分类包括:

  • 查询所有:查询出所有数据,一般测试用。例如:match_all
  • 全文(full_text)检索查询:利用分词器对用户输入内容分词,然后如倒排索引库中匹配。如:
    • match_query
    • multi_match_query
  • 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。如:
    • ids
    • range
    • term(某个值==?)
  • 地理(geo)查询:根据经纬度查询。如:
    • geo_distance
    • geo_bounding_box
  • 复合(compound)查询:可以将上述各种查询条件组合起来。合并查询条件。例如:
    • bool
    • function_score

DSL查询语法

复制代码
GET /indexname/_search
{
    "query": {
        "查询类型": {
            "查询条件": "条件值"
        }
    }
}

全文检索查询

全文检索查询,会对用户输入的内容分词,常用于搜索框搜索。

  • 常见的全文检索包括
    • match查询:单字段查询
    • multi_match查询:多字段查询,任意一个字段符合条件就算符合查询条件

注意:搜索的字段越多,对查询性能影响就越大,因此建议采用**copy_to**,然后使用单字段查询的方式

精确查询

  • 精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有
    • term:根据词条精确值查询
    • range:根据值的范围查询

term查询

  • 因为紧缺查询的字段是不分词的字段,因此查询的条件也必须是部分词的词条。查询时,用户输入的内容跟字段值完全匹配时才认为符合条件。如果用户输入的内容过多或过少,都会搜索不到数据

range查询

  • 范围查询,一般应用在对数值类型做范围过滤的时候。例如做价格范围的过滤

地理坐标查询

常见的使用场景包括

  • 携程:搜索附近的酒店
  • 滴滴:搜索附近的出租车
  • 微信:搜索附近的人

矩形范围查询(geo_bounding_box查询)

  • 查询时。需指定矩形的左上、右下两个点的坐标,然后画出一个矩形,落在该矩形范围内的坐标,都是符合条件的文档

附近查询(geo_distance查询)

  • 查询到指定中心点小于某个距离值的所有文档
  • 以指定中心点为圆心,指定距离为半径,画一个圆,落在圆内的坐标都算符合条件

复合查询

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

  • function score:算分函数查询,可以控制文档相关性算分,控制文档排名(例如搜索引擎的排名,第一大部分都是广告)
  • bool query:布尔查询,利用逻辑关系组合多个其他的查询,实现复杂搜索

相关性算分

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

  • 在ES中,早期使用的打分算法是TF-IDF算法,公式如下

  • 再后来的5.1版本升级中,ES将算法改进为BM25算法,公式如下

TF-IDF算法有一种缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限,曲线更平滑

算分函数查询
  • 可以修改文档的相关性算分(query score),根据新得到的算分排序**。**

布尔查询

  • 布尔查询是一个或多个子查询的组合,每一个子句就是一个子查询。子查询的组合方式有
    • must:必须匹配每个子查询,类似
    • should:选择性匹配子查询,类似
    • must_not:必须不匹配,不参与算分,类似
    • filter:必须匹配,不参与算分
  • must和should一起用的时候,should会不生效

  • 对于DSL语句的解决方案比较麻烦,需要在must里再套一个bool,里面再套should,但是对于Java代码来说比较容易修改

搜索结果处理

排序

elasticsearch 支持对搜索结果的排序,默认根据相关度算分(_score)来排序。

可以排序的字段类型有:

  • keyword类型
  • 数值类型
  • 地理坐标类型
  • 日期类型

分页

  • ES默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。
  • ES中通过修改from、size参数来控制要返回的分页结果
    • from:从第几个文档开始
    • size:总共查询几个文档
  • 类似于mysql中的limit ?, ?

深度分页问题

解决方案

针对深度分页,ES提供而两种解决方案,官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html

  • search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式
  • scrool:原理是将排序后的文档id形成快照,保存在内存。官方已经不推荐使用
总结

分页查询的常见实现方案以及优缺点

from + size:

  • 优点:支持随机翻页
  • 缺点:深度分页问题,默认查询上限是10000(from + size)
  • 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索(百度现在支持翻页到75页,然后显示提示:限于网页篇幅,部分结果未予显示。)

after search:

  • 优点:没有查询上限(单词查询的size不超过10000)
  • 缺点:只能向后逐页查询,不支持随机翻页
  • 场景:没有随机翻页需求的搜索,例如手机的向下滚动翻页

scroll:

  • 优点:没有查询上限(单词查询的size不超过10000)
  • 缺点:会有额外内存消耗,并且搜索结果是非实时的(快照保存在内存中,不可能每搜索一次都更新一次快照)
  • 场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议使用after search方案

高亮

在搜索结果中把关键字突出显示。

原理:

  • 高亮显示的实现分为两步
    1. 给文档中的所有关键字都添加一个标签,例如<em>标签
    2. 页面给<em>标签编写CSS样式

默认情况下就是加的<em>标签,所以我们也可以省略

复制代码
GET /hotel/_search
{
  "query": {
    "match": {
      "all": "上海如家"
    }
  },
  "highlight": {
    "fields": {
      "name": {
        "require_field_match": "false"
      }
    }
  }
}

高亮查询,默认情况下,ES搜索字段必须与高亮字段一致;

但是可通过require_field_match属性配置;

RestClient 查询文档

实现步骤:

  1. 准备request
  2. 准备DSL
  3. 发送请求

通过match_all来演示基本的API:

全文检索查询

精确查询

复合查询

排序和分页

搜索结果的排序和分页是与query同级的参数:

高亮

结果解析处理

相关推荐
2501_946675642 小时前
Flutter与OpenHarmony打卡步进器组件
java·javascript·flutter
莓有烦恼吖2 小时前
基于AI图像识别与智能推荐的校园食堂评价系统研究 05-审核机制模块
java·服务器·python
开开心心就好2 小时前
OCR识别工具可加AI接口,快捷键截图翻译便捷
java·网络·windows·随机森林·电脑·excel·推荐算法
爬山算法2 小时前
Hibernate(15)Hibernate中如何定义一个实体的主键?
java·后端·hibernate
廋到被风吹走2 小时前
【Spring】Spring AMQP 详细介绍
java·spring·wpf
一起养小猫3 小时前
LeetCode100天Day6-回文数与加一
java·leetcode
程序员小假3 小时前
我们来说一下 MySQL 的慢查询日志
java·后端
独自破碎E4 小时前
Java是怎么实现跨平台的?
java·开发语言