我做的项目是在慕课网买的

项目介绍

项目需求背景:模仿大众点评应用提供用户线下搜索推荐服务门店的需求
技术选型:后端业务: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

步骤

  1. 将mysql配置成master,修改my.conf 加入如下配置:server-id = 1; binlog_format = ROW; Log_bin = mysql_bin 重启mysql连接
  2. 授予slave一些权限,读、备份Grant select,replication slave,replication client on . to ‘canal’@’%’ identified by ‘canal’将权限刷新到本地 flush privileges
  3. 打开canal.properties配置文件,修改slaveID 和注意dbUsername dbpassword,再修改url,使得canal adapter只需要监听dianpingapp即可,保存,启动bin/ startup.sh
  4. 对应的是6.8及之前的版本才能使用,所以我们修改对应的canal.adapter,使用inport源码的方式inport出来
  5. 修改通过bin/startup.sh启动
  6. 代码层面接入
    (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"} } } }
 @Overridepublic 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);//构建queryMap<String,Object> cixingMap = analyzeCategoryKeyword(keyword);boolean isAffectFilter = false;boolean isAffectOrder =  true;jsonRequestObj.put("query",new JSONObject());//构建function scorejsonRequestObj.getJSONObject("query").put("function_score",new JSONObject());//构建function score内的queryjsonRequestObj.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 queryint 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>() {@Overridepublic 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>() {@Overridepublic 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>() {@Overridepublic 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>() {@Overridepublic 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());}

【项目介绍】ElasticSearch7+Spark 构建高相关性搜索服务千人千面推荐系统相关推荐

  1. java项目: ElasticSearch+Spark构建高相关性搜索服务千人千面推荐系统

    文章目录 1 概述 2 需求分析 3 项目基础搭建[业务系统之基础能力] 4 用户服务.运营后台.商户服务的搭建 用户模型前后端 运营后台 商户入驻: 商户创建.商户查询.商户禁用 5 基础服务: 品 ...

  2. 【战神工具出品】解读百度的搜索结果千人千面算法原理

    许多网站优化服务提供商在为公司进行关键字优化排名时,都会遇到其百度搜索结果排名不错,但其客户的搜索结果不同,显示的网站排名不仅不稳定,而且有时搜索结果确实如此的情况.不存在.今天,基于多年优化服务的经 ...

  3. 助力工业物联网,工业大数据项目介绍及环境构建【一、二】

    文章目录 工业大数据项目介绍及环境构建 01:专栏目标 02:项目背景 03:项目需求 04:业务流程 05:技术选型 06:Docker的介绍 07:Docker的网络 08:Docker的使用 0 ...

  4. 淘宝号标签,,猜你喜欢推荐,消费潜力值,淘宝号的千人千面,购物足迹,潜在购买类目,淘宝号的潜在成交词,官方推荐的搜索词,淘宝标签查询,淘宝号是否打上标签,标签透视,标签接口,猜你喜欢接口,

    简介: 可以查询到指定淘宝号被淘宝推荐的搜索词,和猜你喜欢的词库和商品id库. 淘宝标签查询基于官方千人千面算法推荐,针对不同的消费者推送不同的潜在成交商品和可消费的金额. 实时查询买家曾浏览过.购买 ...

  5. 搜索技术哪些算法模型可以实现千人千面个性化服务

    用户行为数据如何实时的应用在搜索服务中那? 今天将介绍四个"个性化搜索算法模型",希望对你有所帮助~ 一.个性化排序应用 类目预测 类目预测是基于物品/内容的类目信息改善搜索效果的 ...

  6. 尴尬!Google搜索现在也会“千人千面”了,退出登录也一样

    郭一璞 发自 凹非寺  量子位 报道 | 公众号 QbitAI 最近,Google的小辫子被竞争对手搜索引擎DuckDuckGo揪住了. △ 对,logo是萌萌的小鸭子 这只小鸭子发现,Google搜 ...

  7. OpenSearch:轻松构建大数据搜索服务

    随着互联网数据规模的爆炸式增长,如何从海量的历史.实时数据中快速获取有用信息,变得越来越具有挑战性.搜索是获取信息最高效的途径之一,因此也是各类网站.应用的基础标配功能.开发者想在自己的产品中实现搜索 ...

  8. 淘宝千人千面背后的秘密:搜索推荐广告三位一体的在线服务体系AI·OS

    简介:揭晓三位一体的在线服务体系AI·OS,及其技术架构演进,技术概况,云原生产品与实践. 作者:阿里巴巴搜索推荐事业部高级研究员 沈加翔 一.三位一体的在线服务体系AI·OS介绍 AI·OS(Art ...

  9. 1、点评搜索服务推荐系统项目概述

    ElasticSearch7+Spark 构建搜索服务&推荐系统 文章目录 ElasticSearch7+Spark 构建搜索服务&推荐系统 项目概述 项目设计 业务需求 技术分解&a ...

最新文章

  1. 第三节:框架前期准备篇之利用Newtonsoft.Json改造MVC默认的JsonResult
  2. node.js 实现扫码二维码登录
  3. php 按位左移,PHP位运算符
  4. AngularJS中的表单验证机制
  5. 转:.Net 中AxShockwaveFlash的解析
  6. 【优化求解】基于matlab遗传算法结合粒子群算法求解单目标优化问题【含Matlab源码 1659期】
  7. Oracle账户被锁定后如何解锁
  8. 计算机网络与通信之计算机网络体系结构
  9. dcp1608 linux驱动下载,兄弟激光 DCP-1608驱动
  10. 胡灵 c语言,清华作业们男女主角现身
  11. 嵌入式Linux系统环境搭建全流程-4412友善之臂开发板
  12. 获取URL地址时某些参数被转义
  13. Word无法打开该文件,因为文件格式与文件扩展名不匹配的解决方法
  14. Stanford Algorithms 斯坦福算法课
  15. IKAnalyzer 添加扩展词库和自定义词
  16. TXD,RXD的意思
  17. python统计列表中元素个数_python中计算一个列表中连续相同的元素个数方法
  18. 隔离日记~记录特别的日子
  19. matlab总线,MATLAB SIMULINK 创造总线 Bus Creator
  20. JDBC学习笔记-使用全过程

热门文章

  1. MT4 PC端历史版本更新(老版本MT4下载)
  2. 期末余额 = 期初余额 + 本期增加发生额 - 本期减少发生额
  3. matlab的函数库,matlab函数库大全
  4. 迅睿CMS 网站安全权限划分
  5. 一些方法:JQ: append() 、appendTo() || JS:appendChild():
  6. 打开时空隧道,重演云栖72小时云世界
  7. python如何群控手机_python调用adb脚本来实现群控安卓手机初探
  8. python手机群控(adb命令)实现
  9. AMBER:使用Cpptraj计算RMSD 以及使用中遇到的问题
  10. 基于易灵思开发板RiscV的调试流程