学习谷粒商城之ElasticSearch进阶使用以及项目整合ES

1、ElasticSearch的进阶操作

1.1 携带查询条件的查询操作

在查询的过程中,我们可能会需要在请求路径中携带一些查询条件,希望带着查询条件一起将想要的文档查询出来,因此,接下来就来介绍一些ElasticSearch中携带查询条件的两种查询操作。
查询操作的基本语法是

GET /{索引名}/_search(?查询条件)

1.1.1 在请求路径中携带查询条件

我们想要将所有的数据查询出来,并且按照account_number字段的值升序排序,因此,我们可以使用下面的这条指令

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

下面来解析一下这条指令的含义

GET:表示发送的GET请求,用于查询
bank:是查询哪个索引
_search:是默认的索引
q=*:表示查询的内容为所有,q是query的缩写
sort=account_number:asc:是指定要按照哪个字段进行升序/降序排序

执行该条指令的结果为:
将查询的数据展示在这里显得有点长,因此这里省去了一些字段值,只列出部分

{
 ...
    "max_score" : null, // 最匹配的文档的分数
    "hits" : [  // 存放匹配的文档
      {
        "_index" : "bank", // 索引
        "_type" : "account", // 类型
        "_id" : "0", // 文档的id
        "_score" : null, // 该条文档的相关性分数
        "_source" : { // 这条文档相关的字段和其对应的值
          "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"
        },
        "sort" : [ // 排序值
          0
        ]
      },
      {
        ...
        "_id" : "1",
        "_score" : null,
        "_source" : {
          "account_number" : 1,
          ...
        },
        "sort" : [
          1
        ]
      },
      {
        ...
        "_id" : "2",
        "_score" : null,
        "_source" : {
          "account_number" : 2,
          ...
        },
        "sort" : [
          2
        ]
      },
      {
        ...
        "_id" : "3",
        "_score" : null,
        "_source" : {
          "account_number" : 3,
          ...
        },
        "sort" : [
          3
        ]
      },
      {
        ...
        "_id" : "4",
        "_score" : null,
        "_source" : {
          "account_number" : 4,
          ...
        },
        "sort" : [
          4
        ]
      },
      {
        ...
        "_id" : "5",
        "_score" : null,
        "_source" : {
          "account_number" : 5,
          "...
        },
        "sort" : [
          5
        ]
      },
      {
        ...
        "_id" : "6",
        "_score" : null,
        "_source" : {
          "account_number" : 6,
          ...
        },
        "sort" : [
          6
        ]
      },
      {
        ...
        "_id" : "7",
        "_score" : null,
        "_source" : {
          "account_number" : 7,
          ...
        },
        "sort" : [
          7
        ]
      },
      {
        ...
        "_id" : "8",
        "_score" : null,
        "_source" : {
          "account_number" : 8,
          ...
        },
        "sort" : [
          8
        ]
      },
      {
        ...
        "_id" : "9",
        "_score" : null,
        "_source" : {
          "account_number" : 9,
          ...
        },
        "sort" : [
          9
        ]
      }
    ]
  }
}

这种方法固然可以,但是,当我们的查询条件很多时,会导致整个请求路径有点长,而且不容易检查,所以,ES官方推荐我们使用另外一种方式,使用Query DSL。

1.1.2 Query DSL的方式

官网文档:Query and filter context | Elasticsearch Reference [6.0] | Elastic
官网入门文档:Executing Searches | Elasticsearch Reference [6.0] | Elastic
大家可以翻阅官方文档,这里只是做个简单的介绍。

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

解释一下上面所写的内容:

GET /bank/_search:表示要向bank发送查询请求,具体的查询条件位于在后面的大括号
query:表示查询的具体条件,是一个对象,可以在这里定义所有的查询条件
match_all:匹配所有,查询所有
sort:表示排序,需要指明查出的文档要对哪个字段按哪种方式进行排序
account_number:表示对account_number字段按某种方式操作
order:asc:要对account_number进行排序操作,排序的方式为升序

所以,这种写法所表示的与前面所写的表示的含义是一样的,只是不同的写法。
要注意的是:查询条件的大括号要另起一行,不然会报错,即使用下面的这种写法是不行的
因为所表示的含义一样,其结果肯定是一样的,所以这里就不贴查询结果了。
当然,上述的写法还可以简写,简写的写法如下:

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

而且我们可以注意到,我们上述的三种查询操作中,最终返回的都是10条数据,而在查询结果的最上面,有如下的参数值,其中的value表示当前查出的数据条数,一共是1000条,这里缺只显示出前10条,这是因为,ES会自动帮我们做分页操作,默认是显示第1页的10条数据。所以我们只能看到10条数据,当然我们也可以自定义分页的相关

"total" : {
  "value" : 1000,
  "relation" : "eq"
},

1.2 只返回部分字段值

前面我们使用Query DSL写法查询了所有的字段值,但有时候我们不一定所有的字段值都需要,只要部分的字段值,而ES也提供了对应的API。
假设我们现在只要查出数据表中的firstnamelastname两个字段值,那么我们可以使用_source,在这个参数中,写上想要查询的字段的名字即可。没写默认查询所有的字段。
具体的写法如下:

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

此时查询的结果如下:
同样的省略了一些查询结果

{
  ...
  "hits" : {
    "total" : {
      "value" : 1000,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "bank",
        "_type" : "account",
        "_id" : "0",
        "_score" : null,
        "_source" : {
          "firstname" : "Bradshaw",
          "lastname" : "Mckenzie"
        },
        "sort" : [
          0
        ]
      },
    ...省略9条数据
    ]
  }
}

可以看到此时的_source只剩下两个字段值了。

1.3 match用法

match会针对指明的字段名以及字段值,会去检索与之匹配的文档。
使用match有两种检索,一种是全文检索,一种是精确检索。如何判断这是全文检索还是精确检索,这里先不说明,后面在讲到_mapping时会介绍到。
全文检索的情况:

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

此时返回的数据为:
此处只显示部分数据,这里会显示出所有address中包含Avenue的文档,此时一共有214条文档的address中包含Avenue

"total" : {
  "value" : 214,
  "relation" : "eq"
},
"max_score" : 1.5400246,
"hits" : [
  {
    "_index" : "bank",
    "_type" : "account",
    "_id" : "25",
    "_score" : 1.5400246,
    "_source" : {
      ...
      "address" : "171 Putnam Avenue",
      ...
    }
  },
  {
    ...
    "_score" : 1.5400246,
    "_source" : {
      ...
      "address" : "759 Newkirk Avenue",
      ...
    }
  },
  ...

精确检索的情况:

GET /bank/_search
{
  "query": {
    "match": {
      "age": "20"
    }
  }
}

此时返回的数据为:

"total" : {
  "value" : 44,
  "relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
  {
    ...
    "_score" : 1.0,
    "_source" : {
      ...
      "age" : 20,
      ...
    }
  },
  {
    ...
    "_score" : 1.0,
    "_source" : {
      ...
      "age" : 20,
      ...
    }
  },

从上述的两种检索方式可以看出,对于全文检索,会按照相关性分数从大到小排序,越匹配的分数越高,越会优先显示。而对于精确检索,只要是匹配的文档,其相关性分数为1.0,且是固定的。

1.4 match_phrase短语匹配

短语匹配指,只有当出现同样的短语时,且顺序一致时才会匹配,否则不会匹配,这与全文检索的模糊匹配不一样。
下面的例子是查询address属性中包含了Newkirk Avenue的文档

GET /bank/_search
{
  "query": {
    "match_phrase": {
      "address": "Newkirk Avenue"
    }
  }
}

此时返回的数据为:

"max_score" : 7.5308537,
    "hits" : [
      {
        "_index" : "bank",
        "_type" : "account",
        "_id" : "102",
        "_score" : 7.5308537,
        "_source" : {
          ...
          "address" : "759 Newkirk Avenue",
          ...
        }
      }
    ]

此时就只有一条文档符合,明显看出,此时的文档量少了很多,这与前面使用match时查出的文档量不一样,说明这与全文检索的模糊匹配不一样。

1.5 多字段匹配

在查询的过程中,我们不止想匹配一个字段,而是多个字段一起匹配的文档。
因此就需要使用另外一个参数,multi_match
multi_match支持多个字段进行匹配。

GET /bank/_search
{
  "query": {
    "multi_match": {
      "query": "Newkirk Choctaw",
      "fields": ["address","city"]
    }
  }
}

解释一下multi_match使用到的两个参数

query: 需要匹配的值
fields:需要与query中设置的值进行匹配的字段名

按照match语法对应的话,就是address:Newkirkaddress:Choctawcity:Newkirk以及city:Choctaw
此时返回的数据为:

"total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 6.505949,
    "hits" : [
      {
        ...
        "_score" : 6.505949,
        "_source" : {
          ...
          "address" : "759 Newkirk Avenue",
          ...
          "city" : "Choctaw",
          "state" : "NJ"
        }
      },
      {
        ...
        "_score" : 6.505949,
        "_source" : {
          ...
          "address" : "803 Glenmore Avenue",
          ...
          "city" : "Newkirk",
          "state" : "KS"
        }
      },
      {
        ...
        "_score" : 5.9908285,
        "_source" : {
          ...
          "address" : "865 Newkirk Placez",
          ...
          "city" : "Ada",
          "state" : "ID"
        }
      }
    ]

此时一共只有三条数据符合,而这三条数据都是要么address包含了一个或多个词,要么city中包含了一个或多个词,要么两个都包含。有点类似于MySQL的or查询。

1.6 bool复合查询

bool复合查询是一个结合多个查询条件的查询结构,会有四种大的结构,分别为:mustmust_notshouldfilter

  • must:必须,即在文档中必须要包含的条件,提供相关性分数
  • must_not:必须不,即在文档中一定不能有的条件,不提供相关性分数
  • should:应该,即在文档中可以有也可以没有的条件,匹配时提供相关性分数
  • filter:过滤,即在指子句(查询)必须出现在匹配的文档中,不提供相关性分数
    查询一个地址必须包含Avenue,所在城市不包含Choctaw且性别优先为F的结果
GET /bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "address": "Avenue"
          }
        }
      ],
      "must_not": [
        {
          "match": {
            "city": "Choctaw"
          }
        }
      ],
      "should": [
        {
          "match": {
            "gender": "F"
          }
        }
      ]
    }
  }
}

此时返回的数据必须包含Avenue,所在城市不包含Choctaw且性别优先为F,这里就不贴数据了,篇幅会有点长。
找出age在10-20之间的文档

GET /bank/_search
{
  "query": {
    "bool": {
      "filter": {
        "range": {
          "age": {
            "gte": 10,
            "lte": 20
          }
        }
      }
    }
  }
}

1.7 term

term能做一个精确检索,会拿着整个值去进行检索,在检索时不会将这个词进行分析,且要求大小写以及内容完全一样才会匹配。term能对全文检索类型的字段和精确检索类型的字段进行检索。
下面用两个例子来演示一下

1.7.1 全文检索类型的字段

对于全文检索类型的字段而言,使用term进行检索的话

GET /bank/_search
{
  "query": {
    "term": {
      "lastname": {
        "value": "Terry"
      }
    }
  }
}

此时,返回的数据为:

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}

可以发现,此时并没有查找到任何的文档,因为当我们将文档进行插入时,会使用ES内置的单词分析器将这个词进行拆分,而拆分时会将其进行小写。

GET /bank/_analyze
{
  "analyzer": "standard",
  "text": ["Terry"]
}

这里简单解释一下,analyzer表示单词分析器,这里使用标准的,text是要进行分析的单词。
Terry这个词进行分析之后的结果如下:

{
  "tokens" : [
    {
      "token" : "terry",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "<ALPHANUM>",
      "position" : 0
    }
  ]
}

可以看到,在进行单词分析时,会将其分析为terry,最终存储到倒排索引表时是以terry进行存放的,而term要求的是,单词大小写以及内容完全一样才会匹配,所以,这里匹配失败。

1.7.2 精确检索的字段

对于精确检索的字段而言,使用term进行检索的话,与match没有区别

GET /bank/_search
{
  "query": {
    "term": {
      "age": {
        "value": "20"
      }
    }
  }
}

此时查出来的数据都是age为20的,这里就不列数据了。

1.8 keyword

keyword是一个属性,支持精确检索,对于某些全文检索的字段,我们可以在这个字段中添加一个子属性keyword,当我们想要对其进行精确检索的时候,我们可以拿到这个字段的keyword,用keyword去进行精确检索。
接下来,我们体验一下,在同一情况下,使用了keyword和不使用keyword的区别,事先说明,address是一个全文检索字段,其内部也有一个子属性keyword,我们可以使用_mapping来查看这个字段,这里会查出·这个索引所有的字段的情况

GET /bank/_mapping

我们只看address字段的部分

"address" : {
  "type" : "text",
  "fields" : {
    "keyword" : {
      "type" : "keyword",
      "ignore_above" : 256
    }
  }
},

可以看到,在keyword这个子属性

1.8.1 不使用keyword

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

此时返回的数据为:

 {
    "_index" : "bank",
    "_type" : "account",
    "_id" : "157",
    "_score" : 6.501515,
    "_source" : {
      "account_number" : 157,
      "balance" : 39868,
      "firstname" : "Claudia",
      "lastname" : "Terry",
      "age" : 20,
      "gender" : "F",
      "address" : "132 Gunnison Court",
      "employer" : "Lumbrex",
      "email" : "claudiaterry@lumbrex.com",
      "city" : "Castleton",
      "state" : "MD"
    }
  }

可以看到,能查出一条数据

1.8.2 使用keyword

GET /bank/_search
{
  "query": {
    "match": {
      "address.keyword": "Gunnison"
    }
  }
}

此时返回的数据为:

"hits" : [ ]

此时根本没有找到任何的数据,因为此时会拿着整个Gunnison去进行精确检索,前面提到,对于精确检索,都需要当前字段的值完全等于这个值才会匹配成功。而这里并没有这挑文档,因此检索失败。

1.9 聚合分析

Aggregations | Elasticsearch Guide [7.17] | Elastic
聚合分析指的是,我们在检索时想要对前面检索的结果做一些处理,比如统计一下平均值、每个年龄段都有多少人等等,我们都可以使用聚合分析做到。
接下来直接用三个案例带大家认识一下聚合分析

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

我们使用到的DSL如下:

GET /bank/_search
{
  "query": {
    "match": {
      "address": "mill"
    }
  },
  "aggs": {
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 10
      }
    },
    "ageAvg": {
      "avg": {
        "field": "age"
      }
    }
  }
}
  • query则是先查出所有address中包含了mill的人
  • 后面的aggs则是对前面query查出的结果进行聚合分析
  • ageAgg是当前聚合的名称,可以是任意的
  • terms则是对前面查询的结果按照某种方式进行划分,这里选择了term,对应的field属性是age,因此则是对查询出来的结果按照age进行terms查找划分
  • 后面的ageAvg则是按照age求出前面query中找到的结果的年龄的平均值
    这里使用到了两个聚合,一个是ageAgg,一个是ageAvg,这两个聚合是我们自定义的两个名字,具体的要进行哪种聚合操作写在下面即可。如ageAgg使用的根据年龄进行聚合,ageAvg则是按照年龄取平均值。
    这里返回的结果为:
{
  "hits" : {
    "max_score" : 5.4032025,
    "hits" : [
      {
        "_index" : "bank",
        "_type" : "account",
        "_id" : "970",
        "_score" : 5.4032025,
        "_source" : {
          ...
          "age" : 28,
          ...
        }
      },
      {
        "_index" : "bank",
        "_type" : "account",
        "_id" : "136",
        "_score" : 5.4032025,
        "_source" : {
          ...
          "age" : 38,
          ...
        }
      },
      {
        "_index" : "bank",
        "_type" : "account",
        "_id" : "345",
        "_score" : 5.4032025,
        "_source" : {
          ...
          "age" : 38,
          ...
        }
      },
      {
        "_index" : "bank",
        "_type" : "account",
        "_id" : "472",
        "_score" : 5.4032025,
        "_source" : {
          ...
          "age" : 32,
          ...
        }
      }
    ]
  },
  "aggregations" : {
    "ageAgg" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : 38,
          "doc_count" : 2
        },
        {
          "key" : 28,
          "doc_count" : 1
        },
        {
          "key" : 32,
          "doc_count" : 1
        }
      ]
    },
    "ageAvg" : {
      "value" : 34.0
    }
  }
}

aggregations部分就是我们使用了聚合分析之后得出的结果,会按照每个聚合进行划分。

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

对于query和ageAgg与前面一样,而我们的balanceAvg,这里是要求的年龄段的平均薪资,因此是基于年龄聚合的基础上进行聚合的,所以我们需要在ageAgg内部再次聚合。这与上一个案例的ageAgg和ageAvg不一样。

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

此时返回的数据如下:

"aggregations" : {
"ageAgg" : {
  "doc_count_error_upper_bound" : 0,
  "sum_other_doc_count" : 463,
  "buckets" : [
    {
      // 这两个是由ageAgg聚合分析出来的结果
      "key" : 31,
      "doc_count" : 61,
      // 这个则是由balanceAvg查出来的结果
      "balanceAvg" : {
        "value" : 28312.918032786885
      }
    }

可以看到,对于聚合,我们可以是并列的,也可以是嵌套的。

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

GET /bank/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    // 先按照年龄聚合
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 10
      },
      // 再将年龄聚合后的结果再次进行聚合
      "aggs": {
        "gender": {
          "terms": {
            "field": "gender.keyword",
            "size": 10
          },
          "aggs": {
            "balanceAvg": {
              "avg": {
                "field": "balance"
              }
            }
          }
        },
        "totalBalanceAvg": {
          "avg": {
            "field": "balance"
          }
        }
        
      }
    }
  }
}

可以看到,这次的写法会比前两次要复杂一些,主要是使用到了聚合的多层嵌套。
综上所属,其实也是聚合分析的三种用法,并列、嵌套、嵌套中嵌套。

2、ElasticSearch的映射

官方文档:Mapping | Elasticsearch Guide [7.4] | Elastic
映射,是指定义文档及其包含的字段如何, 进行存储和索引。

2.1 查询映射

前面我们在keyword的使用时有用到映射,也就是

GET /bank/_mapping

这也是最基本的用法,获取某个索引的所有字段的映射规则。

2.1.1 新增映射

新增映射是指创建一个新的索引,并为这个索引添加一些字段,为这些字段设置映射规则。
其基本语法为:

PUT /{index_name}
{
  "mappings": {
    "properties": {
      "{fieldname}": {
        "type": "{type}"
      }
    }
  }
}

{index_name}中填写我们自己的索引名,{filedname}中填写我们要声明的字段名,{type}中填写对应的字段的类型({}是要去掉的)。
字段的类型一共分为三类:

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

此时我们可以通过查询映射的方式来看到当前创建出来新索引的映射规则

{
  "my_index" : {
    "mappings" : {
      "properties" : {
        "age" : {
          "type" : "integer"
        },
        "email" : {
          "type" : "keyword"
        },
        "name" : {
          "type" : "text"
        }
      }
    }
  }
}

2.3 往映射中添加一个新属性

2.3.1 错误写法

如果我们要在一个已有的映射中添加一个新的属性,我们可能第一想到的是想新增文档时一样,在原来的新增映射中添加字段并重新执行。如我们想要添加一个employee_id字段,那么就会是下面的指令:

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

执行完之后返回的数据如下:

{
  "error": {
    "root_cause": [
      {
        "type": "resource_already_exists_exception",
        "reason": "index [my_index/e2ILrRUVQkmL0SzQxZp__g] already exists",
        "index_uuid": "e2ILrRUVQkmL0SzQxZp__g",
        "index": "my_index"
      }
    ],
    "type": "resource_already_exists_exception",
    "reason": "index [my_index/e2ILrRUVQkmL0SzQxZp__g] already exists",
    "index_uuid": "e2ILrRUVQkmL0SzQxZp__g",
    "index": "my_index"
  },
  "status": 400
}

出错了,出错的原因是因为当前索引已经存在了,因此不可以。也就是说,在每次创建索引的时候,都会先去判断当前是否包含该索引,如果没有,则创建成功,如果有,则创建失败。
因此,我们就不可以使用这种方式。

2.3.2 正确写法

PUT /my_index/_mapping
{
  "properties": {
    "employee_id": {
      "type": "keyword",
      "index": false
    }
  }
}

index字段默认为true,意味开启索引,开启索引意味着可以被检索,只要开启了某个属性的索引,那么我们就可以在查找的过程中通过这个字段查找到某些文档,而如果关闭了索引,那么就相当于这个属性只是存在,但不能通过这个属性找到对应的文档记录,即冗余属性。
重新查询映射规则

{
  "my_index" : {
    "mappings" : {
      "properties" : {
        "age" : {
          "type" : "integer"
        },
        "email" : {
          "type" : "keyword"
        },
        "employee_id" : {
          "type" : "keyword",
          "index" : false
        },
        "name" : {
          "type" : "text"
        }
      }
    }
  }
}

可以看到,此时添加成功了。

2.4 更新映射与数据迁移

在实际开发过程中,我们可能觉得某个字段的映射规则不合适,想要进行修改,但,这是不可以的。因为当前映射规则的属性关联了很多的数据,如果我们直接修改的话可能会导致之前创建那些索引失效,不会因为我们修改了映射规则会自动的将之前的数据一起修改。因此我们能做的只能是通过重新创建一个索引,并将整个数据拷贝过去。

2.4.1 获取当前索引的所有映射规则

GET /bank/_mapping

2.4.2 复制并修改原来的映射规则,并放进新的索引里

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

2.4.3 将旧的索引的数据迁移到新的索引

// 发送重新索引请求
POST _reindex
{
  "source": {
    "index": "bank",
    "type": "account"
  },
  "dest": {
    "index": "newbank"
  }
}

这样子就完成了映射规则的修改以及数据的迁移
我们可以查询这个新的索引的文档,可以发现里面的内容跟之前的是一样的。

3、ElasticSearch的分词操作

3.1 ES的分词介绍

分词操作,即会对一句话进行分析,将其拆分成若干个短语或单词。
前面我们也使用到了这个,即在[[#① 全文检索类型的字段]]中有使用到这个。
分词操作的基本语法为:

POST _analyze
{
  "analyzer": "{standard}",
  "text": ["{text}"]
}

其中{standard}是我们要使用的分词器,一般为standard,{text}则是我们要分析的内容。
分词器的工作原理是,背后有个词库,会根据词库的内容对句子进行划分。但是,标准的分词器有时候不能实现我们想要的,比如一些流行用语就不会收录。如我们想要对法外狂徒张三进行划分,可以将其划分为法外狂徒张三

POST _analyze
{
  "analyzer": "standard",
  "text": ["法外狂徒张三"]
}

但是实际会将其划分为什么呢?

{
  "tokens" : [
    {
      "token" : "法",
      ...
    },
    {
      "token" : "外",
      ...
    },
    {
      "token" : "狂",
      ...
    },
    {
      "token" : "徒",
     ...
    },
    {
      "token" : "张",
      ...
    },
    {
      "token" : "三",
      ...
    }
  ]
}

很明显不是我们想要的结果,因此,我们需要一种方式来录入这些流行用语。

3.2 自定义分词器

我们使用ik分词器
ik对应版本的下载地址Site Unreachable

先修改虚拟机的运行内存为3G
接着修改ES的启动内存最大为512
需要先移除ES容器后重新创建一个新的容器

docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e  "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms128m -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 

接着启动一个nginx实例,nginx实例中有很多的配置文件,我们需要利用到这些配置文件

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

如果没有nginx实例的话,会优先去帮我们下载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

访问nginx网站,此时会弹出403forbidden,这是因为当前并没有页面显示,因此我们在可以nginx/html/目录下创建一些页面,而我们自定义的词库也会放在这里。
这里我们生成一个fenci.txt,里面写上一些自定义的词组
使用http://ip/es/fenci.txt访问,即可。
接着我们需要去修改es的配置信息

/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">remote</entry>-->
    <!--用户可以在这里配置远程扩展停止词字典-->
    <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

而我们要进行修改的内容是 远程扩展字典
将其修改如下

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    ...此处省略不展示
    <!--用户可以在这里配置远程扩展字典 -->
    <entry key="remote_ext_dict">http://ip/es/fenci.txt</entry>
    ...此处省略不展示
</properties>

此时我们重新分词,就可以得到我们想要的答案了。

4、Springboot整合ElasticSearch

官方文档:Initialization | Java REST Client [7.4] | Elastic

4.1 整合ElasticSearch

4.1.1 创建新工程
4.1.2 引入依赖
<dependency>  
    <groupId>org.elasticsearch.client</groupId>  
    <artifactId>elasticsearch-rest-high-level-client</artifactId>  
    <version>7.4.2</version>  
</dependency>

同时引入用于解决有些依赖的版本不是7.4.2的问题

<parent>  
    <artifactId>spring-boot-starter-parent</artifactId>  
    <groupId>org.springframework.boot</groupId>  
    <version>2.6.13</version>  
    <relativePath></relativePath>
</parent>

<properties>  
    <elasticsearch.version>7.4.2</elasticsearch.version>  
</properties>
4.1.3 初始化

配置RestHighLevelClient

@Configuration  
public class GulimallElasticSearchConfig {  
    @Bean  
    public RestHighLevelClient getRestHighLevelClient() {  
        RestHighLevelClient client = new RestHighLevelClient(  
                RestClient.builder(  
                        new HttpHost("address", 9200, "http")));  
        return client;  
    }  
}

测试这个bean是否注册

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

此时启动会报错,这是因为我们使用了nacos,需要引入配置

java.lang.IllegalStateException: Failed to load ApplicationContext

    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:98)
    at 
    ...
Caused by: org.springframework.cloud.commons.ConfigDataMissingEnvironmentPostProcessor$ImportException: No spring.config.import set
    at org.springframework.cloud.commons.ConfigDataMissingEnvironmentPostProcessor.postProcessEnvironment

我们直接在application.yml中排除nacos的检测即可

spring:
    cloud:
        nacos:
            config:  
                import-check:  
                    enabled: false

最终运行之后能看到这个即说明配置成功

org.elasticsearch.client.RestHighLevelClient@b791a81
4.1.4 设置请求选项

GulimallElasticSearchConfig.java中进行配置

public static final RequestOptions COMMON_OPTIONS;  
static {  
    RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();  
    COMMON_OPTIONS = builder.build();  
}
4.1.5 新增文档
@Autowired  
private RestHighLevelClient client;  
  
@Data  
class User {  
    private String userName;  
    private Integer age;  
    private String gender;  
}  
  
@Test  
void indexTest() throws IOException {  
    // 创建一个索引请求,users即要创建的或者要使用的索引名字
    // 如果没有,则会创建,如果有则直接使用
    IndexRequest indexRequest = new IndexRequest("users");  
    // 设置该条文档的id
    indexRequest.id("1");  
    // 准备数据
    User user = new User();  
    user.setUserName("法外狂徒——张三");  
    user.setAge(18);  
    user.setGender("男");  
    // 将其转换为json
    String jsonString = JSONValue.toJSONString(user);  
    indexRequest.source(jsonString, XContentType.JSON);  
    IndexResponse index = client.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);  
    System.out.println(index);  
}

在测试之前先进行检索

{
  "error" : {
    "root_cause" : [
      {
        "type" : "index_not_found_exception",
        "reason" : "no such index [users]",
        "resource.type" : "index_or_alias",
        "resource.id" : "users",
        "index_uuid" : "_na_",
        "index" : "users"
      }
    ],
    "type" : "index_not_found_exception",
    "reason" : "no such index [users]",
    "resource.type" : "index_or_alias",
    "resource.id" : "users",
    "index_uuid" : "_na_",
    "index" : "users"
  },
  "status" : 404
}

可以发现,此时连索引都是没有的,接下来执行代码
可以看到,在控制台中会打印出一些信息

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

这些信息类似于ES的信息,那么我们去ES那里查看一下是否存在该条文档

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "gender" : "男",
          "userName" : "法外狂徒——张三",
          "age" : 18
        }
      }
    ]
  }
}

可以发现,确实有这条文档的出现,至此,ES测试就完成了。

4.1.6 检索文档
@Test  
public void searchData() throws IOException {  
    // 1、创建检索请求  
    SearchRequest searchRequest = new SearchRequest();  
    // 2、指定索引  
    searchRequest.indices("bank");  
    // 3、指定DSL检索条件  
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();  
    searchSourceBuilder.query(QueryBuilders.matchQuery("address","mill"));  
    System.out.println(searchSourceBuilder.toString());  
    // 4、封装检索条件  
    searchRequest.source(searchSourceBuilder);  
    // 5、执行检索  
    SearchResponse search = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);  
    System.out.println(search.toString());  
}

运行之后,就可以获得如下的信息

// 查询的条件
{"query":{"match":{"address":{"query":"mill","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}}}

// 返回的数据
{"took":0,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":4,"relation":"eq"},"max_score":5.4032025,"hits":[{"_index":"bank","_type":"account","_id":"970","_score":5.4032025,"_source":{"account_number":970,"balance":19648,"firstname":"Forbes","lastname":"Wallace","age":28,"gender":"M","address":"990 Mill Road","employer":"Pheast","email":"forbeswallace@pheast.com","city":"Lopezo","state":"AK"}},{"_index":"bank","_type":"account","_id":"136","_score":5.4032025,"_source":{"account_number":136,"balance":45801,"firstname":"Winnie","lastname":"Holland","age":38,"gender":"M","address":"198 Mill Lane","employer":"Neteria","email":"winnieholland@neteria.com","city":"Urie","state":"IL"}},{"_index":"bank","_type":"account","_id":"345","_score":5.4032025,"_source":{"account_number":345,"balance":9812,"firstname":"Parker","lastname":"Hines","age":38,"gender":"M","address":"715 Mill Avenue","employer":"Baluba","email":"parkerhines@baluba.com","city":"Blackgum","state":"KY"}},{"_index":"bank","_type":"account","_id":"472","_score":5.4032025,"_source":{"account_number":472,"balance":25571,"firstname":"Lee","lastname":"Long","age":32,"gender":"F","address":"288 Mill Street","employer":"Comverges","email":"leelong@comverges.com","city":"Movico","state":"MT"}}]}}

这些数据,就是我们之前指令时返回的数据,内容是一样的。
而RESTful High Level为我们封装了很多的Api,我们可以获取这里面的每一个值,这里就不演示了。
至此,整个项目整合ES就到此结束了。

学习谷粒商城时,学到了ElasticSearch技术,因此记录下学习过程中的一些笔记以及一些补充。将我做的笔记分享出来,希望能够帮助到其他人,如果有不足的地方也希望大家能指出,谢谢!!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值