谷粒商城-07-p102-p138

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用传统关系性数据库

image-20220422215720180

弊端:

1、 对于传统的关系性数据库对于关键词的查询,只能逐字逐行的匹配,性能非常差。

2、匹配方式不合理,比如搜索“小密手机” ,如果用like进行匹配, 根本匹配不到。但是考虑使用者的用户体验的话,除了完全匹配的记录,还应该显示一部分近似匹配的记录,至少应该匹配到“手机”。

1.2专业 全文索引是怎么处理的

全文搜索引擎目前主流的索引技术就是倒排索引的方式。

img

传统的保存数据的方式都是

记录→单词

而倒排索引的保存数据的方式是

单词→记录

例如

搜索“红海行动”

但是数据库中保存的数据如图:

那么搜索引擎是如何能将两者匹配上的呢?

基于分词技术构建倒排索引:

首先每个记录保存数据时,都不会直接存入数据库。系统先会对数据进行分词,然后以倒排索引结构保存。如下:

image-20220422215840085

然后等到用户搜索的时候,会把搜索的关键词也进行分词,会把“红海行动”分词分成:红海和行动两个词。

这样的话,先用红海进行匹配,得到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的

image-20220422220226637

image-20220422220245191

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

image-20220422223951261

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

image-20220422224605750

105 全文检索-ElasticSearch-入门_cat

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

image-20220422225340581

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

PUTPOST 不带_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 返回时,它将提供每个动作的状态(与发送 的顺序相同),所以您可以检查是否一个指定的动作是不是失败了。

image-20220423000601636

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 风格的 DSLdomain-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


image-20220423103611056

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

自动猜测的映射类型

image-20220423113703557

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": "我是中国人" 
} 

请观察结果

image-20220423161026592

使用分词器

POST _analyze 
{ 
  "analyzer": "ik_smart", 
	"text": "我是中国人" 
} 

请观察结果

image-20220423161013433

另外一个分词器 ik_max_word

POST _analyze 
{ 
  "analyzer": "ik_max_word", 
  "text": "我是中国人" 
} 

请观察结果

image-20220423161041767

能够看出不同的分词器,分词有明显的区别,所以以后定义一个索引不能再使用默认的 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 下

image-20220423115159984

然后重启 es 服务器,重启 nginx。 在 kibana 中测试分词效果

image-20220423115227744

更新完成后,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 模块

image-20220423181009921

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> 即可,然后刷新一下

image-20220423181652056

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

image-20220423190947837

@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) { } 
  } 
}

image-20220423115443253

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中存储模型分析

image-20220423225331275

    
 # 整个商城的业务的商品在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 扁平化处理

image-20220423230226608

如图:我们在没有处理扁平化之前,在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就能单独的部署了。

image-20220428123621746

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目录下

image-20220428125304161

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、测试即可

image-20220428162227603

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")
            ......

5、测试即可

image-20220428172812524

gulimall_pms 商品 drop table if exists pms_attr; drop table if exists pms_attr_attrgroup_relation; drop table if exists pms_attr_group; drop table if exists pms_brand; drop table if exists pms_category; drop table if exists pms_category_brand_relation; drop table if exists pms_comment_replay; drop table if exists pms_product_attr_value; drop table if exists pms_sku_images; drop table if exists pms_sku_info; drop table if exists pms_sku_sale_attr_value; drop table if exists pms_spu_comment; drop table if exists pms_spu_images; drop table if exists pms_spu_info; drop table if exists pms_spu_info_desc; /*==============================================================*/ /* Table: pms_attr */ /*==============================================================*/ create table pms_attr ( attr_id bigint not null auto_increment comment '属性id', attr_name char(30) comment '属性名', search_type tinyint comment '是否需要检索[0-不需要,1-需要]', icon varchar(255) comment '属性图标', value_select char(255) comment '可选值列表[用逗号分隔]', attr_type tinyint comment '属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]', enable bigint comment '启用状态[0 - 禁用,1 - 启用]', catelog_id bigint comment '所属分类', show_desc tinyint comment '快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整', primary key (attr_id) ); alter table pms_attr comment '商品属性'; /*==============================================================*/ /* Table: pms_attr_attrgroup_relation */ /*==============================================================*/ create table pms_attr_attrgroup_relation ( id bigint not null auto_increment comment 'id', attr_id bigint comment '属性id', attr_group_id bigint comment '属性分组id', attr_sort int comment '属性组内排序', primary key (id) ); alter table pms_attr_attrgroup_relation comment '属性&
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值