我做的项目是在慕课网买的
项目介绍
项目需求背景:模仿大众点评应用提供用户线下搜索推荐服务门店的需求
技术选型:后端业务:SpringBoot;后端存储:MySQL、mybatis接入;搜索系统:ElasticSearch、canal;推荐系统:spark mllib;前端页面:html,css,js。
项目介绍:基于大众点评搜索以及推荐业务,使用SpringBoot加mybatis结合前端模板构建运营后台管理功能,借助ElasticSearch,完成高相关性进阶搜索服务,并基于spark mllib构建个性化千人千面推荐系统。
项目模块:(1)用户模块:有用户登录、注册、搜索行为(2)运营后台模块:有商家创建,商家列表查询、商家启用禁用行为(3)商户模块:有商家入驻、更新、被评价、禁用行为(4)门店模块:有创建,定位,被搜索和被推荐行为
项目实现:(1)在搜索1.0结构中,以数据库的关键词模糊匹配方式结合线性计算公式给门店打分后排序输出给用户,在此基础之上,优化推荐结构,通过中文分词器完成中文分词,借助logstash-input-jdbc学习索引构建,通过定制化分词器及同义词扩展丰富搜索准确性,通过定制化canal中间件完成准实时增量索引接入。完成搜索2.0结构。
(2)推荐架构的完成:通过Spark Mllib的ALS算法完成个性化召回体系;通过Spark Mllib的LR算法完成个性化排序体系。至此完善了推荐系统
数据库:
CREATE TABLE dianpingdb
.user
(
id
int(11) NOT NULL,
created_at
datetime(0) NOT NULL DEFAULT ‘0000-00-00 00:00:00’,
updated_at
datetime(0) NOT NULL DEFAULT ‘0000-00-00 00:00:00’,
telphone
varchar(40) NOT NULL,
password
varchar(200) NOT NULL,
nick_name
varchar(40) NOT NULL,
gender
int(11) NOT NULL,
PRIMARY KEY (id
),
UNIQUE INDEX telphone_unique_index
(telphone
) USING BTREE
);
CREATE TABLE dianpingdb
.seller
(
id
int(0) NOT NULL AUTO_INCREMENT,
name
varchar(80) NOT NULL,
create_at
datetime(0) NOT NULL DEFAULT ‘0000-00-00 00:00:00’,
update_at
datetime(0) NOT NULL DEFAULT ‘0000-00-00 00:00:00’,
remark_score
decimal(2, 1) NOT NULL DEFAULT 0,
disabled_flag
int(0) NOT NULL DEFAULT 0,
PRIMARY KEY (id
)
);
CREATE TABLE dianpingdb
.category
(
id
int(0) NOT NULL AUTO_INCREMENT,
create_at
datetime(0) NOT NULL DEFAULT ‘0000-00-00 00:00:00’,
update_at
datetime(0) NOT NULL DEFAULT ‘0000-00-00 00:00:00’,
name
varchar(20) NOT NULL,
icon_url
varchar(200) NOT NULL,
sort
int(0) NOT NULL DEFAULT 0,
PRIMARY KEY (id
),
UNIQUE INDEX name_unique_in
(name
) USING BTREE
);
CREATE TABLE dianpingdb
.shop
(
id
int(0) NOT NULL AUTO_INCREMENT,
created_at
datetime(0) NOT NULL DEFAULT ‘0000-00-00 00:00:00’,
updated_at
datetime(0) NOT NULL DEFAULT ‘0000-00-00 00:00:00’,
name
varchar(80) NOT NULL DEFAULT ‘’,
remark_score
decimal(2, 1) NOT NULL DEFAULT 0,
price_per_man
int(0) NOT NULL DEFAULT 0,
latitude
decimal(10, 6) NOT NULL DEFAULT 0,
longitude
decimal(10, 6) NOT NULL DEFAULT 0,
category_id
int(0) NOT NULL DEFAULT 0,
tags
varchar(2000) NOT NULL,
start_time
varchar(200) NOT NULL DEFAULT ‘’,
end_time
varchar(200) NOT NULL DEFAULT ‘’,
address
varchar(200) NOT NULL DEFAULT ‘’,
seller_id
int(0) NOT NULL DEFAULT 0 AUTO_INCREMENT,
icon_url
varchar(100) NOT NULL DEFAULT ‘’,
PRIMARY KEY (id
)
);
canal介绍
Canal:canal是用java开发的基于数据库增量日志解析,提供增量数据订阅&消费的中间件。目前,canal主要支持了MySQL的binlog解析,解析完成后才利用canal client 用来处理获得的相关数据。(数据库同步需要阿里的otter中间件,基于canal)
工作原理:模拟MySQL slave的交互协议向MySQL Mater发送 dump协议,MySQL mater收到canal发送过来的dump请求,开始推送binary log给canal,然后canal解析binary log,再发送到存储目的地,比如MySQL,Kafka,Elastic Search
好处:canal的好处在于对业务代码没有侵入,因为是基于监听binlog日志去进行同步数据的。实时性也能做到准实时,其实是很多企业一种比较常见的数据同步的方案。
ES:ES 是使用 Java 编写的一种开源搜索引擎,它在内部使用 Lucene 做索引与搜索,通过对 Lucene 的封装,隐藏了 Lucene 的复杂性,取而代之的提供一套简单一致的 RESTful API。
Elasticsearch 介绍
Elasticsearch 是一个分布式、可扩展、近实时的搜索与数据分析引擎。
常见的正常索引
假设我们要通过id去查找content,我们会将ID做成索引,加快我们的查找
现在的问题是我们要去查到那些content中有b这两个词
首先想到的是依次去尝试content中是否有b,显然在时间复杂度上是很大的,尤其是数据库中数量很多时
索引建立:
1、全量索引构建:(原本没有数据,全量的将数据库的数据同步到索引内)使用whitespace分词器,可以从空格中隔开
Logstash-input-jdbc(管道):本质上是数据源,数据目标,同步方式组合生成的
Bin目录下新建一个mysql目录,引入mysql-conecter驱动包和数据库语句(jdbc.conf,jdbc.sql)
2、增量索引
新建一个data文件,将更新的时间记录到文件内,设置一个事件间隔,使得在五秒或十秒内更新一次,可以感知到数据库的变化
3、准实时增量索引:数据库一旦发生改动,立即更新
代码中使用rest client方式,只要连接任何一个node,发送http请求,就可以交互
(1)导包:elasticsearch、elasticsearch-rest-client、elasticsearch-rest-high-level-client
(2)配置,新建一个config文件夹,写一个配置类ElasticsearchRestClient,properties中声明es地址服务,在类中构造bean,highLevelClient,使用ip、port和http协议指定一个HTTPHost,最后使用它构造了一个RestClient
关系型数据库在做全量索引这块不是强项,所以使用关系型数据库将查询条件下发给es,然后es查询出来后,mysql根据id全部找到返回给前端
代码中采用阿里提供的jsonRequestObj,每构建一个query,将queryIndex++,这样如果后序拓展就可以以一种非常优雅的方式拓展代码,接入es字段中的排序模型(默认排序,低价排序),标签筛选
搜索进阶:
1、定制化中文分词器(扩展词库、同义词)
做法:进入一个es节点—进入config中—进入analysis-ik—新增一个new_word.dic文件夹—加入凯悦(加入你想不被分词的词语)—修改IKAnalyzer.cfg.xml配置—修改entry ext_dict配置扩展字典—copy到另外两个节点中
2、同义词扩展
做法:进入一个es节点—进入config中—进入analysis-ik—新增一个synomyms.txt文件夹—加入你想要加入的同义词—copy到另外两个节点中
—重建如下索引—重建完记得运行./logstash -f mysql/jdbc.conf
//定义支持同义词的门店索引结构
PUT /shop {
"settings": {
"number_of_replicas": 1,
"number_of_shards": 1
“analysis”:{
“filter”:{
“my_synonym_filter”:{
“type”:”synonym”,
“synonyms_path”:”analysis-ik/synonyms.txt”
}
}
},
“analyzer”:{
“ik_syno”:{
“type”:”custom”,
“tokenizer”:”ik_smart”,
“filter”:[“my_synonym_filter”]//会查询过滤词库
},
“ik_syno_max”:{
“type”:”custom”,
“tokenizer”:”ik_max_word”,
“filter”:[“my_synonym_filter”]
}
},
}
"mappings": {
"properties": {
"id":{"type":"integer"},
"name":{"type":"text","analyzer": "ik_syno_max", "search_analyzer": "ik_syno"},
"tags":{"type":"text","analyzer": "whitespace","fielddata":true}, "location":{"type": "geo_point"}, //经纬方式
"remark_score":{"type": "double"}, "price_per_man":{"type": "integer"}, "category_id":{"type": "integer"},
"category_name":{"type": "keyword"}, //不分词
"seller_id":{"type": "integer"},
"seller_remark_score":{"type": "double"},
"seller_disabled_flag":{"type": "integer"} } } }
3、重塑相关性
(1)相关性搜索
(2)让搜索引擎理解语义
(3)影响召回及排序
做法:采取词性影响召回策略
使得搜索住宿的时候也可以出来酒店
“function_score”: {
“query”: {
“bool”: {
“must”: [
{
“bool”: {
“should”: [
{ “match”: {“name”: {“query”:“住宿”,“boost”: 0.1}}},
{“term”: {“category_id”: {“value”:2,“boost”: 0.1}}}
]
}
},
{"term":{"seller_disabled_flag": 0}}
]
}
},
代码实现:
1、构造分词函数识别器init,借助hashmap存储categoryid和与之对应的相关类目名字
categoryWorkMap.put(1,new ArrayList<>());
categoryWorkMap.put(2,new ArrayList<>());
2、写对应的getCategoryIdByToken,如果tokenlist中包含这个token,则返回这个key
3、写主函数analyzeCategoryKeyword
private Map<String,Object> analyzeCategoryKeyword(String keyword) throws IOException {
Map<String,Object> res = new HashMap<>();
Request request = new Request("GET","/shop/_analyze");
request.setJsonEntity("{" + " \"field\": \"name\"," + " \"text\":\""+keyword+"\"\n" + "}");
Response response = highLevelClient.getLowLevelClient().performRequest(request);
String responseStr = EntityUtils.toString(response.getEntity());
JSONObject jsonObject = JSONObject.parseObject(responseStr);
JSONArray jsonArray = jsonObject.getJSONArray("tokens");
for(int i = 0; i < jsonArray.size(); i++){
String token = jsonArray.getJSONObject(i).getString("token");
Integer categoryId = getCategoryIdByToken(token);
if(categoryId != null){
res.put(token,categoryId);
}
}
return res;
}
4、修改之前的requestOBJ,加入这条shuould所对应的语句
(可以把boost设为0,就可以不影响打分,只影响召回)
Boolean isAffectFilter=true
5、java代码中也可以设置影响排序,但召回策略和排序策略一般不会共同使用
Functions函数下的filter,weight(权重)可以控制排序
Canal索引构建
准实时索引
Canal是一个消息管道,source为mysql数据库,还会有一个target作为其他存储
Canal工作原理,借助mysql主备复制原理,MySQL master将数据变更写入二进制文件(binlong),mysql slave将master的binlong拷贝到它的中继日志
Canal就是伪装成一个slave节点,canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议,mysql master收到dump请求,开始推送binary log给slave,canal解析binglong对象
我实现的就是canal的消费端
下载了canal.adapter1.1.3,又下载了canal.deployer
步骤
- 将mysql配置成master,修改my.conf 加入如下配置:server-id = 1; binlog_format = ROW; Log_bin = mysql_bin 重启mysql连接
- 授予slave一些权限,读、备份Grant select,replication slave,replication client on . to ‘canal’@’%’ identified by ‘canal’将权限刷新到本地 flush privileges
- 打开canal.properties配置文件,修改slaveID 和注意dbUsername dbpassword,再修改url,使得canal adapter只需要监听dianpingapp即可,保存,启动bin/ startup.sh
- 对应的是6.8及之前的版本才能使用,所以我们修改对应的canal.adapter,使用inport源码的方式inport出来
- 修改通过bin/startup.sh启动
- 代码层面接入
(1)接入canal client
导包:canal.client,canal.common,canal.protocol
@Bean
public CanalConnector getCanalConnector(){
canalConnector = CanalConnectors.newClusterConnector(Lists.newArrayList(
new InetSocketAddress("127.0.0.1", 11111)),
"example","canal","canal"
);
canalConnector.connect();
//指定filter,格式{database}.{table}
canalConnector.subscribe();
//回滚寻找上次中断的为止
canalConnector.rollback();
return canalConnector;
}
(2)接入消息消费模型
(3)接入消息处理模型
PUT /shop {
"settings": {
"number_of_replicas": 1, "number_of_shards": 1 },
"mappings": {
"properties": {
"id":{"type":"integer"},
"name":{"type":"text","analyzer": "ik_max_word", "search_analyzer": "ik_smart"},
"tags":{"type":"text","analyzer": "whitespace","fielddata":true}, "location":{"type": "geo_point"}, //经纬方式
"remark_score":{"type": "double"}, "price_per_man":{"type": "integer"}, "category_id":{"type": "integer"},
"category_name":{"type": "keyword"}, //不分词
"seller_id":{"type": "integer"},
"seller_remark_score":{"type": "double"},
"seller_disabled_flag":{"type": "integer"} } } }
@Override
public Map<String, Object> searchES(BigDecimal longitude, BigDecimal latitude, String keyword, Integer orderby, Integer categoryId, String tags) throws IOException {
Map<String, Object> result = new HashMap<>();
// SearchRequest searchRequest = new SearchRequest("shop");
// SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// sourceBuilder.query(QueryBuilders.matchQuery("name",keyword));
// sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
// searchRequest.source(sourceBuilder);
//
// List<Integer> shopIdsList = new ArrayList<>();
// SearchResponse searchResponse = highLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// SearchHit[] hits = searchResponse.getHits().getHits();
// for(SearchHit hit : hits){
// shopIdsList.add(new Integer(hit.getSourceAsMap().get("id").toString()));
// }//每一个hits相当于docemont命中的内容
Request request = new Request("GET","/shop/_search");
//构建请求
JSONObject jsonRequestObj = new JSONObject();
//构建source部分
jsonRequestObj.put("_source","*");
//构建自定义距离字段
jsonRequestObj.put("script_fields",new JSONObject());
jsonRequestObj.getJSONObject("script_fields").put("distance",new JSONObject());
jsonRequestObj.getJSONObject("script_fields").getJSONObject("distance").put("script",new JSONObject());
jsonRequestObj.getJSONObject("script_fields").getJSONObject("distance").getJSONObject("script")
.put("source","haversin(lat, lon, doc['location'].lat, doc['location'].lon)");
jsonRequestObj.getJSONObject("script_fields").getJSONObject("distance").getJSONObject("script")
.put("lang","expression");
jsonRequestObj.getJSONObject("script_fields").getJSONObject("distance").getJSONObject("script")
.put("params",new JSONObject());
jsonRequestObj.getJSONObject("script_fields").getJSONObject("distance").getJSONObject("script")
.getJSONObject("params").put("lat",latitude);
jsonRequestObj.getJSONObject("script_fields").getJSONObject("distance").getJSONObject("script")
.getJSONObject("params").put("lon",longitude);
//构建query
Map<String,Object> cixingMap = analyzeCategoryKeyword(keyword);
boolean isAffectFilter = false;
boolean isAffectOrder = true;
jsonRequestObj.put("query",new JSONObject());
//构建function score
jsonRequestObj.getJSONObject("query").put("function_score",new JSONObject());
//构建function score内的query
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").put("query",new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").put("bool",new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool").put("must",new JSONArray());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").add(new JSONObject());
//构建match query
int queryIndex = 0;
if(cixingMap.keySet().size() > 0 && isAffectFilter){
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).put("bool",new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("bool").put("should", new JSONArray());
int filterQueryIndex = 0;
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("bool").getJSONArray("should").add(new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("bool").getJSONArray("should").getJSONObject(filterQueryIndex)
.put("match",new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("bool").getJSONArray("should").getJSONObject(filterQueryIndex)
.getJSONObject("match").put("name",new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("bool").getJSONArray("should").getJSONObject(filterQueryIndex)
.getJSONObject("match").getJSONObject("name").put("query",keyword);
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("bool").getJSONArray("should").getJSONObject(filterQueryIndex)
.getJSONObject("match").getJSONObject("name").put("boost",0.1);
for(String key : cixingMap.keySet()) {
filterQueryIndex++;
Integer cixingCategoryId = (Integer) cixingMap.get(key);
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("bool").getJSONArray("should").add(new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("bool").getJSONArray("should").getJSONObject(filterQueryIndex)
.put("term", new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("bool").getJSONArray("should").getJSONObject(filterQueryIndex)
.getJSONObject("term").put("category_id", new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("bool").getJSONArray("should").getJSONObject(filterQueryIndex)
.getJSONObject("term").getJSONObject("category_id").put("value", cixingCategoryId);
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("bool").getJSONArray("should").getJSONObject(filterQueryIndex)
.getJSONObject("term").getJSONObject("category_id").put("boost", 0);
}
}else{
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).put("match",new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("match").put("name", new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("match").getJSONObject("name").put("query",keyword);
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("match").getJSONObject("name").put("boost",0.1);
}
queryIndex++;
//构建第二个query的条件
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").add(new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).put("term",new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("term").put("seller_disabled_flag",0);
if(tags != null){
queryIndex++;
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").add(new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).put("term",new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("term").put("tags",tags);
}
if(categoryId != null){
queryIndex++;
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").add(new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).put("term",new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONObject("query").getJSONObject("bool")
.getJSONArray("must").getJSONObject(queryIndex).getJSONObject("term").put("category_id",categoryId);
}
//构建functions部分
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").put("functions",new JSONArray());
int functionIndex = 0;
if(orderby == null) {
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").add(new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).put("gauss", new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).getJSONObject("gauss").put("location", new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).getJSONObject("gauss")
.getJSONObject("location").put("origin", latitude.toString() + "," + longitude.toString());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).getJSONObject("gauss")
.getJSONObject("location").put("scale", "100km");
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).getJSONObject("gauss")
.getJSONObject("location").put("offset", "0km");
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).getJSONObject("gauss")
.getJSONObject("location").put("decay", "0.5");
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).put("weight", 9);
functionIndex++;
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").add(new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).put("field_value_factor", new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).getJSONObject("field_value_factor")
.put("field", "remark_score");
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).put("weight", 0.2);
functionIndex++;
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").add(new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).put("field_value_factor", new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).getJSONObject("field_value_factor")
.put("field", "seller_remark_score");
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).put("weight", 0.1);
if(cixingMap.keySet().size() > 0 && isAffectOrder){
for(String key : cixingMap.keySet()){
functionIndex++;
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").add(new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).put("filter",new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).getJSONObject("filter")
.put("term",new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).getJSONObject("filter")
.getJSONObject("term").put("category_id",cixingMap.get(key));
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).put("weight",3);
}
}
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").put("score_mode","sum");
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").put("boost_mode","sum");
}else{
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").add(new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).put("field_value_factor",new JSONObject());
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").getJSONArray("functions").getJSONObject(functionIndex).getJSONObject("field_value_factor")
.put("field","price_per_man");
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").put("score_mode","sum");
jsonRequestObj.getJSONObject("query").getJSONObject("function_score").put("boost_mode","replace");
}
//排序字段
jsonRequestObj.put("sort",new JSONArray());
jsonRequestObj.getJSONArray("sort").add(new JSONObject());
jsonRequestObj.getJSONArray("sort").getJSONObject(0).put("_score",new JSONObject());
if(orderby == null){
jsonRequestObj.getJSONArray("sort").getJSONObject(0).getJSONObject("_score").put("order","desc");
}else{
jsonRequestObj.getJSONArray("sort").getJSONObject(0).getJSONObject("_score").put("order","asc");
}
//聚合字段
jsonRequestObj.put("aggs",new JSONObject());
jsonRequestObj.getJSONObject("aggs").put("group_by_tags",new JSONObject());
jsonRequestObj.getJSONObject("aggs").getJSONObject("group_by_tags").put("terms",new JSONObject());
jsonRequestObj.getJSONObject("aggs").getJSONObject("group_by_tags").getJSONObject("terms").put("field","tags");
String reqJson = jsonRequestObj.toJSONString();
System.out.println(reqJson);
request.setJsonEntity(reqJson);
Response response = highLevelClient.getLowLevelClient().performRequest(request);
String responseStr = EntityUtils.toString(response.getEntity());
System.out.println(responseStr);
JSONObject jsonObject = JSONObject.parseObject(responseStr);
JSONArray jsonArr = jsonObject.getJSONObject("hits").getJSONArray("hits");
List<ShopModel> shopModelList = new ArrayList<>();
for(int i = 0; i < jsonArr.size(); i++){
JSONObject jsonObj = jsonArr.getJSONObject(i);
Integer id = new Integer(jsonObj.get("_id").toString());
BigDecimal distance = new BigDecimal(jsonObj.getJSONObject("fields").getJSONArray("distance").get(0).toString());
ShopModel shopModel = get(id);
shopModel.setDistance(distance.multiply(new BigDecimal(1000).setScale(0,BigDecimal.ROUND_CEILING)).intValue());
shopModelList.add(shopModel);
}
List<Map> tagsList = new ArrayList<>();
JSONArray tagsJsonArray = jsonObject.getJSONObject("aggregations").getJSONObject("group_by_tags").getJSONArray("buckets");
for(int i = 0; i < tagsJsonArray.size();i++){
JSONObject jsonObj = tagsJsonArray.getJSONObject(i);
Map<String,Object> tagMap = new HashMap<>();
tagMap.put("tags",jsonObj.getString("key"));
tagMap.put("num",jsonObj.getInteger("doc_count"));
tagsList.add(tagMap);
}
result.put("tags",tagsList);
result.put("shop",shopModelList);
return result;
}
推荐做法:
规则
1、千人千面(不同手机号登录发现推荐的商品是不一样的)
2、场景决定推荐规则
方法
1、基于规则的推荐
2、基于传统机器学习的推荐(可以从用户历史行为)
3、基于深度学习(神经网络)
推荐模型
1、规则模型:规则定义,简单的算术公式
2、基于机器学习模型训练:数据训练后的算术公式
3、机器学习模型预测:待预测数据经过训练模型算数公式后的结果
模型评价指标
1、离线指标:查全率,查准率,auc等
2、在线指标:点击率,交易转化率等
3、A/B测试
比如我API传过来的是什么场景(美食,住宿……)—推荐服务负责场景规则选取(每个场景可能对应不同的模型)—选定模型之后进入核心
一般召回是海量数据,往往是一个离线预测的结果
个性化推荐召回算法ALS
1、本质上是运用最小二乘法
2、利用矩阵分解的结果无限逼近现有数据,得到隐含特征
3、利用隐含特征预测其余结果
比如说这样一个矩阵,3分代表user1浏览了pro1并且产生购买的行为,推荐算法要做的就是发掘其他未被购买和浏览商品的潜力,在开发中,往往没有分数的项目比有分数的项目多得多,所以系统要做那些分数为0的产品的分数高低
V‘=UP^T
1、将user矩阵和product矩阵的转置相乘;
2、获取无限逼近于真实数据的分数
3、同时预测其余节点的分数,排序后输出
隐式特征
将两个矩阵转置相乘相加,最后生成打分
粗排,基于隐式特征
//初始化spark运行环境
SparkSession spark = SparkSession.builder().master("local").appName("DianpingApp").getOrCreate();
//加载模型进内存
ALSModel alsModel = ALSModel.load("file:///Users/hzllb/Desktop/devtool/data/alsmodel");
JavaRDD<String> csvFile = spark.read().textFile("file:///Users/hzllb/Desktop/devtool/data/behavior.csv").toJavaRDD();
JavaRDD<Rating> ratingJavaRDD = csvFile.map(new Function<String, Rating>() {
@Override
public Rating call(String v1) throws Exception {
return Rating.parseRating(v1);
}
});
Dataset<Row> rating = spark.createDataFrame(ratingJavaRDD,Rating.class);
//给5个用户做离线的召回结果预测
Dataset<Row> users = rating.select(alsModel.getUserCol()).distinct().limit(5);
Dataset<Row> userRecs = alsModel.recommendForUserSubset(users,20);
userRecs.foreachPartition(new ForeachPartitionFunction<Row>() {
@Override
public void call(Iterator<Row> t) throws Exception {
//新建数据库链接
Connection connection = DriverManager.
getConnection("jdbc:mysql://127.0.0.1:3306/dianpingdb?" +
"user=root&password=root&useUnicode=true&characterEncoding=UTF-8");
PreparedStatement preparedStatement = connection.
prepareStatement("insert into recommend(id,recommend)values(?,?)");
List<Map<String,Object>> data = new ArrayList<Map<String, Object>>();
t.forEachRemaining(action->{
int userId = action.getInt(0);
List<GenericRowWithSchema> recommendationList = action.getList(1);
List<Integer> shopIdList = new ArrayList<Integer>();
recommendationList.forEach(row->{
Integer shopId = row.getInt(0);
shopIdList.add(shopId);
});
String recommendData = StringUtils.join(shopIdList,",");
Map<String,Object> map = new HashMap<String, Object>();
map.put("userId",userId);
map.put("recommend",recommendData);
data.add(map);
});
data.forEach(stringObjectMap -> {
try {
preparedStatement.setInt(1, (Integer) stringObjectMap.get("userId"));
preparedStatement.setString(2, (String) stringObjectMap.get("recommend"));
preparedStatement.addBatch();
} catch (SQLException e) {
e.printStackTrace();
}
});
preparedStatement.executeBatch();
connection.close();
}
});
个性化排序算法LR
1、逻辑回归
2、Y=ax1+bx2+cx3+dx4……(Y介于0–1)
3、计算拟合公式
蓝点表示被点击的,紫色点表示还未点击,我们要做的就是训练出这条红线,预判下一个点时我们只需要看这个点是在红色的上方还是下方
//初始化spark运行环境
SparkSession spark = SparkSession.builder().master("local").appName("DianpingApp").getOrCreate();
//加载特征及label训练文件
JavaRDD<String> csvFile = spark.read().textFile("file:///Users/hzllb/Desktop/devtool/data/feature.csv").toJavaRDD();
//做转化
JavaRDD<Row> rowJavaRDD = csvFile.map(new Function<String, Row>() {
@Override
public Row call(String v1) throws Exception {
v1 = v1.replace("\"","");
String[] strArr = v1.split(",");
return RowFactory.create(new Double(strArr[11]), Vectors.dense(Double.valueOf(strArr[0]),Double.valueOf(strArr[1]),
Double.valueOf(strArr[2]),Double.valueOf(strArr[3]),Double.valueOf(strArr[4]),Double.valueOf(strArr[5]),
Double.valueOf(strArr[6]),Double.valueOf(strArr[7]),Double.valueOf(strArr[8]),Double.valueOf(strArr[9]),Double.valueOf(10)));
}
});
StructType schema = new StructType(
new StructField[]{
new StructField("label", DataTypes.DoubleType,false, Metadata.empty()),
new StructField("features",new VectorUDT(),false,Metadata.empty())
}
);
Dataset<Row> data = spark.createDataFrame(rowJavaRDD,schema);
//分开训练和测试集
Dataset<Row>[] dataArr = data.randomSplit(new double[]{0.8,0.2});
Dataset<Row> trainData = dataArr[0];
Dataset<Row> testData = dataArr[1];
LogisticRegression lr = new LogisticRegression().
setMaxIter(10).setRegParam(0.3).setElasticNetParam(0.8).setFamily("multinomial");
LogisticRegressionModel lrModel = lr.fit(trainData);
lrModel.save("file:///Users/hzllb/Desktop/devtool/data/lrmode");
//测试评估
Dataset<Row> predictions = lrModel.transform(testData);
//评价指标
MulticlassClassificationEvaluator evaluator = new MulticlassClassificationEvaluator();
double accuracy = evaluator.setMetricName("accuracy").evaluate(predictions);
System.out.println("auc="+accuracy);
Spark原理讲解
1、Spark:大规模数据处理而设计的快速通用的计算引擎
2、Spark core,Spark Sql,Spark on Hive,Spark Streaming等
3、Spark mllib:机器学习库
Spark core,可以将原本某一种数据结构的方式转换为另一种数据结构
Spark sql:数据量非常大时,分别计算,最后通过reduce的操作汇总起来
Spark streaming:
代码接入:
1、导包(spark-mllib_2.12.2.4.4、guava.14.0.1)
2、导入数据,csv文件有三类数据,userid shopid 评分
3、在recommend下,创建AlsRecall类,内部类Rating中包含三个数据,userID shopid rating,以此来作为矩阵
4、Main函数:
(1)初始化spark运行环境
SparkSession spark = SparkSession.builder().master(“local”).appName(“DianpingApp”).getOrCreate();
(2)将csvFile存放成JavaRDD形式,然后转为map形式,再定义一个Dataset的rating,这个相当于数据库对应的一个表,将所有的rating数据分成82份
//过拟合:增大数据规模,减少RANK,增大正则化的系数
//欠拟合:增加rank,减少正则化系数
ALS als = new ALS().setMaxIter(10).setRank(5).setRegParam(0.01).
setUserCol("userId").setItemCol("shopId").setRatingCol("rating");
//模型训练
ALSModel alsModel = als.fit(trainingData);
//模型评测
Dataset<Row> predictions = alsModel.transform(testingData);
//rmse 均方根误差,预测值与真实值的偏差的平方除以观测次数,开个根号
RegressionEvaluator evaluator = new RegressionEvaluator().setMetricName("rmse")
.setLabelCol("rating").setPredictionCol("prediction");
//数值越小代表表现越好
double rmse = evaluator.evaluate(predictions);
System.out.println("rmse = "+rmse);
alsModel.save("file:///Users/hzllb/Desktop/devtool/data/alsmodel");
离线预测AlsRecallPredict
1、初始化spark运行环境
2、加载模型进内存
3、给5个用户做离线的召回结果预测
4、在userRecs这一list中再做遍历拿到shopid
5、想办法把结果存储起来,在分布式环境下,单纯使用springboot的注入是不可行的,因为这个foreachPartition是执行在各个节点中,所以我们需要在call方法下新建数据库的连接,
个性化排序算法实现:
特征处理:
1、离散特征:one-hot编码
2、连续特征:z-score标准化(x-mean)/std
3、连续特征:max-min标准化(x-min)/(max-min)
4、连续特征离散化:bucket编码
将future生成特征文件
代码实现:
1、初始化spark运行环境
2、加载特征及label训练文件
3、做转化,将训练文件转化为row,一个11位的double再加一个Vector,再定义一个schema,以rowJavaRDD和schema一起做一个dataset
4、分开训练集和测试集,还是以28来分
5、模型初始化
LogisticRegression lr = new LogisticRegression().
setMaxIter(10).setRegParam(0.3).setElasticNetParam(0.8).setFamily(“multinomial”);
6、调用fit方法传入trainData即可,得到一个lrModel
7、进行预测调用transform方法
8、评价指标
离线召回:离线运行召回算法,从海量数据中拿出对应的候选数据集并预存入某种存储中,供在线系统直接拿取对应场景的召回数据。
由于召回是从海量数据中过滤出一部分数据,运算时效往往很长,因此一般都采用离线召回算法。
点评推荐接入
1、改造shopService的recommend方法
2、构建RecommendService类,具有recall方法,根据userid召回shopid的list
select
from recommend
where id = #{id,jdbcType=INTEGER}
3、使用mybatis-generator将recommend表导入
使用recommendDOMapper.selectByPrimaryKey(userId);得到recommendDO
排序:在召回出的候选推荐数据集内利用算法给每个结果集打分,最终排序出对应候选集内top n的数据并返回外部系统
排序内分为:
在线排序:实时运行排序算法,给对应召回的数据集打分并输出
点评排序接入
1、构建RecommendSortService类
2、在init方法下,加载LR模型,初始化spark运行环境和设置lrModel路径
3、在sort方法下,需要根据lrModel所需要的11位的x,生成特征,然后调用其预测方法;我们可以从数据库中拿到对应的数据
4、拿到shopmodel的list后做一个排序的操作,重写compara方法,根据分数排序
@PostConstruct
public void init(){
//加载LR模型
spark = SparkSession.builder().master("local").appName("DianpingApp").getOrCreate();
lrModel = LogisticRegressionModel.load("file:///Users/hzllb/Desktop/devtool/data/lrmode");
}
public List<Integer> sort(List<Integer> shopIdList,Integer userId){
//需要根据lrmode所需要11唯的x,生成特征,然后调用其预测方法
List<ShopSortModel> list = new ArrayList<>();
for(Integer shopId : shopIdList){
//造的假数据,可以从数据库或缓存中拿到对应的性别,年龄,评分,价格等做特征转化生成feture向量
Vector v = Vectors.dense(1,0,0,0,0,1,0.6,0,0,1,0);
Vector result = lrModel.predictProbability(v);
double[] arr = result.toArray();
double score = arr[1];
ShopSortModel shopSortModel = new ShopSortModel();
shopSortModel.setShopId(shopId);
shopSortModel.setScore(score);
list.add(shopSortModel);
}
list.sort(new Comparator<ShopSortModel>() {
@Override
public int compare(ShopSortModel o1, ShopSortModel o2) {
if(o1.getScore() < o2.getScore()){
return 1;
}else if(o1.getScore() > o2.getScore()){
return -1;
}else{
return 0;
}
}
});
return list.stream().map(shopSortModel -> shopSortModel.getShopId()).collect(Collectors.toList());
}