Redis 实战 —— 10. 实现内容搜索、定向广告和职位搜索

使用 Redis 进行搜索 P153

经过改变程序搜索数据的方式,并使用 Redis 来减小绝大部分基于单词或者关键字进行的内容搜索操做的执行时间。 P154git

基本搜索原理 P154

倒排索引 (inverted indexes) 是互联网上绝大部分搜索引擎使用的底层结构,它相似于书本末尾的索引。倒排索引从每一个被索引的文档里面提取一些单词,并记录包含每一个单词的文档集合。 P154github

示例redis

假设有三个文档:网络

  • R = "it is what it is"
  • S = "what is it"
  • T = "it is a banana"

咱们就能获得下面的倒排索引集合:数据结构

  • "a": {2}
  • "banana": {2}
  • "is": {0, 1, 2}
  • "it": {0, 1, 2}
  • "what": {0, 1}

检索的条件 "what", "is" 和 "it" 将对应这个集合:{0,1} ∩ {0,1,2} ∩ {0,1,2} = {0,1}异步

能够发现 Redis 的集合和有序集合很是适合处理倒排索引。ide

基本索引操做函数

从文档里面提取单词的过程一般被成为语法分析 (parsing) 和标记化 (tokenization) ,这个过程能够产生一系列用于表示文档的标记 (token) ,有时又被成为单词 (word) 。 P155性能

标记化的一个常见的附加步骤就是移除非用词 (stop word) 。非用词就是那些在文档中频繁出现却没有提供相应信息量的单词,对这些单词进行搜索将返回大量无用的结果。 P155学习

本书中实现方向索引的逻辑很是简单:

  1. 将文档划分为单词,并移除一个字符的单词
  2. 对于每一个单词获取或建立对应的集合,将当前文档的惟一标识放入集合中

若是须要支持中文等,就不能简单进行英文分词,须要分词器进行处理。第一次接触倒排索引是在 Elasticsearch 中,感兴趣的能够了解 Elasticsearch 中倒排索引的实现以及 IK 中文分词器。

基本搜索操做

在索引里面查找一个单词是很是容易的,只须要获取单词集合里面的全部文档便可。根据多个单词查找文档时,就须要根据条件处理对应的集合,再从最终集合中获取全部文档。 P156

可使用 Redis 的集合操做完成对不一样条件的处理:

  • SINTER / SINTERSTORE: 找出同时包含全部指定单词的文档集合
  • SUNION / SUNIONSTORE: 找出至少包含一个指定单词的文档集合
  • SDIFF / SDIFFSTORE: 找出包含某个单词且包含其余某些单词的文档集合

经过以上三类命令,咱们基本能实现条件大部分的与或非操做。

分析并执行搜索

咱们使用的查询语句进行分词后具备如下特征:

  • 以 + 开头的单词:表示这个单词是前一个单词的同义词,须要取并集
  • 以 - 开头的单词:表示这个单词不但愿包含在文档中,须要取差集
  • 其余普通单词:表示用户须要查询这个单词,须要取交集

即: "connect +connection chat -proxy -proxies" 表示查询的文档须要包含 "connect" 或 "connection" ,同时也要包含 "chat" ,而且不能包含 "proxy" 和 "proxies" 。

实际处理时,先对同义词组分别取并集,而后与须要查询的单词一块儿取交集,最后与不但愿包含的单词取差集,这样所获得的集合就是用户查询的结果集。

对搜索结果进行排序和分页 P160

上述搜索功能以及可以搜索出用户查询的全部文档惟一标识的集合,如今咱们将根据这个文档惟一标识集合以及每一个文档的具体信息进行排序分页。

  • 文档惟一标识集合: 存储每一个文档的惟一标识,例如: {1, 2, 276}
  • 每一个文档的具体信息: 数据结构为 HASH, 以 doc_{id} 为键,内部存储对应文档的相关信息,例如: "doc:276": {"id": 276, "created": 1324114412, "updated": 132562777, "title": "Troubleshooting...", ...}

对于这种状况咱们可使用 Redis 的 SORT 命令对文档惟一标识集合经过引用每一个文档的具体信息进行排序分页。 (05. Redis 其余命令简介

有序索引 P162

上面介绍了使用 Redis 进行搜索,并经过引用存储在 HASH 里面的数据对搜索结果进行排序分页。接下来将介绍利用集合和有序集合实现基于多个分值的复合排序操做,它能提供比 SORT 命令更高的灵活性。 P162

对多个数值字段进行排序 P162

假设咱们目前须要根据文档对更新时间和得票数进行排序,为此咱们须要用两个有序集合存储相关信息。这两个有序集合的成员都是文档惟一标识,成员的分值则分别是文档的更新时间和得票数。

设通过搜索后知足搜索条件的文档惟一标识集合为 filtered_doc_ids ,文档惟一标识及其更新时间对应的有序集合为 doc_ids_with_update ,文档惟一 标识及其得票数对应的有序集合为 doc_ids_with_votes 。那么能够经过 ZINTERSTORE 命令对这三个集合求交集,最后得出的知足搜索条件的文档惟一标识及其排序分值对应的有序集合,再使用 ZRANGE, ZREVRANGE 进行分页获取便可。 P162

示例: ZINTERSTORE filtered_doc_ids_with_sort_score 3 filtered_doc_ids doc_ids_with_update doc_ids_with_votes WEIGHTS 0 {update_weight} {vote_weight}

其中:

  • filtered_doc_ids_with_sort_score 为结果有序集合
  • filtered_doc_ids 的权重为 0 ,仅用作筛选结果,不用于排序
  • doc_ids_with_update, doc_ids_with_votes 的权重能够进行设置,为 0 时表示不用于排序;为其余数时,表示对应字段对最终排序分所占的权重,正数至关于该字段须要正序排序,负数至关于该字段须要倒序排序。

所思

这种利用分值的方法很巧妙,基本能够实现多字段排序,可是优先级可能难以掌控,难以作到先按照某一字段排序,再按照另外一字段排序的形式。由于每一个字段对应的分值的数量级可能差异比较小,这个时候若是须要有排序字段的优先级,那么可能须要对每一个权重进行精巧地设计才行。

对非数值字段进行排序 P164

上面介绍了使用有序集合对多个数值字段进行排序,因为有序集合的分值只能是浮点数,因此非数值字段不能直接用于排序,须要转换成对应的浮点数。但因为双精度浮点数只有 64 个二进制位,实际能使用 63 个二进制位,因此能表示的字符串并很少,只能使用字符串的前几个字符进行分值估计,不足指定字符数的须要补齐到指定字符数。固然若是字符集缩小的话,能够从新进行编码计算,进而能够对更长的字符串进行分值估计。 P165

当这个分值特别大的时候,可能会引起最终计算的分值溢出而出错的问题。

广告定向 P166

接下来将介绍使用集合和有序集合构建出一个几乎完整的广告服务平台 (ad-serving platform) 。 P166

对广告进行索引 P167

针对广告的索引操做和针对其余内容的索引操做并无太大的不一样,被索引的的广告一般都拥有像位置、年龄和性别这类必需的定向参数,而且每每只会返回单个广告。 P167

广告的价格 P167

  • 按展现次数计费 (cost per view) :这种广告又称 CPM 广告或按千次计费 (cost per mille) ,每展现 1000 次就须要收取固定的费用
  • 按点击次数计费 (cost per click) :这种广告又称 CPC 广告,根据被点击的次数收取固定费用
  • 按动做执行次数计费 (cost per action) :又称按购买次数计费 (cost per acquisition) ,这种广告又称 CPA 广告,根据用户在广告的目的地网站上执行的动做收取不一样的费用

为了尽量简化广告价格的计算方式,将对全部类型的广告进行转换,使得它们的价格能够基于每千次展现进行计算,产生出一个估算 CPM (estimated CPM, eCPM) 。 P168

  • CPM 的 eCPM 价格能够直接使用 CPM 价格
  • CPC 的 eCPM 价格能够经过将广告的每次点击价格乘以广告的点击经过率 (click-through rate, CTR) ,而后再乘以 1000 获得
  • CPA 的 eCPM 价格能够将广告的点击经过率、用户在广告投放者的目标页面上执行动做的几率、被执行动做的价格这三者相乘起来,而后再乘以 1000 获得

将广告插入倒排索引 P169

咱们基本能够复用上面提到的搜索功能,除了会将广告的关键词插入倒排索引,还会将广告的定向参数(位置、年龄和性别等)插入倒排索引中,并记录广告的类型、基本价格和 eCPM 价格。 P169

执行广告定向操做 P170

当系统收到广告定向请求的时候,它要作的就是在匹配用户定向参数的一系列广告里面,找出 eCPM 最高的那一个广告。同时,程序还会记录页面内容与广告内容的匹配程度,以及不一样匹配程度对广告点击经过率的影响等统计数据。经过使用这些统计数据,广告中与页面相匹配的那些内容就会做为附加值被计入 CPC 和 CPA 的 eCPM 价格,使得那些包含了匹配内容的广告可以更多地被展现出来。 P170

计算附加值
计算附加值就是基于页面内容和广告内容二者之间匹配的单词,计算出应该给广告的 eCPM 价格加上多少增量。每一个单词都有一个有序集合,成员为广告 id ,成员的分值为当前单词对这则广告的 eCPM 的附加值。 P171

在寻找合适的广告时,咱们首先会过滤出匹配定位位置且至少包含一个页面单词的广告,而后经过计算附加值的方法替代搜索,以便实现每次投放价值最高的广告,并可以根据用户的行为学习。同时因为每一个广告匹配的内容不一样,最优方式应该是使用加权平均值来计算单词部分的附加值,但限于 Redis 自己的命令,咱们最终采起 (max + min) / 2 的形式计算单词部分的附加值(max 表示全部匹配单词的最大附加值, min 表示全部匹配单词的最小附加值),采用以下命令便可: ZUNIONSTORE final_score 3 base max min WEIGHTS 1 0.5 0.5

从用户行为中学习 P175

首先须要存储用户的浏览记录,包括三部分:(每 100 次就主动更新一次 eCPM ) P175

  • 被定向至给定广告的单词(即:内容中单词和给定广告单词的交集)
  • 给定广告被定向的次数
  • 广告中的某个单词被用于计算附加值的次数

其次须要存储用户的点击和动做记录,用于计算 点击经过率 = 点击量或动做次数 / 广告展现次数。(每次都更新 eCPM) P176

最后就是更新 eCPM ,包括两部分:

  • 广告的 eCPM :根据广告的实际价格和当前广告的点击经过率,计算出最新的 eCPM
  • 广告单词的 eCPM 附加值:根据广告的基本价格和每一个单词的点击经过率,计算出每一个单词最新的 eCPM 附加值
改进方案 P179
  • 随时间流逝:能够仿照 03. Redis 简单实践 - Web应用 文章的 RescaleItemViewedNum 函数进行按期下降广告的展现次数和点击次数(或者动做执行次数)
  • 增长计数值:能够考虑前一天、前一星期或者其余时间段的点击技术,并基于时间段的长短给予不一样的权重
  • 使用第二价格拍卖 (second-price auction) 的方式来决定广告位的费用
  • 给予低价广告必定曝光量:部分时间内,获取收益排名前 100 的广告,基于它们的 eCPM 的相对值来挑选广告,而不是挑选 eCPM 最高的广告
  • 优化新广告初始 eCPM :
    • 初期使用同类型广告的平均点击数据
    • 在同类型广告的平均点击经过率和当前实际点击经过率之间,构建一种简单的反线性关系 (inverse linear relationship) 或者反 S 关系 (inverse sigmoid relationship) ,直到广告有足够的展现次数为止
    • 人为提升点击经过率,保证有足够多的流量学习真正 eCPM
  • 考虑使用真正的贝叶斯统计、神经网络、关联规则学习、聚类计算或者其余技术来计算附加值
  • 将记录信息的逻辑变为异步(能够利用 09. 实现任务队列、消息拉取和文件分发 中的任务队列实现),提升响应效率

职位搜索 P180

接下来将使用集合和有序集合实现职位搜索功能,并根据求职者拥有技能来为他们寻找合适的职位。 P180

遍历合适的职位 P180

第一反应确定是直接对每个求职者搜索全部的岗位,从而找到求职者合适的岗位。但这种方法效率极低(大部分岗位确定是技能对不上的),并且没法进行性能扩展。 P181

搜索合适的岗位 P181

使用相似上面提到的附加值形式,每次添加一个岗位时,在对应的技能集合中添加这个岗位的 id (SADD idx:skill:{skill} {job_id}),再在岗位有序集合中进行添加,成员为岗位 id ,成员的分值为所需的技能数量 (ZADD job_required_skill_count {job_id} {required_skill_count})。搜索的时候就先对求职者全部技能对应的集合使用 ZUNIONSTORE 操做计算每一个公司匹配的技能数量 (ZUNIONSTORE matched {n} idx:skill:{skill} ... WEIGHTS 1 ...),而后再与岗位有序集合求交集,并让公司有序集合的权重为 -1 (ZINTERSTORE result 2 job_required_skill_count matched WEIGHTS -1 1),最后获取分值为 0 的全部岗位便可完成搜索。 P181

所思

书上的这个方法比较麻烦,其实可使用文章最开始的无序倒排索引,岗位至关于要搜索的文档,岗位所需的技能至关于单词。

本文首发于公众号:满赋诸机(点击查看原文) 开源在 GitHub :reading-notes/redis-in-action

相关文章
相关标签/搜索