Elasticsearch分布式搜索介绍

部分资料来自与黑马程序员公开课程

何为Elasticsearch

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

image-20210720194008781

而elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。

image-20210720194230265

倒排索引

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

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

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

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

如图:

image-20210720200457207

倒排索引的搜索流程如下(以搜索”华为手机”为例):

1)用户输入条件"华为手机"进行搜索。

2)对用户输入内容分词,得到词条:华为手机

3)拿着词条在倒排索引中查找,可以得到包含词条的文档id:1、2、3。

4)拿着文档id到正向索引中查找具体文档。

如图:

image-20210720201115192

虽然要先查询倒排索引,再查询倒排索引,但是无论是词条、还是文档id都建立了索引,查询速度非常快!无需全表扫描。

倒排索引就是按照关键词定义文档(数据)id

Mysql和Es概念对比

Es在存储数据(文档)时,其存储形式为json型

MySQL Elasticsearch 说明
Table Index 索引(index),就是文档的集合,类似数据库的表(table)
Row Document 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
Column Field 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)
Schema Mapping Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
SQL DSL DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD

安装ES和Kibana

安装过程网上有很多,此处不多做赘述,利用docker容器可以很容易构建。

docker启动命令:

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

访问默认端口9200可以看到下面的样子

image-20231227151810464

Kibana启动命令

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

注意kibana版本要和es版本保持一致,然后要在kibana中指定es端口

安装并启动kinaba后就可以打开他的可视化页面

image-20231227152329445

查看分词器

使用Kibana自带的工具可以发送DSL请求

1
2
3
4
5
POST /_analyze
{
"text": "我是林峰我爱玩原神",
"analyzer": "chinese"
}

得到分词结果

image-20231227153308991

显然不符合我们中文的分词规则

安装IK分词器

可以选择在线安装,或者是离线下载好后自己找到es插件目录,给他塞进去

在线:

1
2
3
4
5
6
7
8
9
10
# 进入容器内部
docker exec -it elasticsearch /bin/bash

# 在线下载并安装
./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

离线:

安装插件需要知道elasticsearch的plugins目录位置,而我们用了数据卷挂载,因此需要查看elasticsearch的数据卷目录,通过下面命令查看:

1
docker volume inspect es-plugins

显示结果:

{
    "CreatedAt": "2023-12-27T15:12:44+08:00",
    "Driver": "local",
    "Labels": null,
    "Mountpoint": "/var/lib/docker/volumes/es-plugins/_data",
    "Name": "es-plugins",
    "Options": null,
    "Scope": "local"
}

然后进入到所给的目录中,将下载并解压好的IK文件放进去即可

现在再回去验证一下分词结果

1
2
3
4
5
POST /_analyze
{
"text": "林峰爱玩游戏",
"analyzer": "ik_smart"
}

ik分词器有两种模式:

  • ik_smart:最少切分

  • ik_max_word:最细切分

ik_smart:

image-20231227154345716

ik_max_word:
image-20231227154410448

分词器扩展

找到ik/config/IKAnalyzer.cfg.xml,在其中添加我们扩展的ext.dic和拦截词stopword.dic

1
2
3
4
5
6
7
8
9
10
11
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">ext.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords">stopwords.dic</entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

这个ext.dic和stopwords.dic需要自己配置

举个例子:

ext.dic:

image-20231227160051426

stopword.dic:

image-20231227160311420

配置完成后重启,然后回kibana试试

1
2
3
4
5
POST /_analyze
{
"text": "我是林峰我爱玩原神爱吃奥利给",
"analyzer": "ik_smart"
}
image-20231227160714658

完美

索引库操作

常见mapping类型

mapping是对索引库中文档的约束,常见的mapping属性包括:

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

创建索引库语法

举例

  • 请求方式:PUT
  • 请求路径:/索引库名,可以自定义
  • 请求参数:mapping映射
  • properties: 字段

格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PUT /索引库名称
{
  "mappings": {
    "properties": {
      "字段名":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "字段名2":{
        "type": "keyword",
        "index": "false"
      },
      "字段名3":{
        "properties": {
          "子字段": {
            "type": "keyword"
          }
        }
      },
// ...略
    }
  }
}

实例

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
PUT /test01
{
"mappings": {
"properties": {
"info": {
"type": "text",
"analyzer": "ik_smart"
},
"email": {
"type": "keyword",
"index": false
},
"name": {
"type": "object",
"properties": {
"firstname": {
"type": "keyword"
},
"lastname":{
"type": "keyword"
}
}
}
}
}
}

介绍

info:字符串类型,可分割,分词器是ik_smart,默认可搜索(index为true)

email:关键词类型,不可分割,不搜索

name:对象类型,存放字段,默认可搜索

查询,删除,修改库

索引库的操作完全符合restful风格,即获取为get,创建put(这个好像不太符合),删除 delete,索引库不允许直接修改字段,只允许添加字段

查询:
从put改为get

GET /test01

删除

DELETE /test01

修改(添加age字段)

1
2
3
4
5
6
7
8
PUT /test01/_mapping
{
"properties":{
"age": {
"type": "integer"
}
}
}

再通过get查询

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
{
"test01" : {
"aliases" : { },
"mappings" : {
"properties" : {
"age" : {
"type" : "integer"
},
"email" : {
"type" : "keyword",
"index" : false
},
"info" : {
"type" : "text",
"analyzer" : "ik_smart"
},
"name" : {
"properties" : {
"firstname" : {
"type" : "keyword"
},
"lastname" : {
"type" : "keyword"
}
}
}
}
},

文档操作

添加(post)

语法:

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

例子:

1
2
3
4
5
6
7
8
9
10
POST /test01/_doc/1
{
"age":"21",
"info":"安理计科林峰",
"name": {
"firstname":"林",
"lastname":"峰"
},
"email":"827498@qq.com"
}

一定不要漏”,“

查询(get)

GET /test01/_doc/1

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"_index" : "test01",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"age" : "21",
"info" : "安理计科林峰",
"name" : {
"firstname" : "林",
"lastname" : "峰"
},
"email" : "827498@qq.com"
}
}

修改(put/post)

修改有两种方式:

  • 全量修改:直接覆盖原来的文档(put)
  • 增量修改:修改文档中的部分字段(post)

全量修改

全量修改是覆盖原来的文档,其本质是:

  • 根据指定的id删除文档
  • 新增一个相同id的文档

注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。

语法:

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

增量修改

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

语法:

1
2
3
4
5
6
POST /{索引库名}/_update/文档id
{
    "doc": {
"字段名": "新的值",
}
}

实例:

1
2
3
4
5
6
post /test01/_update/1
{
"doc":{
"age":25
}
}

再查看一下结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"_index" : "test01",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
"age" : 25,
"info" : "安理计科林峰",
"name" : {
"firstname" : "林",
"lastname" : "峰"
},
"email" : "827498@qq.com"
}
}

age成功修改,且版本号加1(自带版本控制)

ps:其实上面的没什么用,因为我们最终肯定是要用java控制DSL

RestClient操作索引库

使用RestClinet

引入es依赖

1
2
3
4
5
6
7
8
9
10
11
    <properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
<dependencies>

<!-- es-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

初始化RestClient对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class HotelTest {

// 所有业务都需要该对象,因此直接放到类中
private RestHighLevelClient client;

// 初始化client
@BeforeEach
void setUp(){
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.10.128:9200")
));
}

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

}

创建索引库

  • 1)创建Request对象。因为是创建索引库的操作,因此Request是CreateIndexRequest。
  • 2)添加请求参数,其实就是DSL的JSON参数部分。因为json字符串很长,这里是定义了静态字符串常量MAPPING_TEMPLATE,让代码看起来更加优雅。
  • 3)发送请求,client.indices()方法的返回值是IndicesClient类型,封装了所有与索引库操作有关的方法。
1
2
3
4
5
6
7
8
9
@Test
void creatIndex() throws IOException {
CreateIndexRequest request = new CreateIndexRequest("hotel");

request.source(MAPPING_TEMPLATE, XContentType.JSON);

client.indices().create(request, RequestOptions.DEFAULT);

}

此处的MAPPING_TEMPLATE作为一个静态变量放在别处了

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class hotel_json {
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" +
" \"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" +
"}";
}

回到浏览器确认一下,发现成功创建index

删除索引库

举一反三,删除索引库无非就是把创建请求转变为删除请求,且除了库名不需要其他参数

1
2
3
4
5
6
7
@Test
void deleteIndex() throws IOException {

DeleteIndexRequest request = new DeleteIndexRequest("hotel");

client.indices().delete(request,RequestOptions.DEFAULT);
}

返回浏览器测试,删除成功

image-20231230225601384

判断是否存在

使用get请求

1
2
3
4
5
6
7
8
9
@Test
void testExistsHotelIndex() throws IOException {
// 1.创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel");
// 2.发送请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.输出
System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
}

往索引库里添加数据

注意如果是索引库操作,那调用的是client的indices,如果是文档操作,则调用的是client.index

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    @Test
void AddHotelTest() throws IOException {
// 获取数据库数据
Hotel hotel = hotelService.getById(36934L);
HotelDoc hotelDoc = new HotelDoc(hotel);
// 创建请求对象
IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
// 将查询出来的内容作为source赋值给请求
// 此处调用fastJson的方法,可以将对象快速转换为json格式
request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
// 调用client方法add
client.index(request, RequestOptions.DEFAULT);

}

回网页鉴定一下,确实没问题

image-20231230233408321

按id获取索引库中的值

1
2
3
4
5
6
7
8
9
10
11
    @Test
void getHotelByIdTest() throws IOException {
// 构造请求
GetRequest request = new GetRequest("hotel", "36934");
// 调用方法
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 获取返回对象中的source参数
String hotel = response.getSourceAsString();

System.out.println(hotel);
}

image-20231230234114954

更新文档数据

之前有说更新文档存在两种形式,一是直接删除原来的,重新加入一条新的。二是在原来的基础上修改

此处演示第二种

调用的是client的update方法,update必然要提供要修改的键还有他的新值,这些都放在updaterequest的doc中,每两个作为一对键值对

1
2
3
4
5
6
7
8
9
10
11
12
    void updateByIdTest() throws IOException {

UpdateRequest request = new UpdateRequest("hotel", "36934");

request.doc(
// 每两个参数作为一对 key value
"brand","666天酒店",
"city","淮南"
);

client.update(request,RequestOptions.DEFAULT);
}

修改成功

image-20231230234946907

删除文档

太简单,不多说了

1
2
3
4
5
6
@Test
void deleteById() throws IOException {
DeleteRequest request = new DeleteRequest("hotel", "36934");

client.delete(request,RequestOptions.DEFAULT);
}

批量操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
void testBulkRequest() throws IOException {
// 批量查询酒店数据
List<Hotel> hotels = hotelService.list();

// 1.创建Request
BulkRequest request = new BulkRequest();
// 2.准备参数,添加多个新增的Request
for (Hotel hotel : hotels) {
// 2.1.转换为文档类型HotelDoc
HotelDoc hotelDoc = new HotelDoc(hotel);
// 2.2.创建新增文档的Request对象
request.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc), XContentType.JSON));
}
// 3.发送请求
client.bulk(request, RequestOptions.DEFAULT);
}

DSL批量插叙指令:
GET /_hotel/_search

DSL查询语句

DSL分类

  • 查询所有:查询出所有数据
  • 全文检索:利用分词器将用户输入分词,然后从索引库中查询
    • match_query
    • multi_match_query
  • 精确查找:根据精确词条查找

查询所有

1
2
3
4
5
6
7
8
// 查询所有
GET /indexName/_search
{
  "query": {
    "match_all": {
}
  }
}

全文检索

match查询语法如下:

1
2
3
4
5
6
7
8
GET /indexName/_search
{
  "query": {
    "match": {
      "FIELD": "TEXT"
    }
  }
}

mulit_match语法如下:

1
2
3
4
5
6
7
8
9
GET /indexName/_search
{
  "query": {
    "multi_match": {
      "query": "TEXT",
      "fields": ["FIELD1", " FIELD12"]
    }
  }
}

精确查询

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

  • term:根据词条精确值查询
  • range:根据值的范围查询

语法说明:

1
2
3
4
5
6
7
8
9
10
11
// term查询
GET /indexName/_search
{
  "query": {
    "term": {
      "FIELD": {
        "value": "VALUE"
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
// range查询
GET /indexName/_search
{
  "query": {
    "range": {
      "FIELD": {
        "gte": 10, // 这里的gte代表大于等于,gt则代表大于
        "lte": 20 // lte代表小于等于,lt则代表小于
      }
    }
  }
}

RestClient查询

查询所有 + 查询内容处理

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
    void testMatchAll() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("test01");

// 2.准备DSL
request.source().query(QueryBuilders.matchAllQuery());
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

// 4.解析结果
// 获取hit对象
SearchHits hits = response.getHits();
// 获取hit数目
long value = hits.getTotalHits().value;
// 获取hit到的数据
SearchHit[] searchHits = hits.getHits();

for (SearchHit hit :
searchHits) {
String s = hit.getSourceAsString();
System.out.println(s);
}

// System.out.println(response);
}

image-20240120230439233

match查询

RestClient查询整体代码流程没有区别,不同的查询方式之间只有QueryBuilder不一样,下面再拿match举例,直接借用machall代码

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
    void testMatchAll() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("test01");

// 2.准备DSL
request.source().query(QueryBuilders.matchQuery("all","林"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

// 4.解析结果
// 获取hit对象
SearchHits hits = response.getHits();
// 获取hit数目
long value = hits.getTotalHits().value;
// 获取hit到的数据
SearchHit[] searchHits = hits.getHits();

for (SearchHit hit :
searchHits) {
String s = hit.getSourceAsString();
System.out.println(s);
}

// System.out.println(response);
}

精确查询

多余代码省略

1
2
3
4
5
QueryBuilders.termQuery("gender","男");

//范围
QueryBuilders.rangeQuery("price").gte(100).lte(150);

复合查询

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

排序分页

1
2
3
request.source().sort("age", SortOrder.ASC);
int page = 2,size = 1;
request.source().from((page - 1) * size).size(1);

高亮

1
request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));

数据聚合

聚合种类

聚合常见的有三类:

  • 桶(Bucket)聚合:用来对文档做分组

    • TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组
    • Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
  • 度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等

    • Avg:求平均值
    • Max:求最大值
    • Min:求最小值
    • Stats:同时求max、min、avg、sum等
  • 管道(pipeline)聚合:其它聚合的结果为基础做聚合

DSL聚合语法(bucket)

可以对数据进行一个分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
get /hotel/_search
{
"query": { // 限定条件
"range": {
"price": {
"lte": 200
}
}
},
"size": 0, // 显示多少条数据
"aggs": { // 聚合
"brandAgg": {
"terms": {
"field": "brand", // 参与聚合的字段
"order": {
"_count": "desc" // 排序方式
},
"size": 10 // 显示多少bucket(桶)
}
}
}
}

DSL聚合语法(Metric)

结合上一个简单聚合使用,可以在每一个bucket中查找最大,最小,平均等等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
get /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"order": {
"scoreAgg.avg": "asc"
},
"size": 10
},
"aggs": { // 在基础聚合基础上
"scoreAgg":{
"stats": {
"field": "score"
}
}
}
}

}

RestClient实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    void testAggregation() throws IOException {
// 准备请求
SearchRequest request = new SearchRequest("hotel");
// 准备DSL
request.source().size(0);
request.source().aggregation(
AggregationBuilders.terms("brandAgg")
.field("brand")
.size(10)
);
// 发出请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 解析结果
Aggregations aggregations = response.getAggregations();
Terms brandTerms = aggregations.get("brandAgg");

List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
for(Terms.Bucket bucket : buckets){
String s = bucket.getKeyAsString();
System.out.println(s);
}

}

自动补全

拼音分词器

拼音分词器的官方地址:https://github.com/medcl/elasticsearch-analysis-pinyin

下载完毕后,将其解压放入es插件的本地挂载目录中即可

image-20240131011145793

演示根据拼音分词

1
2
3
4
5
post /_analyze
{
"text":["林峰学分布式"],
"analyzer": "pinyin"
}

得到的部分结果:

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
{
"tokens" : [
{
"token" : "lin",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 0
},
{
"token" : "lfxfbs",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 0
},
{
"token" : "feng",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 1
},
{
"token" : "xue",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 2
},
……

自定义分词器

elasticsearch中分词器(analyzer)的组成包含三部分:

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

我们可以自定义这三部分,可以让这三部分分别使用不同的分词器

比如在tokenizer阶段,我们指定使用ik分词器,然后在tokenizer filter阶段,我们选用pinyin分词器,这样出来的结果和直接使用拼音分词器的不同之处在于,拼音分词器会将ik分词器的结果转换为拼音(比如苹果,直接使用拼音分词器结果是[“ping”,”guo”],而使用自定义分词器,结果为[“pingguo”])

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
// 自定义拼音分词器
PUT /test
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": {
"py": {
"type": "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
}
}
}
}
}

自动补全

  • 参与补全查询的字段必须是completion类型。

  • 字段的内容一般是用来补全的多个词条形成的数组。

例子

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
32
33
34
35
36
37
38
39
// 自动补全的索引库
PUT test
{
"mappings": {
"properties": {
"title":{
"type": "completion"
}
}
}
}
// 示例数据
POST test/_doc
{
"title": ["Sony", "WH-1000XM3"]
}
POST test/_doc
{
"title": ["SK-II", "PITERA"]
}
POST test/_doc
{
"title": ["Nintendo", "switch"]
}

// 自动补全查询
POST /test/_search
{
"suggest": {
"title_suggest": {
"text": "s", // 关键字
"completion": {
"field": "title", // 补全字段
"skip_duplicates": true, // 跳过重复的
"size": 10 // 获取前10条结果
}
}
}
}

数据同步

es里存放的数据索引一般仅用作查询使用,其数据来源于数据库,当数据库中的数据遭到增删改查时,es中的数据并不会自动地同步修改,因此需要我们通过一些方法来同步修改es和数据库中的数据。

三种方法

法一:

同步调用:在修改数据库时,同步调用es的更新服务接口,这样每次修改都同时进行,三个步骤依次执行,缺点会导致业务耦合,整个业务流程被拉长,在更新完数据库后整个进程还要等待es更新,且如果其中某个业务出现问题,整个流程都会出现问题

法二:

异步调用,运用MQ信息通道,将法一调用服务接口的过程转变为在通道中发送消息,然后由es的更新服务接收消息,并更新es索引库,这样整个过程就将更新数据库和更新索引库分开了,且两者之间是异步的关系,缺点是复杂度略有提高

法三;

利用mysql自带的binlog,binlog是mysql自带的一个日志文件,对数据的增删改查都会在binlog中进行记录,然后使用canal中间件监听binlog的变化,然后再同步修改es。

推荐第二种

es集群

集群的目的:解决海量存储问题还有单点故障问题