创建一个微服务来即时索引大量内容并返回准确的结果
说 Course Hero 拥有庞大的内容库是轻描淡写的。 Course Hero 拥有数以亿计的文档、问题和其他学习资料。 用户可以通过多种方式找到此内容,例如 Google 等搜索引擎。
您登陆的页面会显示您感兴趣的文档的预览。如果您正在搜索的短语或主题不是可用预览的一部分,您可能会跳出,不知道您正在搜索的内容可能仍然是其中的一部分 文件。 或者您可能会订阅以获取该文档,假设它完全符合您的要求,就在预览后面,结果却发现完整的文档并不是您想要的。
我们正在测试一个文档内搜索栏,它允许用户搜索整个文档,而不仅仅是预览中的文本,从而在用户订阅完全访问之前建立文档包含相关信息的信心。
我们的用例很简单,但有些独特:允许以预先输入的方式快速搜索文档的全部内容。
我们需要找到与用户对给定文档的查询相匹配的所有片段,并且由于我们正在做预先输入,我们不想删除停用词或标点符号。 但是,我们确实希望帮助解决拼写错误并支持同义词。
我们评估了 ElasticSearch、AWS Open Search 和 Algolia 等解决方案。这些都旨在搜索数百万个文档,而不是真正在单个文档中进行搜索。他们还要求我们预先索引我们的整个内容库,这是我们不想预先支付的成本——尤其是不知道我们的用户是否会找到足够的价值来证明它的合理性。
我们需要一个服务,它可以接收用户对给定文档的查询,加载该文档的索引(或即时索引文档)并返回结果,所有这些都在一两秒内完成。
我们最终使用了一个名为 Bleve 的开源库来处理全文搜索。 Bleve 具有许多开箱即用的功能,使我们的工作变得更加轻松,例如处理部分匹配、同义词和停用词。它还具有足够的可定制性,允许我们编写标记器、字符过滤器和碎片器,以确保我们得到我们关心的所有结果。最重要的是,它允许我们针对单个文档创建索引并立即搜索该索引。
在我们最初的概念验证期间,我们确定我们能够在 1-2 秒 (p95) 内下载、索引和返回大约 90% 的文档库的相关结果。这意味着我们只需要对最大的 10% 文档进行预索引,从而节省大量时间和金钱。
服务架构如下:
我们有 AWS 简单队列服务 (SQS) 后端工作人员,只要上传文档就会收到通知。这些工作人员确定文档是否足够大以至于应该对其进行预索引,如果是,则开始该过程。
当对给定文档的请求进入时,我们的文档搜索 REST/gRPC 服务会检查该索引是否已经加载到内存中。如果是这样,它会立即提供结果。如果不存在,则检查索引是否存在于 S3 中,如果存在,则下载并加载索引,再次返回结果。
如果这是一个从未被索引过的新文档,那么该服务将下载原始文档,将其加载到索引中,并提供结果,同时将该索引上传到 S3 以便它可以在未来。
如果索引没有加载到内存中,那么服务将获得一个锁,这将阻止对同一索引的其他请求。一旦加载了索引,就会释放锁,并允许所有阻塞的请求通过。索引存储在 LRU 堆栈中,我们只在内存中保留一定数量的索引,过期的索引不再被主动搜索。
最后,我们使用 Istio 作为我们的服务网格在 Kubernetes 内部运行我们的服务。 Istio 提供了对粘性的内置支持,我们可以将同一文档的请求定向到我们服务的同一实例。通过 typeahead search,我们希望在用户键入时每隔几秒就可以快速查询到相同的文档。初始结果可能是 1 到 2 秒,但是所有后续请求都应该在几毫秒内得到处理,因为这些请求会遇到索引已经在内存中的服务。
借助这种架构,我们能够为 90% 的库提供即时索引的搜索结果,并对任何太大的内容进行预索引,从而为我们的用户提供快速的搜索体验。