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
      
  • 相关阅读:
    Validation failed for one or more entities. See 'EntityValidationErrors' property for more details
    Visual Studio断点调试, 无法监视变量, 提示无法计算表达式
    ASP.NET MVC中MaxLength特性设置无效
    项目从.NET 4.5迁移到.NET 4.0遇到的问题
    发布网站时应该把debug设置false
    什么时候用var关键字
    扩展方法略好于帮助方法
    在基类构造器中调用虚方法需谨慎
    ASP.NET MVC中商品模块小样
    ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现
  • 原文地址:https://www.cnblogs.com/DepthCh/p/13876772.html
Copyright © 2011-2022 走看看