当我们想要在 Redis 文档网站 中添加实时 全文搜索 时,我们求助于 RediSearch。RediSearch 模块中的强大搜索功能帮助我们将一个乏味的表单转变成了超棒的搜索体验。为了展示 RediSearch 的部分功能,并帮助启动你的搜索项目,我想聊聊我们项目的架构并分享我们的代码。我们构建的 Python 应用程序被称为 redis-sitesearch。如果你想查看代码或在自己的网站上运行它,它是 开源的,你可以在 Redis Cloud Essentials 上免费试用 RediSearch。阅读正文了解更多细节!
我们构建了新的搜索体验,因为我们的文档网站已经超越了我们之前的搜索引擎 Lunr.js。具体来说,我们希望从新的搜索体验中获得以下功能
我们知道 RediSearch 可以提供我们的列表中的一切。(如果你不熟悉 RediSearch,它是一款适用于 Redis 的出色的查询和索引系统,包含强大的全文搜索功能。)所以,我们开始了工作。在几周内,我们就拥有了一个全新的搜索 API,为我们的文档网站上的搜索提供支持。让我们从宏观角度了解一下项目的各个部分,然后我们再深入了解。
我们的文档搜索有两个主要部分:一个 JavaScript 前端,包含一个 HTML 输入和一个搜索结果列表,以及一个查询 RediSearch 的 REST API 后端。
在你输入内容时,前端会向后端 API 发起搜索请求并呈现结果。得益于 Redis 是一个内存数据库并在多个区域部署,我们可以快速获取这些结果,在俄勒冈州进行的测试中平均大约需要 40 到 50 毫秒。
我们知道,人类会将低于 100 毫秒的响应时间感知为即时,所以我们很高兴达到了 40ms - 60ms 的范围。
但为了实现这一点,搜索应用程序内部到底发生了什么?以下是相关部分
下面是这些部分如何组合在一起的图表
我们会在 Google Cloud 中的容器上运行这些部分,并通过全局负载均衡器采用多区域部署在全球范围内分发容器实例。
现在,让我们详细了解后端应用程序涉及的四个步骤
在处理任何搜索查询前,我们需要编制文档网站的索引。要执行此操作,我们使用包含所有搜索用字段的 RediSearch 索引定义。Python 应用程序中的后台作业会按计划抓取文档网站,并将其内容添加到 RediSearch 索引。让我们了解这两个部分的工作原理。
索引定义
使用 RediSearch 时,需要创建索引定义,然后才能执行任何查询。
RediSearch 索引会与 Redis 数据库中的 Hash 保持同步。在 redis-sitesearch 应用程序中,我们为要添加到搜索索引的每个文档创建一个 Hash。
然后,我们为想编制的网站创建一个 RediSearch 索引。每个索引会与键中匹配前缀 sitesearch:{environment}:{url}:doc: 的 Hash 同步。目前,我们仅对文档网站编制索引,但此设计允许我们使用相同的应用程序实例对多个网站编制索引。索引定义采用 SCHEMA 参数,此参数定义哪些 Hash 字段应添加到索引中。 redis-sitesearch 对以下字段编制索引
使用 redisearch-python 客户端,架构如下所示
TextField("title", weight=10), TextField("section_title"), TextField("body", weight=1.5), TextField("url"),
您会看到我们已向标题和正文字段添加了“权重”。这意味着与其他字段相比,搜索查询会认为在这些字段中的匹配命中是更相关的。
索引编制作业
在有了索引定义后,我们需要一个后台作业来抓取文档网站,并创建 供 RediSearch 编制的索引的 Hash。
为此,我们使用 Redis Queue 来构建一个每 30 分钟运行的索引编制作业。此作业使用 Scrapy 库,我们已对其进行了扩展,添加了自定义逻辑来将抓取的页面内容解析为结构化数据。
使用 Scrapy 解析结构化页面数据
我们对新搜索体验的目标之一是展示我们在其上找到搜索结果的页面部分。例如,如果查询“gcp”在“帐号和团队设置”页面的“团队管理”部分中找到了一个命中,我们希望在搜索结果中同时展示此页面和此部分。下面是搜索结果的屏幕截图,展示了一个示例
在该屏幕截图中,“GCP”是命中,因此该术语以粗体呈现,这是 RediSearch 的亮点功能的一个示例。“帐号和团队设置”是我们找到命中的页面的标题,“团队管理”是包含该命中的部分的名称。
为此,我们要将每个抓取页面分解为SearchDocument对象。 SearchDocument是我们应用程序用于表示网页中我们想要使其可搜索的部分的域模型。
Scrapy 完成对站点的抓取后,我们就把SearchDocument对象转换为 Redis 哈希。然后 RediSearch 在后台索引这些哈希。
评分文件
当你用 RediSearch 配置为索引的一个键创建一个哈希时,可以选择提供“__score”字段。如果你这样做了,RediSearch 将把查询中的该文件的最终相关性值乘以__score。这意味着你可以在索引时对有关数据的某些相关性相关事实进行建模。我们来回顾一下redis-sitesearch中的几个示例。
我们创建了评分函数,以根据以下规则调整文件的评分
这种细粒度的控制是我们转而使用 RediSearch 的一个主要原因。
验证文件
根据规则对文件赋予不同的权重有助于我们提升搜索命中的相关性,但有某些类型的文件是我们希望完全避免索引的。我们的验证器是决定是否根据任意逻辑索引SearchDocument的简单函数。我们用它们跳过发行说明和所有类似于 404 页面的文件。
通过在 RediSearch 中定期对我们的文档网站进行后台作业索引,我们最终可以构建搜索 API!我们对 API 使用了Falcon网络框架,这是由于其快速性能。
一个有效的搜索 API 可将前端用户查询翻译为 RediSearch 查询。RediSearch 支持数字范围、标签、地理位置过滤器和许多其他类型的查询。对于此应用程序,最合适的方案是 前缀匹配。借助前缀匹配,RediSearch 会对比索引中的所有词条与给定的前缀。如果用户在搜索窗体中输入“红色”,则 API 将发出前缀查询“red*”。
借助前缀匹配,“red*” 将找到许多匹配项,包括
在用户输入时,搜索窗体将开始为所有这些词条的匹配项显示结果。当用户完成他们正在输入的短语时,结果将开始聚焦。如果最终的搜索是“redisearch”,则应用程序会向 Redis 发出最后一个查询“redisearch*”,结果将专门针对 RediSearch。
Google Cloud 让 redis-sitesearch 的部署变得轻松。我们用一个 单容器 作为后端应用程序,构建在 Google Cloud 的 Compute Engine 上,运行下列进程
我们在 Google Cloud 的美国西部、美国东部、苏黎世和孟买区域中的实例组中部署此容器。
注意:Compute Engine 每个节点只能运行一个容器,这就是我们让 Python 应用程序与 Redis 并行运行的原因。然而,这样做的好处是降低了延迟!
一个 全局负载均衡器 根据请求的地理来源和实例组内的当前负载,将流量路由到这些实例。
我们想要为我们的用户提供尽可能好的文档搜索体验。而 RediSearch 便是实现这一目标的最佳方式,就摆在我们的眼前。
如果您正在构建一个全文本网站搜索功能,请看看 RediSearch。它将您的整个搜索索引保存在内存中,因此查询的运行速度极快。RediSearch 还可以按数值范围、地域半径等等进行筛选。您甚至可以使用我们 开源的 redis-sitesearch 代码库 来启动您的项目。如果您想在无需自己托管的情况下试用 RediSearch,您可以在 Redis Cloud Essentials 上免费试用。
想要了解更多关于 RediSearch 的信息?请观看我们的最新 YouTube 视频:在 Redis 中进行查询、编制索引、全文本搜索。