谷粒商城高级篇-Elasticsearch

一、Elasticsearch

1、Elasticsearch简介

全文搜索属于最常见的需求,开源的 Elasticsearch 是目前全文搜索引擎的首选。它可以快速地储存、搜索和分析海量数据。维基百科、Stack Overflow、Github 都采用它

文档:

官方文档:Elasticsearch Guide [8.15] | Elastic

官方中文:序言 | Elasticsearch: 权威指南 | Elastic

社区中文:

https://es.xiaoleilu.com/index.html
http://doc.codingdict.com/elasticsearch/0/

(其中中文文档都是比较旧的版本)

2、基本概念

1、Index(索引):

动词,相当于 MySQL 中的 insert;
名词,相当于 MySQL 中的 Database

2、Type(类型):

在 Index(索引)中,可以定义一个或多个类型。

类似于 MySQL 中的 Table;每一种类型的数据放在一起

3、Document(文档)

保存在某个索引(Index)下,某种类型(Type)的一个数据(Document),文档是 JSON 格式的,Document 就像是 MySQL 中的某个 Table 里面的内容

4、为什么ES搜索快? 倒排索引

对每条要存储的文档进行分词,然后对每个词和所在的记录进行存储。在搜索时,先搜索含有其关键字的记录,然后对所记录计算相关性得分,得出最终的搜索结果

3、Docker安装Elasticsearch

1、下载镜像文件

docker pull elasticsearch:7.4.2  存储和检索数据
docker pull kibana: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/ 保证权限



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.6.2

以后再外面装好插件重启即可;

特别注意:
-e ES_JAVA_OPTS="-Xms64m -Xmx256m" \ 测试环境下,设置 ES 的初始内存和最大内存,否则导致过大启动不了 ES

3、访问测试

4、安装Kibana

docker run --name kibana -e ELASTICSEARCH_HOSTS=http://62.234.37.187:9200 -p 5601:5601 \
-d kibana:7.6.2

62.234.37.187:9200 一定改为自己虚拟机的地址

5、访问测试62.234.37.187:5601

4、初步检索

1、_cat

GET /_cat/nodes:查看所有节点
GET /_cat/health:查看 es 健康状况
GET /_cat/master:查看主节点
GET /_cat/indices:查看所有索引 show databases;

2、索引一个文档(保存)

保存一个数据,保存在哪个索引的哪个类型下,指定用哪个唯一标识PUT customer/external/1;在 customer 索引下的 external 类型下保存 1 号数据为

# # 在customer索引下的external类型下保存1号数据
PUT customer/external/1

httpclinet

PUT http://59.110.106.16:9200/customer/external/1
Content-Type: application/json

{
  "name": "John Doe"
}

返回数据

返回数据:
带有下划线开头的,称为元数据,反映了当前的基本信息。
{
  "_index": "customer", 表明该数据在哪个数据库下;
    "_type": "external", 表明该数据在哪个类型下;
    "_id": "1",  表明被保存数据的id;
    "_version": 1,  被保存数据的版本
    "result": "created", 这里是创建了一条数据,如果重新put一条数据,则该状态会变为updated,并且版本号也会发生变化。
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 1,
  "_primary_term": 1
}

PUT 和 POST 都可以,
POST 新增。如果不指定 id,会自动生成 id, 新增这个数据。指定 id 就会修改这个数据,并新增版本号
PUT 可以新增可以修改。PUT 必须指定 id;由于 PUT 需要指定 id,我们一般都用来做修改操作,不指定 id 会报错。

3、查询文档

GET customer/external/1

httpclient

### GET request with a header
GET http://59.110.106.16:9200/customer/external/1
Accept: application/json

返回数据:

{
  "_index": "customer",
  "_type": "external",
  "_id": "1",
  "_version": 2,
  "_seq_no": 1, //并发控制字段,每次更新都会+1,用来做乐观锁
  "_primary_term": 1,//同上,主分片重新分配,如重启,就会变化
  "found": true,
  "_source": { //存储的信息
    "name": "John Doe"
  }
}

乐观锁用法:通过“if_seq_no=1&if_primary_term=1”,当序列号匹配的时候,才进行修改,否则不修改。

示例:

当seq_no=1&primary_term=1时,就修改

###
PUT http://59.110.106.16:9200/customer/external/1?if_seq_no=1&if_primary_term=1
Content-Type: application/json

{
  "name": "John Doe"
}

修改成功,修改之后seq_no变成2

再次发送上面的请求,修改失败,乐观锁是生效

4、更新文档

POST customer/external/1/_update
{
   "doc":{
       "name": "John Doew"
   }
}

或者
POST customer/external/1
{

   "name": "John Doe2"
}

或者
PUT customer/external/1
{
   "name": "John Doe"
}
  • 注意带不带_update的语法不同
  • 带_update会跟原数据对比,跟原来一样就什么都不做,version不增加。 应用场景:对于大并发查询,偶尔更新,带update;对比更新呢,重新计算分配规则
  • post不带_update,不会检查原数据,直接更新。 应用场景:对于大并发更新,不带update

发送之后,与元素据对比,发现相同,不做任何操作

POST http://59.110.106.16:9200/customer/external/1/_update
Content-Type: application/json

{
  "doc":{
    "name": "John"
  }
}

5、删除文档&索引

DELETE customer/external/1
DELETE customer

注:elasticsearch并没有提供删除类型的操作,只提供了删除索引和文档的操作。

删除id=1的数据

DELETE http://59.110.106.16:9200/customer/external/1

删除customer索引

DELETE http://59.110.106.16:9200/customer
响应
{
    "acknowledged": true
}

6、ES的批量操作——bulk

用Kibana之中的Dev Tools

示例1:

POST customer/external/_bulk

{"index":{"_id":"1"}}
{"name": "John Doe" }

{"index":{"_id":"2"}}
{"name": "Jane Doe" }

语法格式: 两行一组,第一行为操作,第二行为数据

{ action: { metadata }}\n
{ request body}\n

{ action: { metadata }}\n
{ request body}\n

这里的批量操作,当发生某一条执行发生失败时,其他的数据仍然能够接着执行,也就是说彼此之间是独立的。

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

返回数据

#! Deprecation: [types removal] Specifying types in bulk requests is deprecated.
{
  "took" : 318,  花费了多少ms
  "errors" : false, 没有发生任何错误
  "items" : [ 每个数据的结果
    {
      "index" : { 保存
        "_index" : "customer", 索引
        "_type" : "external", 类型
        "_id" : "1", 文档
        "_version" : 1, 版本
        "result" : "created", 创建
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201 新建完成
      }
    },
    {
      "index" : { 第二条记录
        "_index" : "customer",
        "_type" : "external",
        "_id" : "2",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 1,
        "_primary_term" : 1,
        "status" : 201
      }
    }
  ]
}

示例2:

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"}}

7、样本测试数据

一份顾客银行账户信息的虚构的 JSON 文档样本。每个文档都有下列的 schema(模式):

{
"account_number": 0,
"balance": 16623,
"firstname": "Bradshaw",
"lastname": "Mckenzie",
"age": 29,
"gender": "F",
"address": "244 Columbus Place",
"employer": "Euron",
"email": "bradshawmckenzie@euron.com",
"city": "Hobucken",
"state": "CO"
}

测试数据地址es测试数据.json · 坐看云起时/common_content - Gitee.com

POST bank/account/_bulk

//上面的数据

//GET _cat/indices, 查看索引
//刚导入了1000条数据
yellow open bank                     dno5JY9tTrGHdsjIMkvQyA 1 1 1000 0 414.2kb 414.2kb

5、进阶检索

1、SearchAPI

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

  • 一个是通过使用 REST request URI 发送搜索参数(uri+检索参数)
  • 另一个是通过使用 REST request body 来发送它们(uri+请求体)

1)、信息检索

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

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

返回内容:

took – 花费多少ms搜索
timed_out – 是否超时
_shards – 多少分片被搜索了,以及多少成功/失败的搜索分片
max_score –文档相关性最高得分
hits.total.value - 多少匹配文档被找到
hits.sort - 结果的排序key(列),没有的话按照score排序
hits._score - 相关得分 (not applicable when using match_all)

uri+请求体行检索

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

HTTP客户端工具(POSTMAN),get请求不能携带请求体,我们变为post也是一样的。我们POST一个JSON风格的查询请求体到_search API。

需要了解,一旦搜索的结果被返回Elasticsearch就完成了这次请求,并且不会维护任何服务端的资源或者结果的cursor(游标)

2、Query DSL

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

1、基本语法格式

如果针对于某个字段,那么它的结构如下:
{
  QUERY_NAME:{   # 使用的功能
     FIELD_NAME:{  #  功能参数
       ARGUMENT:VALUE,
       ARGUMENT:VALUE,...
      }   
   }
}
示例  使用时不要加#注释内容
GET bank/_search
{
  "query": {  #  查询的字段
    "match_all": {}
  },
  "from": 0,  # 从第几条文档开始查
  "size": 5,  # 查几条文档
  "_source":["balance"], # 返回的数据字段, 这里只查balance
  "sort": [
    {
      "account_number": {  # 返回结果按哪个列排序
        "order": "desc"  # 降序
      }
    }
  ]
}

query定义如何查询;

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

2、query/match匹配查询

如果是非字符串,会进行精确匹配。如果是字符串,会进行全文检索

  • 基本类型(非字符串),精确控制: 查询account_number == 20的文档
GET bank/_search
{
  "query": {
    "match": {
      "account_number": "20"
    }
  }
}
  • 字符串,全文检索: 查询address中含有kings的文档
GET bank/_search
{
  "query": {
    "match": {
      "address": "kings"
    }
  }
}
  • 字符串,全文检索:最终查询出 address 中包含 mill 或者 road 或者 mill road 的所有记录,并给出相关性得分
GET bank/_search
{
   "query": {
      "match": {
         "address": "mill road"
      }
   }
}

全文检索,最终会按照评分进行排序,会对检索条件进行分词匹配。

3、query/match_phrase【短语匹配】

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

  • match:拆分字符串进行检索。 包含mill 或 road 或 mill road
  • match_phrase:不拆分字符串进行检索。 包含mill road(模糊)
  • 字段.keyword:必须全匹配上才检索成功。(精确)
GET bank/_search
{
  "query": {
    "match_phrase": {
      "address": "mill road"   #  就是说不要匹配只有mill或只有road的,要匹配mill road一整个子串
      
      # "address.keyword": "990 Mill"  # 字段后面加上 .keyword, 必须完全匹配
      
    }
  }
}

4、query/multi_math【多字段匹配】

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

GET bank/_search
{
  "query": {
    "multi_match": {  # 前面的match仅指定了一个字段。
      "query": "mill",
      "fields": [ # state和address有mill子串  不要求都有
        "state",
        "address"
      ]
    }
  }
}

5、query/bool/must复合查询

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

  • must:必须达到must所列举的所有条件
  • must_not:必须不匹配must_not所列举的所有条件。
  • should:应该满足should所列举的条件。满足条件最好,不满足也可以,满足得分更高
GET bank/_search
{
  "query": {
    "bool": {
      "must": [   # gender必须是M, address必须包含mill
        {
          "match": {
            "gender": "M"
          }
        },
        {
          "match": {
            "address": "mill"
          }
        }
      ],
      "must_not": [   # age必须不等于18
        {
          "match": {
            "age": "18"
          }
        }
      ],
      "should": [    # lastname最好包含wallace
        {
          "match": {
            "lastname": "Wallace"
          }
        }
      ]
    }
  }
}

6、query/filter【结果过滤】

  • 上面的must和should影响相关性得分,而must_not仅仅是一个filter ,不贡献得分
  • must改为filter就使must不贡献得分
  • 如果只有filter条件的话,我们会发现得分都是0

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

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

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

7、query/term

和 match 一样。匹配某个属性的值。全文检索字段用 match,其他非 text 字段匹配用 term(精确的数值)。

比如:年龄为23岁,用term, address为mill road就用match

GET bank/_search
{
   "query": {
      "bool": {
         "must": [
            {"term": {
               "age": {
                  "value": "28"
               }
            }},
            {"match": {
               "address": "990 Mill Road"
            }}
         ]
      }
   }
}

8、aggregations(执行聚合)

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

例:搜索 address 中包含 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  # 不看详情
}

查询结果:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4, // 命中4条
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "ageAgg" : { // 第一个聚合的结果
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : 38,  # age为38的有2条
          "doc_count" : 2
        },
        {
          "key" : 28,
          "doc_count" : 1
        },
        {
          "key" : 32,
          "doc_count" : 1
        }
      ]
    },
    "ageAvg" : { // 第二个聚合的结果
      "value" : 34.0  # balance字段的平均值是34
    },
    "balanceAvg" : {
      "value" : 25208.0
    }
  }
}

例:按照年龄聚合,并且求这些年龄段的这些人的平均薪资

aggs/aggName/aggs/aggName子聚合

写到一个聚合里是基于上个聚合进行子聚合。

下面求每个age分布的平均balance

GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "ageAgg": {
      "terms": { # 看分布
        "field": "age",
        "size": 100
      },
      "aggs": { # 与terms并列
        "ageAvg": { #平均
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  },
  "size": 0
}

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

GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "ageAgg": {
      "terms": {  #  看age分布
        "field": "age",
        "size": 100
      },
      "aggs": { # 子聚合
        "genderAgg": {
          "terms": { # 看gender分布
            "field": "gender.keyword" # 注意这里,文本字段应该用.keyword
          },
          "aggs": { # 子聚合
            "balanceAvg": {
              "avg": { # 男性的平均
                "field": "balance"
              }
            }
          }
        },
        "ageBalanceAvg": {
          "avg": { #age分布的平均(男女)
            "field": "balance"
          }
        }
      }
    }
  },
  "size": 0
}

3、Mapping字段映射

1、字段类型

  • text ⽤于全⽂索引,搜索时会自动使用分词器进⾏分词再匹配
  • keyword 不分词,搜索时需要匹配完整的值

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

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

3、新版本改变

ElasticSearch7-去掉type概念

  • 关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用,但ES中不是这样的。elasticsearch是基于Lucene开发的搜索引擎,而ES中不同type下名称相同的filed最终在Lucene中的处理方式是一样的。
    • 两个不同type下的两个user_name,在ES同一个索引下其实被认为是同一个filed,你必须在两个不同的type中定义相同的filed映射。否则,不同type中的相同字段名称就会在处理中出现冲突的情况,导致Lucene处理效率下降。
    • 去掉type就是为了提高ES处理数据的效率。

Elasticsearch 7.x URL中的type参数为可选。比如,索引一个文档不再要求提供文档类型。

Elasticsearch 8.x 不再支持URL中的type参数。

解决:
1)将索引从多类型迁移到单类型,每种类型文档一个独立索引

2)将已存在的索引下的类型数据,全部迁移到指定位置即可。详见数据迁移

4、对映射的操作

1)创建索引并指定映射

PUT /my_index
{
  "mappings": {
    "properties": {
      "age": {
        "type": "integer"
      },
      "email": {
        "type": "keyword" # 指定为keyword
      },
      "name": {
        "type": "text" # 全文检索。保存时候分词,检索时候进行分词匹配
      }
    }
  }
}

2)添加新的字段映射

PUT /my_index/_mapping
{
  "properties": {
    "employee-id": {
      "type": "keyword",
      "index": false # 字段不能被检索。检索
    }
  }
}

3)更新映射

对于已经存在的映射字段,我们不能更新。更新必须创建新的索引进行数据迁移

4)数据迁移

先创建new_twitter的正确映射,然后使用如下方式进行数据迁移。

6.0以后写法
POST reindex
{
  "source":{
      "index":"twitter"
   },
  "dest":{
      "index":"new_twitters"
   }
}


老版本写法
POST reindex
{
  "source":{
      "index":"twitter",
      "twitter":"twitter"
   },
  "dest":{
      "index":"new_twitters"
   }
}

示例:把bank/account下的文档迁移到newbank下

  • 创建newbank索引
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"
      }
    }
  }
}
  • 数据迁移
POST _reindex
{
  "source": {
    "index": "bank",
    "type": "account"
  },
  "dest": {
    "index": "newbank"
  }
}
  • 查看newbank中的数据
GET /newbank/_search

输出
  "hits" : {
    "total" : {
      "value" : 1000,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "newbank",
        "_type" : "_doc", # 没有了类型

4、分词

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

示例:

POST _analyze
{
  "analyzer": "standard",
  "text": "Quick brown fox!"
}

分词结果:

1、安装ik分词器

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

下载地址:Releases · infinilabs/analysis-ik · GitHub

在前面安装的elasticsearch时,我们已经将elasticsearch容器的“/usr/share/elasticsearch/plugins”目录,映射到宿主机的“ /mydata/elasticsearch/plugins”目录下,所以比较方便的做法就是下载“/elasticsearch-analysis-ik-7.4.2.zip”文件,然后解压到该文件夹下即可。安装完毕后,需要重启elasticsearch容器。

2、测试分词器

1、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
    }
  ]
}

2、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、自定义词库

先安装nginx

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

docker run -p 80:80 --name nginx -d nginx:1.10

2)将容器内的配置文件拷贝到当前目录

docker container cp nginx:/etc/nginx .

3)修改文件名称:mv nginx conf 把这个 conf 移动到/mydata/nginx 下

4)终止原容器:docker stop nginx

5)执行命令删除原容器:docker rm $ContainerId

6)创建新的 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

7)给 nginx 的 html 下面放的所有资源可以直接访问;

8)在nginx/html/中创建一个es文件夹,在里面新建文件fenci.txt,用作分词表,写入:尚硅谷

9)访问,由于访问nginx中资源,都在html文件夹下查找,所以直接访问/es/fenci.txt即可(乱码先不用管,访问到了即可)

10)修改/usr/share/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.56.10/es/fenci.txt</entry> 
    <!--用户可以在这里配置远程扩展停止词字典-->
    <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

docker update elasticsearch --restart=always

11)重启es,验证分词效果

POST _analyze
{
  "analyzer": "ik_max_word",
  "text": "尚硅谷不错"
}

分词效果

{
  "tokens" : [
    {
      "token" : "尚硅谷",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "硅谷",
      "start_offset" : 1,
      "end_offset" : 3,
      "type" : "CN_WORD",
      "position" : 1
    },
    {
      "token" : "不错",
      "start_offset" : 3,
      "end_offset" : 5,
      "type" : "CN_WORD",
      "position" : 2
    }
  ]
}

6、整合springboot

选择Elasticsearch-Rest-Client(elasticsearch-rest-high-level-client)操作es

官方文档:Java High Level REST Client | Java REST Client [7.17] | Elastic

1、创建项目gulimall-search

pom.xml

<dependency>

<groupId>org.elasticsearch.client</groupId>

<artifactId>elasticsearch-rest-high-level-client</artifactId>

</dependency>

    <dependencies>
        <dependency>
            <groupId>com.xmh.gulimall</groupId>

            <artifactId>gulimall-common</artifactId>

            <version>1.0.0-SNAPSHOT</version>

            <!--去除数据源-->
            <exclusions>
                <exclusion>
                    <groupId>com.baomidou</groupId>

                    <artifactId>mybatis-plus-boot-starter</artifactId>

                </exclusion>

            </exclusions>

        </dependency>

        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-web</artifactId>

        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-actuator</artifactId>

        </dependency>

        <dependency>
            <groupId>org.elasticsearch.client</groupId>

            <artifactId>elasticsearch-rest-high-level-client</artifactId>

        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-test</artifactId>

            <scope>test</scope>

        </dependency>

    </dependencies>

父工程pom.xml

<elasticsearch.version>7.4.2</elasticsearch.version>


    <!--在spring-boot-dependencies中所依赖的ES版本位6.8.5,要改掉-->
      <dependency>
        <groupId>org.elasticsearch.client</groupId>

        <artifactId>elasticsearch-rest-high-level-client</artifactId>

        <version>${elasticsearch.version}</version>

      </dependency>

      <dependency>
        <groupId>org.elasticsearch</groupId>

        <artifactId>elasticsearch</artifactId>

        <version>${elasticsearch.version}</version>

      </dependency>

      <dependency>
        <groupId>org.elasticsearch.client</groupId>

        <artifactId>elasticsearch-rest-client</artifactId>

        <version>${elasticsearch.version}</version>

        <exclusions>
          <exclusion>
            <groupId>commons-logging</groupId>

            <artifactId>commons-logging</artifactId>

          </exclusion>

        </exclusions>

      </dependency>

主启动类:com/xmh/gulimall/search/GulimallSearchApplication.java

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

配置文件application.yml

spring:
  application:
    name: gulimall-search
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

GulimallElasticSearchConfig,官方建议把requestOptions创建成单实例

@Configuration
public class GulimallElasticSearchConfig {

    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("59.110.106.16", 9200, "http")));

        return client;
    }

}

创建单元测试,查看是否注入成功

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

    @Autowired
    private RestHighLevelClient client;

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

}

输出成功

2、保存数据

官方文档:Index API | Java REST Client [7.17] | Elastic

保存方式分为同步和异步,异步方式多了个listener回调,下面测试同步保存

@Test
public void indexData() throws IOException {
    IndexRequest indexRequest = new IndexRequest("users");//新建users索引的请求
    indexRequest.id("1");//数据的id

    User user = new User();//要保存的数据
    user.setUsername("张三");
    user.setAge(18);
    user.setGender("男");
    //把对象转化成json,填入请求之中
    String jsonString = JSON.toJSONString(user);
    indexRequest.source(jsonString, XContentType.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;
}

查看结果GET /users/_search

3、复杂检索

内部实体类:

    @Data
    @ToString
    public 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;
    }

复杂检索:1、创建检索请求;2、执行检索;3、分析结果

@Test
public void searchData() throws IOException {
    //1、创建检索请求
    SearchRequest searchRequest = new SearchRequest();
    //指定索引
    searchRequest.indices("bank");
    //指定DSL,检索条件
    //SearchSourceBuilder sourceBuilder 封装查询的条件
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

    //1.1、构造检索条件,查询地址包含mill的
    //sourceBuilder.query();
    //sourceBuilder.from();
    //sourceBuilder.size();
    //sourceBuilder.aggregation();
    sourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));
    searchRequest.source(sourceBuilder);
    System.out.println(sourceBuilder.toString()); //打印构造条件

    //1.2、按照年龄的值分布进行聚合
    TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
    sourceBuilder.aggregation(ageAgg);

    //1.3、计算平均薪资
    AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
    sourceBuilder.aggregation(balanceAvg);


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


    //3、分析结果
    System.out.println(search.toString());
    //3.1、获取所有查到的数据
    SearchHits hits = search.getHits();
    SearchHit[] searchHits = hits.getHits();
    for (SearchHit hit : searchHits) {
        //hit.getIndex();hit.getType();hit.getId();  //获取一些基本属性
        String sourceAsString = hit.getSourceAsString();  //把查询出来的结果转化为json字符串
        Account account = JSON.parseObject(sourceAsString, Account.class);
        System.out.println("account" + account);
    }

    //3.2、获取这次检索到的聚合信息
    Aggregations aggregations = search.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");
    double value = balanceAvg1.getValue();
    System.out.println("平均薪资为:" + value);

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值