认识与安装

Lucene是Java语言的搜索引擎类库,其具有易扩展高性能(基于倒排索引) 的优势
Elastic — 搜索 AI 公司 | Elastic具备以下两点优势:

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

什么是ELK:elasticsearch结合kibana、Logstash、Beats一整套技术栈,被广泛应用在日志数据分析、实时监控等领域

🥲安装

1
docker network create itmentu-net	

es:

1
docker run -d --name elasticsearch -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 itmentu-net -p 9200:9200 -p 9300:9300 elasticsearch:7.12.1

kibana:

1
docker run -d --name kibana -e ELASTICSEARCH_HOSTS=http://elasticsearch:9200 --network itmentu-net -p 5601:5601 kibana:7.12.1

注意!!!IK分词器严格对应es的版本安装

基础概念

  • elasticsearch中的文档数据会被序列化为jJSON格式后存储在elasticsearch中

  • 文档(Document): 就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式

  • 字段(Field): 就是JSON文档中的字段,类似数据库中的列(Column)

  • 索引库: 相同类型的文档的集合

  • 映射(mapping): 索引中文档的字段约束信息,类似表的结构约束

  • DSL: 是elasticsearch提供的JSON风格的请求语句,用来定义搜索条件

索引库操作

Mapping映射属性

✨常见的mapping属性包括:

  • type: 字段数据类型,常见的简单类型有:
    • 字符串: text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
    • 数值: long、intege、short、byte、double、float
    • 布尔: boolean
    • 日期: date
    • 对象: object
  • index: 是否创建索引,默认为true
  • analyzer: 使用哪种分词器(除了text以外,大部分时候都不需要)
  • properties: 该字段的子字段

索引库的CRUD

ElasticSearch重建/创建/删除索引操作

这里注意elasticsearch不支持索引库修改,但是可以添加新的字段

文档操作

文档的CRUD

  • 创建文档:POST /{索引库名}/_doc/文档id
  • 查询文档:GET /{索引库名}/_doc/文档id
  • 删除文档:DELETE /{索引库名}/_doc/文档id
  • 修改文档:修改有两种方式:
    • 全量修改:直接覆盖原来的文档
      • PUT /{索引库名}/_doc/文档id
      • 注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
    • 增量修改:修改文档中的部分字段,增量修改是只修改指定id匹配的文档中的部分字段。
      • POST /{索引库名}/_update/文档id { “doc”: {字段}}

批量处理

Elasticsearch中允许通过一次请求中携带多次文档操作,也就是批量处理,语法格式如下:

1
2
3
4
5
6
7
8
9
POST /_bulk
["index":{"_index":"索引库名","_id":"1"}}
("字段1":"值1","字段2":"值2"}
["index":{"_index":"索引库名","_id":"1"}}
("字段1":"值1","字段2”:"值2”}
["index":{"_index":"索引库名","_id":"1"}}
("字段1":"值1","字段2":"值2”"}
( "delete" :{"_index": "test", "_id" : "2" } }[ "update" : {"_id" : "1", "_index" : "test"} }
[ "doc" : {"field2" : "value2"} }

JavaRestClient

参考博客

客户端初始化

  1. 引入依赖
    1
    2
    3
    4
    <dependency>   
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    </dependency>
  2. 覆盖默认的ES版本
    1
    2
    3
    <properties>
    <elasticsearch.version>7.12.1</elasticsearch.version>
    </properties>
  3. 初始化RestHighLevelClient
    1
    2
    3
    4
    5
    	RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(       
    HttpHost.create("http://192.168.150.101:9200"),
    HttpHost.create("http://192.168.150.102:9200"),
    HttpHost.create("http://192.168.150.103:9200") //集群模式写多个
    ));

操作索引库

  1. 创建索引库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @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);
    }

    创建索引库的DSL语句以静态字符串常量的形式统一写在常量类里

  2. 查询索引库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    	@Test
    void testDeleteHotelIndex() throws IOException {
    // 准备Request请求
    GetIndexRequest request = new GetIndexRequest("items");
    // 发送请求
    GetIndexResponse response = client.indices().get(request,
    RequestOptions.DEFAULT);

    }
  3. 删除索引库

    1
    2
    3
    4
    5
    6
    7
    @Test
    void testDeleteHotelIndex() throws IOException {
    // 1.创建Request对象
    DeleteIndexRequest request = new DeleteIndexRequest("hotel");
    // 2.发起请求
    client.indices().delete(request, RequestOptions.DEFAULT);
    }
  4. 判断索引库是否存在

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @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);
    }


操作文档

  1. 新增文档
  • 这是tb_hotel表对应的实体类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Data
    @TableName("tb_hotel")
    public class Hotel {
    @TableId(type = IdType.INPUT)
    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 longitude;
    private String latitude;
    private String pic;
    }
  • 这里写个新类HotelDoc,将Hotel类封装成为和ES索引库字段对应的类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    @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(); //对于这些不用包装的字段,
    //直接get到后赋值给包装对象的属性就行
    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();
    }
    }
  • 接下来实现去数据库查询酒店数据,导入到hotel索引库:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Resource
    IHotelService iHotelService;

    @Test
    void testIndexDocument() throws IOException {
    //从MySQL查
    Hotel hotel = iHotelService.getById(60359L);
    //封装
    HotelDoc hotelDoc = new HotelDoc(hotel);
    //创建request
    IndexRequest request = new
    IndexRequest("hotel").id(hotelDoc.getId().toString());
    //放入要新增的json传
    request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
    //发请求
    client.index(request, RequestOptions.DEFAULT);
    }
  1. 查询文档
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Test
    void testGetDocumentById() throws IOException {
    GetRequest request = new GetRequest("hotel","60359");
    GetResponse response = client.get(request,RequestOptions.DEFAULT);
    //看上图DSL执行返回结果中有个_source,这个getSource方法就是拿这个数据
    String json = response.getSourceAsString();
    //解析
    HotelDoc hotelDoc = JSON.parseObject(json,HotelDoc.class);
    System.out.println(hotelDoc);
    }
  2. 修改文档
1
2
3
4
5
6
7
8
9
10
//局部更新
@Test
void testUpdateDocumentById() throws IOException {
UpdateRequest request = new UpdateRequest("hotel","60359");
request.doc(
"price","0.01",
"startName","四星级"
);
client.update(request,RequestOptions.DEFAULT);
}
  1. 删除文档
1
2
3
4
5
6
//根据id删除文档
@Test
void testDeleteDocumentById() throws IOException {
DeleteRequest request = new DeleteRequest("hotel","60359");
client.delete(request,RequestOptions.DEFAULT);
}
  1. 批量导入文档
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
void testBulk() throws IOException {
//从MySQL查到所有数据
List<Hotel> hotels = iHotelService.list();
BulkRequest request = new BulkRequest();
//遍历Hotel的对象集合
for(Hotel hotel : hotels){
//Hotel转HotelDoc
HotelDoc hotelDoc = new HotelDoc(hotel);
//创建新增文档的Request对象
request.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc),XContentType.JSON)
);
}
//发送请求
client.bulk(request,RequestOptions.DEFAULT);
}

DSL查询

叶子查询

  • 一般是在特定的字段里查询特定值,属于简单查询,很少单独使用

叶子查询通常分为三类:

  1. 全文检索(full tetx)查询:利用分词器对用户输入内容分词,然后去词条列表中匹配。

    • match查询:全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索
    1
    2
    3
    4
    5
    6
    7
    8
    GET /indexName/_search
    {
    "query":{
    "match":{
    "FIELD": "TEXT"
    }
    }
    }
    • multi_match:与match查询类似,只不过允许同时查询多个字段
    1
    2
    3
    4
    5
    6
    7
    8
    9
    GET /indexName/_search
    {
    "query":{
    "multi_match":{
    "query": "TEXT",
    "fields": ["FIELD1", "FIELD12"]
    }
    }
    }
  2. 精确查询:不对用户输入内容分词,直接精确匹配,一般是查找keyword、数值、日期、布尔等类型。

    精确查询没有得分,或者说得分都是固定值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // term查询语法
    GET /indexName/_search
    {
    "query":{
    "term":{
    "FIELD"{
    "value": "VALUE"
    }
    }
    }
    }
    • gte指的就是最小值,而lte指的就是最大值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // range查询语法
    GET /indexName/_search
    {
    "query":{
    "range":{
    "FIELD"{
    "gte": 10,
    "lte": 20
    }
    }
    }
    }
  3. 地理(geo)查询:用于搜索地理位置,搜索方式很多。

复合查询

  • 以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式

基于逻辑运算

  • 基于逻辑运算组合叶子查询,实现组合条件

布尔查询(Boolean Query)

布尔查询主要用于根据不同的逻辑条件,组合多个查询子句。常见的逻辑运算符 有“与(AND)”、“或(OR)”、“非(NOT)”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GTE /items/_search {
"query": {
"bool": {
"must": [
{ "match": { "field": "value" } }
],
"should": [
{ "term": { "field": "value" } }
],
"must_not": [
{ "term": { "field": "value" } }
],
"filter": [
{ "range": { "field": { "gte": 10, "lte": 100 } } }
]
}
}
}

  • must必须匹配,等同于逻辑运算符“与(AND)”,参与评分计算。
  • should可以匹配,等同于逻辑运算符“或(OR)”,参与评分计算。
  • must_not必须不匹配,等同于逻辑运算符“非(NOT)”不参与评分计算。
  • filter过滤条件,与 must 相似,但不参与评分计算。由于不计算评分,它的性能通常更优。

影响相关性评分

  • 基于某种算法修改查询时的文档相关性算分,从而改变文档排名。
  • 其中最基础的算法评分就是match查询multi_match查询

boost设置

通过 boost 参数可以增强某些查询条件对相关性评分的影响。boost 是一个浮动系数,表示该查询条件相对于其他条件的权重。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GET /indexName/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"title": {
"query": "智能手机",
"boost": 2
}
}
},
{
"match": {
"description": "智能手机"
}
}
]
}
}
}

在此查询中,title 字段的匹配结果会被加权处理,相对于 description 字段的匹配结果 ,其得分为 (boost 值的)两倍。

function_score查询

function_score 查询允许开发者通过自定义的函数对查询结果的评分进行修改。可以使用不同的评分函数(如权重、衰减函数、随机函数等)来影响文档的最终评分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
GET /indexName/_search
{
"query": {
"function_score": {
"query": {
"match": {
"description": "智能手机"
}
},
"boost_mode": "multiply",
"functions": [
{
"gauss": {
"price": {
"origin": "1000",
"scale": "500",
"offset": "0",
"decay": 0.5
}
}
}
]
}
}
}
  • function_score 查询:这部分会基于查询的结果修改文档的评分。它接收一个查询和一个或多个评分函数,在这个例子中有一个评分函数是 gauss。
  • boost_mode: "multiply":这个选项表示查询的原始相关性评分(来自 match 查询)将与函数评分结果相乘。这意味着如果函数评分较高,文档的总评分会被放大,影响其在结果中的排名。
  • functions部分:
    • gauss 函数是一个常用的数学函数,通常用来对某些字段(比如 price)进行评分调整。
    • price 字段表示的是文档的价格。gauss 函数的作用是根据价格的值来对文档的评分进行调整。
    • origin: "1000":这是函数的起始值,即价格为 1000 时,评分的衰减(Decay)开始。
    • scale: "500":这是衰减的范围。当价格偏离 1000 达到 500 时,评分会降低。
    • offset: "0":这个参数表示从 1000 开始,计算衰减的“偏移量”,通常设置为0。
    • decay: 0.5:这个参数决定了衰减的速率。衰减越快,价格越高的文档评分会降低得越快

dis_max查询

dis_max 查询允许我们根据多个查询结果返回得分最高的一个,通常用于组合多个查询条件。它会选择所有查询中得分最高的一个,并返回该文档。这个查询适用于某些场景下,想要从多个查询条件中选择最匹配的一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GET /indexName/_search
{
"query": {
"dis_max": {
"queries": [
{
"match": {
"title": "智能手机"
}
},
{
"match":
{
"description": "智能手机"
}
}
],
"tie_breaker": 0.7
}
}
}

dis_max 查询的核心组成部分:

  • queries:这是一个数组,里面包含了多个查询。在这个例子中,有两个 match 查询:
    • match 查询在 title 字段中查找“智能手机”。
    • match 查询在 description 字段中查找“智能手机”。
  • tie_breaker:这个参数用于控制当多个查询的相关性评分差距较小(即评分接近时)如何合并它们的评分。如果 tie_breaker 为 0(默认值),那么 dis_max 查询只会选择评分最高的查询结果。如果设置为一个值(例如 0.7),则当多个查询的评分相近时,它们的相关性评分会按比例合并,tie_breaker 表示合并的权重。

DSL查询结果处理

排序

默认根据相关度算分 (_score)来排序,也可以指定字段排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。

1
2
3
4
5
6
7
8
9
10
11
GET /indexName/_search
{
"query": {
"match_all":{}
},
"sort":[
{
"FIELD": "desc" //排序字段和排序方式ASC、DESC(升序和降序)
}
]
}

分页

  1. From + Size
1
2
3
4
5
6
7
8
9
10
11
GET /your_index/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 10,
"sort": [
{ "field_name": { "order": "asc" } }
]
}

优点:

  • 简单直观,支持随机跳转分页。

缺点:

  • 深度分页性能差,from 值过大会导致内存和 CPU 消耗增加。

适用场景:

  • 数据量较小或仅需浅分页的场景。
  1. Scroll
  • 初始化滚动上下文:
1
2
3
4
5
6
7
8
9
10
11
12
POST /your_index/_search?scroll=1m

{

"size": 100,

"query": {

"match_all": {}

}
}
  • 使用返回的 scroll_id 获取下一批数据:
1
2
3
4
5
6
7
8
9
POST /_search/scroll

{

"scroll": "1m",

"scroll_id": "your_scroll_id"

}

优点:

  • 高效处理大数据集,适合全量遍历。

缺点:

  • 非实时,消耗服务器资源,不支持随机跳转。

适用场景:

  • 数据导出或日志分析等需要全量遍历的场景。
  1. Search After

步骤:

  • 初始查询并获取排序值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GET /your_index/_search

{

"size": 10,

"query": {

"match_all": {}

},

"sort": [

{ "price": { "order": "desc" } },

{ "created_at": { "order": "asc" } }

]

}
  • 使用上一页最后一条记录的排序值进行下一页查询:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GET /your_index/_search

{

"size": 10,

"query": {

"match_all": {}

},

"sort": [

{ "price": { "order": "desc" } },

{ "created_at": { "order": "asc" } }

],

"search_after": [129.99, "2023-10-23T12:00:00Z"]
}

优点:

  • 深度分页性能优异,不受 max_result_window 限制。

缺点:

  • 不支持随机跳转,依赖唯一排序字段。

适用场景:

  • 深度分页或需要实时性较高的场景。

高亮显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET /items/_search
{
"query": {
"match": {
"FIELD": "TEXT"
}
},
"highlight": {
"fields": {
"FIELD": {
"pre_tags": "<em>", //高亮的前置标签
"post_tags": "</em>" //高亮的后置标签
}
}
}
}

数据聚合(待补充)

  • 桶(Bucket)聚合: 用来对文档做分组
    • Term聚合:以分类字段对数据分组
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    GET /items/_search
    "query":{"match_all":{}},// 可以省略
    "size":0//设置size为0,结果中不包含文档,只包含聚合结果
    "aggs":{ //定义聚合
    "cateAgg":{ //给聚合起个名字
    "terms":{//聚合的类型,按照品牌值聚合,所以选择term
    "field":"category"//参与聚合的字段
    "size":20 //希望获取的聚合结果数量
    }
    }
    }
  • 度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等
  • 管道(pipeline)聚合:其他聚合的结果为基础做聚合

RestClient查询与结果处理

记得引入依赖和添加配置,这里不多赘述

基础语法如下:

1
2
3
4
5
6
7
8
9
10
11
@Test
void testMatchAll() throws IOException {
//1.准备Request
SearchRequest request = new SearchRequest("indexName");
//2.组织DSL参数
request.source()
.query(QueryBuilders.matchAllQuery());
//3.发送请求,得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//...解析响应结果
}

构建查询条件

  • 全文检索的查询条件构造API
1
2
3
4
// 单字段查询
QueryBuilders.matchQuery("name", "商品名称");
// 多字段查询
QueryBuilders.multiMatchQuery("商品名称", "字段1", "字段2");
  • 精确查询的查询条件构造API如下:
1
2
3
4
// 词条查询
QueryBuilders.termQuery("category", "item");
// 范围查询
QueryBuilders.rangeQuery("price").gte(100).lte(150);
  • 布尔查询的查询条件构造API
1
2
3
4
5
6
7
8
// 创建布尔查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//添加mnust条件
boolQuery.must(
QueryBuilders.termQuery("brand", "华为"));
//添加filter条件
boolQuery.filter(
QueryBuilders.rangeQuery("price").lte(2500));

排序与分页

1
2
3
4
5
6
// 查询
request.source().query(QueryBuilders.matchAllQuery());
// 分页
request.source().from(0).size(5);
// 价格排序
request.source().sort("price", SortOrder.ASC);

高亮显示

1
2
3
4
5
6
request.source().highlighter(
SearchSourceBuilder.highlight()
.field("name")
.preTags("<em>")
.postTags("</em>")
);

结果解析API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//得到_source,也就是原始json文档
String source = hit.getSourceAsString();
//反序列化
ItemDoc item = JSONUtil.toBean(source, ItemDoc.class);
//获取高亮结果
Map<String, HighlightField> hfs = hit.getHighlightFields();
if (CollUtils.isNotEmpty(hfs)) {
//有高亮结果,获取name的高亮结果
HighlightField hf = hfs.get("name");if (hf 1= null) {
//获取第一个高亮结果片段,就是商品名称的高亮值
String hfName = hf.getFragments([0].string();
item.setName(hfName);
}
}

聚合

以品牌为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
request.source().size(0);
request.source().aggregation(
AggregationBuilders
.terms ("brand_agg")
.field("brand")
.size(20)
);
// 解析聚合结果
Aggregations aggregations = response.getAggregations();
//根据名称获取聚合结果
Terms brandTerms = aggregations.get("brand_agg");
//获取桶
List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
//遍历
for (Terms.Bucket bucket : buckets) {
//获取key,也就是品牌信息
String brandName = bucket.getKeyAsString();
System.out.println(brandName);