ElasticSearch详细笔记

ElasticSearch详细笔记


什么是ElasticSearch

Elasticsearch(简称ES)是一个基于Apache Lucene(TM)的开源搜索引擎,无论在开源还是专有领域,Lucene 可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。注意,Lucene 只是一个库。想要发挥其强大的作用,你需使用 Java 并要将其集成到你的应用中。

重要特性:

  • 分布式的实时文件存储,每个字段都被索引并可被搜索
  • 实时分析的分布式搜索引擎
  • 可以扩展到上百台服务器,处理PB级结构化或非结构化数据

基本概念&倒排索引

需要了解ElasticSearch中的一些基本概念。

- 索引(indices)
	-- Databases 数据库
	
- 类型(type)
	-- Table 数据表

- 文档(Document)
	-- Row 行

- 字段(Field)
	-- Columns 列

ElasticSearch中的倒排索引

ElasticSearch在插入数据的同时还会为这些数据维护了一张倒排索引表,通过这个倒排索引可以大大的提高搜索的性能

倒排索引(Inverted Index)也叫反向索引,有反向索引必有正向索引。简单来讲,正向索引是通过key找value,反向索引则是通过value找key。

举个例子:

  1. comment 表有 idcontent 两个字段,现在向 comment 表插入如下一条数据:

    id:1
    content:今天天气很好
    

    ElasticSearch 会把 content 的内容进行分词,可以分成三个词:今天天气很好。倒排索引表就如下:

    今天		[1]
    天气		[1]
    很好		[1]
    

    表示 "今天"、"天气"、"很好"这三个词在 1号记录中存在。

  2. 再向 comment 表中插入一条数据:

    id: 2
    content: 今天天气好冷
    

    继续将 content 的内容进行分词,得到:今天天气好冷。将这三个词添加到倒排索引表中

    今天		[1,2]
    天气		[1,2]
    很好		[1]
    好冷		[2]
    

    "今天""天气" 这两个词在 1号2号记录中都存在。

    "很好"1号 记录存在

    "好冷"2号记录存在

  3. 现在查询记录,检索条件:今天好冷

    通过 "今天好冷" 这个字符串进行检索记录,这种就属于通过value查找key

    ElasticSearch 首先将 "今天好冷"进行分词为:"今天""好冷"两个词。

    然后在倒排索引表中查询,发现 "今天" 这个词命中了 1号2号 记录,再看 "好冷"这个词命中了 2号记录。

    这里有个评分机制,2号记录经过对比发现命中 2次1号记录命中 1次。因此 2号记录的评分就比 1号 记录高。查询出来的结果顺序就是:

    id: 2 		content: 今天天气好冷
    id: 1		content: 今天天气很好
    

这就是倒排索引的基本逻辑,通过 value 查找 key。实际上,ElasticSearch引擎创建的倒排索引比这个复杂得多。


安装ElasticSearch&Kibana

Docker安装ElasticSearch

  1. 下载镜像

    docker pull elasticsearch:7.4.2		#存储和检索数据
    
  2. 创建实例需要挂载目录

    mkdir -p /mydata/elasticsearch/config
    mkdir -p /mydata/elasticsearch/data
    echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
    
    chmod -R 777 /mydata/elasticsearch/
    
  3. 创建运行实例

    docker run --name es -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
    

    -e "discovery.type=single-node":单实例模式

    -e ES_JAVA_OPTS="-Xms64m -Xmx512m":设置运行的初始内存和最大内存

    -v:将本地文件映射到容器中对应文件

  4. 浏览器访问主机 9200 端口

    image-20201021155940036


Docker安装Kibana

Kibana 是一个基于 Node.js 的 Elasticsearch 索引库数据统计工具,可以利用 Elasticsearch 的聚合功能,生成各种图表,如柱形图,线状图,饼图等。

而且还提供了操作 Elasticsearch 索引数据的控制台,并且提供了一定的API提示,非常有利于我们学习 Elasticsearch 的语法。

安装步骤:

  1. 下载镜像

    docker pull kibana:7.4.2
    
  2. 查看 ElasticSearch 实例地址

    docker inspect es
    

    image-20201021155709453

    es:运行的 elasticsearch 容器实例名

  3. 创建运行实例

    docker run --name kibana -e ELASTICSEARCH_HOSTS=http://172.17.0.3:9200 -p 5601:5601 
    -d kibana:7.4.2
    

    172.17.0.3:地址填写上一步查询到的地址

  4. 浏览器访问主机 5601 端口

    image-20201021160408049

    注意:kibana启动可能有点慢,需要等待一会


ES基本操作

_cat

elasticsearch 提供 _cat API 来查看ElasticSearch状态

#0. 查看_cat支持的命令
GET /_cat

#1. 查看所有节点
GET /_cat/nodes

#2. 查看es健康状态
GET /_cat/health

#3. 查看主节点
GET /_cat/master

#4. 查看所有索引
GET /_cat/indices

例子:http://192.168.23.6:9200/_cat/indices


新增数据

elasticsearch 中保存的都是 json 格式的数据

现在向 es 添加一条数据 { "msg": "Hello ElasticSearch" }

  1. 访问 kibana,选择 Dev Tools

    image-20201021162052766

  2. 在这个界面操作 es

    image-20201021162155249


  • PUT 方式

    PUT /news/comment/1
    {
     "msg":"Hello ElasticSearch"
    }
    

    执行结果:

    image-20201021162451925

  • POST 方式

    POST /news/comment/1
    {
     "name":"Hello ElasticSearch"
    }
    

    执行结果:

    image-20201021162710889

可以理解为向 news 数据库的 comment 表中添加了一条记录,不过这里叫做索引和类型

分析结果:

_index: 索引,对应就是数据库名
_type: 类型,对应就是数据表
_id: 数据的id
_version: 版本号,通过操作数据版本号会不断增加
_result: created表示创建了一条数据,如果重新put一条数据,则该状态会变为updated,并且版本号也会发生变化。
_shards: 分片信息
_seq_no: 序列号
_primary_term:	
  • PUT可以新增也可以修改。PUT必须指定id;
  • POST添加数据的时候不指定id,会自动的生成id,并且类型是新增
  • 由于PUT需要指定id,我们一般用来做修改操作,不指定id会报错。

查询数据

查看使用GET请求方式检索数据

GET /news/comment/1

image-20201021164233574

可以理解为向 news 数据库的 comment 表中查询一条id1的记录

_source:保存的数据


更新数据

  • POST 方式

    POST /news/comment/1/_update
    {
    	"doc": {
    		"msg": "Hello ES"
    	}
    }
    

    POST /customer/external/1
    {
    	"msg": "Hello ES"
    }
    

    使用 _update 需要加 doc

    区别:

    • 使用 _update修改数据,版本号不会增加
  • PUT 方式

    PUT /news/comment/1
    {
      "msg": "Hello ES"
    }
    

删除数据

删除一条数据

DELETE /news/comment/1

删除一个索引

DELETE /customer

bulk批量API

bulk相当于数据库里的bash操作, 其支持的操作类型包括:index, create, update, delete

  • bulk 语法:

    { action: { metadata } }
    { requstbody }
    

批量新增 index

POST /news/comment/_bulk
{ "index": {"_id": 2} }
{ "msg": "zhangsan" }
{ "index": {"_id": 3} }
{ "msg": "lisi" }
{ "index": {"_id": 4} }
{ "msg": "wangwu" }

执行结果:

image-20201021170543125

  • 参数解析:

    { "index": {"_id": 2} }

    { "msg": "zhangsan" }

    这两行为一次操作,第一行指定了数据的id(还可以指定 indextype;以下划线开头)

    第二行是保存的数据体

  • 结果解析:

    "took": 31:请求执行时间(毫秒)

    "error": false :请求是否出错,返回flase表示没有出错

    "items":操作过的文档的具体信息

    "static":响应状态码


批量新增 create

POST /news/comment/_bulk
{ "create": {"_id": 2} }
{ "msg": "zhangsan" }
{ "create": {"_id": 3} }
{ "msg": "lisi" }
{ "create": {"_id": 4} }
{ "msg": "wangwu" }

执行结果:

image-20201021182310732

新增失败了,因为id重复问题

  • create方式新增,如果id已存在了就会报错
  • index方式新增,如果id存在不会报错,并且 version 增加

批量更新 update

POST /news/comment/_bulk
{ "update": {"_id": 2} }
{ "doc":{"msg": "zhangsan.cn"} }
{ "update": {"_id": 3} }
{ "doc":{"msg": "lisi.cn"} }
{ "update": {"_id": 4} }
{ "doc":{"msg": "wangwu.cn"} }

执行结果:

image-20201021182742765

更新操作需要多加一层 doc

  • { "update": {"_id": 2} }

  • { "doc":{"msg": "zhangsan.cn"} }

​ 第一行update为更新操作,并指定了更新数据的id

​ 第二行 doc里面是更新的新数据。


批量删除 delete

批量删除不需要请求体(数据体)

POST /news/comment/_bulk
{ "delete": {"_id": 2} }
{ "delete": {"_id": 3} }
{ "delete": {"_id": 4} }

执行结果:

image-20201021171911072


进阶检索

学习之前先为es添加一些测试数据,这里使用官方提供的测试数据 https://gitee.com/depthch/elasticsearch/blob/master/doc/test/resourses/accounts.json

  1. 打开上面链接将里面的数据复制

    image-20201021192145259

  2. 在 kibana 中使用 bulk 批量添加数据

    image-20201021192334733

    在索引为 bank,类型 account中批量插入了数据

ES支持两种基本方式检索

  • 通过REST request uri 发送搜索参数 (uri +检索参数)
  • 通过REST request body 来发送它们(uri+请求体)

请求方式:uri + 检索参数

1、检索 bank 下所有信息

GET /bank/_search

image-20201021192706209

响应结果解析:

  • took:Elasticsearch执行搜索的时间(毫秒)
  • time_out:告诉我们搜索是否超时
  • _shards:告诉我们多少个分片被搜索了,以及统计了成功/失败的搜索分片
  • hits:搜索结果
  • hits.total:搜索结果数量
  • hits.hits:实际的搜索结果数组(默认为前10的文档)
  • sort:结果的排序key (键) (没有则按score排序)
  • score和max score:相关性得分和最高得分(全文检索用)

2、检索 bank 下所有信息,并按照 account_number升序

GET /bank/_search?q=*&sort=account_number:asc

image-20201021193508003

  • q=*:* 是通配符,表示查询所有的数据
  • sort=account_number:asc:按照 account_number 排序,asc 是升序

queryDSL

请求方式:uri + 请求体

基本语法

QUERY_NAME:{
   ARGUMENT:VALUE,
   ARGUMENT:VALUE,...
}

1、检索 bank 下所有信息,并按照 account_number降序

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

image-20201021193913924

查询参数解析:

  • match_all查询类型【代表查询所有的所有】,es中可以在query中组合非常多的查询类型完成复杂查询;
  • 除了query参数之外,我们可也传递其他的参数以改变查询结果,如sort,size;
  • from+size限定,完成分页功能;
  • sort排序,多字段排序,会在前序字段相等时后续字段内部排序,否则以前序为准;

_source

_source:返回指定的部分字段

GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0,
  "size": 5,
  "sort": [
    {
      "account_number": {
        "order": "desc"
      }
    }
  ],
  "_source": ["balance","firstname"]
}

_source:指定返回的部分字段


match匹配查询

  • 基本类型(非字符串),精确控制

    GET bank/_search
    {
      "query": {
        "match": {
          "account_number": "20"
        }
      }
    }
    
  • 字符串,全文检索

    GET bank/_search
    {
      "query": {
        "match": {
          "address": "kings"
        }
      }
    }
    

match_phrase

match_phrase: 短句匹配,将需要匹配的值当成一整个单词(不分词)进行检索

GET bank/_search
{
  "query": {
    "match_phrase": {
      "address": "mill road"
    }
  }
}

查出 address 中包含 mill road 的所有记录,并给出相关性得分

match_phrasematch 的区别:

  • match:匹配时会分词,如:mill road,会拆分成:mill、road。然后检索出包含这两个词的记录(包含其中一个词也满足条件)
  • match_phrase:匹配时不会分词,如:mill road,会被当成一个整体来检索记录,必须包含整个整体的记录才会被检索出来

match.keyword

keyword 是精确匹配,就是说某条记录必须完全满足匹配条件才会被检索出来

GET bank/_search
{
  "query": {
    "match": {
      "address.keyword": "990 Mill Road"
    }
  }
}

multi_math

multi_math:多字段匹配可以在多个字段中去匹配条件

GET bank/_search
{
  "query": {
    "multi_match": {
      "query": "mill",
      "fields": [
        "state",
        "address"
      ]
    }
  }
}

state 或者 address 中包含 mill,并且在查询过程中,会对于查询条件进行分词。


bool

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


must

must:必须达到must所列举的所有条件

GET bank/_search
{
   "query":{
        "bool":{
             "must":[
              {"match":{"address":"mill"}},
              {"match":{"gender":"M"}}
             ]
         }
    }
}

匹配 genderM并且 address包含 mill的文档


must_not

must_not,必须不匹配must_not所列举的所有条件。

GET bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "gender": "M"
          }
        },
        {
          "match": {
            "address": "mill"
          }
        }
      ],
      "must_not": [
        {
          "match": {
            "age": "38"
          }
        }
      ]
    }
  }

匹配 genderM并且 address包含 mill的文档,但是 age 不等于38的数据


should

should:应该达到should列举的条件,如果到达会增加相关文档的评分,并不会改变查询的结果。

如果query中只有should且只有一种匹配规则,那么should的条件就会被作为默认匹配条件二区改变查询结果。

GET bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "gender": "M"
          }
        },
        {
          "match": {
            "address": "mill"
          }
        }
      ],
      "must_not": [
        {
          "match": {
            "age": "18"
          }
        }
      ],
      "should": [
        {
          "match": {
            "lastname": "Wallace"
          }
        }
      ]
    }
  }
}

should"应该包含"的意思,不是必须包含,也就是除去其他匹配条件即使 lastName不包含 Wallace也能匹配成功。但是如果有数据的 lastName 包含 Wallace ,那么这条数据的相关性得分会更高,即优先匹配。


Filter

并不是所有的查询都需要产生分数,特别是哪些仅用于filtering过滤的文档。为了不计算分数,elasticsearch会自动检查场景并且优化查询的执行。

GET bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "address": "mill"
          }
        }
      ],
      "filter": {
        "range": {
          "balance": {
            "gte": "10000",
            "lte": "20000"
          }
        }
      }
    }
  }
}

这里先是查询所有匹配 address 包含 mill 的文档,然后再根据 10000<=balance<=20000进行过滤查询结果

filter在使用过程中,并不会计算相关性得分,即 "_score" : 0.0


term

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

  • 全文检索字段用 match

  • 非text字段匹配用term

GET bank/_search
{
  "query": {
    "term": {
      "address": "mill Road"
    }
  }
}

使用 term 匹配 text类型数据是匹配不到任何数据的。


Aggregation

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

聚合语法:

"aggs":{
    "aggs_name这次聚合的名字,方便展示在结果集中":{
        "AGG_TYPE聚合的类型(avg,terms....)":{}
     }
}

常用聚合类型:

  • avg:求平均值
  • max:求最大值
  • min:求最小值
  • sum:求和
  • filter:过滤聚合。基于一个条件,来对当前的文档进行过滤的聚合。
  • terms:词聚合。基于某个field,该 field 内的每一个【唯一词元】为一个桶,并计算每个桶内文档个数。默认返回顺序是按照文档个数多少排序。

搜索address中包含mill的所有人的年龄分布以及平均年龄

GET bank/_search
{
  "query": {
    "match": {
      "address": "Mill"
    }
  },
  "aggs": {
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 10
      }
    },
    "ageAvg": {
      "avg": {
        "field": "age"
      }
    }
  }
}

查出所有年龄分布,并且求这些年龄段的这些人的平均薪资

GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 100
      },
      "aggs": {
        "ageAvg": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  }
}

聚合是可以嵌套聚合的。


查出所有年龄分布,并且这些年龄段中M的平均薪资和F的平均薪资以及这个年龄段的总体平均薪资

GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 100
      },
      "aggs": {
        "genderAgg": {
          "terms": {
            "field": "gender.keyword"
          },
          "aggs": {
            "balanceAvg": {
              "avg": {
                "field": "balance"
              }
            }
          }
        },
        "ageBalanceAvg": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  }
}

先按年龄聚合查出了所有分布情况,再嵌套聚合 按性别聚合查出分布情况,最后再嵌套聚合 按薪资聚合查出平均薪资。

ageBalanceAvg:根据年龄分布计算出平均工资,这个聚合跟性别无关。


mapping

maping是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和索引的。比如:使用maping来定义:

  • 哪些字符串属性应该被看做全文本属性(full text fields);
  • 哪些属性包含数字,日期或地理位置;
  • 文档中的所有属性是否都能被索引(all 配置);
  • 日期的格式;
  • 自定义映射规则来执行动态添加属性;

查看 bank 索引的 mapping 映射信息

GET bank/_mapping

执行结果:

image-20201022093044246

properties 中可以看到每个 field 的字段类型


新版本的改变

ElasticSearch7 去掉了type(表)概念

  1. 关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用,但ES中不是这样的。elasticsearch是基于Lucene开发的搜索引擎,而ES中不同type下名称相同的filed最终在Lucene中的处理方式是一样的。
    • 两个不同type(表)下的两个名称 user_name(字段),在ES同一个索引下其实被认为是同一个filed,你必须在两个不同的type(表)中定义相同的 filed 映射。否则,不同 type(表)中的相同字段名称就会在处理中出现冲突的情况,导致Lucene处理效率下降。
    • 去掉type(表)就是为了提高ES处理数据的效率。
  2. Elasticsearch 7.x URL中的type参数为可选。比如,索引一个文档不再要求提供文档类型。
  3. Elasticsearch 8.x 不再支持URL中的type参数。

创建映射

PUT /student
{
  "mappings": {
    "properties": {
      "age": {
        "type": "integer"
      },
      "email": {
        "type": "keyword"
      },
      "name": {
        "type": "text"
      }
    }
  }
}

执行结果:

image-20201022093735268

创建 student 索引并指定了索引的 mapping 映射信息。

properties:指定映射的字段和字段类型


查看映射

GET /student

执行结果:

image-20201022094212471


添加新的字段映射

student索引添加一个新的字段映射

PUT /student/_mapping
{
  "properties": {
    "id": {
      "type": "keyword",
      "index": false
    }
  }
}

执行结果:

image-20201022094407963

再次查看映射:
image-20201022094455600

"index": false:表明新增的字段不能被检索,只是一个冗余字段。


数据迁移

由于ElasticSearch是不支持修改映射字段的,只能添加映射字段。如果必须修改就需要数据迁移。

需求:将 bank 索引的所有数据迁移到 newbank 索引下,并将 age 字段类型改为 integer。修改 city、email、employer、gender等字段类型改为 keyword


具体步骤:

  1. 查看 bank 的映射信息

    GET /bank
    

    image-20201022100337166

  2. 创建一个跟 bank 索引字段相同 mapping 映射的索引,并且改变字段类型

    PUT /newbank
    {
      "mappings": {
        "properties": {
          "account_number": {
            "type": "long"
          },
          "address": {
            "type": "text"
          },
          "age": {
            "type": "integer"
          },
          "balance": {
            "type": "long"
          },
          "city": {
            "type": "keyword"
          },
          "email": {
            "type": "keyword"
          },
          "employer": {
            "type": "keyword"
          },
          "firstname": {
            "type": "text"
          },
          "gender": {
            "type": "keyword"
          },
          "lastname": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "state": {
            "type": "keyword"
          }
        }
      }
    }
    

    在指定映射信息时改变了字段的类型。

  3. 可以查看到 newbank 索引的映射信息

    GET /newbank
    

    执行结果:
    image-20201022100846045

    字段类型都已经改变。

  4. bank 索引中的数据迁移到 newbank 索引中

    POST _reindex
    {
      "source": {
        "index": "bank",
        "type": "account"
      },
      "dest": {
        "index": "newbank"
      }
    }
    

    执行结果:

    image-20201022100929894

    source:指定旧索引信息

    • index:指定旧的索引名
    • type:指定 type,如果没有 type 可以不指定。

    dest:指定新索引信息

    • index:指定新的索引名
  5. 查看 newbank 中的数据

    GET /newbank/_search
    

    执行结果:

    image-20201022101505363

    检索了1000条数据,数据迁移成功。发现 type: _doc,我们在创建映射关系时并没有设置 type,这是因为ElasticSearch7 去掉了type(表)概念,但是有个默认的 type 就是 _doc


分词

一个 tokenizer(分词器)接收一个字符流,将之分割为独立的 tokens(词元,通常是独立的单词),然后输出tokens 流。例如:hello world 遇到空白字符时分割文本。它会将文本 "hello world" 分割为 [hello, world]

tokenizer(分词器)还负责记录各个terms(词条)的顺序或position位置(用于phrase短语和word proximity词近邻查询),以及term(词条)所代表的原始word(单词)start(起始)end(结束)character offsets(字符串偏移量) (用于高亮显示搜索的内容)

elasticsearch 提供了很多内置的分词器,可以用来构建 custom analyzers(自定义分词器)


使用分词器

POST _analyze
{
  "analyzer": "standard",
  "text": "Nothing is impossible!"
}

执行结果:

image-20201022123521832

standard:ElasticSearch默认的分词器,默认就是按空格进行分词的


安装 ik 分词器

所有的语言分词,默认使用的都是“Standard Analyzer”,但是这些分词器针对于中文的分词,并不友好。为此需要安装中文的分词器。

具体步骤:

  1. 查看 ElasticSearch 的版本

    image-20201022125054594

    访问es主机的 9200 端口查看es版本

  2. 下载对应版本的 ik 分词器

    在前面安装的elasticsearch时,我们已经将elasticsearch容器的“/usr/share/elasticsearch/plugins”目录,映射到本地主机的“ /mydata/elasticsearch/plugins”目录下.

    2.1)进入 /mydata/elasticsearch/plugins目录

    cd /mydata/elasticsearch/plugins
    

    2.2)下载 ik 分词器

    wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
    

    注意 7.4.2是自己es对应的版本。

  3. 解压下载好的文件

    unzip elasticsearch-analysis-ik-7.4.2.zip -d ik
    
  4. 重启es

    docker restart es
    
  5. 查看安装好的ik

    GET _cat/plugins
    

    执行结果:

    image-20201022130421168

    可以看到我们的ik分词器已经配置成功了


使用ik分词器

ik分词器有两种分词模式:ik_max_wordik_smart 模式。

  • ik_max_word

    会将文本做最细粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国、中华人民、中华、华人、人民共和国、人民、共和国、大会堂、大会、会堂等词语。

  • ik_smart

    会做最粗粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为中华人民共和国、人民大会堂。


先来看看默认的分词器

GET _analyze
{
   "text":"每天都要努力"
}

执行结果:

image-20201022135515279



ik 分词器

GET _analyze
{
   "analyzer": "ik_smart", 
   "text":"每天都要努力"
}

执行结果:

image-20201022135630622

可以看到使用 ik 分词器可以把一些常用的中文词分出来了。


自定义词库

虽然使用 ik 分词器默认的词库已经可以实现常用的中文分词了,但是如果我们要分的词不常用,如:张明想学Java

GET _analyze
{
  "analyzer": "ik_smart",
   "text":"张明想学Java"
}

执行结果:

image-20201022140850506

可以看到这里把 "张明" 拆成了 "张""明",这并我是预想的效果,"张明" 应该拆成整体。

使用自定义词库,因为要使用 "远程扩展字典",因此就需要一个远程的字典文件。这里可以使用 nginx来配置远程扩展字典文件。文档最后有nginx安装和配置步骤


具体步骤:

  1. 配置好 nginx 后,创建词库文件

    vi /mydata/nginx/html/fenci.txt
    

    添加如下内容并保存:

    张明
    学java
    

    image-20201022143225338

  2. 修改es-plugins配置文件

    vi /mydata/elasticsearch/plugins/ik/config/IKAnalyzer.cfg.xml
    
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
    <properties>
            <comment>IK Analyzer 扩展配置</comment>
            <!--用户可以在这里配置自己的扩展字典 -->
            <entry key="ext_dict"></entry>
             <!--用户可以在这里配置自己的扩展停止词字典-->
            <entry key="ext_stopwords"></entry>
            <!--用户可以在这里配置远程扩展字典 -->
            <entry key="remote_ext_dict">http://192.168.16.6/fenci.txt</entry>
            <!--用户可以在这里配置远程扩展停止词字典-->
            <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
    </properties>
    

    注意:修改第10行需要改成 nginx 服务的地址(这行默认是注释的)。

  3. 修改了配置文件需要重启 es

    docker restart es
    

  1. 再次使用 ik 分词器

    GET /_analyze
    {
      "analyzer": "ik_max_word",
       "text":"张明想学Java"
    }
    

//TODO 执行结果

......



SpringBoot 整合 ElasticSearch

SpringBoot可以通过 92009300端口来调用 ElasticSearch,它们之间的区别:

  • 9300:TCP

    SpringBoot提供了 spring-data-elasticsearch:transport-api.jar;来对 ES调用。这种方式有些缺陷:

    • springboot版本不同,transport-api.jar不同,不能适配es版本
    • es7.x已经不建议使用,es8以后就要废弃
  • 9200:HTTP

    jestClient:非官方,更新慢;

    RestTemplate:模拟HTTP请求,ES很多操作需要自己封装,麻烦;

    HttpClient:模拟HTTP请求,ES很多操作需要自己封装,麻烦;


    Elasticsearch-Rest-Client:官方RestClient,封装了ES操作,API层次分明,上手简单;

根据上面分析,我们最终选择 Elasticsearch-Rest-Client 来进行调用es


具体步骤:

  1. 添加依赖

    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
        <version>7.4.2</version>
    </dependency>
    
    <dependency>
        <groupId>org.elasticsearch</groupId>
        <artifactId>elasticsearch</artifactId>
        <version>7.4.2</version>
    </dependency>
    
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-client</artifactId>
        <version>7.4.2</version>
    </dependency>
    
  2. 创建一个 ElasticSearch的配置类

    @Configuration
    public class ElasticSearchConfig {
        public static final RequestOptions COMMON_OPTIONS;
        static {
            RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
            COMMON_OPTIONS = builder.build();
        }
    
        @Bean
        public RestHighLevelClient esRestClient(){
            RestHighLevelClient client = new RestHighLevelClient(
                    RestClient.builder(new HttpHost("192.168.16.6", 9200, "http")));
            return client;
        }
    }
    

    HttpHost("192.168.16.6", 9200, "http")

    • 192.168.16.6:es服务的地址
    • 9200:es服务9200端口
    • http:使用http协议

    配置类中创建了一个 JavaBen,之后通过这个 JavaBean 来调用 ElasticSearch 的相关 API


保存数据

@RunWith(SpringRunner.class)
@SpringBootTest
public class MallSearchApplicationTests {
    @Autowired
    RestHighLevelClient client;

    /**
     * 测试保存数据
     */
    @Test
    public void contextLoads() throws IOException {
        IndexRequest request = new IndexRequest("users");  //创建索引对象
        request.id("10");  //设置id
        //source方法可以直接传入多个键值对值保存
		//request.source("name", "lisi", "age", 24, "gender", "男");  

        User user = new User();   //创建一个实体类user
        user.setName("java");
        user.setAge(24);
        user.setGender("男");
        
        String jsonString = JSON.toJSONString(user);  //解析实体转成json字符串

        request.source(jsonString, XContentType.JSON);  //传入json格式字符串保存
        IndexResponse response = client.index(request, ElasticSearchConfig.COMMON_OPTIONS);
        
        System.out.println(response);	//打印结果
    }
	
    /**
   	 * 定义 user 实体类
   	 */
    @Data
    class User{
        private String name;
        private int age;
        private String gender;
 }
}

执行结果:

image-20201022151529191

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

kibana查看:

image-20201022151648229

数据测试添加成功。


检索数据

搜索address中包含mill的所有人的年龄分布以及平均年龄

QueryDSL 实现:

GET bank/_search
{
  "query": {
    "match": {
      "address": "Mill"
    }
  },
  "aggs": {
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 10
      }
    },
    "ageAvg": {
      "avg": {
        "field": "age"
      }
    }
  }
}

执行结果:image-20201022153055301


java代码实现:

首先需要生成实体类,因为从es获取的数据在java中最终都会保存为java对象。

image-20201022154412377

需要根据 _source 中的字段生成 java 类 account

测试类代码:

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

    @Autowired
    RestHighLevelClient client;

    /**
     * 按照 bank 索引里的 _source 数据字段创建对应的实体类
     */
    @Data
    @ToString
    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;
    }
    
    /**
     * 检索数据
     */
    @Test
    public void searchData() throws IOException {
        //1、创建检索对象
            SearchRequest searchRequest = new SearchRequest();
            //指定索引
            searchRequest.indices("bank");
            //指定DSL&检索条件
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

            //构建检索条件
            //sourceBuilder.query();
            //sourceBuilder.from();
            //sourceBuilder.size();
            //sourceBuilder.aggregation();
            sourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));

            //构建聚合条件: 按照年龄值分布进行聚合
            TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
            sourceBuilder.aggregation(ageAgg);

            //构建聚合条件: 计算平均工资
            AvgAggregationBuilder ageAvgAgg = AggregationBuilders.avg("ageAvgAgg").field("age");
            sourceBuilder.aggregation(ageAvgAgg);

            searchRequest.source(sourceBuilder);

            SearchResponse searchResponse = client.search(searchRequest, ElasticSearchConfig.COMMON_OPTIONS);

            //结果分析
            SearchHits hits = searchResponse.getHits();
            SearchHit[] hitsHits = hits.getHits();
            for(SearchHit hit : hitsHits){
                String sourceAsString = hit.getSourceAsString();
                
                //将结果转成 javeBean
                Account account = JSON.parseObject(sourceAsString, Account.class);
                System.out.println("account:" + account);
            }

            //获取检索到的聚合信息
            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 balanceAgg1 = aggregations.get("ageAvgAgg");
            System.out.println("平均年龄:" + balanceAgg1.getValueAsString());
    }
}

执行结果:

image-20201022153827676

这里执行结果跟queryDSL查询是相同的。


附:Docker 安装 Nginx

  1. 随便启动一个nginx实例,只是为了复制出配置

    docker run -p 80:80 --name nginx -d nginx:1.10
    
  2. 创建目录并且将容器内部配置文件拷贝到外部

    mkdir -p /mydata/nginx/html
    mkdir -p /mydata/nginx/logs
    mkdir -p /mydata/nginx/conf
    docker container cp nginx:/etc/nginx /mydata/nginx/conf/ 
    #由于拷贝完成后会在conf中存在一个nginx文件夹,所以需要将它的内容移动到conf中
    mv /mydata/nginx/conf/nginx/* /mydata/nginx/conf/
    rm -rf /mydata/nginx/conf/nginx
    
  3. 终止容器&删除原来的容器

    docker stop nginx
    docker rm nginx
    
  4. 创建新的 nginx 容器

    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
    
原文地址:https://www.cnblogs.com/DepthCh/p/13876772.html