ElasticSearch简介及相关概念

一些简介:
https://www.elastic.co/cn/what-is/elasticsearch 全文搜索属于最常见的需求,开源的 Elasticsearch 是目前全文搜索引擎的首选。 它可以快速地储存、搜索和分析海量数据。
Elastic 的底层是开源库 Lucene。但是,你没法直接用 Lucene,必须自己写代码去调用它的 接口。Elastic 是 Lucene 的封装,提供了 REST API 的操作接口,开箱即用。 REST API:天然的跨平台。
官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
官方中文:https://www.elastic.co/guide/cn/elasticsearch/guide/current/foreword_id.html
社区中文: https://es.xiaoleilu.com/index.html
http://doc.codingdict.com/elasticsearch/0/

1.Index(索引)
做动词时,相当于mysql中的插入insert;
做名词时,相当于mysql中的数据库database;
2.Type(类型)
在Index(索引)中,可以定义一个或多个类型;
类似于Mysql中的Table,没一种类型的数据放在一起;
3.Document(文档)
保存在某个索引(index)下,某种类型(Type)的一个数据(Document),文档是Json格式的,Doucument就像是Mysql中的某个Table下的内容。
4.倒排索引机制

Docker安装ElasticSearch

一、下载镜像文件

我们需要下载两个镜像文件:1.elasticsearch,用来存储和检索数据;2.kibana,是es的可视化界面,用来可视化检索数据。
docker pull elasticsearch:7.4.2
docker pull kibana:7.4.2

二、创建实例

1.安装elasticsearch:
按顺序执行以下语句
1:
mkdir -p /mydata/elasticsearch/config
2:
mkdir -p /mydata/elasticsearch/data
3:
echo “http.host: 0.0.0.0” >> /mydata/elasticsearch/config/elasticsearch.yml
4:
chmod -R 777 /mydata/elasticsearch/
5:
docker run —name elasticsearch -p 9200:9200 -p 9300:9300 \
-e “discovery.type=single-node” \
-e ES_JAVA_OPTS=”-Xms64m -Xmx512m” \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2

执行完上面的语句,docker中的es就启动了。 如果是使用的云服务器,还要记得配置安全策略,防火墙开放es需要用的端口,然后访问ip+端口号 http://116.62.228.75:9200:
image.png
看到这个信息,说明es安装启动成功。
这里我们可以安装kibana可视化界面来操作es,也可以不安装,直接发送请求来操作,比如通过postman向es发送请求,如查看节点信息:
image.png

2.安装kibana:
执行以下命令:
docker run —name kibana -e ELASTICSEARCH_HOSTS=http://xxx.xxx.xx.xx:9200 -p 5601:5601 \
-d kibana:7.4.2

docker run —name kibana -e ELASTICSEARCH_HOSTS=http://116.62.228.75:9200 -p 5601:5601
注意:xxx.xxx.xxx.xxx要改为自己es所在的ip地址(如果在虚拟机就改为自己的虚拟机地址,在云服务器就用云服务器地址)
同样的,也要开启5601端口的防火墙。
访问 http://116.62.228.75:5601
image.png
出现这个页面说明安装启动成功。

初步检索

对ElasticSearch的所有操作,ElasticSearch都封装成了Rest API。

1._cat

_cat下的请求,是用来查询es的一些信息:
GET /_cat/nodes:查看所有节点
GET /_cat/health:查看 es 健康状况
GET /_cat/master:查看主节点
GET /_cat/indices:查看所有索引
此命令相当于mysql中的show databases;刚安装kibana后会默认在ES中添加三个索引;

2.索引一个文档(即:保存一条记录)


es中索引一个文档,即相当于mysql中保存一条记录。
在es中保存一个数据,要告知es,数据保存在那个索引的哪个类型下,并且可以指定用哪个唯一标识。
例:
PUT customer/external/1;在 customer 索引下的 external 类型下保存 1 号数据 。
例子:
请求为: PUT customer/external/1
发送put请求时,带上这个json请求体:
{ “name”: “John Doe” }
请求后,结果为:
image.png
注意:
索引一个文档时,put请求和post请求都可以,有如下区别:
POST 新增,也可以用来更新,可以不指定id。如果不指定 id,会自动生成 id。指定 id 就会修改这个数据,并新增版本号。
如果指定了id,如果这个id下没有文档,则新增;如果有,则更新。
PUT 可以新增可以修改,必须指定id。PUT 必须指定 id;由于 PUT 需要指定 id,我们一般都用来做修改 操作,不指定 id 会报错 。使用put对一个id多次时,为更新操作。

3.查询文档

查询时用GET请求:
GET customer/external/1
查询 哪个索引的哪个类型下的id为哪个的数据。
例: 发送请求http://116.62.228.75:9200/customer/external/1,得到:
image.png
查询结果各参数分析:
{
“_index”: “customer”, //在哪个索引
“_type”: “external”, //在哪个类型
“_id”: “1”, //记录 id
“_version”: 2, //版本号
“_seq_no”: 1, //并发控制字段,每次更新就会+1,用来做乐观锁
“_primary_term”: 1, //同上,主分片重新分配,如重启,就会变化
“found”: true, //为true表示找到了要查找的内容
“_source”: { //真正的内容
“name”: “John Doe”
}
}

4.更新文档

通过put或post请求修改文档。
有以下三种方式:
方式1:
POST customer/external/1/_update
{
“doc”: {
“name”: “John Doew”
}
}
方式2:
POST customer/external/1
{
“name”: “John Doe2”
}
方式3:
PUT customer/external/1
{
“name”: “John Doe2”
}

这三种方式中,POST请求下,请求结尾带了/_update时,请求体的更新内容需要用一层”doc”标签包起来,这时POST操作会对比源文档数据,如果相同不会进行操作,文档version不增加,序列号seq_no也不会变化。
而不带/_update的POST操作,以及正常的PUT操作,请求体的更新内容不需要用一层”doc”标签包起来,这时总会将数据重新保存,并增加version版本。
看场景:
对于大并发更新,不带 update;
对于大并发查询偶尔更新,带 update;对比更新,重新计算分配规则
特殊,方式4,更新的同时,增加属性:
这时,直接用PUT和POST请求不在结尾带上/_update就可以,这时请求体的更新内容不需要用一层”doc”标签包起来
也可以使用POST请求在结尾带上/_update,这时请求体的更新内容需要用一层”doc”标签包起来。
如:
POST customer/external/1/_update
{
“doc”: {
“name”: “Jane Doe”,
“age”: 20
}
}

PUT customer/external/1
{
“name”: “John Doe2”,
“age”: 20
}

5.删除文档或索引

删除哪个索引下的哪个文档下的哪个记录:
DELETE customer/external/1
删除整个索引:
DELETE customer // 删除customer索引

es没有提供Type(类型)的删除操作。

6.bulk批量API

一,执行多条数据,格式为:
POST customer/external/_bulk
{“index”:{“_id”:”1”}}
{“name”:”a”}
{“index”:{“_id”:”2”}}
{“name”:”b”}
其中下面的请求内容格式为:
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
下面的请求内容,每两行为一个整体
这里用postman测不了,要去kibana里Dev Tools中测试:
image.png
运行可得:

  1. #! Deprecation: [types removal] Specifying types in bulk requests is deprecated.
  2. {
  3. "took" : 10, // 花费了多少ms
  4. "errors" : false, // 没有发生错误
  5. "items" : [ // 批量执行的每个数据的结果
  6. {
  7. "index" : { // 保存
  8. "_index" : "customer", // 索引
  9. "_type" : "external", // 类型
  10. "_id" : "3", // 保存的记录的唯一标识
  11. "_version" : 1, // 版本
  12. "result" : "created", // 创建
  13. "_shards" : {
  14. "total" : 2,
  15. "successful" : 1,
  16. "failed" : 0
  17. },
  18. "_seq_no" : 6,
  19. "_primary_term" : 1,
  20. "status" : 201 // 新建完成
  21. }
  22. },
  23. {
  24. "index" : { // 第二条记录
  25. "_index" : "customer",
  26. "_type" : "external",
  27. "_id" : "4",
  28. "_version" : 1,
  29. "result" : "created",
  30. "_shards" : {
  31. "total" : 2,
  32. "successful" : 1,
  33. "failed" : 0
  34. },
  35. "_seq_no" : 7,
  36. "_primary_term" : 1,
  37. "status" : 201
  38. }
  39. }
  40. ]
  41. }

二、对于整个索引执行批量操作
POST /_bulk
{“delete”:{“_index”:”website”,”_type”:”blog”,”_id”:”123”}}
{“create”:{“_index”:”website”,”_type”:”blog”,”_id”:”123”}}
{“title”:”my first blog post”}
{“index”:{“_index”:”website”,”_type”:”blog”}}
{“title”:”my second blog post”}
{“update”:{“_index”:”website”,”_type”:”blog”,”_id”:”123”}}
{“doc”:{“title”:”my updated blog post”}}
执行结果为:

  1. #! Deprecation: [types removal] Specifying types in bulk requests is deprecated.
  2. {
  3. "took" : 275,
  4. "errors" : false,
  5. "items" : [
  6. {
  7. "delete" : { // 执行的删除操作
  8. "_index" : "website",
  9. "_type" : "blog",
  10. "_id" : "123",
  11. "_version" : 1,
  12. "result" : "not_found",
  13. "_shards" : {
  14. "total" : 2,
  15. "successful" : 1,
  16. "failed" : 0
  17. },
  18. "_seq_no" : 0,
  19. "_primary_term" : 1,
  20. "status" : 404
  21. }
  22. },
  23. {
  24. "create" : { // 执行的创建操作
  25. "_index" : "website",
  26. "_type" : "blog",
  27. "_id" : "123",
  28. "_version" : 2,
  29. "result" : "created",
  30. "_shards" : {
  31. "total" : 2,
  32. "successful" : 1,
  33. "failed" : 0
  34. },
  35. "_seq_no" : 1,
  36. "_primary_term" : 1,
  37. "status" : 201
  38. }
  39. },
  40. {
  41. "index" : { // 执行的保存操作
  42. "_index" : "website",
  43. "_type" : "blog",
  44. "_id" : "CddIHH8BVZK7p5Rr2BmT",
  45. "_version" : 1,
  46. "result" : "created",
  47. "_shards" : {
  48. "total" : 2,
  49. "successful" : 1,
  50. "failed" : 0
  51. },
  52. "_seq_no" : 2,
  53. "_primary_term" : 1,
  54. "status" : 201
  55. }
  56. },
  57. {
  58. "update" : { // 执行的更新操作
  59. "_index" : "website",
  60. "_type" : "blog",
  61. "_id" : "123",
  62. "_version" : 3,
  63. "result" : "updated",
  64. "_shards" : {
  65. "total" : 2,
  66. "successful" : 1,
  67. "failed" : 0
  68. },
  69. "_seq_no" : 3,
  70. "_primary_term" : 1,
  71. "status" : 200
  72. }
  73. }
  74. ]
  75. }

注:
bulk API 以此按顺序执行所有的 action(动作)。如果一个单个的动作因任何原因而失败, 它将继续处理它后面剩余的动作。当 bulk API 返回时,它将提供每个动作的状态(与发送 的顺序相同),所以您可以检查是否一个指定的动作是不是失败了。

7.导入整体样本测试数据

准备了一份顾客银行账户信息的虚构的 JSON 文档样本。每个文档都有下列的 schema (模式),这个数据是es官方提供的:
测试数据地址:
https://gitee.com/xlh_blog/common_content/blob/master/es测试数据.json

全选复制拿到测试数据后,在kibana中执行批量导入测试数据:
由于这些测试数据,没指定数据在哪个索引哪个类型下,所以批量执行时要指定以下,如下:
POST /bank/account/_bulk
复制下来的测试数据

执行完成后,执行查看所有索引的请求,可以看到刚刚创建的新的索引:
image.png

进阶检索

以 初步检索 目录下的第七小节中导入 的样本测试数据,学习进阶检索。

1.SearchAPI

概述

es支持两种基本方式检索:

  • 一个是通过使用 REST request URI 发送搜索参数(uri+检索参数),发送GET请求,所有的请求参数拼接在请求地址后面;
  • 另一个是通过使用 REST request body 来发送它们(uri+请求体),发送GET请求,将检索条件放在请求体中。

这两种方式,都需要在请求路径上,拼接/_search,如下:
GET bank/_search?q=*&sort=account_number:asc

GET bank/_search
{
“query”: {
“match_all”: {}
},
“sort”: [{
“account_number”: {
“order”: “desc”
}
}]
}

方式一:uri+拼接检索参数

请求参数方式检索
GET bank/_search?q=&sort=account_number:asc
说明:
q=
# 查询所有
sort # 排序字段
asc #升序

检索bank下所有信息,包括type和docs
GET bank/_search

返回内容:
took – 花费多少ms搜索
timed_out – 是否超时
_shards – 多少分片被搜索了,以及多少成功/失败的搜索分片
max_score –文档相关性最高得分
hits.total.value - 多少匹配文档被找到
hits.sort - 结果的排序key(列),没有的话按照score排序
hits._score - 相关得分 (not applicable when using match_all)
实际操作中,GET bank/_search?q=*&sort=account_number:asc,检索了1000条数据,但是根据相关性算法,只返回10条

方式二:uri+请求体进行检索

GET /bank/_search
{
“query”: { “match_all”: {} },
“sort”: [
{ “account_number”: “asc” },
{ “balance”:”desc”}
]
}
POSTMAN中get不能携带请求体,我们变为post也是一样的,我们post一个jsob风格的查询请求体到_search API。需要了解,一旦搜索的结果被返回,es就完成了这次请求,不能切不会维护任何服务端的资源或者结果的cursor游标

2.Query DSL

2.1 基本语法格式

Elasticsearch 提供了一个可以执行查询的 Json 风格的 DSL(domain-specific language 领域特 定语言)。这个被称为 Query DSL。

(1)基本语法格式

一个查询语句的典型结构:
{
QUERY_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,

}
}
如果针对于某个字段,那么它的结构如下:
{
QUERY_NAME:{ # 使用的功能
FIELD_NAME:{ # 功能参数
ARGUMENT:VALUE,
ARGUMENT:VALUE,…
}
}
}
例:
GET bank/_search
{
“query”: {
“match_all”: {}
},
“from”: 0,
“size”: 5,
“sort”: [{
“account_number”: {
“order”: “desc”
}
}]
}
query定义如何查询,
match_all查询类型【代表查询所有的所有】,es中可以在 query中组合非常多的查询类型完成复杂查询
除了 query参数之外,我们也可以传递其它的参数以改变查询结果。如 sort,size
from+size限定,完成分页功能
sort排序,多字段排序,会在前序字段相等时后续字段内部排序,否则以前序为准

(2)返回部分字段

GET bank/_search
{
“query”: {
“match_all”: {}
},
“from”: 0,
“size”: 5,
“_source”: [“age”, “balance”]
}

(3)match匹配查询

如果是非字符串,会进行精确匹配。如果是字符串,会进行全文检索。
基本类型(非字符串),精确控制
GET bank/_search
{
“query”: {
“match”: {
“account_number”: “20”
}
}
}
此时,match返回account_number=20的数据。
字符串,全文检索
GET bank/_search
{
“query”: {
“match”: {
“address”: “kings”
}
}
}
全文检索,最终会按照评分进行排序,会对检索条件进行分词匹配。

(4)match_parse短语匹配

将需要匹配的值当成一个整体单词(不分词)进行检索。
GET bank/_search
{
“query”: {
“match_phrase”: {
“address”: “mill road” # 就是说不要匹配只有mill或只有road的,要匹配mill road一整个子串
}
}
}

(5)multi_match多字段匹配

GET bank/_search
{
“query”: {
“multi_match”: { # 前面的match仅指定了一个字段。
“query”: “mill”,
“fields”: [ # state和address有mill子串 不要求都有
“state”,
“address”
]
}
}
}
state或者address中包含mill,并且在查询过程中,会对于查询条件进行分词。

(6)bool复合查询

复合语句可以合并,任何其他查询语句,包括符合语句。这也就意味着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。

must:必须达到must所列举的所有条件
must_not:必须不匹配must_not所列举的所有条件。
should:应该满足should所列举的条件。满足条件最好,不满足也可以,满足得分更高
例1:查询gender=m,并且address=mill的数据:
GET bank/_search
{
“query”:{
“bool”:{ #
“must”:[ # 必须有这些字段
{“match”:{“address”:”mill”}},
{“match”:{“gender”:”M”}}
]
}
}
}
例2:查询gender=m,并且address=mill的数据,但是age不等于38的
GET bank/_search
{
“query”: {
“bool”: {
“must”: [
{ “match”: { “gender”: “M” }},
{ “match”: {“address”: “mill”}}
],
“must_not”: [ # 不可以是指定值
{ “match”: { “age”: “38” }}
]
}
}
例3:匹配lastName应该等于Wallace的数据
GET bank/_search
{
“query”: {
“bool”: {
“must”: [
{
“match”: {
“gender”: “M”
}
},
{
“match”: {
“address”: “mill”
}
}
],
“must_not”: [
{
“match”: {
“age”: “18”
}
}
],
“should”: [
{
“match”: {
“lastname”: “Wallace”
}
}
]
}
}
}

(7)filter结果过滤

并不是所有的查询都需要产生分数,特别是那些仅用于filtering(过滤)的文档。为了不计算分数,elasticsearch会自动检查场景并且优化查询的执行。
例:
GET bank/_search
{
“query”: {
“bool”: {
“must”: [
{ “match”: {“address”: “mill” } }
],
“filter”: { # query.bool.filter
“range”: {
“balance”: { # 哪个字段
“gte”: “10000”,
“lte”: “20000”
}
}
}
}
}
}

(8)term

和match一样。匹配某个属性的值。

  • 全文检索字段用match
  • 其他非text字段匹配用term。

不要使用term来进行文本字段查询
es默认存储text值时用分词分析,所以要搜索text值,使用match
例:
GET bank/_search
{
“query”: {
“bool”: {
“must”: [{
“term”: {
“age”: {
“value”: “28”
}
}
},
{
“match”: {
“address”: “990 Mill Road”
}
}
]
}
}
}

(9)aggregations执行聚合

聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于SQL Group by和SQL聚合函数。
在elasticsearch中,执行搜索返回this(命中结果),并且同时返回聚合结果,把以响应中的所有hits(命中结果)分隔开的能力。这是非常强大且有效的,你可以执行查询和多个聚合,并且在一次使用中得到各自的(任何一个的)返回结果,使用一次简洁和简化的API啦避免网络往返。

如: 搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄,但不显示这些人的详情
# 分别为包含mill、,平均年龄、
GET bank/_search
{
“query”: { # 查询出包含mill的
“match”: {
“address”: “Mill”
}
},
“aggs”: { #基于查询聚合
“ageAgg”: { # 聚合的名字,随便起
“terms”: { # 看值的可能性分布
“field”: “age”,
“size”: 10
}
},
“ageAvg”: {
“avg”: { # 看age值的平均
“field”: “age”
}
},
“balanceAvg”: {
“avg”: { # 看balance的平均
“field”: “balance”
}
}
},
“size”: 0 # 不看详情
}

3.Mapping映射

映射是用来定义文档如何被存储和检索的。

(1)字段类型

es6.0之前的版本在用,后面的版本已经不用了。

(2)映射

Mapping(映射)是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和索引的。比如:使用maping来定义:
哪些字符串属性应该被看做全文本属性(full text fields);
哪些属性包含数字,日期或地理位置;
文档中的所有属性是否都嫩被索引(all 配置);
日期的格式;
自定义映射规则来执行动态添加属性;

查看mapping信息:GET bank/_mapping
修改mapping信息:
创建映索引并指定映射:
PUT /my_index
{
“mappings”: {
“properties”: {
“age”: {
“type”: “integer”
},
“email”: {
“type”: “keyword” # 指定为keyword
},
“name”: {
“type”: “text” # 全文检索。保存时候分词,检索时候进行分词匹配
}
}
}
}
第一次存储数据的时候es就猜出了映射
第一次存储数据前可以指定映射
添加新的字段映射:
PUT /my-index/_mapping
{
“properties”: {
“employee-id”: {
“type”: “keyword”,
“index”: false
}
}
更新映射:
对于已经存在的映射字段,我们不能更新。更新必须创建新的索引进行数据迁移。
数据迁移:
百度吧。

4.分词及分词器

一个 tokenizer(分词器)接收一个字符流,将之分割为独立的 tokens(词元,通常是独立的单词),然后输出 tokens 流。
例如,whitespacetokenizer 遇到空白字符时分割文本。它会将文本 “Quick brown fox!“ 分割为[Quick, brown, fox!]。
tokenizer(分词器)还负责记录各个 term(词条)的顺序或 position位置(用于 phrase短语和 wordproximity词近邻查询),以及 term(词条)所代表的原始 word(单词)的 start
(起始)和 end(结束)的 characteroffsets(字符偏移量)(用于高亮显示搜索的内容)。Elasticsearch提供了很多内置的分词器,可以用来构建customanalyzers(自定义分词器)。

(1)安装ik分词器

所有的语言分词,默认使用的都是“Standard Analyzer”,但是这些分词器针对于中文的分词,并不友好。为此需要安装中文的分词器。
步骤1:进入es容器内部plugins目录
注(在前面安装的elasticsearch时,我们已经将elasticsearch容器的“/usr/share/elasticsearch/plugins”目录,映射到宿主机的“ /mydata/elasticsearch/plugins”目录下,所以比较方便的做法就是下载“/elasticsearch-analysis-ik-7.4.2.zip”文件,然后解压到该文件夹下即可。安装完毕后,需要重启elasticsearch容器。)
步骤2:docker exec -it 容器id /bin/bash
步骤3:yum install wget
步骤:4:执行此命令: wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
后面的地址为ik分词器对应版本的下载地址
步骤5:unzip下载的分词器zip文件
unzip elasticsearch-analysis-ik-7.4.2.zip -d ik
步骤6: 移动到plugins目录下
mv ik plugins/
步骤7: 添加权限
chmod -R 777 plugins/ik
步骤8: 重启es容器
docker restart elatsicsearch

(2)测试分词器

默认分词器:
POST _analyze
{
“text”:”我是中国人”
}
执行结果:

{ “tokens” : [ { “token” : “我”, “start_offset” : 0, “end_offset” : 1, “type” : ““, “position” : 0 }, { “token” : “是”, “start_offset” : 1, “end_offset” : 2, “type” : ““, “position” : 1 }, { “token” : “中”, “start_offset” : 2, “end_offset” : 3, “type” : ““, “position” : 2 }, { “token” : “国”, “start_offset” : 3, “end_offset” : 4, “type” : ““, “position” : 3 }, { “token” : “人”, “start_offset” : 4, “end_offset” : 5, “type” : ““, “position” : 4 } ] }

使用ik分词器的ik_smart:
POST _analyze
{
“analyzer”: “ik_smart”,
“text”:”我是中国人”
}
执行结果:

{ “tokens” : [ { “token” : “我”, “start_offset” : 0, “end_offset” : 1, “type” : “CN_CHAR”, “position” : 0 }, { “token” : “是”, “start_offset” : 1, “end_offset” : 2, “type” : “CN_CHAR”, “position” : 1 }, { “token” : “中国人”, “start_offset” : 2, “end_offset” : 5, “type” : “CN_WORD”, “position” : 2 } ] }

使用ik的ik_max_word;
POST _analyze
{
“analyzer”: “ik_max_word”,
“text”:”我是中国人”
}
执行结果:

{ “tokens” : [ { “token” : “我”, “start_offset” : 0, “end_offset” : 1, “type” : “CN_CHAR”, “position” : 0 }, { “token” : “是”, “start_offset” : 1, “end_offset” : 2, “type” : “CN_CHAR”, “position” : 1 }, { “token” : “中国人”, “start_offset” : 2, “end_offset” : 5, “type” : “CN_WORD”, “position” : 2 }, { “token” : “中国”, “start_offset” : 2, “end_offset” : 4, “type” : “CN_WORD”, “position” : 3 }, { “token” : “国人”, “start_offset” : 3, “end_offset” : 5, “type” : “CN_WORD”, “position” : 4 } ] }

(3)自定义词库

方式一:修改配置文件

修改/usr/share/elasticsearch/plugins/ik/config/中的 IKAnalyzer.cfg.xm ,给ik分词器指定一个远程词库,让ik分词器自己向远程仓库发送请求,拿到一些最新的单词。
如下:
<?xml version=”1.0” encoding=”UTF-8”?>
<!DOCTYPE properties SYSTEM “http://java.sun.com/dtd/properties.dtd”>

IK Analyzer 扩展配置





http://192.168.56.10/es/fenci.txt



修改完成后,需要重启elasticsearch容器,否则修改不生效。docker restart elasticsearch
更新完成后,es只会对于新增的数据用更新分词。历史数据是不会重新分词的。如果想要历史数据重新分词,需要执行:

POST my_index/_update_by_query?conflicts=proceed

方式二:通过nginx方式

(此时我们安装这个nginx,不是我们最终要装的,现在只是随便启动一个nginx实例,只是为了复制出配置。弄完配置还会删掉的)
安装nginx,将最新词放到nginx中,让ik分词器给nginx发送请求,由nginx来返回最新的词库,这样使最新的词库和原来的词库合并起来。
1.nginx的安装:
1,在mydata目录下,创建nginx文件夹,以后将所有nginx的东西,都放到nginx文件夹里:
mkdir nginx
2,运行以下命令, 启动一个nginx实例(如果没有下载,会自动下载):
docker run -p 80:80 —name nginx -d nginx:1.10
3, 将容器内的配置文件拷贝到当前目录:docker container cp nginx:/etc/nginx .
(别落下最后的点)
4,把我们需要的nginx配置复制出来后,nginx就可以停掉了: docker stop nginx
5, 执行命令删除原容器:docker rm nginx
6, 为了使nginx下的文件更有层次感,更清晰,
把现在的 mydata下的nginx文件夹,更名为conf: mv nginx conf
7,重新创建一个nginx文件夹: mkdir nginx
8,把mydata下的conf移动到nginx下:mv conf nginx/
9,启动我们真正要用的naginx;
docker run -p 80:80 —name nginx \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v /mydata/nginx/conf:/etc/nginx \
-d nginx:1.10
此时,查看mydata下的nginx目录如下:
image.png
且,这时给 nginx的 html下面放的所有资源可以直接访问
2.安装完nginx后,在mydata/nginx/html目录下,创建一个es目录:
mkdir es
将es、ik分词器需要用到的一些资源,放到这里。
创建完成后,在mydata/nginx/html/es目录下创建一个存放分词内容的文本,如:
vi fenci.txt
执行该语句后,把需要识别的词汇,写在里面,保存退出。
(一个词汇占用一行)
这时,访问nginx所在地址及端口号,拼接fenci.txt路径,可以访问到fenci.txt文件:http://116.62.228.75/es/fenci.txt (如果乱码,暂时先不管)
3.配置ik分词器的远程词库地址
把2中nginx的fenci.txt作为远程词库。
修改/usr/share/elasticsearch/plugins/ik/config/中的 IKAnalyzer.cfg.xml,这里由于ik分词器的信息都挂载到了mydata/elasticsearch目录下,所以修改/mydata/elasticsearch/plugins/ik/config/IKAnalyzer.cfg.xml 这个配置文件也可以。
进入/mydata/elasticsearch/plugins/ik/config目录下:
vi IKAnalyzer.cfg.xml
编辑其配置内容:
<?xml version=”1.0” encoding=”UTF-8”?>
<!DOCTYPE properties SYSTEM “http://java.sun.com/dtd/properties.dtd“>

IKAnalyzer扩展配置





http://116.62.228.75/es/fenci.txt



下面的配置到注释取消掉,里面配上自己的nginx分词文件地址。
改完之后,要重启es:
docker restart elasticsearch
测试效果:
我们在自己的分词文件中,添加了 乔碧萝 这个词。
POST _analyze
{
“analyzer”: “ik_max_word”,
“text”:”乔碧萝殿下”
}
结果如下:

{ “tokens” : [ { “token” : “乔碧萝”, “start_offset” : 0, “end_offset” : 3, “type” : “CN_WORD”, “position” : 0 }, { “token” : “殿下”, “start_offset” : 3, “end_offset” : 5, “type” : “CN_WORD”, “position” : 1 } ] }

Java操作ES

简述

java操作es可以通过9300端口或9200端口。
通过9300端是TCP协议,通过9200端口是HTTP协议。
具体方式有很多种,很多是不适配、落后、或者更新慢的,我们最终选择的是:
9200端口http方式的Elasticsearch-Rest-Client(elasticsearch-rest-high-level-client),这是官方RestClient,封装了ES操作,API层次分明,上手简单。

SpringBoot整合ElasticSearch

我们把检索做成一个单独的模块 gulimall-search:
image.png
第一步:
导入整合es的maven依赖:

<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>

同时,导入gulimall-common模块,导入后,配置springcloudalibaba注册中心等信息:

<dependency>
            <groupId>com.atguigu.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.application.name=gulimall-search

第二步:
springboot自带了es的版本。可能跟我们的es版本不符合,要改掉:

<properties>
        <java.version>1.8</java.version>
        <elasticsearch.version>7.4.2</elasticsearch.version>
    </properties>

第三步:
对es进行配置。在gulimall-search下创建config包,包下新建es配置类:

package com.atguigu.gulimall.search.config;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 本类说明:
 * es配置类
 * @author yuanhai
 * @date 2022年02月23日
 */
@Configuration
public class GulimallElasticSearchConfig {

    @Bean
    public RestHighLevelClient esRestClient() {
        // 如果es有多个,就在 RestClient.builder写多个HttpHost对象就好,目前只有一个,只写一个就好
//        RestHighLevelClient client = new RestHighLevelClient(
//                RestClient.builder(
//                        new HttpHost("000.000.000.000", 9200, "http")));
        // 即:
        RestClientBuilder builder = null;
        builder = RestClient.builder(new HttpHost("000.000.000.000", 9200, "http"));
        RestHighLevelClient client = new RestHighLevelClient(builder);
        return client;
    }

}

启动类如下:

package com.atguigu.gulimall.search;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallSearchApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallSearchApplication.class, args);
    }

}

测试:

@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallSearchApplicationTests {

    @Autowired
    private RestHighLevelClient client;

    @Test
    public void contextLoads() {
        System.out.println(client);
    }

}

运行可以打印出RestHighLevelClient对象:

org.elasticsearch.client.RestHighLevelClient@d16be4f

java操作es测试

参照API https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html
在GulimallElasticSearchConfig中,添加一下文档中提供的一些设置,暂时可能用不到。

1.索引/更新一条数据

package com.atguigu.gulimall.search;

import com.alibaba.fastjson.JSON;
import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
import lombok.Data;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.IOException;

@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallSearchApplicationTests {

    @Autowired
    private RestHighLevelClient client;

    /**
     * 测试存储数据到es
     */
    @Test
    public void indexData() throws IOException {
        IndexRequest indexRequest = new IndexRequest("users");
        indexRequest.id("1");
        // 存入数据方式1
//        indexRequest.source("username","zhangsan","age","18","gender","男");
        // 存入数据方式2,一般都使用这种方式
        User user = new User();
        String jsonString = JSON.toJSONString(user);
        indexRequest.source(jsonString, XContentType.JSON);  // 要保存的内容,参数XContentType.JSON是指明使用json类型

        // 执行保存操作
        IndexResponse index = client.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);

        // 提取有用的响应数据
        System.out.println(index);

    }

    @Data
    class User {
        private String username;
        private String gender;
        private Integer age;
    }

}

运行结果:

IndexResponse[index=users,type=_doc,id=1,version=1,result=created,seqNo=0,primaryTerm=1,shards={“total”:2,”successful”:1,”failed”:0}]

这次是保存了一个空数据。然后使用同样的id,设置各个属性值后在此运行:

package com.atguigu.gulimall.search;

import com.alibaba.fastjson.JSON;
import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
import lombok.Data;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.IOException;

@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallSearchApplicationTests {

    @Autowired
    private RestHighLevelClient client;

    /**
     * 测试存储数据到es
     */
    @Test
    public void indexData() throws IOException {
        IndexRequest indexRequest = new IndexRequest("users");
        indexRequest.id("1");
        // 存入数据方式1
//        indexRequest.source("username","zhangsan","age","18","gender","男");
        // 存入数据方式2,一般都使用这种方式
        User user = new User();
        user.setUsername("zhangsan");
        user.setGender("男");
        user.setAge(18);
        String jsonString = JSON.toJSONString(user);
        indexRequest.source(jsonString, XContentType.JSON);  // 要保存的内容,参数XContentType.JSON是指明使用json类型

        // 执行保存操作
        IndexResponse index = client.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);

        // 提取有用的响应数据
        System.out.println(index);

    }

    @Data
    class User {
        private String username;
        private String gender;
        private Integer age;
    }

}

运行结果:

IndexResponse[index=users,type=_doc,id=1,version=2,result=updated,seqNo=1,primaryTerm=1,shards={“total”:2,”successful”:1,”failed”:0}]

这时可以发现,version变更为2,通过kibana查询可得,1号已经有我们设置的数据了,这次即为更新。

2.查询/删除/更新等

参照API
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html

3.测试复杂检索

package com.atguigu.gulimall.search;

import com.alibaba.fastjson.JSON;
import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
import lombok.Data;
import lombok.ToString;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.Avg;
import org.elasticsearch.search.aggregations.metrics.AvgAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.IOException;
import java.util.Map;

@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallSearchApplicationTests {

    @Autowired
    private RestHighLevelClient client;

    /**
     * 测试复杂检索
     */
    @Test
    public void searchData() throws IOException {
        // 1.创建检索请求
        SearchRequest searchRequest = new SearchRequest();
        // 指定索引
        searchRequest.indices("bank");
        // 指定DSL,索引条件
        // SearchSourceBuilder sourceBuilde 封装的条件
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        // 1.1 构造检索条件
//        sourceBuilder.query();
//        sourceBuilder.from();
//        sourceBuilder.size();
//        sourceBuilder.aggregation()
        sourceBuilder.query(QueryBuilders.matchQuery("address","mill"));
        // 1.2 按照年龄的值分布进行聚合
        TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
        sourceBuilder.aggregation(ageAgg);

        // 1.2 计算平均薪资进行聚合
        AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
        sourceBuilder.aggregation(balanceAvg);

        System.out.println("检索条件:"+sourceBuilder.toString());

        searchRequest.source(sourceBuilder);

        // 2.执行检索
        SearchResponse searchResponse = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);

        // 3.分析结果
        System.out.println(searchResponse.toString());
        // 方式1:可以通过json工具,将返回的结果封装到map中,然后从map中获取想要的数据,这种比较麻烦不清晰。
//        Map map = JSON.parseObject(searchResponse.toString(), Map.class);
        // 方式2:使用对应的官方API,推荐使用这种
        // 3.1 获取所有查到的数据
        SearchHits hits = searchResponse.getHits();
        SearchHit[] searchHits = hits.getHits();
        for (SearchHit hit : searchHits) {
//            hit.getIndex();hit.getType();hit.getId();
            String string = hit.getSourceAsString();
            Account account = JSON.parseObject(string, Account.class);// 将source结果的string映射成java bean对象
            System.out.println("account:"+account);
        }
        // 3.2获取这次检索到的分析信息;
        Aggregations aggregations = searchResponse.getAggregations();
        Terms ageAgg1 = aggregations.get("ageAgg");
        for (Terms.Bucket bucket : ageAgg1.getBuckets()) {
            String keyAsString = bucket.getKeyAsString();
            System.out.println("年龄:"+keyAsString+"==>"+bucket.getDocCount());
        }

        Avg balanceAvg1 = aggregations.get("balanceAvg");
        System.out.println("平均薪资:"+balanceAvg1.getValue());
    }

    @ToString
    @Data
    static class  Account {

        private int account_number;
        private int balance;
        private String firstname;
        private String lastname;
        private int age;
        private String gender;
        private String address;
        private String employer;
        private String email;
        private String city;
        private String state;

    }

}