写在前面面:
文章为作者原创
如需转载,请注明出处
4 电商平台全文检索与分词技术
背景引入
全文检索是通过扫描文本(大文本)中的每一个词,对每一个词建立一个索引,指明该词在文本中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引和位置进行查找
并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中检索字表查字的过程
使用全文检索技术可以实现搜索引擎(百度、google…),也可以搜索互联网上所有的内容(网页、pdf电子书、视频、音乐)。
使用全文检索技术可以实现站内搜索,站内搜索只能搜索本网站的信息(网页、pdf电子书、视频、音乐、关系数据库中的信息等等),比如:论坛网站搜索网内帖子,电商网站搜索商品信息,
本章,我们就使用Elasticsearch自身特性实现全文检索的功能
4.1 IK分词器安装
背景介绍:
在上面,我们刚才说了
全文检索是通过扫描文本(大文本)中的每一个词,对每一个词建立一个索引
那么这个词的建立就需要用到分词器
ES支持6种分词器,默认使用的是标准分词器(standard analyzer)
只是针对英文更友好的支持,所以;中文推荐使用IK分词器
1、什么是IK分词器
IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包。
结合词典分词和语法分析算法的中文分词组件,
4、ik的安装分为两种方式
1、直接下载ik压缩包https://github.com/medcl/elasticsearch-analysis-ik/releases
2、采用源代码编译的方式https://github.com/medcl/elasticsearch-analysis-ik/archive/v7.4.0.zip
无论使用哪种方式,一定要和我们使用的Elasticsearch版本对应,否则在启动Elasticsearch的时候会有各种各样的问题,下图是最新的IK版本
此处我们使用下载压缩包的方式进行构建
https://github.com/medcl/elasticsearch-analysis-ik/archive/v7.4.0.zip
将已有的jar包上传到/usr/local/es/config/plugins(已挂载)
#切换到插件目录
cd /usr/local/es/config/plugins
#创建ik目录
mkdir analysis-ik
#cd analysis-ik
#上传命令
SFTP
解压
#安装解压命令
yum install unzip
#执行解压
uzip elasticsearch-analysis-ik-7.4.0.zip
记得一定要重启Elasticsearch!!!
3.1.4 Ik分词器使用
IK分词器有两种分词模式:ik_max_word和ik_smart模式。
1、ik_max_word
会将文本做最细粒度的拆分
#方式一ik_max_word
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "华为手机销量全球第一"
}
ik_max_word分词器执行如下:
{
"tokens" : [
{
"token" : "华为",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "手机",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "销量",
"start_offset" : 4,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "全球",
"start_offset" : 6,
"end_offset" : 8,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "第一名",
"start_offset" : 8,
"end_offset" : 11,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "第一",
"start_offset" : 8,
"end_offset" : 10,
"type" : "CN_WORD",
"position" : 5
},
{
"token" : "一名",
"start_offset" : 9,
"end_offset" : 11,
"type" : "CN_WORD",
"position" : 6
},
{
"token" : "一",
"start_offset" : 9,
"end_offset" : 10,
"type" : "TYPE_CNUM",
"position" : 7
},
{
"token" : "名",
"start_offset" : 10,
"end_offset" : 11,
"type" : "COUNT",
"position" : 8
}
]
}
2、ik_smart
#方式二ik_smart
GET /_analyze
{
"analyzer": "ik_smart",
"text": "华为手机销量全球第一名"
}
ik_smart分词器执行如下:
{
"tokens" : [
{
"token" : "华为",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "手机",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "销量",
"start_offset" : 4,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "全球",
"start_offset" : 6,
"end_offset" : 8,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "第一名",
"start_offset" : 8,
"end_offset" : 11,
"type" : "CN_WORD",
"position" : 4
}
]
}
由此可见 使用ik_smart可以将文本"text": "华为手机销量全球第一"分成了【华为】【手机】【销量】【全球】【第一名】
这样看的话,这样的分词效果达到了我们的要求。
33.3 IK自定义词库
背景介绍
在利用ik分词的过程中,当ik的分词规则不满足我们的需求了,这个时候就可以利用ik的自定义词库进行筛选,举个例子:
- 每年都会涌现一些特殊的流行词,网红,蓝瘦香菇,喊麦,鬼畜,全面屏;一般不会在ik的原生词典里
- 自己补充自己的最新的词语,到ik的词库里面去
- 补充自己的词语,然后需要重启es,才能生效
1) 新建词库
到elasticsearch/plugins/analysis-ik中寻找ik插件所在的目录,在ik中的config文件中添加词库vimmydic.dic,输入你定义的词,例如:网红蓝瘦香菇喊麦 (注:一行输入一个词)
#打开config目录
cd /usr/local/es/config/plugins/analysis-ik/config
#写入文件
vi vimmydic.dic
如下图:
2) 修改ik配置
修改config中的IKAnalyzer.cfg.xml文件
#打开目录
cd /usr/local/es/config/plugins/analysis-ik/config
#修改配置文件
vi IKAnalyzer.cfg.xml
修改配置文件IKAnalyzer.cfg.xml如下图:
3) 重启Elasticsearch容器
docker restart 容器id
4) 效果演示
GET /_analyze
{
"analyzer": "ik_smart",
"text": "网红蓝瘦香菇喊麦全面屏"
}
使用之前
{
"tokens" : [
{
"token" : "网红",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "蓝瘦香菇",
"start_offset" : 2,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "喊麦",
"start_offset" : 6,
"end_offset" : 8,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "全面屏",
"start_offset" : 8,
"end_offset" : 11,
"type" : "CN_WORD",
"position" : 3
}
]
}
使用之后
由此可见,我们使用自定义词库可以将【网红蓝瘦香菇喊麦】分词为【网红、蓝瘦香菇、喊麦、全面屏】
4.2 商城平台数据准备
4.2.1 数据源
背景介绍
我们的终搜的平台当前所有的案例都是基于京东平台的数据
可以大体的看下磁盘的数据目录(各个品类;手机通讯、带脑)
电商产品数据5072825条数据
1、关系数据库查询
logstash增量、全量使用
SELECT
id,
trim( REPLACE ( product_name, ' ', '' ) ) AS productname,
price,
trim( REPLACE ( store_name, ' ', '' ) ) AS storename,
eval_count AS evalcount,
purchase_index AS purchaseindex,
trim( REPLACE ( store_type, ' ', '' ) ) AS storetype,
trim( REPLACE ( sku, ' ', '' ) ) AS sku,
trim( REPLACE ( one_level_category, ' ', '' ) ) AS onelevel,
trim( REPLACE ( two_level_category, ' ', '' ) ) AS twolevel,
trim( REPLACE ( three_level_category, ' ', '' ) ) AS threelevel,
trim( REPLACE ( four_level_category, ' ', '' ) ) AS fourlevel,
update_time AS updatetime
FROM
product_list
evalcount【选购指数】
select count(1) from product_list
4.2.2 全量同步
1、修改logstash.conf文件
修改上面(全量同步章节)的logstash.conf(/usr/local/logstash/config/conf.d)文件
input {
stdin {}
#使用jdbc插件
jdbc {
# mysql数据库驱动
#jdbc_driver_library => "/usr/share/logstash/logstash-core/lib/jars/mysql-connector-java-5.1.48.jar"
jdbc_driver_class => "com.mysql.jdbc.Driver"
# mysql数据库链接,数据库名
jdbc_connection_string => "jdbc:mysql://172.188.0.55:33066/product?characterEncoding=UTF-8&useSSL=false"
# mysql数据库用户名,密码
jdbc_user => "root"
jdbc_password => "root"
# 分页
jdbc_paging_enabled => "true"
# 分页大小
jdbc_page_size => "50000"
# sql语句执行文件,也可直接使用 statement => 'select * from t'
statement_filepath => "/usr/share/logstash/pipeline/sql/full_jdbc.sql"
#statement => " select * from product where id <=100 "
}
}
# 过滤部分(不是必须项)
filter {
json {
source => "message"
remove_field => ["message"]
}
}
# 输出部分
output {
elasticsearch {
# elasticsearch索引名
index => "product_list"
# elasticsearch的ip和端口号
hosts => ["172.188.0.88:9200","172.188.0.89:9201","172.188.0.90:9202"]
# 同步mysql中数据id作为elasticsearch中文档id
document_id => "%{id}"
}
stdout {
codec => json_lines
}
}
2、/usr/local/logstash/config/conf.d/sql目录修改full_jdbc.sql文件
cd /usr/local/logstash/config/conf.d/sql
vi full_jdbc.sql
full_jdbc内容如下
SELECT
id,
trim( REPLACE ( product_name, ' ', '' ) ) AS productName,
price,
trim( REPLACE ( store_name, ' ', '' ) ) AS storeName,
eval_count AS evalCount,
purchase_index AS purchaseIndex,
trim( REPLACE ( store_type, ' ', '' ) ) AS storeType,
trim( REPLACE ( sku, ' ', '' ) ) AS sku,
trim( REPLACE ( one_level_category, ' ', '' ) ) AS oneLevel,
trim( REPLACE ( two_level_category, ' ', '' ) ) AS twoLevel,
trim( REPLACE ( three_level_category, ' ', '' ) ) AS threeLevel,
trim( REPLACE ( four_level_category, ' ', '' ) ) AS fourLevel,
update_time AS updateTime ,
trim( REPLACE ( search_key, ' ', '' ) ) AS searchkey
FROM
product_list
3、打开Kibana创建索引和映射
创建索引和映射
坑
mysql---logstash---es
如果创建的映射是有大写的时候,es会自动转成小写
而且查看映射数据结构的时候会出现两个相同的字段(productname和productName)
这样就导致我们自己定义的映射无法使用,而有数据的是es自动生成的那个小写
PUT product_list_info
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"productname": {
"type": "text",
"analyzer": "ik_smart"
},
"searchkey": {
"type": "completion",
"analyzer": "ik_smart"
},
"price": {
"type": "double"
},
"storename": {
"type": "keyword"
},
"evalcount": {
"type": "integer"
},
"purchaseindex": {
"type": "float"
},
"storetype": {
"type": "keyword"
},
"sku": {
"type": "keyword"
},
"onelevel": {
"type": "keyword"
},
"twolevel": {
"type": "keyword"
},
"threelevel": {
"type": "keyword"
},
"fourlevel": {
"type": "keyword"
},
"updatetime": {
"type": "date"
}
}
}
}
4、查询索引以及数据
如果对映射没有硬性要求,可以忽略当前步骤,会自动创建索引
GET product_list_info/_search
上图显示无数据;"value" : 0表示命中0条
4、重启logstash进行全量同步
docker restart 82b89c39282a
查看日志
docker logs -f --tail=200 82b89c39282a
稍等片刻.........
5、再次查看索引以及数据
ps:大于过了20多分钟后
GET _cat/indices/product_list_info/?v
如上图所示文档数量为5072825;数据分布到了3个主分片和1个副本分片,其他分片大小为5.7G ,主分片为2.8G
下面的查找最精准
http://192.168.23.129:9200/_cat/count/product_list_info?v
5、遇到的问题
es集群数据量增速过快,如果磁盘空间过小;会导致个别es node节点磁盘使用率在%90以上 ,由于ES新节点的数据目录data存储空间不足,导致从master主节点接收同步数据的时候失败,此时ES集群为了保护数据,会自动把索引分片index置为只读read-only.
到达85的时候;就停止导入,不停重试.....
查看空间大小
df -h
解决方案一:
PUT _settings
{
"index": {
"blocks": {
"read_only_allow_delete": "false"
}
}
}
解决方案二
find / -type f -size +2000M
找cf94b7124ef90b9ea6a2a9c48494bd307661829daa245320a408fc392370fb14-json.log
路径是
/var/lib/docker/containers/cf94b7124ef90b9ea6a2a9c48494bd307661829daa245320a408fc392370fb14
使用黑洞设置为空
cat /dev/null > cf94b7124ef90b9ea6a2a9c48494bd307661829daa245320a408fc392370fb14-json.log
4.2.3 增量同步
使用方式与第三章节【数据收集引擎之Logstash数据同步】用法一模一样
4.3 分布式框架集成
4.3.1 分布式集成
背景介绍
Spring Cloud集成Elasticsearch、Spring Cloud alibaba
1、微服务介绍(起步依赖)
2、依赖关系
版本说明:
Spring Cloud 集成Spring Cloud alibaba
https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E
4.3.2 电商平台全文检索
京东搜索【华为全面屏】,搜索结果包含了华为各个产品线的数据【手机】【全面屏手机】【手机贴膜】【华为笔记本】【华为电视】等等...
搜索结果(手机)
搜索结果(钢化膜)
搜索结果(笔记本、电视)
实现目标
模拟京东的全文检索,主要体验在以下几个维度
1、通过关键字进行全文匹配产品搜索
2、从海量数据中索取数据做到近实时
3、获取全部产品数量以及实现分页逻辑
4、检索后的产品进行关键字高亮显示
4.3.3 传统实现方式
1、MySql的实现方式
查询包含【华为全面屏】的所有数据
SELECT
id,
trim( REPLACE ( product_name, ' ', '' ) ) AS productname,
price,
trim( REPLACE ( store_name, ' ', '' ) ) AS storename,
eval_count AS evalcount,
purchase_index AS purchaseindex,
trim( REPLACE ( store_type, ' ', '' ) ) AS storetype,
trim( REPLACE ( sku, ' ', '' ) ) AS sku,
trim( REPLACE ( one_level_category, ' ', '' ) ) AS onelevel,
trim( REPLACE ( two_level_category, ' ', '' ) ) AS twolevel,
trim( REPLACE ( three_level_category, ' ', '' ) ) AS threelevel,
trim( REPLACE ( four_level_category, ' ', '' ) ) AS fourlevel,
update_time AS updatetime
FROM
product_list
WHERE
product_name LIKE '%华为全面屏%' LIMIT 0,10000
不考虑两个like语句进行union操作
效率太低无法使用
查询结果
4.3.4 终搜平台OpenAPI实现方式
背景介绍
Open API 即开放 API,也称开放平台。 所谓的开放 API(OpenAPI)是服务型网站常见的一种应用,网站的服务商将自己的网站服务封装成一系列
API(Application Programming Interface,应用编程接口)开放出去,供第三方开发者使用,这种行为就叫做开放网站的 API,所开放的 API 就被称作 OpenAPI(开放 API )
通过终搜实现全文检索,虽然我们以京东电商平台为背景,但是终搜全文检索的能力包含、但不限于电商
我们通过对API高度封装,终搜可无缝各类型系统,比如B2B C2C、O2O等
1、Es高阶客户端连接Elasticsearch:
步骤:
1、Es高阶客户端连接Elasticsearch代码编写
2、全文检索代码编写:
1)配置编写与全文检索编写
1、Es高阶客户端连接Elasticsearch:
package com.itheima.config;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Class: ElasticsearchConfig
* @Package com.itheima.config
* @Description: 配置类:构建Elasticsearch客户端连接配置
* @date 2020/3/19 16:46
* @Company: http://www.itheima.com/
*/
@Configuration
@ConfigurationProperties(prefix = "elasticsearch")
public class ElasticsearchConfig {
//es集群ip
private String cluster_host;
//es集群节点一端口
private Integer eNode1_port;
//es集群节点二端口
private Integer eNode2_port;
//es集群节点三端口
private Integer eNode3_port;
/**
* 超时时间设为5分钟
*/
private static final int TIME_OUT = 5 * 60 * 1000;
/*
* @Description:
* @Method: Es高阶客户端构建器
* @Param:
* @Date: 2020/4/17 11:31
* @Update:
* @since: 1.0.0
* @Return:
*
*/
@Bean
public RestClientBuilder restClientBuilder() {
return RestClient.builder(
new HttpHost(cluster_host, eNode1_port, "http"),
new HttpHost(cluster_host, eNode2_port, "http"),
new HttpHost(cluster_host, eNode3_port, "http"));
}
/*
* @Description: 构建Es高阶客户端
* @Method: highLevelClient
* @Param: [restClientBuilder]
* @Date: 2020/4/17 13:54
* @Update:
* @since: 1.0.0
* @Return: org.elasticsearch.client.RestHighLevelClient
*
*/
@Bean(destroyMethod = "close")
public RestHighLevelClient highLevelClient(@Autowired RestClientBuilder restClientBuilder) {
restClientBuilder.setRequestConfigCallback(
new RestClientBuilder.RequestConfigCallback() {
@Override
public RequestConfig.Builder customizeRequestConfig(
RequestConfig.Builder requestConfigBuilder) {
return requestConfigBuilder.setSocketTimeout(TIME_OUT);
}
});
return new RestHighLevelClient(restClientBuilder);
}
public String getCluster_host() {
return cluster_host;
}
public void setCluster_host(String cluster_host) {
this.cluster_host = cluster_host;
}
public Integer geteNode1_port() {
return eNode1_port;
}
public void seteNode1_port(Integer eNode1_port) {
this.eNode1_port = eNode1_port;
}
public Integer geteNode2_port() {
return eNode2_port;
}
public void seteNode2_port(Integer eNode2_port) {
this.eNode2_port = eNode2_port;
}
public Integer geteNode3_port() {
return eNode3_port;
}
public void seteNode3_port(Integer eNode3_port) {
this.eNode3_port = eNode3_port;
}
}
全文检索代码编写:
1、编写全文检索接口
package com.itheima.service;
import com.itheima.commons.pojo.CommonEntity;
import org.elasticsearch.action.DocWriteResponse;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import java.util.List;
import java.util.Map;
/**
* @Class: ElasticsearchDocumentService
* @Package com.itheima.service
* @Description: 文档操作接口
* @date 2020/3/19 23:13
* @Company: http://www.itheima.com/
*/
public interface ElasticsearchDocumentService {
//全文检索
public SearchResponse matchQuery(CommonEntity commonEntity) throws Exception;
}
CommonEntity公共实体类介绍
2、编写全文检索实现
/*
* @Description: 全文检索
* 使用matchQuery在执行查询时,搜索的词会被分词器分词
* @Method: searchMatch
* @Param: [indexName, key, value]
* @Date: 2020/3/20 15:08
* @Update:
* @since: 1.0.0
* @Return: org.elasticsearch.search.SearchHit[]
*
*/
public SearchResponse matchQuery(CommonEntity commonEntity) throws Exception {
//构建查询响应
SearchResponse response = null;
//构建查询请求用来完成和搜索文档,聚合,建议等相关的任何操作同时也提供了各种方式来完成对查询结果的高亮操作。
SearchRequest searchRequest = new SearchRequest(commonEntity.getIndexName());
//构建DSL请求体;trackTotalHits如果不设置true,查询数据最大值还是10000
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().trackTotalHits(true);
//获取前端的查询条件(Map查询条件)
getClientConditions(commonEntity, searchSourceBuilder);
//高亮设置
searchSourceBuilder.highlighter(SearchTools.getHighlightBuilder(commonEntity.getHighlight()));
//前端页码
int pageNumber = commonEntity.getPageNumber();
//前端每页数量
int pageSize = commonEntity.getPageSize();
//通过每页数量和页数计算出从哪个下标from 开始查询,
searchSourceBuilder.from((pageNumber == 0 || pageSize == 0) ? START_OFFSET : (pageNumber == 1 ? 0 : pageNumber * pageSize + 1));
//每页数量
searchSourceBuilder.size(pageSize == 0 ? MAX_COUNT : pageSize);
//查询条件对象放入请求对象中
searchRequest.source(searchSourceBuilder);
//方法执行开始时间
long startTime = System.currentTimeMillis();
System.out.println("开始Elasticsearch查询...");
//执行远程查询
response = client.search(searchRequest, RequestOptions.DEFAULT);
//计算远程查询耗时
System.out.println("结束Elasticsearch查询总耗时:" + (System.currentTimeMillis() - startTime) + "毫秒");
//处理高亮
SearchTools.setHighResultForCleintUI(response, commonEntity.getHighlight());
return response;
}
3、编写控制器
@RestController
@RequestMapping("v1/docs")
public class ElasticsearchDocController {
private static final Logger logger = LoggerFactory
.getLogger(ElasticsearchDocController.class);
@Autowired
ElasticsearchDocumentService elasticsearchDocumentService;
/*
* @Description: 全文检索
* @Method: matchQuery
* @Param: [commonEntity]
* @Date: 2020/3/22 3:09
* @Update:
* @since: 1.0.0
* @Return: com.itheima.commons.result.ResponseData
*
*/
@GetMapping(value = "/mquery")
public ResponseData matchQuery(@RequestBody CommonEntity commonEntity) {
// 构造返回数据
ResponseData rData = new ResponseData();
//批量查询返回结果
SearchResponse result = null;
try {
//通过高阶API调用批量新增操作方法
result = elasticsearchDocumentService.matchQuery(commonEntity);
//查询数量除以每页数量 等于合计分页数量
long aSize = result.getHits().getTotalHits().value;
logger.info("总数据量:" + aSize + "条");
int cSize = result.getHits().getHits().length;
logger.info("当前获取数据:" + cSize + "条");
//通过类型推断自动装箱(多个参数取交集)
rData.setResultEnum(result.getHits().getHits(), ResultEnum.success, Integer.valueOf(String.valueOf(aSize)));
//日志记录
logger.info(TipsEnum.batch_get_doc_success.getMessage());
} catch (Exception e) {
//打印到控制台
e.printStackTrace();
//日志记录
logger.error(TipsEnum.batch_get_doc_fail.getMessage());
//构建错误返回信息
rData.setResultEnum(ResultEnum.error);
}
return rData;
}
}
4、配置文件编写
http://172.17.0.225:8848/nacos/
service-elasticsearch-dev.yml
server:
port: 8888
spring:
sleuth:
sampler:
probability: 1.0 # 将采样比例设置为 1.0,也就是全部都需要。默认是 0.1
zipkin:
base-url: http://172.17.0.225:9411/ # 指定了 Zipkin 服务器的地址
cloud:
nacos:
discovery:
server-addr: 172.17.0.225:8848,172.17.0.225:8849,172.17.0.225:8850
elasticsearch:
cluster_host: 172.17.0.225
eNode1_port: 9200
eNode2_port: 9201
eNode3_port: 9202
bootstrap.yml
spring:
application:
name: service-elasticsearch #微服务名称,对应dataId
profiles:
active: dev
cloud:
nacos:
config:
#配置中心服务地址
server-addr: 172.17.0.225:8848,172.17.0.225:8849,172.17.0.225:8850
#配置中心文件类型
file-extension: yml
#配置中心编码
encode: utf-8
management:
endpoints:
web:
exposure:
include: '*'
server-addr:
C:\Users\My\nacos\config
2) 服务网关路由全局配置
bootstrap.yml
spring:
application:
name: service-gateway
profiles:
active: dev
cloud:
nacos:
config:
#配置中心文件类型
file-extension: yml
#配置中心服务地址
server-addr: 172.17.0.225:8848,172.17.0.225:8849,172.17.0.225:8850
#配置中心编码
encode: utf-8
service-gateway-dev.yml
server:
port: 6666
spring:
sleuth:
sampler:
probability: 1.0 # 将采样比例设置为 1.0,也就是全部都需要。默认是 0.1
zipkin:
base-url: http://172.17.0.225:9411/ # 指定了 Zipkin 服务器的地址
application:
name: service-gateway
cloud:
nacos:
discovery:
server-addr: 172.17.0.225:8848,172.17.0.225:8849,172.17.0.225:8850
gateway:
discovery:
#为true表明Gateway开启服务注册和发现的功能
locator:
#是否与服务发现组件进行结合,通过 serviceId 转发到具体的服务实例。默认为false,设为true便开启通过服务中心的自动根据 serviceId 创建路由的功能。
#路由访问方式:http://Gateway_HOST:Gateway_PORT/serviceId/**,其中微服务应用名默认大写访问。
enabled: false
#默认false;serviceId为大写
lowerCaseServiceId: false
routes: #配置路由
- id: service-elasticsearch
uri: lb://service-elasticsearch
predicates:
# 匹配路径转发
- Path=/v1/docs/**,/v1/indices/**
- id: service-analysis
uri: lb://service-analysis
predicates:
# 匹配路径转发
- Path=/v1/analysis/**
4、Postman访问
http://172.17.0.225:6666/v1/docs/mquery
参数
{
"pageNumber": 0,
"pageSize": 1,
"indexName": "product_list_info",
"highlight": "productname",
"map": {
"productname": "华为全面屏"
}
}
返回
{
"code": "200",
"desc": "操作成功!",
"data": [
{
"score": 8.343819,
"id": "5818822",
"type": "_doc",
"nestedIdentity": null,
"version": -1,
"seqNo": -2,
"primaryTerm": 0,
"fields": {},
"highlightFields": {
"productname": {
"name": "productname",
"fragments": [
{
"fragment": true
}
],
"fragment": true
}
},
"sortValues": [],
"matchedQueries": [],
"explanation": null,
"shard": null,
"index": "product_list_info",
"clusterAlias": null,
"sourceAsMap": {
"twolevel": "功能箱包",
"storename": "鹏若拼购店",
"evalcount": 415097,
"threelevel": "旅行配件",
"storetype": "店铺发货",
"@timestamp": "2020-03-21T19:49:03.148Z",
"purchaseindex": "0",
"onelevel": "箱包皮具",
"price": 40.6,
"fourlevel": null,
"@version": "1",
"productname": "<span style= color:red;font-weight:bold;font-size:15px;>华为</span>watch2保护壳<span style= color:red;font-weight:bold;font-size:15px;>华为</span>WatchGT手表保护壳<span style= color:red;font-weight:bold;font-size:15px;>华为</span>watch2pro黑色适用:<span style= color:red;font-weight:bold;font-size:15px;>华为</span>watch2Pro",
"id": 5818822,
"sku": "54011029913",
"updatetime": "2020-03-18T05:01:57.000Z"
},
"innerHits": null,
"sourceRef": {
"fragment": true
},
"sourceAsString": "{\"storetype\":\"店铺发货\",\"@version\":\"1\",\"@timestamp\":\"2020-03-21T19:49:03.148Z\",\"price\":40.6,\"sku\":\"54011029913\",\"onelevel\":\"箱包皮具\",\"id\":5818822,\"evalcount\":415097,\"purchaseindex\":\"0\",\"updatetime\":\"2020-03-18T05:01:57.000Z\",\"fourlevel\":null,\"twolevel\":\"功能箱包\",\"threelevel\":\"旅行配件\",\"storename\":\"鹏若拼购店\",\"productname\":\"\\n华为watch2保护壳华为WatchGT手表保护壳华为watch2pro黑色适用:华为watch2Pro\"}",
"rawSortValues": [],
"fragment": false
}
],
"count": 70854
}
【笔记本 】
[全面屏手机]
高亮显示:在线html编辑器
https://c.runoob.com/front-end/61
HTML代码如下:
<span style= color:red;font-weight:bold;font-size:15px;>华为</span>(HUAWEI)<span style= color:red;font-weight:bold;font-size:15px;>华为</span>mate20pro手机<span style= color:red;font-weight:bold;font-size:15px;>全面屏</span>亮黑色全网通8GB+128GB
4.3.5 底层实现原理
1、产品多品类查询原理
为什么会检索到华为相关的不同产品
分通过词
GET /_analyze
{
"analyzer": "ik_smart",
"text": "华为全面屏"
}
分词结果(自定义词库)
2、为什么会分词
GET product_list_info/_mapping
分词器需要在mapping中指定,一旦指定就不能再修改
2、NRT近实时
为什么从500多万查询中只用了300多毫秒
倒排索引:
倒排索引简介
tips
选讲
在关系数据库系统里,索引是检索数据最有效率的方式。相对于es,它可称为正排索引
关系型数据库中一般存储的都是结构化的数据,数据格式都是一定的,操作上一般也都是curd等比较简单的操作。
但对于搜索引擎,它并不能满足其特殊要求,比如海量数据下。比如百度或者谷歌要搜索百亿级的网页,如果使用类似关系型数据库使用的B+树索引,可想而知其对cpu的计算能力要求得有多高。
倒排索引区别于正向索引,一般的倒排索引被用来做全文搜索。
比如现在有海量的球员信息,我们要从这些球员信息中通过某个词条查找出球员名字携带这个词条的所有球员信息
正排索引:遍历这些球员信息,记录球员信息出现的位置。(上面的例子即使不查询华为全面屏,只查询手机,结果肯定是对的,但是时间太久,like无法索引)
like '%全面屏'; 不会触发索引
like '%1全面屏'; 不会触发索引
like '全面屏%'; 会触发索引,但是数据不对了
那么如果我们使用倒排索引该怎么实现呢,比如:
如果我们通过james查找到名字带有james的球员呢
如果按照正排索引查找的算法,我们是不是得把这5个文档遍历一遍,把文档带有james的球员查找出来?
如果按照这种顺序扫描,那每次输入不同的关键字,岂不是要从头到尾遍历一遍,这样的话,在海量数据检索,效率是非常不可观的。
如上图,我们把这个5个球员的名字进行分词,每个分词转成小写字母,并且以每个分词分组,统计它 所在文档的位置。 当有关键字请求过来的时候,将关键字转成小写,查找出关键字匹配到的文档位置,然后全 部返回。
这样的话,查询数据的效率明显上升
完整的倒排索引列表如下图:
参数解释:
DocId:单词出现的文档id
TF:单词在某个文档中出现的次数
POS:单词在文档中出现的位置
我们把所有些球员信息通过分词,得到一个新的球员索引列表,这个列表记录了所有词条出现的文档id、出现的测试以及出现的位置,只要客户端请求某个词条做全文检索的时候,我们就可以把索引列表的信息全部返回,快速提高了查询效率。
3、集群数据读取原理
tips:
选讲
1)查看文档在哪个分片上
我们通过_search_shards来查找我们刚才创建的所以对应id为【1】的文档存放到了哪个节点哪个分片,routing=1表示路由值,表示id为【1】的文档
GET product_list_info/_search_shards?routing=1
执行结果如下
{
"nodes" : {
"zrZh5FpFTVODE5BfVzkOVg" : {
"name" : "eNode3",
"ephemeral_id" : "RMmMVv_NQWi1YExfwycfdg",
"transport_address" : "172.18.0.90:9900",
"attributes" : {
"ml.machine_memory" : "9089953792",
"ml.max_open_jobs" : "20",
"xpack.installed" : "true"
}
},
"NwskJ_GHRlWIL2rJKlIEhA" : {
"name" : "eNode1",
"ephemeral_id" : "ukjypMDNR2u6uokJr4672A",
"transport_address" : "172.18.0.88:9700",
"attributes" : {
"ml.machine_memory" : "9089953792",
"xpack.installed" : "true",
"ml.max_open_jobs" : "20"
}
}
},
"indices" : {
"product_list_info" : { }
},
"shards" : [
[
{
"state" : "STARTED",
"primary" : false,
"node" : "zrZh5FpFTVODE5BfVzkOVg",
"relocating_node" : null,
"shard" : 0,
"index" : "product_list_info",
"allocation_id" : {
"id" : "stci85_RRje_HpBiUXDRFw"
}
},
{
"state" : "STARTED",
"primary" : true,
"node" : "NwskJ_GHRlWIL2rJKlIEhA",
"relocating_node" : null,
"shard" : 0,
"index" : "product_list_info",
"allocation_id" : {
"id" : "FooiskH2Tdqn_A9bUjaqFA"
}
}
]
]
}
由返回的结果可知:返回了一个【nodes】里面包含了【eNode1】和【eNode3】节点
返回了一个【shards】里面包含了两个【0】分片,而其中一个0分片【"primary" : true】对应的节点是【"node" : "NwskJ_GHRlWIL2rJKlIEhA"】,所以,我们id为【10001】的文档被放在了节点id为【NwskJ_GHRlWIL2rJKlIEhA】的节点上,也就是【eNode1】节点的主分片【0】上
总结,这个地方有点绕,其实就是先找分片,找到主分片,然后主分片会对应一个【node】,通过【ndoe】的id找到节点名称。
4.4 全文检索遇到的问题
4.4.1 深度受限
坑
关于搜索elasticsearch的数据条数大于10000的坑 max_result_window的两种设置方式
当用elasticsearch进行深度分页查询时的size-from大于10000的时候,就会报错“”
Caused by: ElasticsearchException[Elasticsearch exception [type=illegal_argument_exception, reason=Result window is too large, from + size must be less than or equal to: [10000] but was [30001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting.]]
at org.elasticsearch.ElasticsearchException.innerFromXContent(ElasticsearchException.java:491)
at org.elasticsearch.ElasticsearchException.fromXContent(ElasticsearchException.java:402)
at org.elasticsearch.ElasticsearchException.innerFromXContent(ElasticsearchException.java:432)
... 75 more
官方默认10000
index.max_result_window
The maximum value of from + size for searches to this index.
Defaults to 10000. Search requests take heap memory and time proportional to from + size and this limits that memory.
See Scroll or Search After for a more efficient alternative to raising this.
官方推荐是scroll查询返回结果是无序的不满足业务需求,所以还是通过设置最大返回结果数来达到我们的目的
然后我们可以通过以下方法设置:
第一种:
PUT product_list_info/_settings
{
"index":{
"max_result_window":6000000
}
}
返回
第二种:在config/elasticsearch.yml文件中的最后加上index.max_result_window: 100000000
4.5 微服务容器化部署
在构建镜像前,先把项目引用的起步依赖itheima-service-commons进行install
4.5.1 发布前的准备
1、暴露端口
POM配置了
这个标签,他的意思是Docker Maven插件在构建镜像的时候,需要通过rest和Docker通讯,而通讯的接口就是2375,所以,我们在构建镜像前,需要将Docker的2375端口暴露出来
- 进行VMware Docker或者Linux Docker,执行
vim /usr/lib/systemd/system/docker.service
如下图:
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock \
进入到VIM之后、点击键盘i(进入编辑模式),将红框的地方加入到对应的地方
然后键盘ESC退出、:wq!保存即可
接着重新加载docker配置,执行下面的命令
systemctl daemon-reload // 1,加载docker守护线程
systemctl restart docker // 2,重启docker
systemctl stop firewalld.service //3一定关闭防火墙
这样的话,我们的2375就成功对外暴露了
8. 打开telnet
下面将使用telnet测试下我们的2375端口是否成功打开,首先,打开操作系统(这里是win7)的telnet功能
第一步:打开系统的控制面板:
...
第二步:打开程序和功能
第三步:打开或关闭windows功能
第四步:将telnet客户端、telnet服务端勾选,点击确定按钮
第五步:打开cmd窗口,测试telnet功能是否打开,成功打开如下图
点击回车,如下图,出现这个界面说明telnet成功启用
9. 暴露端口测试
测试我们的2375端口是否成功打开
cmd窗口输入
telnet 172.17.0.225 2375
点击回车,出现下面的画面,说明2375端口暴露成功
出现下面的画面,说明2375端口暴露失败
2、下载基础镜像
在启动我们刚才生成的itheima-service-elasticsearch镜像前,我们需要下载JDK基础镜像,为我们的微服务提供一个JDK运行环境
打开Docker界面,输入下面的命令,如下:
docker pull openjdk:11
这个下载基础镜像的操作只操作一次
以后我们如果在把其他的微服务部署到Docker中的时候就使用就这个基础镜像就可以了,以后就不用下载基础镜像了.
4.5.2 查询服务发布
1、pom增加docker-maven-plugin
<build>
<plugins>
<!-- maven打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- 镜像构建、推送插件-->
<plugin>
<!-- Spotify公司开发的Maven插件-->
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>1.0.0</version>
<!--执行例如mvn clean package时,插件就自动构建Docker镜像。 要想实现这点,只须将插件的goal绑定在某个phase即可 -->
<executions>
<execution>
<!--就可将插件绑定在package这个phase上。也就是说,用户只须执行mvn clean package,就会自动执行mvn docker:build-->
<id>build-image</id>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
<configuration>
<!--覆盖已存在的标签 镜像-->
<forceTags>true</forceTags>
<!--镜像名称:命令规则为artifactId和版本,可以自定义镜像名称, 比如指定镜像名称 仓库/镜像名:标签:itheima/eureka:0.0.1-->
<imageName>${project.artifactId}:${project.version}</imageName>
<!--使用 Dockerfile,查找Dockfile文件-->
<dockerDirectory>src/main/resources</dockerDirectory>
<!-- 指定Docker仓库地址,需要暴露2375端口, 因为maven docker插件需要通过rest方式调用Docker API进行构建和上传镜像 -->
<dockerHost>http://172.17.0.225:2375</dockerHost>
<resources>
<!-- 指定资源文件 -->
<resource>
<!-- 指定要复制的目录路径,这里是当前目录 -->
<targetPath>/</targetPath>
<!-- 指定要复制的根目录,这里是target目录 -->
<directory>${project.build.directory}</directory>
<!-- 指定需要拷贝的文件,这里指最后生成的jar包 -->
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
<dependencies>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
上面的plugin主要是加入了Docker maven插件的管理,引入docker-maven-plugin插件后,
我们的模块itheima-service-elasticsearch就具备了Docker构建镜像、上传的能力了。
坑:(jdk8不存在这个问题)
docker-maven-plugin在Build image的时候会依赖
javax.activation包的DataSource类
Caused by: java.lang.ClassNotFoundException: javax.activation.DataSource
at org.codehaus.plexus.classworlds.strategy.SelfFirstStrategy.loadClass(SelfFirstStrategy.java:50)
at org.codehaus.plexus.classworlds.realm.ClassRealm.unsynchronizedLoadClass(ClassRealm.java:271)
at org.codehaus.plexus.classworlds.realm.ClassRealm.loadClass(ClassRealm.java:247)
javax.activation.DataSource类应该是jdk自带的,我们知道javax.activation从jdk11就已经被移除掉了,所以,,给插件添加依赖activation,也就是xml的最后一部分,放到外面不可以。
2. 增加Dockerfile
参数讲解:
需要一个基础镜像,可以是公共的或者是私有的, 后续构建会基于此镜像,如果同一个Dockerfile中建立多个镜像时,可以使用多个FROM指令VOLUME
: 配置一个具有持久化功能的目录,主机 /var/lib/docker 目录下创建了一个临时文件,并链接到容器的/tmp。改步骤是可选的,如果涉及到文件系统的应用就很有必要了。/tmp目录用来持久化到 Docker 数据文件夹,因为 Spring Boot 使用的内嵌 Tomcat 容器默认使用/tmp作为工作目录ARG
: 设置编译镜像时加入的参数, ENV 是设置容器的环境变量COPY
: 只支持将本地文件复制到容器 ,还有个ADD更强大但复杂点ENTRYPOINT
: 容器启动时执行的命令EXPOSE
: 8080 暴露镜像端口
#指定基础镜像,必须为第一个命令这里使用openjdk的基础镜像
FROM openjdk:11
#MAINTAINER: 维护者信息,这里是维护者是itheima
MAINTAINER itheima
#VOLUME:用于指定持久化目录,挂载镜像
VOLUME /itheima
#将本地文件添加到容器中,这里是从target下复制itheima-service-elasticsearch-1.0.0.RELEASE.jar到根目录
ADD itheima-service-elasticsearch-1.0.0.RELEASE.jar app.jar
#RUN:构建镜像时执行的命令,这里是设置时区
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' >/etc/timezone
EXPOSE 8888
#ENV:设置环境变量
ENV JAVA_OPTS=""
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-Dspring.profiles.active=pro","-jar","/app.jar"]
上面定义的是Dockerfile文件(上面提到:此处也可以不写Dockerfile,直接将Dockerfile文件内容写到我们的POM文件中也可以), 这样的话,我们的服务就完全具备了构建镜像和发布镜像的能力了,此处我们还是讲所有的打包镜像的脚本写到Dockfile文件中
Dockerfile文件根据模块pom中定义,我们需要把文件放到resources下面
3、构建镜像
这个环节是最重要最核心的步骤,我们之前把Docker maven插件、Dockerfile文件都编写好了,这个时候需要打包、自动构建镜像、自动上传镜像
然后,执行打包操作,如下图,点击右三角后,就开始执行打包操作
开始构建镜像,如下图
第一次构建镜像、上传镜像会下载大量的依赖,时间会稍微长一点
镜像构建成功后,查看控制台输出,如下图:
我们的镜像已经成功构建并上传到了Docker
4. Docker查看镜像
在上面我们的镜像已经成功构建并上传到了Docker,我们去VMware的Docker查看下,如下图
可以到看我们的itheima-service-elasticsearch镜像已经成功生成。
5. Docker启动镜像
1这里是启动我们的itheima-service-elasticsearch 的镜像,启动如下:
docker run --name service-elasticsearch --net czbkNetwork --ip 172.188.0.33 --privileged=true --restart=always -p 8888:8888 -d itheima-service-elasticsearch:1.0.0.RELEASE
2) 查看容器(镜像启动起来就叫容器):
docker ps
4.5.3 网关服务发布
步骤和上面一模一样,唯一的区别就是DockfIle文件和启动不一样
1、设置Dockfile文件
#指定基础镜像,必须为第一个命令这里使用openjdk的基础镜像
FROM openjdk:11
#MAINTAINER: 维护者信息,这里是维护者是itheima
MAINTAINER itheima
#VOLUME:用于指定持久化目录,挂载镜像
VOLUME /itheima
#将本地文件添加到容器中,这里是从target下复制itheima-service-gateway-1.0.0.RELEASE.jar到根目录
ADD itheima-service-gateway-1.0.0.RELEASE.jar app.jar
#RUN:构建镜像时执行的命令,这里是设置时区
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' >/etc/timezone
EXPOSE 6666
#ENV:设置环境变量
ENV JAVA_OPTS=""
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-Dspring.profiles.active=pro","-jar","/app.jar"]
2、启动
docker run --name service-gateway --net czbkNetwork --ip 172.188.0.22 --privileged=true -p 6666:6666 --restart=always -d itheima-service-gateway:1.0.0.RELEASE
3、Postman访问
http://172.17.0.225:6666/v1/docs/mquery
参数
{
"pageNumber": 0,
"pageSize": 1,
"indexName": "product_list_info",
"highlight": "productname",
"map": {
"productname": "华为全面屏"
}
}
写在后面:
技术中台下终搜技术解决方案 课程合计分为三天
持续更新
-
第一天内容介绍
ps:当前页面为第一天的【第一章节】
-
第二天内容介绍:
-
第三天内容介绍: