102 全文检索-ElasticSearch-简介
简介
https://www.elastic.co/cn/what-is/elasticsearch
全文搜索属于最常见的需求,开源的 Elasticsearch 是目前全文搜索引擎的首选。它可以快速地储存、搜索和分析海量数据。维基百科、Stack Overflow、Github 都采用它Elastic 的底层是开源库 Lucene。但是,你没法直接用 Lucene,必须自己写代码去调用它的 接口。Elastic 是 Lucene 的封装,提供了 REST API 的操作接口,开箱即用。
REST API:天然的跨平台。
官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
官方中文:https://www.elastic.co/guide/cn/elasticsearch/guide/current/foreword_id.html
社区中文:
https://es.xiaoleilu.com/index.html
http://doc.codingdict.com/elasticsearch/0/
什么是搜索, 计算机根据用户输入的关键词进行匹配,从已有的数据库中摘录出相关的记录反馈给用户。
常见的全网搜索引擎,像百度、谷歌这样的。但是除此以外,搜索技术在垂直领域也有广泛的使用,比如淘宝、京东搜索商品,万芳、知网搜索期刊,csdn中搜索问题贴。也都是基于海量数据的搜索。
如何处理搜索
1.1用传统关系性数据库
弊端:
1、 对于传统的关系性数据库对于关键词的查询,只能逐字逐行的匹配,性能非常差。
2、匹配方式不合理,比如搜索“小密手机” ,如果用like进行匹配, 根本匹配不到。但是考虑使用者的用户体验的话,除了完全匹配的记录,还应该显示一部分近似匹配的记录,至少应该匹配到“手机”。
1.2专业 全文索引是怎么处理的
全文搜索引擎目前主流的索引技术就是倒排索引的方式。
传统的保存数据的方式都是
记录→单词
而倒排索引的保存数据的方式是
单词→记录
例如
搜索“红海行动”
但是数据库中保存的数据如图:
那么搜索引擎是如何能将两者匹配上的呢?
基于分词技术构建倒排索引:
首先每个记录保存数据时,都不会直接存入数据库。系统先会对数据进行分词,然后以倒排索引结构保存。如下:
然后等到用户搜索的时候,会把搜索的关键词也进行分词,会把“红海行动”分词分成:红海和行动两个词。
这样的话,先用红海进行匹配,得到id=1和id=2的记录编号,再用行动匹配可以迅速定位id为1,3的记录。
那么全文索引通常,还会根据匹配程度进行打分,显然1号记录能匹配的次数更多。所以显示的时候以评分进行排序的话,1号记录会排到最前面。而2、3号记录也可以匹配到。
lucene与elasticsearch
咱们之前讲的处理分词,构建倒排索引,等等,都是这个叫lucene的做的。那么能不能说这个lucene就是搜索引擎呢?还不能。lucene只是一个提供全文搜索功能类库的核心工具包,而真正使用它还需要一个完善的服务框架搭建起来的应用。好比lucene是类似于jdk,而搜索引擎软件就是tomcat 的。目前市面上流行的搜索引擎软件,主流的就两款,elasticsearch和solr,这两款都是基于lucene的搭建的,可以独立部署启动的搜索引擎服务软件。由于内核相同,所以两者除了服务器安装、部署、管理、集群以外,对于数据的操作,修改、添加、保存、查询等等都十分类似。就好像都是支持sql语言的两种数据库软件。只要学会其中一个另一个很容易上手。从实际企业使用情况来看,elasticSearch的市场份额逐步在取代solr,国内百度、京东、新浪都是基于elasticSearch实现的搜索功能。国外就更多了 像维基百科、GitHub、Stack Overflow等等也都是基于ES的
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 、 Index (索引)
动词,相当于 MySQL 中的 insert;
名词,相当于 MySQL 中的 Database
2 、 Type (类型)
在 Index(索引)中,可以定义一个或多个类型。
类似于 MySQL 中的 Table;每一种类型的数据放在一起;
3 、 Document (文档)
保存在某个索引(Index)下,某种类型(Type)的一个数据(Document),文档是 JSON 格
式的,Document 就像是 MySQL 中的某个 Table 里面的内容;
4 、倒排索引机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dGnW7jW0-1652453219936)(https://gitee.com/jiushuli/images/raw/master/image-20220422215419698.png)]
103 全文检索-ElasticSearch-Docker安装ES
一、 Docker 安装 Es
1 、下载镜像文件
# 存储和检索数据 (在使用的时候要爆出kibana 和 elasticSearch 的版本一致)
docker pull elasticsearch:7.4.2
# 可视化检索数据
docker pull kibana:7.4.2
2 、创建实例
1 、 ElasticSearch
# 创建配置文件的挂载目录
mkdir -p /mydata/elasticsearch/config
# 创建数据文件的挂载目录
mkdir -p /mydata/elasticsearch/data
# 表示可以接受任何的请求
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
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.4.2
# 以上的运行参数详解
# --name elasticsearch 容器的名字为elasticsearch
# -p 9200:9200 是elasticsearch通过restful Api 调用的端口
# -p 9300:9300 是elasticsearch 集群通讯的端口
# -e "discovery.type=single-node" 表示以单机的形式启动
# -e ES_JAVA_OPTS="-Xms64m -Xmx512m" 限制占用的内存,测试环境下,设置 ES 的初始内存和最大内存,否则导 致过大启动不了 ES
# -v 都是对应的挂载配置文件 数据 插件安装 的挂载目录
# -d elasticsearch:7.4.2 以守护式容器的方式运行镜像版本为 7.4.2
特别注意
-e ES_JAVA_OPTS=“-Xms64m -Xmx256m” \ 测试环境下,设置 ES 的初始内存和最大内存,否则导致过大启动不了 ES
测试 http://119.3.105.108:9200
104 全文检索-ElasticSearch-Docker安装kibana
1 **、**Kibana
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://119.3.105.108:9200 -p 5601:5601 -d kibana:7.4.2
# 创建配置文件的挂载目录
# --name kibana 容器的名字
# -e ELASTICSEARCH_HOSTS=http://119.3.105.108:9200 kibana 绑定elasticsearch,一定要写成自己的
# -p 5601:5601 kibana 映射的端口
# -d kibana:7.4.2 创建镜像的版本
测试 http://119.3.105.108:5601
105 全文检索-ElasticSearch-入门_cat
# 查看所有节点
GET /_cat/nodes
# 查看 es 健康状况
GET /_cat/health
# 查看主节点
GET /_cat/master
# 查看所有索引 show databases
GET /_cat/indices
106 全文检索-ElasticSearch–入门-put&post
1 、索引一个文档(保存)
保存一个数据,保存在哪个索引的哪个类型下,指定用哪个唯一标识
PUT customer/external/1 在 customer 索引下的 external 类型下保存 1 号数据为
PUT customer/external/1
{
"name": "John Doe"
}
结果如下:(红色的提示信息,表示类型已经弃用,es不在使用type,就是直接在index下保存数据了)
#! Deprecation: [types removal] Specifying types in document index requests is deprecated, use the typeless endpoints instead (/{index}/_doc/{id}, /{index}/_doc, or /{index}/_create/{id}).
{
"_index" : "customer",
"_type" : "external",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
PUT 和 POST 都可以新增数据,
POST 新增。如果不指定 id,会自动生成 id。指定 id 就会修改这个数据,并新增版本号
PUT 可以新增可以修改。PUT 必须指定 id;由于 PUT 需要指定 id,我们一般都用来做修改
操作,不指定 id 会报错。
107 全文检索-ElasticSearch-入门-get查询数据&乐观锁字段
查询文档
GET customer/external/1
结果如下:
{
"_index" : "customer", //在哪个索引
"_type" : "external", //在哪个类型
"_id" : "1", //记录 id
"_version" : 1, //版本号
"_seq_no" : 0, //并发控制字段,每次更新就会+1,用来做乐观锁
"_primary_term" : 1, //同上,主分片重新分配,如重启,就会变化
"found" : true,
"_source" : { //真正的内容
"name" : "John Doe"
}
更新携带 ?if_seq_no=0&if_primary_term=1
乐观锁,并发问题参考下一章节的put&post修改数据
108 全文检索-ElasticSearch-入门-put&post修改数据
POST customer/external/1/_update
{
"doc": {
"name": "John Doew"
}
}
或者
POST customer/external/1
{
"name": "John Doe2"
}
或者
PUT customer/external/1
{
"name": "John Doe"
}
不同:
POST 操作会对比源文档数据,如果相同不会有什么操作,文档 version 不增加
PUT 操作总会将数据重新保存并增加 version 版本; 带_update 对比元数据如果一样就不进行任何操作。
看场景; 对于大并发更新,不带 update;
对于大并发查询偶尔更新,带 update;对比更新,重新计算分配规则。
并发乐观锁演示
//第一步: 先查询出customer索引下type为external中 id为 1 的数据
GET customer/external/1
结果如下:
{
"_index" : "customer",
"_type" : "external",
"_id" : "1",
"_version" : 4,
"_seq_no" : 3,
"_primary_term" : 1,
"found" : true,
"_source" : {
"name" : "John Doe"
}
}
//第二步:模拟两个并发修改,分被在窗口创建两个更新语(暂不执行语句)句其中_seq_no=3和_primary_term都写上面查出来结果,表示同一个时间两个线程查到了同一条数据,然后进行修改,都是修改的name 字段
POST customer/external/1?if_seq_no=3&if_primary_term=1
{
"name": "John Doe3"
}
PUT customer/external/1?if_seq_no=3&if_primary_term=1
{
"name": "John Doe4"
}
//第三步:分别执行第二步的两个语句,只有一个能成功,报错如下
{
"error": {
"root_cause": [
{
"type": "version_conflict_engine_exception",
"reason": "[1]: version conflict, required seqNo [3], primary term [1]. current document has seqNo [4] and primary term [1]",
"index_uuid": "yHjUvhSjQs2_kMzChT-WKA",
"shard": "0",
"index": "customer"
}
],
"type": "version_conflict_engine_exception",
"reason": "[1]: version conflict, required seqNo [3], primary term [1]. current document has seqNo [4] and primary term [1]",
"index_uuid": "yHjUvhSjQs2_kMzChT-WKA",
"shard": "0",
"index": "customer"
},
"status": 409
}
更新同时增加属性
POST customer/external/1/_update
{
"doc": {
"name": "Jane Doe",
"age": 20
}
}
PUT 和 POST 不带_update 也可以 如果带了update 则必须用 "doc"
109 全文检索-ElasticSearch-入门-删除数据&bulk批量导入样本测试数据
1、删除数据
//删除customer索引下的类型为external id为1的数据
DELETE customer/external/1
// 直接删除customer索引
DELETE customer
//注意es 中没有删除某一个type的操作
2、bulk批量导入数据
//两行为一条数据,第一行的{}块中写元数据(id,分区,等那些信息),第二行才是保存的数据
POST customer/external/_bulk
{"index":{"_id":"1"}}
{"name": "John Doe" }
{"index":{"_id":"2"}}
{"name": "Jane Doe" }
语法格式:
{ action: { metadata }}
{ request body }
{ action: { metadata }}
{ request body }
//复杂实例:
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"}}
bulk API 以此按顺序执行所有的 action(动作)。如果一个单个的动作因任何原因而失败, 它将继续处理它后面剩余的动作。当 bulk API 返回时,它将提供每个动作的状态(与发送 的顺序相同),所以您可以检查是否一个指定的动作是不是失败了。
3、样本数据准备
我准备了一份顾客银行账户信息的虚构的 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 官网提供的)如果官网打不开直接百度查就可以了,一定会有帖子给你答案
https://github.com/elastic/elasticsearch/blob/master/docs/src/test/resources/accounts.json?raw=true
110 全文检索-ElasticSearch-进阶-两种查询方式
1 、 SearchAPI
ES 支持两种基本方式检索 :
-
一个是通过使用 REST request URI 发送搜索参数(uri+检索参数)
-
另一个是通过使用 REST request body 来发送它们(uri+请求体)
1 )、检索信息
- 一切检索从_search 开始
GET bank/_search //检索 bank 下所有信息,包括 type 和 docs
GET bank/_search?q=*&sort=account_number:asc //请求参数方式检索
响应结果解释:
took - Elasticsearch 执行搜索的时间(毫秒)
time_out - 告诉我们搜索是否超时
_shards - 告诉我们多少个分片被搜索了,以及统计了成功/失败的搜索分片
hits - 搜索结果
hits.total - 搜索结果
hits.hits - 实际的搜索结果数组(默认为前 10 的文档)
sort - 结果的排序 key(键)(没有则按 score 排序)
score 和 max_score –相关性得分和最高得分(全文检索用)
//uri+请求体进行检索
GET bank/_search
{
"query": {
"match_all": {}
},
"sort": [{
"account_number": {
"order": "desc"
}
}]
}
HTTP 客户端工具(POSTMAN),get 请求不能携带请求体,我们变为 post 也是一样的 我们 POST 一个 JSON 风格的查询请求体到 _search API。 需要了解,一旦搜索的结果被返回,Elasticsearch 就完成了这次请求,并且不会维护任何 服务端的资源或者结果的 cursor(游标)
111 全文检索-ElasticSearch-进阶-QueryDSL基本使用&match_all
Query DSL
1 )、基本语法格式
Elasticsearch 提供了一个可以执行查询的 Json 风格的 DSL(domain-specific language 领域特定语言)。这个被称为 Query DSL。该查询语言非常全面,并且刚开始的时候感觉有点复杂, 真正学好它的方法是从一些基础的示例开始的。
一个查询语句 的典型结构 :
{
QUERY_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,
...
}
}
如果是针对某个字段,那么它的结构如下:
{
QUERY_NAME: {
FIELD_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,
...
}
}
}
GET bank/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 5,
"sort": [{
"account_number": {
"order": "desc"
}
}]
}
query 定义如何查询,
match_all 查询类型【代表查询所有的所有】,es 中可以在 query 中组合非常多的查 询类型完成复杂查询除了 query 参数之外,我们也可以传递其它的参数以改变查询结果。如 sort,size
from+size 限定,完成分页功能
sort 排序,多字段排序,会在前序字段相等时后续字段内部排序,否则以前序为准
2 )、返回部分字段
GET bank/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 5,
"_source": ["age", "balance"]
}
//_source 里面写了什么字段就会只返回什么字段
112 全文检索-ElasticSearch-进阶-match全文匹配
match 【匹配查询】
# 基本类型(非字符串),精确匹配
GET bank/_search
{
"query": {
"match": {
"account_number": "20"
}
}
}
match 返回 account_number=20 的
# 字符串,全文检索
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
}
}
最终查询出 address 中包含 mill 单词的所有记录 match 当搜索字符串类型的时候,会进行全文检索,并且每条记录有相关性得分。
# 字符串,多个单词(分词+全文检索)
GET bank/_search
{
"query": {
"match": {
"address": "mill road"
}
}
}
最终查询出 address 中包含 mill 或者 road 或者 mill road 的所有记录,并给出相关性得分
113 全文检索-ElasticSearch-进阶-match_phrase短语匹配
# 将需要匹配的值当成一个整体单词(不分词)进行检索
GET bank/_search
{
"query": {
"match_phrase": {
"address": "mill road"
}
}
}
查出 address 中包含 mill road 的所有记录,并给出相关性得分
114 全文检索-ElasticSearch-进阶-multi_match多字段匹配
# multi_match【多字段匹配】
GET bank/_search
{
"query": {
"multi_match": {
"query": "mill",
"fields": [
"state",
"address"
]
}
}
}
state 或者 address 包含 mill
115 全文检索-ElasticSearch-进阶-bool复合查询
bool 用来做复合查询:
复合语句可以合并 任何 其它查询语句,包括复合语句,了解这一点是很重要的。这就意味着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。
# must:必须达到 must 列举的所有条件
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"address": "mill"
}
},
{
"match": {
"gender": "M"
}
}
]
}
}
}
# should:应该达到 should 列举的条件,如果达到会增加相关文档的评分,并不会改变 查询的结果。如果 query 中只有 should 且只有一种匹配规则,那么 should 的条件就会 被作为默认匹配条件而去改变查询结果
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"address": "mill"
}
},
{
"match": {
"gender": "M"
}
}
],
"should": [
{
"match": {
"address": "lane"
}
}
]
}
}
}
# must_not 必须不是指定的情况
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"address": "mill"
}
},
{
"match": {
"gender": "M"
}
}
],
"should": [
{
"match": {
"address": "lane"
}
}
],
"must_not": [
{
"match": {
"email": "baluba.com"
}
}
]
}
}
}
address 包含 mill,并且 gender 是 M,如果 address 里面有 lane 最好不过,但是 email 必 须不包含 baluba.com
116 全文检索-ElasticSearch-进阶-filter过滤
并不是所有的查询都需要产生分数,特别是那些仅用于 “filtering”(过滤)的文档。为了不计算分数 Elasticsearch 会自动检查场景并且优化查询的执行。
# 并不是所有的查询都需要产生分数,特别是那些仅用于 “filtering”(过滤)的文档。为了不 计算分数 Elasticsearch 会自动检查场景并且优化查询的执行。
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"address": "mill"
}
}
],
"filter": {
"range": {
"balance": {
"gte": 10000,
"lte": 20000
}
}
}
}
}
}
117 全文检索-ElasticSearch-进阶-term查询
和 match 一样。匹配某个属性的值。全文检索字段用 match,其他非 text 字段匹配用 term 。
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"age": {
"value": "28"
}
}
},
{
"match": {
"address": "990 Mill Road"
}
}
]
}
}
}
118 全文检索-ElasticSearch-进阶-aggregations聚合分析
聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于 SQL GROUP BY 和 SQL 聚合函数。在 Elasticsearch 中,您有执行搜索返回 hits(命中结果),并且同时返 回聚合结果,把一个响应中的所有 hits(命中结果)分隔开的能力。这是非常强大且有效的, 您可以执行查询和多个聚合,并且在一次使用中得到各自的(任何一个的)返回结果,使用 一次简洁和简化的 API 来避免网络往返。
// 查询地址包含 mill的,在根据年龄分布(分组,term中的size就是取100种可能的值,不写就是显示所有的可能的值),然后再聚合一个年龄的平均年龄
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
},
"aggs": {
"group_by_state": {
"terms": {
"field": "age",
"size":100
}
},
"avg_age": {
"avg": {
"field": "age"
}
}
},
"size": 0
}
size:0 不显示搜索数据
aggs:执行聚合。聚合语法如下
"aggs": {
"aggs_name 这次聚合的名字,方便展示在结果集中": {
"AGG_TYPE 聚合的类型(avg,term,terms)": {
}
}
},
//复杂: 按照年龄聚合,并且请求这些年龄段的这些人的平均薪资
GET bank/account/_search
{
"query": {
"match_all": {}
},
"aggs": {
"age_avg": {
"terms": {
"field": "age",
"size": 1000
},
"aggs": {
"banlances_avg": {
"avg": {
"field": "balance"
}
}
}
}
},
"size": 1000
}
//复杂:查出所有年龄分布,并且这些年龄段中 M 的平均薪资和 F 的平均薪资以及这个年龄 段的总体平均薪资
GET bank/account/_search
{
"query": {
"match_all": {}
},
"aggs": {
"age_agg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"gender_agg": {
"terms": {
"field": "gender.keyword",
"size": 100
},
"aggs": {
"balance_avg": {
"avg": {
"field": "balance"
}
}
}
},
"balance_avg": {
"avg": {
"field": "balance"
}
}
}
}
},
"size": 1000
}
具体查看es的聚合函数中有多少种聚合可以查看官网。
119 全文检索-ElasticSearch-映射-映射-mapping创建
1 )、字段类型
# 核心类型
1、字符串 (string)
text ,keyword
2、数字类型(Numeric)
long,integer,short,byte,double,float,half,scaled_float
3、日期类型(Date)
date
4、布尔类型(Boolean)
boolean
5、二进制类型(binary)
binary
# 复合类型
1、数组类型(Array)
Array支持不针对特定的类型
2、对象性(Object)
object用于单json对象
3、嵌套类型(Nested)
nested 用于json对象数组
# 地理类型(Geo)
1、地理坐标(Geo-points)
geo_point 用于描述经纬度坐标
2、地理图形(Geo-Shape)
geo_shape用于描述复复杂形状,如多变形
# 特定类型
1、IP类型
ip用于描述Ipv4 和 Ipv6地址
2、补全类型(Completion)
completion提供自动完成提示
3、令牌计数类型(Token Count)
token_count用于统计字符串中的词条数量
4、附件类型(Attachment)
参考mapper-attachements插件,支持将附件如 Microsoft Office格式,Open Document格式,ePub,Html等等索引为attachement数据类型
5、抽取类型(Percolator)
接受特定领域查询语言(query-dsl)的查询
# 多字段
通常用于不同目的用不同的方法索引同一个字段,列如 string 字段可以映射为一个text字段用于索引,同样可以映射为一个keyword字段用于排序和聚合,另外,你可以使用standard analyzer,english analyzer,french analyzer来索引一个text字段
这就是muti-fields的目的,大多数的数据类型通过fileds参数支持muti-fields.
2 )、映射
Mapping(映射)
Mapping 是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和
索引的。比如,使用 mapping 来定义:
-
哪些字符串属性应该被看做全文本属性(full text fields)。
-
哪些属性包含数字,日期或者地理位置。
-
文档中的所有属性是否都能被索引(_all 配置)。
-
日期的格式。
-
自定义映射规则来执行动态添加属性。
# 查看 mapping 信息:
GET bank/_mapping
# 修改 mapping 信息
https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html
自动猜测的映射类型
3 )、新版本改变
Es7 及以上移除了 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)、将已存在的索引下的类型数据,全部迁移到指定位置即可。详见数据迁移
120 全文检索-ElasticSearch-映射-添加新的字段映射
1 、创建映射
//创建索引并指定映射
PUT /my-index
{
"mappings": {
"properties": {
"age": {
"type": "integer"
},
"email": {
"type": "keyword"
},
"name": {
"type": "text"
}
}
}
}
2 、添加新的字段映射
//添加新的字段映射
PUT /my-index/_mapping
{
"properties": {
"employee-id": {
"type": "keyword",
"index": false
}
}
}
121 全文检索-ElasticSearch-映射-修改映射&数据迁移
1 、更新映射
对于已经存在的映射字段,我们不能更新。更新必须创建新的索引进行数据迁移
2、数据迁移
先创建出 new_twitter 的正确映射。然后使用如下方式进行数据迁移
//索引下没有类型的迁移,source源索引,dest目标索引
POST _reindex [固定写法]
{
"source": {
"index": "twitter"
},
"dest": {
"index": "new_twitter"
}
}
//索引下没有有类型的迁移,source源索引哪个type下的数据迁移到目标的索引,dest目标索引
将旧索引的 type 下的数据进行迁移
POST _reindex {
"source": {
"index": "twitter",
"type": "tweet"
},
"dest": {
"index": "tweets"
}
}
122 全文检索-ElasticSearch-分词-安装ik分词
一个 tokenizer(分词器)接收一个字符流,将之分割为独立的 tokens(词元,通常是独立 的单词),然后输出 tokens 流。
例如,whitespace tokenizer 遇到空白字符时分割文本。它会将文本 “Quick brown fox!” 分割 \为 [Quick, brown, fox!]。 该 tokenizer(分词器)还负责记录各个 term(词条)的顺序或 position 位置(用于 phrase 短 语和 word proximity 词近邻查询),以及 term(词条)所代表的原始 word(单词)的 start (起始)和 end(结束)的 character offsets(字符偏移量)(用于高亮显示搜索的内容)。
Elasticsearch 提供了很多内置的分词器,可以用来构建 custom analyzers(自定义分词器)。
1 )、安装 ik 分词器
**注意:**不能用默认 elasticsearch-plugin
install xxx.zip 进行自动安装
https://github.com/medcl/elasticsearch-analysis-ik/releases?after=v6.4.2 对应 es 版本安装
进入 es 容器内部 plugins 目录 (如果之前讲es的插件目录挂载到外面的话 都不用进入容器的内部)
docker exec -it 容器 id /bin/bash
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
unzip 下载的文件
rm –rf *.zip
mv elasticsearch/ ik
可以确认是否安装好了分词器
cd ../bin
elasticsearch plugin list:即可列出系统的分词器
# 执行步骤如下
1、cd 到自己挂载的plugins的目录下
[root@ecs-284198 ~]# cd /mydata/elasticsearch/plugins
2、在线下载分词器的(也可以现在后上传)
[root@ecs-284198 plugins]# wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
3、查看下载完成的插件
[root@ecs-284198 plugins]# pwd
/mydata/elasticsearch/plugins
[root@ecs-284198 plugins]# ll
total 4404
-rw-r--r-- 1 root root 4504487 Dec 8 01:01 elasticsearch-analysis-ik-7.4.2.zip
[root@ecs-284198 plugins]#
4、解压分词器
[root@ecs-284198 plugins]# unzip elasticsearch-analysis-ik-7.4.2.zip
5、在plugins目录下新建一个ik文件夹,然后将所有解压出来的移动到ik文件夹下
先删除zip
[root@ecs-284198 plugins]# rm -rf *.zip
[root@ecs-284198 plugins]# mv ./commons-codec-1.9.jar commons-logging-1.2.jar config elasticsearch-analysis-ik-7.4.2.jar httpclient-4.5.2.jar httpcore-4.4.4.jar plugin-descriptor.properties plugin-security.policy ik
[root@ecs-284198 plugins]# cd ik
[root@ecs-284198 ik]# ll
total 1432
-rw-r--r-- 1 root root 263965 May 6 2018 commons-codec-1.9.jar
-rw-r--r-- 1 root root 61829 May 6 2018 commons-logging-1.2.jar
drwxr-xr-x 2 root root 4096 Oct 7 2019 config
-rw-r--r-- 1 root root 54643 Nov 4 2019 elasticsearch-analysis-ik-7.4.2.jar
-rw-r--r-- 1 root root 736658 May 6 2018 httpclient-4.5.2.jar
-rw-r--r-- 1 root root 326724 May 6 2018 httpcore-4.4.4.jar
-rw-r--r-- 1 root root 1805 Nov 4 2019 plugin-descriptor.properties
-rw-r--r-- 1 root root 125 Nov 4 2019 plugin-security.policy
[root@ecs-284198 ik]#
6、进入容器内部在进入bin目录
[root@ecs-284198 ik]# docker exec -it 06de913c848e /bin/bash
[root@06de913c848e elasticsearch]# cd bin
7、查看已经安装好的插件
[root@06de913c848e bin]# elasticsearch-plugin list
ik
[root@06de913c848e bin]#
---------------完成---------------------
重启es docker 容器
2 )、测试分词器
当前步骤是基于上一步安装了ik 分词器的
使用默认
POST _analyze
{
"text": "我是中国人"
}
请观察结果
使用分词器
POST _analyze
{
"analyzer": "ik_smart",
"text": "我是中国人"
}
请观察结果
另外一个分词器 ik_max_word
POST _analyze
{
"analyzer": "ik_max_word",
"text": "我是中国人"
}
请观察结果
能够看出不同的分词器,分词有明显的区别,所以以后定义一个索引不能再使用默认的 mapping 了,要手工建立 mapping, 因为要选择分词器。
123 补充-修改Linux网络设置&开启root密码访问
略。。。
124 全文检索-ElasticSearch-分词-自定义分词扩展库
修改/usr/share/elasticsearch/plugins/ik/config/中的 IKAnalyzer.cfg.xml
/usr/share/elasticsearch/plugins/ik/config
<?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.128.130/fenci/myword.txt</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
http://192.168.128.130/fenci/myword.txt 就是我们部署到服务器上一个nginx下的一个文件,里面写入了我们需要扩展词库,如何安装装nginx 参照下面的附录部分
原来的 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">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
按照标红的路径利用 nginx 发布静态资源,按照请求路径,创建对应的文件夹以及文件,放在nginx 的 html 下
然后重启 es 服务器,重启 nginx。 在 kibana 中测试分词效果
更新完成后,es 只会对新增的数据用新词分词。历史数据是不会重新分词的。如果想要历史数据重新分词。需要执行:
POST my_index/_update_by_query?conflicts=proceed
附录 - 安装 nginx
-
随便启动一个 nginx 实例,只是为了复制出配置
-
docker run -p 80:80 --name nginx -d nginx:1.10
-
将容器内的配置文件拷贝到当前目录:docker container cp nginx:/etc/nginx .
-
别忘了后面的点
-
修改文件名称:mv nginx conf 把这个 conf 移动到/mydata/nginx 下
-
终止原容器:docker stop nginx
-
执行命令删除原容器:docker rm $ContainerId
-
创建新的 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 的 html 下面放的所有资源可以直接访问;
125 全文检索-ElasticSearch-整合-springboot整合high-level-client
1)、9300:TCP
-
spring-data-elasticsearch:transport-api.jar;
-
springboot 版本不同, transport-api.jar 不同,不能适配 es 版本 7.x 已经不建议使用,8 以后就要废弃
2)、9200:HTTP
-
JestClient:非官方,更新慢
-
RestTemplate:模拟发 HTTP 请求,ES 很多操作需要自己封装,麻烦
-
HttpClient:同上
-
Elasticsearch-Rest-Client:官方 RestClient,封装了 ES 操作,API 层次分明,上手简单
最终选择 Elasticsearch-Rest-Client(elasticsearch-rest-high-level-client)
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html
1、在项目中新建gulimall-search 模块
2 、 SpringBoot 整合
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
引入es的maven依赖后,查看jar包下所有的版本,发现还有两个版本是不是7.4.2的,有两个是7.15.2的这是因为我们的springboot的starter默认有相关的依赖,我们只需要加上指定版本<elasticsearch.version>7.4.2</elasticsearch.version> 即可,然后刷新一下
3 、配置
3.1 配置类GulimallElasticSearchConfig
package com.atguigu.gulimall.search.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @创建人: 放生
* @创建时间: 2022/4/23
* @描述:
*/
@Configuration
public class GulimallElasticSearchConfig {
public static final RequestOptions COMMON_OPTIONS;
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
// builder.addHeader("Authorization", "Bearer " + TOKEN);
// builder.setHttpAsyncResponseConsumerFactory(
// new HttpAsyncResponseConsumerFactory
// .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));
COMMON_OPTIONS = builder.build();
}
@Bean
public RestHighLevelClient esRestClient(@Value("${spring.elasticsearch.jest.uris}")String esUrl){
//TODO 修改为线上的地址
RestClientBuilder builder = null;
//final String hostname, final int port, final String scheme
// builder = RestClient.builder(new HttpHost("119.3.105.108", 9200, "http"));
builder = RestClient.builder(HttpHost.create(esUrl));
RestHighLevelClient client = new RestHighLevelClient(builder);
// RestHighLevelClient client = new RestHighLevelClient(
// RestClient.builder(
// new HttpHost("119.3.105.108", 9200, "http")));
return client;
}
}
3.2 配置文件
bootstrap.properties
spring.application.name=gulimall-search
spring.cloud.nacos.config.server-addr=119.3.105.108:8848
spring.cloud.nacos.config.namespace=faf54575-dedd-455f-80cc-0d90933b23d9
spring.elasticsearch.jest.uris=http://119.3.105.108:9200
application.yml
server:
port: 12000
spring:
application:
name: gulimall-search
cloud:
nacos:
discovery:
server-addr: 119.3.105.108:8848
主启动
package com.atguigu.gulimall.search;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallSearchApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallSearchApplication.class, args);
}
}
4、使用
参照官方文档:更多的java api 的操作文档可以参照官网
package com.atguigu.gulimall.search;
import com.alibaba.fastjson.JSON;
import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
import lombok.Data;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
@SpringBootTest
class GulimallSearchApplicationTests {
@Autowired
private RestHighLevelClient esRestClient;
@Test
void contextLoads() {
System.out.println(esRestClient);
}
/**
* 测试存储数据到es
* 更新也可以
*/
@Test
public void indexData() throws IOException {
IndexRequest indexRequest = new IndexRequest("users");
indexRequest.id("1");//数据的id
// indexRequest.source("userName","zhangsan","age",18,"gender","男");
User user = new User();
user.setUserName("zhangsan");
user.setAge(18);
user.setGender("男");
String jsonString = JSON.toJSONString(user);
indexRequest.source(jsonString, XContentType.JSON);//要保存的内容
//执行操作
IndexResponse index = esRestClient.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//提取有用的响应数据
System.out.println(index);
}
@Data
class User{
private String userName;
private String gender;
private Integer age;
}
}
测试 GET users/_search
@Test
void test1() throws IOException {
Product product = new Product();
product.setSpuName("华为");
product.setId(10L);
IndexRequest request = new IndexRequest("product").id("20") .source("spuName","华为","id",20L);
try {
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
System.out.println(request.toString());
IndexResponse response2 = client.index(request, RequestOptions.DEFAULT);
} catch (ElasticsearchException e) {
if (e.status() == RestStatus.CONFLICT) { }
}
}
126 全文检索-ElasticSearch-整合-测试保存
上一章节已经演示
127 全文检索-ElasticSearch-整合-测试复杂检索
package com.atguigu.gulimall.search;
import com.alibaba.fastjson.JSON;
import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
import lombok.Data;
import lombok.ToString;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.Avg;
import org.elasticsearch.search.aggregations.metrics.AvgAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
@SpringBootTest
class GulimallSearchApplicationTests {
@Autowired
private RestHighLevelClient esRestClient;
@ToString
@Data
static class Accout {
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 sourceBuilde 封装的条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//1.1)、构造检索条件
// sourceBuilder.query();
// sourceBuilder.from();
// sourceBuilder.size();
// sourceBuilder.aggregation()
sourceBuilder.query(QueryBuilders.matchQuery("address","mill"));
//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);
System.out.println("检索条件"+sourceBuilder.toString());
searchRequest.source(sourceBuilder);
//2、执行检索;
SearchResponse searchResponse = esRestClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//3、分析结果 searchResponse
System.out.println(searchResponse.toString());
// Map map = JSON.parseObject(searchResponse.toString(), Map.class);
//3.1)、获取所有查到的数据
SearchHits hits = searchResponse.getHits();
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
/**
* "_index": "bank",
* "_type": "account",
* "_id": "345",
* "_score": 5.4032025,
* "_source":
*/
// hit.getIndex();hit.getType();hit.getId();
String string = hit.getSourceAsString();
Accout accout = JSON.parseObject(string, Accout.class);
System.out.println("accout:"+accout);
}
//3.2)、获取这次检索到的分析信息;
Aggregations aggregations = searchResponse.getAggregations();
// for (Aggregation aggregation : aggregations.asList()) {
// System.out.println("当前聚合:"+aggregation.getName());
aggregation.get
//
// }
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");
System.out.println("平均薪资:"+balanceAvg1.getValue());
// Aggregation balanceAvg2 = aggregations.get("balanceAvg");
}
}
128 商城业务-商品上架-sku在es中存储模型分析
# 整个商城的业务的商品在es中的存储模型大概有两种,先脑海中想一下京东商城首页,第一种是用宽表冗余存储,一种是多表
# 方案一: 假设我们的商城中以华为手机Mate40为列,可能会有下面的情况:
{
skuId:1
spuId:11
skuTitle:华为 Mata 40
price:7999
saleCount:99
attrs:[
{尺寸:5寸},
{CPU:高通945},
{分辨率:全高清}
]
},
{
skuId:2
spuId:11
skuTitle:华为 Mata 40
price:7999
saleCount:98
attrs:[
{尺寸:4.5寸},
{CPU:高通945},
{分辨率:全高清}
]
}
{
skuId:3
spuId:11
skuTitle:华为 Mata 40
price:7999
saleCount:99
attrs:[
{尺寸:5.5寸},
{CPU:高通945},
{分辨率:全高清}
]
},
只列举了三个sku 发现他们的spu 等很多信息是相同的,但是我们每条数据都要冗余,假如每条数据有20kb冗余,如果100万个商品那么就会多出20G 的数据,这就是牺牲空间换时间
(1)、方便检索{
skuId:1
spuId:11
skuTitle:华为xx
price:998
saleCount:99
attrs:[
{尺寸:5寸},
{CPU:高通945},
{分辨率:全高清}
]
}
冗余:
100万20=10000002KB=2000MB=2G 20
# 方案二: 就是不冗余,分表,先查出部分数据后然后联动查出其他需要的数据,这样是节省了空间,但是效率很慢
(2)、
sku索引{
skuId:1
spuId:11
xxxxx
}
attr索引{
spuId:11,
attrs:[
{尺寸:5寸},
{CPU:高通945},
{分辨率:全高清}
]
}
搜索 小米; 粮食,手机,电器。
10000个,4000个spu
分步,4000个spu对应的所有可能属性;
esClient: spuId:[4000个spuid] 40008=32000byte=32kb
32kb10000=32000mb;=32GB
1 根据业务搭建数据结构
这时我们要思考三个问题:
1、 哪些字段需要分词
2、 我们用哪些字段进行过滤
3、 哪些字段我们需要通过搜索显示出来。
需要分词的字段 | sku名称 sku描述 | 分词、定义分词器 |
---|---|---|
有可能用于过滤的字段 | 平台属性、三级分类、价格 | 要索引 |
其他需要显示的字段 | skuId 图片路径 | 不索引 |
2、最终的结构
根据以上制定出如下结构:
PUT gmall
{
"mappings": {
"SkuInfo": {
"properties": {
"id": {
"type": "keyword",
"index": false
},
"price": {
"type": "double"
},
"skuName": {
"type": "text",
"analyzer": "ik_max_word"
},
"skuDesc": {
"type": "text",
"analyzer": "ik_smart"
},
"catalog3Id": {
"type": "keyword"
},
"skuDefaultImg": {
"type": "keyword",
"index": false
},
"skuAttrValueList": {
"properties": {
"valueId": {
"type": "keyword"
}
}
}
}
}
}
}
129 商城业务-商品上架-nested数据类型场景
1、nested 扁平化处理
如图:我们在没有处理扁平化之前,在es中保存了这样一条数据,这条数据有一个属性是一个对象的集合 user, 这个属性的集合有两条数据,但是属性user的type没有指定为nested的时候,es会扁平化处理,会把所有的user.first放到里面的一个数组中,所有的user.last也放到一个数组中,这样我们查询一个 user.first=Alice user.last=Smith 就会查询出数据,要避免这个问题,所有的数据的属性为对象集合的字段,类型都要申明为nested的
# 演示:
# 创建my_index索引并存入数据
PUT my_index/_doc/1
{
"group":"fans",
"user":[
{"first":"John",
"last":"Smith"
},
{"first":"Alice",
"last":"White"
}
]
}
# 查询
GET my_index/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"user.first": "John"
}
},
{
"match": {
"user.last": "White"
}
}
]
}
}
}
# 结果 果真能查询
{
"took" : 6,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.5753642,
"hits" : [
{
"_index" : "my_index",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.5753642,
"_source" : {
"group" : "fans",
"user" : [
{
"first" : "John",
"last" : "Smith"
},
{
"first" : "Alice",
"last" : "White"
}
]
}
}
]
}
}
# -----------------------------------------修改 --------------------------------------
# 查看修改之前的mapping
GET my_index/_mapping
# 删除之前的索引
DELETE my_index
# 新建索引 并且指定 user的类型为nested
PUT my_index
{
"mappings": {
"properties": {
"user":{
"type": "nested"
}
}
}
# 再存入数据
PUT my_index/_doc/1
{
"group":"fans",
"user":[
{"first":"John",
"last":"Smith"
},
{"first":"Alice",
"last":"White"
}
]
}
# 再次查询
GET my_index/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"user.first": "John"
}
},
{
"match": {
"user.last": "White"
}
}
]
}
}
}
# 结果接没有了
130 商城业务-商品上架-构造基本数据
1、根据我们128节课程的最终结构 ,构建对应的model
在common 的to包下新建es包,创建SkuEsModel类
package com.atguigu.common.to.es;
/**
* @创建人: 放生
* @创建时间: 2022/4/23
* @描述:
*/
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* "properties": {
* "attrs": {
* "type": "nested",
* "properties": {
* "attrId": {
* "type": "long"
* },
* "attrName": {
* "type": "keyword",
* "index": false,
* "doc_values": false
* },
* "attrValue": {
* "type": "keyword"
* }
* }
* }
* }
* }
*/
@Data
public class SkuEsModel {
private Long skuId;
private Long spuId;
private String skuTitle;
private BigDecimal skuPrice;
private String skuImg;
private Long saleCount;
private Boolean hasStock;
private Long hotScore;
private Long brandId;
private Long catalogId;
private String brandName;
private String brandImg;
private String catalogName;
private List<Attrs> attrs;
@Data
public static class Attrs{
private Long attrId;
private String attrName;
private String attrValue;
}
}
2、SpuInfoController 新增上架方法up
///product/spuinfo/{spuId}/up
@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId){
spuInfoService.up(spuId);
return R.ok();
}
3、SpuInfoService
public interface SpuInfoService extends IService<SpuInfoEntity> {
......
+ void up(Long spuId);
}
4、SpuInfoServiceImpl
@Override
public void up(Long spuId) {
//1、查出当前spuid对应的所有sku信息,品牌的名字。
List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId);
List<Long> skuIdList = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
//TODO 4、查询当前sku的所有可以被用来检索的规格属性,
List<ProductAttrValueEntity> baseAttrs = attrValueService.baseAttrlistforspu(spuId);
List<Long> attrIds = baseAttrs.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds);
Set<Long> idSet = new HashSet<>(searchAttrIds);
List<SkuEsModel.Attrs> attrsList = baseAttrs.stream().filter(item -> {
return idSet.contains(item.getAttrId());
}).map(item -> {
SkuEsModel.Attrs attrs1 = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, attrs1);
return attrs1;
}).collect(Collectors.toList());
//TODO 1、发送远程调用,库存系统查询是否有库存
Map<Long, Boolean> stockMap = null;
try{
R r = wareFeignService.getSkusHasStock(skuIdList);
//
TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {
};
stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
}catch (Exception e){
log.error("库存服务查询异常:原因{}",e);
}
//2、封装每个sku的信息
Map<Long, Boolean> finalStockMap = stockMap;
List<SkuEsModel> upProducts = skus.stream().map(sku -> {
//组装需要的数据
SkuEsModel esModel = new SkuEsModel();
BeanUtils.copyProperties(sku,esModel);
//skuPrice,skuImg,
esModel.setSkuPrice(sku.getPrice());
esModel.setSkuImg(sku.getSkuDefaultImg());
//hasStock,hotScore
//设置库存信息
if(finalStockMap == null){
esModel.setHasStock(true);
}else {
esModel.setHasStock(finalStockMap.get(sku.getSkuId()));
}
//TODO 2、热度评分。0
esModel.setHotScore(0L);
//TODO 3、查询品牌和分类的名字信息
BrandEntity brand = brandService.getById(esModel.getBrandId());
esModel.setBrandName(brand.getName());
esModel.setBrandImg(brand.getLogo());
CategoryEntity category = categoryService.getById(esModel.getCatalogId());
esModel.setCatalogName(category.getName());
//设置检索属性
esModel.setAttrs(attrsList);
return esModel;
}).collect(Collectors.toList());
//TODO 5、将数据发送给es进行保存;gulimall-search;
R r = searchFeignService.productStatusUp(upProducts);
if(r.getCode() == 0){
//远程调用成功
//TODO 6、修改当前spu的状态
baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
}else {
//远程调用失败
//TODO 7、重复调用?接口幂等性;重试机制?xxx
//Feign调用流程
/**
* 1、构造请求数据,将对象转为json;
* RequestTemplate template = buildTemplateFromArgs.create(argv);
* 2、发送请求进行执行(执行成功会解码响应数据):
* executeAndDecode(template);
* 3、执行请求会有重试机制
* while(true){
* try{
* executeAndDecode(template);
* }catch(){
* try{retryer.continueOrPropagate(e);}catch(){throw ex;}
* continue;
* }
*
* }
*/
}
}
131 商城业务-商品上架-构造sku检索属性
略。。
132 商城业务-商品上架-远程查询库存&泛型结果封装
package com.atguigu.common.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.apache.http.HttpStatus;
import java.util.HashMap;
import java.util.Map;
/**
* 返回数据
*
* @author Mark sunlightcs@gmail.com
*/
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
//利用fastjson进行逆转
public <T> T getData(String key,TypeReference<T> typeReference){
Object data = get(key);//默认是map
String s = JSON.toJSONString(data);
T t = JSON.parseObject(s, typeReference);
return t;
}
//利用fastjson进行逆转
public <T> T getData(TypeReference<T> typeReference){
Object data = get("data");//默认是map
String s = JSON.toJSONString(data);
T t = JSON.parseObject(s, typeReference);
return t;
}
public R setData(Object data){
put("data",data);
return this;
}
public R() {
put("code", 0);
put("msg", "success");
}
public static R error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
public Integer getCode() {
return (Integer) this.get("code");
}
}
133 商城业务-商品上架-远程上架接口
package com.atguigu.gulimall.search.controller;
import com.atguigu.common.exception.BizCodeEnume;
import com.atguigu.common.to.es.SkuEsModel;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.search.service.ProductSaveService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @创建人: 放生
* @创建时间: 2022/4/24
* @描述:
*/
@Slf4j
@RequestMapping("/search/save")
@RestController
public class ElasticSaveController {
@Autowired
ProductSaveService productSaveService;
//上架商品
@PostMapping("/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels) {
boolean b = false;
try {
b = productSaveService.productStatusUp(skuEsModels);
} catch (Exception e) {
log.error("ElasticSaveController商品上架错误:{}", e);
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
if (!b) {
return R.ok();
} else {
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
}
}
/**
* @创建人: 放生
* @创建时间: 2022/4/24
* @描述:
*/
public interface ProductSaveService {
boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException;
}
package com.atguigu.gulimall.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.atguigu.common.to.es.SkuEsModel;
import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
import com.atguigu.gulimall.search.constant.EsConstant;
import com.atguigu.gulimall.search.service.ProductSaveService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* @创建人: 放生
* @创建时间: 2022/4/24
* @描述:
*/
@Slf4j
@Service
public class ProductSaveServiceImpl implements ProductSaveService {
@Autowired
RestHighLevelClient restHighLevelClient;
@Override
public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
//保存到es
//1、给es中建立索引。product,建立好映射关系。
//2、给es中保存这些数据
//BulkRequest bulkRequest, RequestOptions options
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel model : skuEsModels) {
//1、构造保存请求
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
indexRequest.id(model.getSkuId().toString());
String s = JSON.toJSONString(model);
indexRequest.source(s, XContentType.JSON);
bulkRequest.add(indexRequest);
}
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//TODO 1、如果批量错误
boolean b = bulk.hasFailures();
List<String> collect = Arrays.stream(bulk.getItems()).map(item -> {
return item.getId();
}).collect(Collectors.toList());
log.info("商品上架完成:{},返回数据:{}",collect,bulk.toString());
return b;
}
}
134 商城业务-商品上架-上架接口调试&feign源码
//远程调用失败
//TODO 7、重复调用?接口幂等性;重试机制?xxx
//Feign调用流程
/**
* 1、构造请求数据,将对象转为json;
* RequestTemplate template = buildTemplateFromArgs.create(argv);
* 2、发送请求进行执行(执行成功会解码响应数据):
* executeAndDecode(template);
* 3、执行请求会有重试机制
* while(true){
* try{
* executeAndDecode(template);
* }catch(){
* try{retryer.continueOrPropagate(e);}catch(){throw ex;}
* continue;
* }
*
* }
*/
135 商城业务-商品上架-抽取响应结果&上架测试完成
略 。。。
136 商城业务-首页-整合thymeleaf渲染首页
前面所有的操作都是采用前后端分离的,接下来我们要整合thymeleaf做成单独的服务,因为每一个模块都是有自己的数据,现在整合上thymeleaf就能单独的部署了。
1、引入依赖
在product的模块引入thymeleaf的依赖
<!-- 模板引擎: thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2、cope资料
将资料中的页面前端index的包cope到static包下,把index.htmlcope到template目录下
3、yaml配置
添加thymeleaf的相关配置
spring:
thymeleaf:
cache: false
suffix: .html # 默认配置可以不配置
prefix: classpath:/templates/ # 默认配置可以不配置
4、在product包下新建web包
web包用于存放所有的页面跳转的,整合thymeleaf的controller
5、测试
启动product服务 访问 http://localhost:10000/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-13SwfTKI-1652453482293)(https://gitee.com/jiushuli/images/raw/master/image-20220428130358806.png)]
5、模板引擎
* 1)、thymeleaf-starter:关闭缓存
* 2)、静态资源都放在static文件夹下就可以按照路径直接访问
* 3)、页面放在templates下,直接访问
* SpringBoot,访问项目的时候,默认会找index
* 4)、页面修改不重启服务器实时更新
* 1)、引入dev-tools
* 2)、修改完页面 controller shift f9重新自动编译下页面,代码配置,推荐重启
137 商城业务-首页-整合dev-tools渲染一级分类数据
要实现不管访问的是localhost:10000 还是 localhost:10000/index都是访问的是首页,
1、先加上热启动的功能
加上依赖后 需要启动的话 ctrl + F9 即可编译
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
2、获取一级菜单的controller
package com.atguigu.gulimall.product.web;
import com.atguigu.gulimall.product.entity.CategoryEntity;
import com.atguigu.gulimall.product.service.CategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
/**
* @创建人: 放生
* @创建时间: 2022/4/28
* @描述:
*/
@Controller
public class IndexController {
@Autowired
CategoryService categoryService;
@GetMapping({"/","/index.html"})
public String indexPage(Model model){
System.out.println(""+Thread.currentThread().getId());
//TODO 1、查出所有的1级分类
List<CategoryEntity> categoryEntities = categoryService.getLevel1Categorys();
// 视图解析器进行拼串:
// classpath:/templates/ +返回值+ .html
model.addAttribute("categorys",categoryEntities);
return "index";
}
}
3、CategoryServiceImpl
@Override
public List<CategoryEntity> getLevel1Categorys() {
System.out.println("getLevel1Categorys.....");
long l = System.currentTimeMillis();
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return categoryEntities;
}
4、页面渲染index.html
先引入thymeleaf的标签
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!--轮播主体内容-->
<div class="header_main">
<div class="header_banner">
<div class="header_main_left">
<ul>
<li th:each="category : ${categorys}">
<a href="/static/#" class="header_main_left_a" th:attr="ctg-data=${category.catId}"><b
th:text="${category.name}">家用电器</b></a>
</li>
</ul>
..........
5、测试即可
138 商城业务-首页-渲染二级三级分类数据
在左侧显示一级菜单,鼠标放在一级上才显示对应的二三级菜单 。之前所有的数据是当鼠标放在一级菜单上是去catalogLoader.js调用的index/json/catalog.json中的数据,所以我们按照index/json/catalog.json的数据格式来写一个我们自己的接口
1、写个Catelog2Vo
Catelog2Vo 的数据格式就是参照index/json/catalog.json
package com.atguigu.gulimall.product.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @创建人: 放生
* @创建时间: 2022/4/28
* @描述:
*/
//2级分类vo
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Catelog2Vo {
private String catalog1Id; //1级父分类id
private List<Catelog3Vo> catalog3List; //三级子分类
private String id;
private String name;
/**
*
* 三级分类vo
* "catalog2Id":"1",
* "id":"1",
* "name":"电子书"
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public static class Catelog3Vo{
private String catalog2Id;//父分类,2级分类id
private String id;
private String name;
}
}
2、IndexController
//index/catalog.json
@ResponseBody
@GetMapping("/index/catalog.json")
public Map<String, List<Catelog2Vo>> getCatalogJson(){
Map<String, List<Catelog2Vo>> catalogJson = categoryService.getCatalogJson();
return catalogJson;
}
3、CategoryServiceImpl
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
System.out.println("查询了数据库.....");
List<CategoryEntity> selectList = baseMapper.selectList(null);
List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
//2、封装数据
Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//1、每一个的一级分类,查到这个一级分类的二级分类
List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
//2、封装上面面的结果
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
//1、找当前二级分类的三级分类封装成vo
List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());
if (level3Catelog != null) {
List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
//2、封装成指定格式
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(collect);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
return parent_cid;
}
private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList, Long parent_cid) {
List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid() == parent_cid).collect(Collectors.toList());
//return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
return collect;
}
4、catalogLoader.js中的请求路径改成我们的url
$(function(){
$.getJSON("index/catalog.json",function (data) {
var ctgall=data;
$(".header_main_left_a").each(function(){
var ctgnums= $(this).attr("ctg-data")
......