基于word2vec和Elasticsearch实现个性化搜索

标签: Elasticsearch Elasticsearch word2vec 个性化 搜索 | 发表时间:2017-03-28 15:51 | 作者:
出处:http://ginobefunny.com/

word2vec学习小记一文中我们曾经学习了word2vec这个工具,它基于神经网络语言模型并在其基础上进行优化,最终能获取词向量和语言模型。在我们的商品搜索系统里,采用了word2vec的方式来计算用户向量和商品向量,并通过Elasticsearch的function_score评分机制和自定义的脚本插件来实现个性化搜索。

背景介绍

先来看下 维基百科上对于个性化搜索的定义和介绍:

Personalized search refers to web search experiences that are tailored specifically to an individual’s interests by incorporating information about the individual beyond specific query provided. Pitkow et al. describe two general approaches to personalizing search results, one involving modifying the user’s query and the other re-ranking search results.

由此我们可以得到两个重要的信息:

  1. 个性化搜索需要充分考虑到用户的偏好,将 用户感兴趣的内容优先展示给用户;
  2. 另外是对于实现个性化的方式上主要有 查询修改和对搜索结果的重排序两种。

而对我们电商网站来说,个性化搜索的重点是当用户搜索某个关键字,如【卫衣】时,能将用户最感兴趣最可能购买的商品(如用户偏好的品牌或款式)优先展示给用户,以提升用户体验和点击转化。

设计思路

  1. 在此之前我们曾经有一般的个性化搜索实现,其主要是通过计算用户和商品的一些重要属性(比如品牌、品类、性别等)的权重,然后得到一个用户和商品之间的关联系数,然后根据该系数进行重排序。
  2. 但是这一版从效果来看并不是很好,我个人觉得主要的原因有以下几点:用户对商品的各个属性的重视程度并不是一样的,另外考虑的商品的属性并不全,且没有去考虑商品和商品直接的关系;
  3. 在新的版本的设计中,我们考虑通过 用户的浏览记录这种时序数据来获取用户和商品以及商品和商品直接的关联关系,其核心就是通过类似于语言模型的词出现的顺序来训练向量表示结果;
  4. 在获取用户向量和商品向量表示后,我们就可以 根据向量直接的距离来计算相关性,从而将用户感兴趣的商品优先展示;

实现细节

商品向量的计算

  • 根据用户最近某段时间(如30天内)的浏览记录,获取得到浏览SKN的列表并使用空格分隔;核心的逻辑如下面的SQL所示:
  select concat_ws(' ', collect_set(product_skn)) as skns 
from 
 (select uid, cast(product_skn as string) as product_skn, click_time_stamp 
  from product_click_record 
  where date_id <= $date_id and date_id >= $date_id_30_day_ago
  order by uid, click_time_stamp) as a 
group by uid;
  • 将该SQL的执行结果写入文件作为word2vec训练的输入;
  • 调用word2vec执行训练,并保存训练的结果:
  time ./word2vec -train $prepare_file -output $result_file -cbow 1 -size 20 
-window 8 -negative 25 -hs 0 -sample 1e-4 -threads 20 -iter 15 
  • 读取训练结果的向量,保存到搜索库的商品向量表。

用户向量的计算

  • 在计算用户向量时采用了一种简化的处理,即通过用户最近某段时间(如30天内)的商品浏览记录,根据这些商品的向量进行每一维的求平均值处理来计算用户向量,核心的逻辑如下:
  vec_list = []
for i in range(feature_length):
    vec_list.append("avg(coalesce(b.vec[%s], 0))" % (str(i)))
vec = ', '.join(vec_list)


select a.uid as uid, array(%s) as vec 
from 
 (select * from product_click_record where date_id <= $date_id and date_id >= $date_id_30_day_ago) as a
left outer join
 (select * from product_w2v where date_id = $date_id) as b
on a.product_skn = b.product_skn
group by a.uid;
  • 将计算获取的用户向量,保存到Redis里供搜索服务获取。

搜索服务时增加个性化评分

  • 商品索引重建构造器在重建索引时设置商品向量到product_index的某个字段中,比如下面例子的productFeatureVector字段;
  • 搜索服务在默认的cross_fields相关性评分的机制下,需要增加个性化的评分,这个可以通过 function_score来实现。
     
1
2
3
4
5
6
     
Map<String, Object> scriptParams = new HashMap<>();
scriptParams.put("field", "productFeatureVector");
scriptParams.put("inputFeatureVector", userVector);
scriptParams.put("version", version);
Script script = new Script("feature_vector_scoring_script", ScriptService.ScriptType.INLINE, "native", scriptParams);
functionScoreQueryBuilder.add(ScoreFunctionBuilders.scriptFunction(script));
  • 这里采用了 elasticsearch-feature-vector-scoring插件来进行相关性评分,其核心是向量的余弦距离表示,具体见下面一小节的介绍。在脚本参数中,field表示索引中保存商品特征向量的字段;inputFeatureVector表示输入的向量,在这里为用户的向量;
  • 这里把version参数单独拿出来解释一下,因为每天计算出来的向量是不一样的,向量的每一维并没有对应商品的某个具体的属性(至少我们现在看不出来这种关联),因此我们要特别避免不同时间计算出来的向量的之间计算相关性。在实现的时候,我们是通过一个中间变量来表示最新的版本,即在完成商品向量和用户向量的计算和推送给搜索之后,再更新这个中间向量;搜索的索引构造器定期轮询这个中间变量,当发现发生更新之后,就将商品的特征向量批量更新到ES中,在后面的搜索中就可以采用新版本的向量了;

elasticsearch-feature-vector-scoring插件

这是我自己写的一个插件,具体的使用可以看下 项目主页,其核心也就一个类,我将其主要的代码和注释贴一下:

     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
     
public class FeatureVectorScoringSearchScript extends AbstractSearchScript {
public static final ESLogger LOGGER = Loggers.getLogger("feature-vector-scoring");
public static final String SCRIPT_NAME = "feature_vector_scoring_script";
private static final double DEFAULT_BASE_CONSTANT = 1.0D;
private static final double DEFAULT_FACTOR_CONSTANT = 1.0D;
// field in index to store feature vector
private String field;
// version of feature vector, if it isn't null, it should match version of index
private String version;
// final_score = baseConstant + factorConstant * cos(X, Y)
private double baseConstant;
// final_score = baseConstant + factorConstant * cos(X, Y)
private double factorConstant;
// input feature vector
private double[] inputFeatureVector;
// cos(X, Y) = Σ(Xi * Yi) / ( sqrt(Σ(Xi * Xi)) * sqrt(Σ(Yi * Yi)) )
// the inputFeatureVectorNorm is sqrt(Σ(Xi * Xi))
private double inputFeatureVectorNorm;
public static class ScriptFactory implements NativeScriptFactory {
@Override
public ExecutableScript newScript(@Nullable Map<String, Object> params) throws ScriptException {
return new FeatureVectorScoringSearchScript(params);
}
@Override
public boolean needsScores() {
return false;
}
}
private FeatureVectorScoringSearchScript(Map<String, Object> params) throws ScriptException {
this.field = (String) params.get("field");
String inputFeatureVectorStr = (String) params.get("inputFeatureVector");
if (this.field == null || inputFeatureVectorStr == null || inputFeatureVectorStr.trim().length() == 0) {
throw new ScriptException("Initialize script " + SCRIPT_NAME + " failed!");
}
this.version = (String) params.get("version");
this.baseConstant = params.get("baseConstant") != null ? Double.parseDouble(params.get("baseConstant").toString()) : DEFAULT_BASE_CONSTANT;
this.factorConstant = params.get("factorConstant") != null ? Double.parseDouble(params.get("factorConstant").toString()) : DEFAULT_FACTOR_CONSTANT;
String[] inputFeatureVectorArr = inputFeatureVectorStr.split(",");
int dimension = inputFeatureVectorArr.length;
double sumOfSquare = 0.0D;
this.inputFeatureVector = new double[dimension];
double temp;
for (int index = 0; index < dimension; index++) {
temp = Double.parseDouble(inputFeatureVectorArr[index].trim());
this.inputFeatureVector[index] = temp;
sumOfSquare += temp * temp;
}
this.inputFeatureVectorNorm = Math.sqrt(sumOfSquare);
LOGGER.debug("FeatureVectorScoringSearchScript.init, version:{}, norm:{}, baseConstant:{}, factorConstant:{}."
, this.version, this.inputFeatureVectorNorm, this.baseConstant, this.factorConstant);
}
@Override
public Object run() {
if (this.inputFeatureVectorNorm == 0) {
return this.baseConstant;
}
if (!doc().containsKey(this.field) || doc().get(this.field) == null) {
LOGGER.error("cannot find field {}.", field);
return this.baseConstant;
}
String docFeatureVectorStr = ((ScriptDocValues.Strings) doc().get(this.field)).getValue();
return calculateScore(docFeatureVectorStr);
}
public double calculateScore(String docFeatureVectorStr) {
// 1. check docFeatureVector
if (docFeatureVectorStr == null) {
return this.baseConstant;
}
docFeatureVectorStr = docFeatureVectorStr.trim();
if (docFeatureVectorStr.isEmpty()) {
return this.baseConstant;
}
// 2. check version and get feature vector array of document
String[] docFeatureVectorArr;
if (this.version != null) {
String versionPrefix = version + "|";
if (!docFeatureVectorStr.startsWith(versionPrefix)) {
return this.baseConstant;
}
docFeatureVectorArr = docFeatureVectorStr.substring(versionPrefix.length()).split(",");
} else {
docFeatureVectorArr = docFeatureVectorStr.split(",");
}
// 3. check the dimension of input and document
int dimension = this.inputFeatureVector.length;
if (docFeatureVectorArr == null || docFeatureVectorArr.length != dimension) {
return this.baseConstant;
}
// 4. calculate the relevance score of the two feature vector
double sumOfSquare = 0.0D;
double sumOfProduct = 0.0D;
double tempValueInDouble;
for (int i = 0; i < dimension; i++) {
tempValueInDouble = Double.parseDouble(docFeatureVectorArr[i].trim());
sumOfProduct += tempValueInDouble * this.inputFeatureVector[i];
sumOfSquare += tempValueInDouble * tempValueInDouble;
}
if (sumOfSquare == 0) {
return this.baseConstant;
}
double cosScore = sumOfProduct / (Math.sqrt(sumOfSquare) * inputFeatureVectorNorm);
return this.baseConstant + this.factorConstant * cosScore;
}
}

总结与后续改进

  • 基于word2vec、Elasticsearch和自定义的脚本插件,我们就实现了一个个性化的搜索服务,相对于原有的实现,新版的点击率和转化率都有大幅的提升;
  • 基于word2vec的商品向量还有一个可用之处,就是可以用来实现相似商品的推荐;
  • 但是以我个人的理解,使用word2vec来实现个性化搜索或个性化推荐是有一定局限性的,因为它只能处理用户点击历史这样的时序数据,而无法全面的去考虑用户偏好,这个还是有很大的改进和提升的空间;
  • 后续的话我们会更多的参考业界的做法,更多地更全面地考虑用户的偏好,另外还需要考虑时效性的问题,以优化商品排序和推荐。

参考资料

相关 [word2vec elasticsearch 个性] 推荐:

基于word2vec和Elasticsearch实现个性化搜索

- - GinoBeFunny
在 word2vec学习小记一文中我们曾经学习了word2vec这个工具,它基于神经网络语言模型并在其基础上进行优化,最终能获取词向量和语言模型. 在我们的商品搜索系统里,采用了word2vec的方式来计算用户向量和商品向量,并通过Elasticsearch的function_score评分机制和自定义的脚本插件来实现个性化搜索.

Word2Vec

- - Yonglong.Zhou
Word2Vec核心包括四个组组件,Context Builder、Input Vectors、Output Vectors和Parameter Learner. Context Builder决定是了是采用CBOW模型还是Skip-Gram模型,一般采用Input Vectors作为最终的词向量输出结果,参数学习可以按照常规的反向传播算法,以及为了提升计算速度提出来的霍夫曼树softmax和负采样方法.

词向量工具word2vec的学习

- - 标点符
word2vec是Google在2013年开源的一款将词表征为实数值向量(word vector)的高效工具,采用的模型有CBOW(Continuous Bag-Of-Words,即连续的词袋模型)和Skip-Gram两种. word2vec通过训练,可以把对文本内容的处理简化为K维向量空间中的向量运算,而向量空间上的相似度可以用来表示文本语义上的相似度.

使用Word2Vec/Doc2Vec对IMDB情感分析

- - 标点符
情感分析是一种常见的自然语言处理(NLP)方法的应用,特别是在以提取文本的情感内容为目标的分类方法中. 通过这种方式,情感分析可以被视为利用一些情感得分指标来量化定性数据的方法. 尽管情绪在很大程度上是主观的,但是情感量化分析已经有很多有用的实践,比如企业分析消费者对产品的反馈信息,或者检测在线评论中的差评信息.

深度学习word2vec笔记之应用篇

- - 我爱机器学习
1)该博文是Google专家以及多位博主所无私奉献的论文资料整理的. 2)本文仅供学术交流,非商用. 所以每一部分具体的参考资料并没有详细对应,更有些部分本来就是直接从其他博客复制过来的. 如果某部分不小心侵犯了大家的利益,还望海涵,并联系老衲删除或修改,直到相关人士满意为止. 3)本人才疏学浅,整理总结的时候难免出错,还望各位前辈不吝指正,谢谢.

[译]elasticsearch mapping

- - an74520的专栏
es的mapping设置很关键,mapping设置不到位可能导致索引重建. 请看下面各个类型介绍^_^. 每一个JSON字段可以被映射到一个特定的核心类型. JSON本身已经为我们提供了一些输入,支持 string,  integer/ long,  float/ double,  boolean, and  null..

Elasticsearch as Database - taowen - SegmentFault

- -
【北京上地】滴滴出行基础平台部招聘 Elasticsearch 与 Mysql binlog databus 开发工程师. 内推简历投递给: [email protected]. 推销Elasticsearch. 时间序列数据库的秘密(1)—— 介绍. 时间序列数据库的秘密(2)——索引.

word2vec词向量训练及中文文本相似度计算 - CSDN博客

- -
本文是讲述如何使用word2vec的基础教程,文章比较基础,希望对你有所帮助. 参考:《Word2vec的核心架构及其应用 · 熊富林,邓怡豪,唐晓晟 · 北邮2015年》.           《Word2vec的工作原理及应用探究 · 周练 · 西安电子科技大学2014年》.           《Word2vec对中文词进行聚类的研究 · 郑文超,徐鹏 · 北京邮电大学2013年》.

ElasticSearch 2 的节点调优(ElasticSearch性能)

- - 行业应用 - ITeye博客
一个ElasticSearch集群需要多少个节点很难用一种明确的方式回答,但是,我们可以将问题细化成一下几个,以便帮助我们更好的了解,如何去设计ElasticSearch节点的数目:. 打算建立多少索引,支持多少应用. elasticsearch版本: elasticsearch-2.x. 需要回答的问题远不止以上这些,但是第五个问题往往是容易被我们忽视的,因为单个ElasticSearch集群有能力支持多索引,也就能支持多个不同应用的使用.

Elasticsearch:使用 Elasticsearch 进行语义搜索

- - 掘金 后端
在数字时代,搜索引擎在通过浏览互联网上的大量可用信息来检索数据方面发挥着重要作用. 此方法涉及用户在搜索栏中输入特定术语或短语,期望搜索引擎返回与这些确切关键字匹配的结果. 虽然关键字搜索对于简化信息检索非常有价值,但它也有其局限性. 主要缺点之一在于它对词汇匹配的依赖. 关键字搜索将查询中的每个单词视为独立的实体,通常会导致结果可能与用户的意图不完全一致.