如何使用 Java 和 MongoDB 构建搜索服务
目录
我们需要编写代码,打通从搜索框到搜索索引的路径。执行搜索并以可展示的方式渲染结果本身并不是一件难事:将用户的查询发送到搜索服务器,并将响应数据转换为某种用户界面技术。然而,有一些重要问题需要解决,例如安全性、错误处理、性能以及其他需要隔离和控制的问题。
典型的三层系统包含一个表现层,它将用户请求发送到中间层(即应用服务器),后者与后端数据服务进行交互。这些层分离了关注点,使得每一层都能专注于自身的职责。
[LOADING...]
如果您构建过用于管理数据库集合的应用程序,那么您无疑实现过增删改查(CRUD)功能,这些功能将业务逻辑隔离在中间应用层中。
搜索是一种略有不同类型的服务,因为它是只读的,访问非常频繁,必须快速响应才有用,并且通常返回的不仅仅是文档。搜索结果返回的额外元数据通常包括关键词高亮、文档得分、分面以及找到的结果数量。此外,搜索通常匹配的文档数量远超合理展示的范围,因此分页和过滤搜索是必要的功能。
我们的搜索服务通过以下方式提供了上述三层架构的好处:
- 安全性:数据库连接字符串隔离到服务环境中。参数经过验证和清理。客户端/用户无法请求大量结果或进行深度分页。
- 可扩展性:该服务是无状态的,可以轻松多次部署并进行负载均衡。
- 更快的部署:服务端点可以进行版本控制,并在部署增强版本时保持运行。可以在不一定影响表现层或数据库及搜索索引配置的情况下修改行为。
在本文中,我们将详细介绍一个 HTTP Java 搜索服务,该服务设计为由表现层调用,并依次将请求转换为查询我们 Atlas 数据层的聚合管道。这纯粹是一个服务实现,没有最终用户 UI;用户界面留给读者作为练习。换句话说,作者在向用户界面提供搜索服务方面拥有丰富的经验,但他本人并不是 UI 开发人员。🙂
前置条件
本文的代码位于 GitHub 仓库 中。
该项目是使用以下工具构建的:
- Gradle 8.5
- Java 21
使用了标准的 Java 和 Servlet API,应该可以直接工作或轻松移植到较新的 Java 版本。
为了运行此处提供的示例,需要加载 Atlas 示例数据,并在 sample_mflix.movies 集合上创建 movies_index,如下所述。如果您是 Atlas Search 的新手,一个好的起点是 Using Atlas Search from Java。
搜索服务设计
前端表现层提供搜索框,渲染搜索结果,并提供排序、分页和过滤控件。中间层通过 HTTP 请求验证并将搜索请求参数转换为聚合管道规范,然后将其发送到数据层。
搜索服务需要快速、可扩展,并处理以下基本参数:
- 查询本身:这是用户在搜索框中输入的内容。
- 要返回的结果数量:通常一次只需要 10 个左右的结果。
- 搜索结果的起始点:这允许对搜索结果进行分页。
此外,高性能查询应该只搜索并返回少量字段,尽管搜索的字段和需要返回的字段不一定相同。例如,在搜索电影时,您可能想要搜索 fullplot 字段,但不返回可能很长的文本用于展示。或者,您可能希望在结果中包含电影发行的年份,但不搜索 year 字段。
此外,搜索服务必须提供一种将搜索结果限制在特定类别、类型或演员阵容的方法,而不影响结果的相关性排序。这种过滤功能也可用于强制执行访问控制,服务层是添加此类约束的理想场所,表现层可以依赖这些约束而无需自行管理。
搜索服务接口
现在让我们根据设计具体定义服务接口。我们的目标是支持一个请求,例如 针对 title 和 plot 字段查询 "purple rain",查找 "Music" 类型的电影,每次只返回五个结果,且仅包含字段 title、genres、plot 和 year。从我们表现层的角度来看,该请求是以下 HTTP GET 请求:
这些参数以及一个 debug 参数将在下表中详细说明:
返回结果
给定指定的请求,让我们定义响应 JSON 结构,以便在 docs 数组中返回匹配文档的请求字段(project)。此外,搜索服务返回一个 request 部分,显示用于构建 Atlas $search 管道的显式和隐式参数,以及一个 meta 部分,该部分将返回匹配文档的总数。这个结构完全是我们自己的设计,并不打算作为聚合管道响应的直接透传,这允许我们隔离、操作和映射响应,以最大程度地满足我们表现层的需要。
这是实现的骨架,一个标准的 doGet servlet 入口点,用于获取我们指定的所有参数:
请注意,已经定义了一些实例变量,它们在标准的 servlet init 方法中根据 web.xml 部署描述符中指定的值以及 ATLAS_URI 环境变量进行初始化:
为了最大程度地保护我们的 ATLAS_URI 连接字符串,我们在环境中定义它,这样它既不会被硬编码,也不会在应用程序初始化之外可见,而我们在标准的 web.xml 部署描述符中指定数据库、集合和索引名称,这允许我们为想要支持的每个索引定义端点。这是一个基本的 web.xml 定义:
获取搜索结果
请求搜索结果是一个无状态操作,对数据库没有副作用,并且作为一个简单的 HTTP GET 请求工作得很好,因为查询本身不应该是一个非常长的字符串。我们的前端层可以适当地限制长度。如果需要,可以通过调整为 POST/getPost 来支持更大的请求。
幕后的聚合管道
最终,为了支持我们要返回的信息(如上面的示例响应所示),上面显示的请求示例被转换为这个聚合管道请求:
关于这个生成的聚合管道,有几个方面值得进一步解释:
- 查询 (
q) 被转换为指定search字段上的text操作符。这两个参数在此实现中都是必需的。 filter参数被转换为使用equals操作符的非评分filter子句。equals操作符要求将字符串字段索引为token类型;这就是为什么你会看到genres和cast字段被设置为string和token两种类型。这两个字段可以进行全文搜索(通过text或其他支持字符串类型的操作符),也可以用作精确匹配的equals过滤器。- 在 $search 中请求匹配文档的计数,该计数在
$$SEARCH_META聚合变量中返回。由于此元数据不是特定于文档的,因此需要特殊处理才能从聚合调用返回到我们的搜索服务器。这就是利用$facet阶段的原因,以便将此信息拉入我们服务响应的meta部分。
使用 $facet 是一个有点棘手的技巧,这也为我们的聚合管道响应提供了未来扩展的空间。
侧边栏(> markdown)部分:
$facet聚合阶段的名称与 Atlas Search 的facet收集器同名,这容易引起混淆。搜索结果分面给出匹配搜索结果中组的标签和该组的计数。例如,对genres进行分面(这需要调整此处的索引配置)除了提供符合搜索条件的文档外,还会提供这些搜索结果中所有genres的列表以及每个的数量。将facet操作符添加到此搜索服务已列入下面提到的路线图。
代码中的 $search
给定查询 (q)、搜索字段列表 (search) 和过滤器(零个或多个 filter 参数),使用 Java 驱动程序的便捷方法以编程方式构建 $search 阶段非常简单:
当 debug=true 时,我们添加了 Atlas Search 的 scoreDetails 功能,允许我们仅在需要时检查详细的 Lucene 评分细节;请求评分细节会对性能造成轻微影响,并且对于我们大多数人来说通常太底层了。
字段投影
我们服务实现的最后一个有趣的部分涉及字段投影。返回 _id 字段或不返回,需要特殊处理。我们的服务代码会检查 project 参数中是否存在 _id,如果未指定则显式将其关闭。我们还添加了一项功能,如果需要,可以通过查看 project 参数中指定的特殊 _score 伪字段来包含文档的计算相关性分数。以编程方式构建投影阶段如下所示:
聚合与响应
在参数处理和阶段构建的最后,非常简单直接,我们构建完整的管道,调用 Atlas,构建 JSON 响应,并将其返回给调用客户端。这里唯一独特的事情是,当 debug=true 时添加 .explain() 调用,以便我们的客户端可以从 Atlas 的角度看到发生了什么的完整图景:
投入生产
这是一个标准的 Java servlet 扩展,旨在 Tomcat、Jetty 或其他符合 servlet API 的容器中运行。构建运行 Gretty,它平稳地允许开发人员运行 jettyRun 或 tomcatRun 来启动此示例 Java 搜索服务。
为了构建可以部署到生产环境的发行版,请运行:
未来路线图
我们的搜索服务目前对于基本的搜索用例来说足够健壮,但仍有改进空间。以下是服务未来演变的一些想法:
- 添加否定过滤器。目前,我们支持使用
filter=field:value参数的肯定过滤器。否定过滤器前面可以有一个减号。例如,为了排除“Drama”(剧情)电影,可以实现支持filter=-genres:Drama。 - 支持高亮显示,以返回匹配查询词的字段值片段。
- 实现分面。
- 等等……请参阅 问题列表 以获取更多想法并添加你自己的想法。
由于服务层是一个中间层,可以独立部署,而不必一定更改前端或数据层,因此其中一些功能可以在不更改这些层的情况下添加。
结论
实现中间层搜索服务提供了许多好处,从安全性到可伸缩性,再到能够隔离更改和部署,而不受表示层和其他搜索客户端的影响。此外,搜索服务允许客户端使用标准的 HTTP 和 JSON 技术轻松利用复杂的搜索功能。
有关将 Java 与 Atlas Search 结合使用的基础知识,请查看 Using Atlas Search from Java | MongoDB。当你开始利用 Atlas Search 时,请务必查看 Query Analytics 功能以帮助改进你的搜索结果。