胡蜂属与Elasticsearch匹配数以百万计的人

交友应用,不可或缺的一部分经历巨大的潜力为您推荐匹配的基础上,无数的喜好你和你的潜在的匹配设置。你可以想象,有许多激励优化这部分的经验是第一步,每个人都开始在比赛之前,对话和超越。

你的偏好,但并不是唯一因素如何向您推荐潜在的匹配推荐其他潜在的匹配(或你)。如果我们简单地显示所有符合你的标准的用户没有任何形式的排名,最终的结果将是不匹配的方法。举个例子,如果我们不试着把用户的最近的活动结果,会有更高的机会你花更多的时间与人交流最近没有使用这个应用程序。这当然不设置用户成功!除了简单地喜好你和其他人,我们利用大量的算法和因素推荐用户,我们认为你应该看到。

当服务建议我们需要提供最好的结果在那个时间点,让你不断看到更多的建议或传递你的潜在的匹配。在其他应用程序内容本身可能不会经常改变或少这样的及时性是至关重要的,这可以通过离线系统,再生这些建议。例如,当使用Spotify的每周“发现”功能你可以享受一系列的推荐歌曲但这集是冻结,直到下周。OkCupid的情况下,我们允188bet金宝搏官网许用户不断地实时查看自己的建议。我们推荐的“内容”——我们的用户,在本质上是高度动态的(例如,一个用户可以加入,改变他们的偏好,概要文件的细节,位置,随时关闭,等等),可以改变和他们应该如何推荐谁,所以我们要确保潜在的匹配你看到一些最好的建议,你可以看到在那个时间点上。

利用各种排名算法虽然能够连续实时提供建议,我们需要使用一个搜索引擎,不断保持最新的用户数据和提供过滤和排名潜在候选人的能力。

什么问题现有的匹配系统

支持各种推荐项目在未来几年随着团队的成长,我们知道我们需要修改这个系统。最大的难点之一是发展等模式更新添加新数据对用户(例如用户偏爱的性别标签)要求数百数千行仔细的协调,确保所需的样板代码和部署系统的所有部分被部署以正确的顺序。只是试图添加一个新的方法来过滤用户设置或添加一个新的方法排序结果的工程师需要半天的时间手动部署到每一个碎片在生产和保持通知可能出现的问题;回滚没有更快。更重要的是,它变得很难操作和规模系统自碎片和副本是手动分配和分布在裸露的金属机器的舰队。在2019年初随着负载匹配系统的增加,我们需要提高搜索能力添加另一个副本集手动放置在多台计算机服务实例——一个多周后端和操作团队之间的工作。这个时候我们也开始注意到内部建立了服务发现系统性能瓶颈,消息队列,等等。而这些组件之前适合这家公司,我们是在负载达到一个点,我们不确定是否其中一个子系统自己可能规模。我们有目标,我们更多的工作负载转移到云环境和改变匹配系统,本身一个艰难的任务,也需要带上所有这些其他子系统组件。

今天在OkCupi188bet金宝搏官网d这些子系统提供更健壮的OSS云友好选项和团队已经在过去的两年里采取了各种不同的技术取得了巨大的成功。我们不会谈论这些努力在这个博客上,而是集中在解决上述问题的努力,我们已经包括通过移动开发者更友好的和可扩展的搜索引擎对我们的建议:胡蜂属

这是一个比赛!为什么和188bet金宝搏官网胡蜂属OkCupid匹配吗

Elasticsearch

最大的一个原因我们选择Elasticsearch是缺乏真正的内存部分更新。这是非常重要的对于我们的用例,因为文件我们将索引,我们的用户,需要经常更新通过喜欢/传球,消息,等。这些文件在本质上是高度动态的,而内容如广告或图像大多是静态对象与属性变化很少,所以效率低下的读写周期更新是主要的表现也让人担忧。

胡蜂属

  • 高饲料性能通过真正的内存部分更新,而不需要对整个文档进行检索(据报道,每秒40-50k更新每个节点)
  • 排名提供了一个灵活的框架允许在查询时处理
  • 直接支持集成机器学习模型(例如TensorFlow)排名
  • 查询可以通过表达YQL查询语言(雅虎)的REST调用
  • 自定义逻辑通过Java组件的能力

扩展和维护时,您从来没有想到碎片了——你配置你的内容节点的布局和胡蜂属自动处理分割文档放入桶中,复制,分发数据。此外,数据自动恢复和重新分配副本每当你添加或删除节点。扩展仅仅意味着更新配置添加节点,让胡蜂属自动重新分配这些数据生活。

整体胡蜂属似乎最好的支持我们的用例。188bet金宝搏官网OkCupid包含很多不同的信息用户,帮助他们找到最佳匹配——的过滤和排序有超过100的!我们总是会添加更多的过滤和排序,所以能够支持工作流是重要的。在写和查询,胡蜂属是最类似于我们现有的匹配系统;这是我们匹配系统,还需要快速的内存部分更新和实时处理在查询时间排名。胡蜂属排名还有一个更加灵活和简单的框架;表达的能力在YQL查询而不是尴尬Elasticsearch查询的结构是另一种不错的奖金。扩展和维护,胡蜂属的自动数据分布能力非常吸引我们的团队规模相对较小。总之看来胡蜂属将提供我们更好地支持用例和性能需求,Elasticsearch相比,更容易维护。

Elasticsearch更广为人知,我们可以学习易燃物的使用它,但要么选择需要大量的前期研究和调查。胡蜂属已经为许多生产使用情况下,ZedgeFlickr提供数十亿的图片,雅虎双子座广告平台拥有超过每秒十万个请求服务广告每月10亿活跃用户。这给了我们信心,这是一个测试,性能,和可靠的选择——事实上,胡蜂属的起源在长时间Elasticsearch。

此外胡蜂属团队一直积极参与和帮助。胡蜂属最初是用来提供广告和内容页,据我们所知它尚未用于一个交友平台。我们最初使用的胡蜂属挣扎,因为它是这样一个独特的用例,但是胡蜂属团队已经超级响应和快速优化系统来帮助我们处理出现的一些问题。

胡蜂属是如何工作和OkCupid搜索是什么样子的188bet金宝搏官网

在我们深入胡蜂属用例,这是一个快速概述胡蜂属是如何工作的。胡蜂属是一个集众多服务但每个集装箱码头工人可以配置为履行管理/配置节点的角色,一个无状态的Java容器节点,和/或一个有状态的c++内容节点。一个应用程序包包含配置、组件毫升模型等可以通过部署州API配置集群,集群处理应用更改容器和内容。饲料请求和查询所有通过无状态的Java容器通过HTTP(它允许定制的处理),之前在集群或查询内容提要更新土地扇出的内容层分布式查询执行发生。在大多数情况下,部署一个新应用程序包只需要几秒钟,胡蜂属处理这些变化生活在容器和内容集群,这样你很少需要重启。

搜索是什么样子?

搜索用户{

文档用户{

字段标识类型长{
索引:总结|属性
属性:快速搜索
排名:过滤器
}

字段latLong类型位置{
索引:属性
}

# UNIX时间戳
长整型字段lastOnline {
索引:属性
属性:快速搜索
}

#包含该用户文档的用户喜欢
#和相应的权重是UNIX时间戳时发生
字段likedUserSet类型weightedset <时间> {
索引:属性
属性:快速搜索
}

}

rank-profile myRankProfile继承默认{
rank-properties {
查询(lastOnlineWeight): 0
查询(incomingLikeWeight): 0
}

函数lastOnlineScore () {
表达式:查询(lastOnlineWeight) *新鲜(lastOnline)
}

函数incomingLikeTimestamp () {
表情:rawScore (likedUserSet)
}

函数hasLikedMe () {
表达:如果(incomingLikeTimestamp > 0 1 0)
}

函数incomingLikeScore () {
(incomingLikeWeight) * hasLikedMe表达式:查询
}

第一阶段{
{表达
lastOnlineScore + incomingLikeScore
}
}

总结特性{
lastOnlineScore incomingLikeScore
}
}

}

索引:属性指定显示这些字段应该保持内存允许我们得到最好的这些字段的读写性能。

假设我们密集的集群的用户文档。我们可以做一个搜索过滤和排序的字段。例如,我们可以做一个POST请求的默认搜索处理程序http://localhost: 8080 /搜索/找到用户,除了我们自己的用户777年从我们的位置在50英里,一直以来在线时间戳1592486978在最近的活动,保持最高的两名候选人。也让我们选择summaryfeatures帮助我们看到每个排名的贡献表达,我们已经在我们的排名简介:

{
“yql”:“从用户选择userId, summaryfeatures lastOnline > 1592486978和! (userId包含\“777 \”)限制2;”,
“排名”:{
“简介”:“myRankProfile”,
“功能”:{
“查询(lastOnlineWeight)”:“50”
}
},
" pos ": {
“半径”:“50米”,
“我”:“N40o44 22; W74o0 2”,
“属性”:“latLong”
},
“演讲”:{
“摘要”:“默认”
}
}

我们可能会得到这样的结果:

{
"根":{
“id”:“最高级的”,
“相关性”:1.0,
“字段”:{
“totalCount”: 317
},
“覆盖”:{
“覆盖”:100年,
“文件”:958年,
“全”:没错,
“节点”:1、
“结果”:1、
“resultsFull”: 1
},
“孩子”:(
{
“id”:“指数:用户/ 0 / bde9bd654f1d5ae17fd9abc3”,
“相关性”:48.99315843621399,
“源”:“用户”,
“字段”:{
“标识”:-5800469520557156329,
" summaryfeatures ": {
“rankingExpression (incomingLikeScore)”:0.0,
“rankingExpression (lastOnlineScore)”:48.99315843621399,
“vespa.summaryFeatures。缓存”:0.0
}
}
},
{
“id”:“指数:用户/ 0 / e8aa37df0832905c3fa1dbbd”,
“相关性”:48.99041280864198,
“源”:“用户”,
“字段”:{
“标识”:6888497210242094612,
" summaryfeatures ": {
“rankingExpression (incomingLikeScore)”:0.0,
“rankingExpression (lastOnlineScore)”:48.99041280864198,
“vespa.summaryFeatures。缓存”:0.0
}
}
}
]
}
}

在匹配滤波后击中第一阶段排名表达式是用来衡量。的相关性返回的总体评分结果第一阶段排名功能的rank-profile我们中指定的查询,即。ranking.profilemyRankProfile。的列表中ranking.features我们指定的一个特性查询(lastOnlineWeight)50,然后只排名表达式中引用我们使用:lastOnlineScore。,使用一个内置的排序功能新鲜是一个数接近1如果最近的时间戳属性相比,当前的时间戳。到目前为止一切顺利,没什么太复杂的。

与静态内容,这些内容可以影响他们是否应该被你。例如,他们可以喜欢你!我们可以指数加权集likedUserSet字段在每个用户文档保存键他们喜欢的用户id值等发生的时间戳。将简单过滤那些喜欢你(如添加一个likedUserSet包含\“777 \”YQL条款),但是我们如何整合期间加权集信息的排名?我们怎么可能会增加一个用户,喜欢我们的用户?

排名在前面的结果表达式incomingLikeScore这两个命中是0。用户6888497210242094612实际上喜欢用户777年,但这不是目前在排名访问,即使我们有提供“查询(incomingLikeWeight)”:50。我们可以利用排名YQL函数(第一,只有第一个论点的排名()函数确定是否匹配一个文档,但所有参数用于计算等级分数),然后使用dotProduct在我们的YQL排名条款来存储和检索原始分数(在本例中,当用户喜欢我们的时间戳)如下:

{
“yql”:“从用户选择userId, summaryfeatures ! (userId包含\“777 \”)和等级(lastOnline > 1592486978, dotProduct (likedUserSet,{\“777 \”: 1}))限制2;”,
“排名”:{
“简介”:“myRankProfile”,
“功能”:{
“查询(lastOnlineWeight)”:“50”,
“查询(incomingLikeWeight)”:“50”
}
},
" pos ": {
“半径”:“50米”,
“我”:“N40o44 22; W74o0 2”,
“属性”:“latLong”
},
“演讲”:{
“摘要”:“默认”
}
}
{
"根":{
“id”:“最高级的”,
“相关性”:1.0,
“字段”:{
“totalCount”: 317
},
“覆盖”:{
“覆盖”:100年,
“文件”:958年,
“全”:没错,
“节点”:1、
“结果”:1、
“resultsFull”: 1
},
“孩子”:(
{
“id”:“指数:用户/ 0 / e8aa37df0832905c3fa1dbbd”,
“相关性”:98.97595807613169,
“源”:“用户”,
“字段”:{
“标识”:6888497210242094612,
" summaryfeatures ": {
“rankingExpression (incomingLikeScore)”:50.0,
“rankingExpression (lastOnlineScore)”:48.97595807613169,
“vespa.summaryFeatures。缓存”:0.0
}
}
},
{
“id”:“指数:用户/ 0 / bde9bd654f1d5ae17fd9abc3”,
“相关性”:48.9787037037037,
“源”:“用户”,
“字段”:{
“标识”:-5800469520557156329,
" summaryfeatures ": {
“rankingExpression (incomingLikeScore)”:0.0,
“rankingExpression (lastOnlineScore)”:48.9787037037037,
“vespa.summaryFeatures。缓存”:0.0
}
}
}
]
}
}

现在用户6888497210242094612被提高到为他们喜欢我们的用户和他们incomingLikeScore网的全部价值。当然,我们有时间戳时喜欢我们,所以我们可以利用更复杂的表达式,但我们会保持简单。

这说明如何过滤和排序结果的机制通过排名框架。排名框架提供了一种灵活的方式来应用排名表达式(大多只是数学)在查询时。

定制Java中间件层

@After (PhaseNames.TRANSFORMED_QUERY)
类MatchSearcher:搜索器(){

伴星{
/ / HTTP查询参数
val USERID_QUERY_PARAM = "标识"

val ATTRIBUTE_FIELD_LIKED_USER_SET = " likedUserSet "
}

覆盖有趣的搜索(查询:查询,执行:执行):结果{
val userId = query.properties () .getString (USERID_QUERY_PARAM) ? .toLong ()

/ /添加dotProduct条款
如果(userId ! = null) {
val rankItem = query.model.queryTree.getRankItem ()
val likedUserSetClause = DotProductItem (ATTRIBUTE_FIELD_LIKED_USER_SET)
likedUserSetClause。addToken (userId, 1)
rankItem.addItem (likedUserSetClause)
}

/ /执行查询
查询。跟踪(“YQL后是:$ {query.yqlRepresentation ()} ", 2)
返回execution.search(查询)
}
}

然后在我们的services . xml文件我们可以配置组件如下:


<搜索>
<链id =“默认”继承= "胡蜂属" >
<搜索器id = " com.okc188bet金宝搏官网upid.match。MatchSearcher "包= " match-searcher " / >
< /链>
< /搜索>
<处理程序id =“默认”包= " match-searcher " >
<绑定> http:// *: 8080 /匹配< /绑定>
< /处理程序>

然后我们简单地构建和部署应用程序包,现在当我们做一个查询,自定义处理程序http://localhost: 8080 /匹配?用户id = 777:

{
“yql”:“从用户选择userId, summaryfeatures ! (userId包含\“777 \”)和等级(lastOnline > 1592486978)限制2;”,
“排名”:{
“简介”:“myRankProfile”,
“功能”:{
“查询(lastOnlineWeight)”:“50”,
“查询(incomingLikeWeight)”:“50”
}
},
" pos ": {
“半径”:“50米”,
“我”:“N40o44 22; W74o0 2”,
“属性”:“latLong”
},
“演讲”:{
“摘要”:“默认”
}
}

之前我们得到同样的结果!注意,在芬兰湾的科特林的代码示例中,我们添加了一个跟踪我们修改后打印YQL表示如果我们集tracelevel = 2在URL参数,反应还显示:


{
“消息”:“YQL后是:选择userId, summaryfeatures从用户(排名(lastOnline > 1592486978, dotProduct (likedUserSet,{\“777 \”: 1}))和! (userId包含\“777 \”)限制2;”
},

Java容器中间件层是一种强大的方法来添加自定义逻辑处理通过搜索者或自定义渲染的结果通过渲染器。我们定制搜索者处理类似上述和其他方面,我们想让隐含在搜索。例如,我们支持一个产品概念是“相互配合”的想法,你可能会寻找用户提供特定的偏好标准(如年龄和距离),但你也必须符合候选人的搜索条件。我们支持这样一个用例搜索者组件我们可能获取搜索用户的文档提供他们的一些属性在随后的扇出查询过滤和排名。排名框架和自定义中间件层一起为我们提供了一个灵活的方式来支持我们的许多用例。这些示例中我们只涉及几个方面但有丰富的文档在这里

如何去构建和productionizing胡蜂属集群

早期原型阶段

从那里,这是多年来大量的移植的应用程序逻辑和胡蜂属发现什么是可能的,必要时咨询胡蜂属团队。我们的匹配系统的逻辑是在c++中我们还增加了逻辑将我们目前的过滤和排序数据模型转化为等价的YQL查询,我们通过其他问题胡蜂属集群。在早期我们也确保构建好管道重新繁衍集群文件的全部用户群;原型将涉及许多变化来确定正确的字段类型使用,无意中需要重新喂料文件。

监控和负载测试

在负载测试之前,我们添加了普罗米修斯指标无处不在。Vespa-exporter提供了大量的统计数据和胡蜂属本身也暴露出少量额外的指标。从这个我们创造了各种Grafana仪表板大约每秒查询,延迟,胡蜂属资源使用的过程,等我们也跑了vespa-fbench测试查询性能和胡蜂属团队的帮助下确定,由于相对较高静态查询成本,一个分组的布局将为我们提供更高的吞吐量。在一个平面布局,添加更多的节点只会主要是减少动态查询成本(即查询的一部分取决于文档索引)的数量。分组的布局配置意味着每个组的节点将包含完整的文档集,因此一个集团可以查询。由于我们高静态查询成本,同时保持我们的节点数相同的我们的吞吐量更增加了增加有效的群体数量从平面布局一组三组。最后,我们也表现生活的影子流量测试后,我们得到了静态基准的信心。

性能优化

推荐质量

系统图

胡蜂属是如何做的,接下来是什么

模式更新

  • :一个日历星期花在数百行代码更改,与多个子系统和精心协调部署
  • :几个小时后添加一个简单的模式定义字段,并部署应用程序包

添加一个新的类

  • :半天用于部署
  • 后:排名表达式也更新模式定义,可以部署到活动的系统。这意味着只需要几秒生效!

扩展和维护

  • :多周努力生产手动分发碎片和位置服务运行文件来实现高可用性
  • 后:将新节点添加到配置文件和胡蜂属自动分发数据,以满足所需的冗余水平。大部分我们的操作不需要任何人工干预或重新启动任何状态的节点

胡蜂属集群的整体开发和维护方面是有利于OkCupid的产品路线图。188bet金宝搏官网2020年1月结束以来,我们productionized胡蜂属集群和服务我们所有的建议。我们还添加了许多新领域,排名表达式和用例支持等主要产品发布今年。与我们之前的匹配系统,我们现在住在查询时使用机器学习模型。

接下来是什么?

此外,胡蜂属最近发布了支持高维近似最近邻的索引是完全实时、并行搜索,动态更新的。我们期待着与实时探索其他用例最近的邻居搜索。

188bet金宝搏官网OkCupid x胡蜂属。船吧!

我们喜欢的第一步和胡蜂属团队发送一条消息。他们给我们,这是一个比赛!我们不可能这样做没有胡蜂属团队的帮助。特别感谢@jobergum@geirst提供指导查询和排名和一个超级特殊shoutout@kkraune@vekterli所有他们的支持。支持水平和工作团队提供我们真是太棒了,从深入研究我们的用例诊断性能问题在短时间内增强胡蜂属引擎。@vekterli甚至飞出我们的办公室在纽约,直接与我们工作了一个星期,以确保我们的集成和用例可以满足。非常感谢你的团队胡蜂属!

最后,我们只涉及几个方面对我们使用胡蜂属,但这一切都是不可能的工作由我们后台和运营团队在过去的一年。有很多独特的挑战我们之间的鸿沟在现有系统和更现代的技术堆栈但这些博客文章另一个时间。

如果你有兴趣挑战后端团队OkCupid工作,188bet金宝搏官网我们正在招聘!

最初发表在https://tech.188bet金宝搏官网okcupid.com2020年9月29日。

188bet金宝搏官网OkCupid科技博客

读故事OkCupid工程团队每天连接数以百万188bet金宝搏官网计的人

188bet金宝搏官网OkCupid科技博客

188bet金宝搏官网OkCupid的工程团队负责匹配每天数以百万计的人。OkCupid科技博客上读到他们的故事188bet金宝搏官网

188bet金宝搏官网OkCupid科技博客

188bet金宝搏官网OkCupid的工程团队负责匹配每天数以百万计的人。OkCupid科技博客上读到他们的故事188bet金宝搏官网