zoukankan      html  css  js  c++  java
  • 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
      
  • 相关阅读:
    poj 3348 Cows
    HDOJ4467 ( 分块 思想 )
    HDOJ 4858 项目管理 ( 只是有点 莫队的分块思想在里面而已啦 )
    luogu P1494 [国家集训队]小Z的袜子 ( 普 通 )
    CREATEINPUTLAYOUT_INCOMPATIBLEFORMAT
    load d3dcompiler_46.dll failed
    HDAO one error
    PS4 Razor GPU
    BLUR
    疑难杂症rendering(对角线上的线)
  • 原文地址:https://www.cnblogs.com/DepthCh/p/13876772.html
Copyright © 2011-2022 走看看