2014 年 12 月 15 日
关注 @hankeTL;DR:CocoaPods 搜索引擎 不是网络规模的,而是根据我们的需求量身定制的。以下是原因和方法。
首先,我们来看一些数字... 目前,CocoaPods 搜索引擎索引了7378 个 Pod。从这些 Pod 中,它索引了名称、作者、版本、依赖项、平台、摘要和综合标签。
平均而言,人们每分钟搜索13 次,工作日搜索次数较多,周末搜索次数较少(切换到月视图即可查看)。偶尔的峰值大约是这个数字的十倍,即每秒大约两次。这些峰值在状态页面上不可见,因为请求数量是按五分钟的平均值计算的。正如您所看到的,搜索引擎永不休息:CocoaPods 一直处于搜索状态。
我们的用户如何搜索?
在 cocoapods.org 上搜索
可以通过搜索 API搜索 CocoaPods,例如在http://search.cocoapods.org/api/v1/pods.flat.hash.json?query=author:tony%20author:arnold或通过cocoapods.org。
后者,即网站前端界面,使用前者并且更有趣,所以我们来了解一下
- 这是一个快速随打随搜的功能——如果你在 166 毫秒 内没有输入,它会向搜索 API 发送一个查询。
- 第二项功能我将在下一节中详细介绍:显示查询词在何处找到并提供最合适的猜测。
- 它会让你知道它没有找到任何内容,但会提供有用的建议,例如当你搜索 datanotfound 时。为此,它会 尝试根据索引数据拆分 你的查询 。它还会显示一个 标签小分类云。
- 它按 流行度 对结果进行排序,这是一个 orta 与 David Grandinetti 共同提出的指标。这是当前的默认排序算法,但我们正在 研究 更多 内容。添加一个新算法很容易,并且 已经完成。
其他功能包括将查询存储在 URL 中以便轻松复制,或结果添加(“添加”和“分页”的混成词),或能够选择过滤 Pod 的平台。
使用随打随搜功能时,我们遇到了一个有趣的问题:因为较短的查询会产生更多结果,因此需要更长时间,有可能先发出的请求会在后发出的请求之后到达。这导致结果出现令人不快的“跳动”。
为了解决这个问题,我们 仅显示按我们发送顺序到达的结果。如果我们发送请求 A、B 和 C,并且 A 先返回,我们就会显示它。如果随后 C 返回,我们也会显示它,因为它按顺序到达。如果随后 B 在 C 之后到达,我们会丢弃它,因为它不在顺序中。这巧妙地解决了结果跳动的问题,还确保了用户体验非常灵敏。
但是第二个功能是什么?
挑剔
Picky 是一款语义文本搜索引擎,基于 Ruby 运行。它速度快(核心部分使用 C 语言),灵活性强(使用 Ruby 以实现最大的灵活性),且用途广泛。另一方面,它尚未提供即插即用的服务器,因此如果你需要一项网络服务,则需要编写一项,就像我们所做的那样。我们对search.cocoapods.org使用了 Picky。
使用 Picky 产生了两个主要影响:我们能够为 Pods 创建一个高度专业化的搜索引擎,并且我们可以利用语义搜索引擎的力量。
那么你提到的语义搜索是什么?如果你运行orta,在右上角你可以看到“作者 5”。这意味着 Orta 已被发现五次作为作者。这可能不太令人兴奋。
然而,如果你输入两个单词或单词的一部分,那么 Picky 就可以开始猜测你的意思。例如,如果你输入orta m,那么 Picky 猜测你的意思是“作者+姓名”(2 个结果),首先是“作者”(1 个结果),其次是“作者+姓名”。前者指的是两个名为“M*”且由 Orta 作为作者的 Pods。后者指的是 Mixpanel Pod,它有两个作者:Orta 和 Mixpanel(这就是 M* 找到它的原因)。
如果你单击其中一个建议,那么你可以看到,如果你已经知道(“author:orta name:m”),则可以告知 Picky 你正在寻找什么。通常情况下,你不需要它,但如果你几乎不记得 Pod 的名称,它可以提供帮助。例如,你可能还记得作者可能是 Bob 或 Bert,因此你输入“B”。Pod 几乎肯定有一个包含 c 的单词,因此你添加一个“C”。然后 Picky 将向你显示类似于“B* C”查询的所有可能性。 此查询是生成的结果。
为了决定查询词最可能出现在哪些类别的组合中,Picky 根据概率做出决定。然而,由于我们也知道人们最可能寻找哪些组合,因此我们已经告诉 Picky 优先考虑哪些内容:对于经常出现的组合,我们已经添加了提升。因此,如果在名称(+3)和依赖项(-4)中找到了查询,那么它很可能会在顶部显示名称结果。
它如何专门针对 Pod 进行优化?例如,在索引 Pod 名称时,我们会向搜索引擎提供我们期望被查询的部分。这通常意味着例如对于 "TICoreDataSync",我们会使用整个词,将其拆分为两个大写首字母,以及以大写字母开头的每个词,以及不带两个首字母的整个词。这使您可以搜索 "coredata" 并获得 "TICoreDataSync" Pod。或者,您可以搜索 "network",并获得 AFNetworking。
就我个人而言,我强烈认为,如果您想为用户提供最佳体验,则创建专门的搜索引擎非常重要。
限制
由于 CocoaPods 团队无法负担服务器场,而只能在 Heroku 上使用一台服务器,并且使用廉价的共享 CPU,因此我们需要在设计基础设施时保持明智。我们从一系列要求开始
- 它应该适合并在共享的 Heroku CPU 上运行。
- 它应该使用 Trunk Web 钩子调用重新索引 Pod。因此,如果将 Pod 推送到 Trunk,搜索应该尽快(不到 30 秒)了解它。
- 它应该是稳定的。没有人喜欢无法搜索。
- 重新启动时它应该有很少的停机时间。
为了满足这些要求,我们开始考虑设计。
设计
在运行搜索引擎几年后,我们知道
- 为了使其响应迅速,我们需要多个 Web 工作程序。
- 搜索通常很快。
- 为了处理 Trunk webhook 调用(类似于 GitHub webhook 调用),我们需要一个提供这些服务的 HTTP 端点。
- 为了在进行搜索时对 pod 进行索引,我们需要在同一进程中完成此操作(因为我们使用 Picky 内存索引,这是最快的选项,而 Redis 不是我们预算内的选项)。
- 对 pod 进行索引几乎不需要时间。
- 在单个 Heroku CPU 上实现零停机时间是不可能的,因为无法避免重启。
- 我们不需要每秒处理 1000 个请求,即使像我这样追求性能的人想要这样做。20-30 个就足够了(每分钟 1200-1800 个),我们应该为此进行设计。
因此,我们得出了以下设计
在 Unicorn 生成两个 Web 工作进程之前,我们派生出一个分析和搜索进程。这两个进程在派生后只加载它们需要的内容。在我们派生任何一个进程之前,我们打开通道,以便 Web 工作进程可以与搜索进程和分析进程进行通信。
当 Web 工作进程派生时,它选择上述通道之一来与搜索引擎和分析进程进行通信。Web 工作进程非常精简——它们只将查询发送到搜索引擎进程。这意味着任何 Web 工作进程都可以毫无问题地重新启动,因为它们相对较轻。
处理查询
如果收到任何查询,它将传递给搜索引擎进程,并且还发送到分析进程。分析进程只关注将分析数据发送到 Google Analytics。我们需要在单独的进程中运行它,因为我们目前正在使用一个同步库,该库会阻塞进程。
搜索引擎进程在分叉后会直接进入事件循环。在循环期间,它会检查是否有任何请求通过通道到达。之后,它会开始索引 Pod,一次从数据库中获取几个。通过这种方式,我们可以避免长时间的停机,因为我们可以立即返回几个 Pod。这种技术的一个缺点是,人们无法获得他们通常会获得的所有 Pod,例如,索引中可能只有 100 个 Pod。但是,我们按受欢迎程度对 Pod 进行索引,因此我们从结果列表的顶部填充它们,可以说。因此,即使搜索引擎仍在索引,您也会获得相关结果。
此事件循环还会计算查询次数,以便在status.cocoapods.org上显示。每30 秒,它会将统计信息发送到状态页面。为了不停止事件循环,我们分叉这些 Web 服务调用。
在上线后大约 9 小时,出现了一个问题,即搜索引擎用尽了资源。在调查后,我不得不羞愧地承认,我忘记了在这些 fork 之后清理。这导致单个Heroku进程处理大约 2000 个子进程,直到它崩溃,这发生在重新启动后大约 9 小时。我当时和现在都对 Heroku 能够坚持这么长时间感到印象深刻。现在,例如,我们每 30 秒向状态页面发送一次调用,记住子进程 PID,并在发送下一个调用之前再次清理它。现在搜索引擎运行得很顺利。
处理 Trunk webhook 调用
为了能够即时索引,三个 Web 工作线程中的任何一个处理 Trunk webhook 调用。然后,它们通过通道通知搜索引擎进程,然后搜索引擎进程将从 Trunk DB 重新加载正确的 pod并重新索引它。这导致您推送到 Trunk 与搜索引擎吐出更新结果之间的时间延迟不到 30 秒。请注意,结果会进行一些浏览器缓存,因此当您尝试时,请清除缓存并重新加载。
总结和未来
为了制作一个真正针对 CocoaPod 的搜索引擎,使我们能够进行试验,同时又能安装在单个共享 Heroku 实例上,我们使用了Picky、Heroku和基于多进程的结构(使用单个进程搜索事件循环),同时仍然能够一次提供足够多的结果。
我们目前正在开发 CocoaPods 搜索界面版本 2,我认为它会让您大吃一惊。它将是数月(真的!)志愿者工作的结晶,并将从我们的众多 API 中提取数据,以提供最炫酷的 pod 搜索界面。
如果您有想法要补充,请留下您的想法。