使用 FHIR 搜索实现分页和搜索总数

Cloud Healthcare API FHIR 搜索实现具有高度可扩缩性和高性能,同时仍遵循 REST 和 FHIR 规范的准则和限制。为此,FHIR 搜索具有以下特性:

  • 在返回最后一页之前,搜索总次数是估算值。fhir.search 方法返回的搜索结果包含搜索总数(Bundle.total 属性),即搜索中的匹配项总数。在返回搜索结果的最后一页之前,搜索总数是估算值。随最后一页搜索结果返回的搜索总数是搜索中的所有匹配项的准确总和。

  • 搜索结果提供顺序向前的分页功能。如果有更多搜索结果要返回,响应中会包含用于获取下一页结果的分页网址 (Bundle.link.url)。

基本用例

FHIR 搜索可为以下用例提供解决方案:

  • 按顺序浏览。用户会按顺序浏览有限数量的搜索结果页面。系统会随每个网页一起返回估算的搜索次数总和。
  • 处理一组搜索结果。应用会获取一组搜索结果,逐个读取结果并处理数据。

如需了解这些用例的可能解决方案,请参阅以下部分。

按顺序浏览

您可以构建低延迟应用,让用户能够按顺序浏览结果页面,直到找到所需的匹配项。如果匹配项数量足够少,用户可以逐页浏览找到所需的匹配项,而无需跳过结果,则此解决方案可行。如果您的用例要求用户一次前进多个页面或向后导航,请参阅导航到附近的页面

在返回最后一页搜索结果之前,此解决方案无法提供准确的搜索总数。不过,它可以为每页搜索结果提供大致的搜索总数。虽然后台进程可能需要准确的搜索总数,但对于人为用户而言,大致的搜索总数通常就足够了。

工作流

以下是此解决方案的工作流程示例:

  1. 应用调用 fhir.search 方法,该方法会返回搜索结果的第一页。如果有更多结果要返回,响应中会包含分页网址 (Bundle.link.url)。响应还包含搜索总数 (Bundle.total)。如果要返回的结果比初始响应中的结果多,则搜索总数只是一个估算值。如需了解详情,请参阅分页和排序

  2. 应用会显示搜索结果页、指向下一页搜索结果的链接(如果有)以及搜索结果总数。

  3. 如果用户想查看下一页的结果,则会点击相应链接,这会导致系统调用分页网址。如果有更多结果要返回,响应中会包含新的分页网址。响应还包含搜索总次数。如果有更多结果需返回,则此值是经过更新的估算值。

  4. 应用会显示新的搜索结果页、指向下一页搜索结果的链接(如果有)以及搜索结果总数。

  5. 系统会重复前两个步骤,直到用户停止搜索或返回最后一页搜索结果为止。

最佳做法

选择合适的页面大小,以便用户轻松阅读。根据您的使用场景,这可能意味着每页显示 10 到 20 个匹配项。网页越小,加载速度就越快。如果网页上的链接过多,用户可能难以管理。您可以使用 _count 参数控制页面大小。

处理一组搜索结果

您可以使用分页网址连续调用 fhir.search 方法,获取一组搜索结果。如果搜索结果数量足够少,您可以继续滚动浏览,直到系统不再返回任何页面,从而获取完整的结果集。搜索结果的最后一页会显示准确的搜索结果总数。获取搜索结果后,您的应用可以逐一读取这些结果,并执行您需要的任何处理、分析或汇总操作。

请注意,如果搜索结果非常多,您可能无法在合理的时间内获取最后一页的搜索结果(以及准确的搜索总数)。

工作流

以下是此解决方案的工作流程示例:

  1. 应用调用 fhir.search 方法,该方法会返回搜索结果的第一页。如果有更多结果要返回,响应中会包含分页网址 (Bundle.link.url)。

  2. 如果还有更多结果要返回,应用会调用上一步中的分页网址,以获取下一页搜索结果。

  3. 应用会重复上一步,直到没有更多结果可返回或达到某个其他预定义限制为止。如果您到达搜索结果的最后一页,则搜索结果总数是准确的。

  4. 应用会处理搜索结果。

根据您的使用情形,您的应用可以执行以下任一操作:

  • 等待所有搜索结果都收到后,再处理数据。
  • 在每次连续调用 fhir.search 时,对收到的数据进行处理。
  • 设置某种限制,例如返回的匹配项数量或经过的时间。达到上限后,您可以处理数据、不处理数据,或执行其他操作。

设计选项

以下是一些可能会缩短搜索延迟时间的设计选项:

  • 设置较大的页面大小。使用 _count 参数设置较大的页面大小(可能为 500 到 1,000),具体取决于您的用例。使用较大的页面大小会增加每次提取页面的延迟时间,但可能会加快整个过程,因为获取一组完整的搜索结果所需提取的页面次数会减少。

  • 限制搜索结果。如果您只需要准确的搜索总数(无需返回资源内容),请将 fhir.search 方法的 _elements 参数设置为 identifier。与请求返回完整 FHIR 资源相比,这可能会缩短搜索查询的延迟时间。如需了解详情,请参阅限制搜索结果中返回的字段

需要预加载和缓存的用例

您可能需要的功能超出了使用简单机制(使用分页网址依次调用 fhir.search 方法)所能实现的功能。一种可能的方法是在应用和 FHIR 存储空间之间构建一个缓存层,以便在预提取和缓存搜索结果时维护会话状态。该应用可以将搜索结果分组为包含 10 或 20 个匹配项的小型“应用页面”。然后,应用可以根据用户的选择,以直接的非顺序方式快速向用户提供这些小型应用页面。如需查看此类工作流的示例,请参阅导航到附近的页面

您可以构建低延迟解决方案,让用户浏览大量搜索结果,直到找到所需的匹配项。它可以扩展到几乎无限数量的匹配,同时保持较低的延迟时间,并且资源消耗相对较小。用户可以直接前往某个搜索结果页,最多可向前或向后浏览预先指定的页数。您可以在每页搜索结果中提供估算的搜索总数。请注意,此设计类似于 Google 搜索的设计。

工作流

图 1 显示了此解决方案的工作流程示例。采用此工作流程后,每当用户选择要查看的结果页面时,应用都会提供指向附近页面的链接。在这种情况下,应用会提供指向从所选页面向前最多四个页面和向后最多五个页面的页面的链接。为了让用户可以查看前四个页面,应用会在用户选择一个结果页时预提取另外 40 个匹配项。

预提取和缓存

图 1. 应用会将搜索结果分组为缓存并提供给用户的“应用页面”。

图 1 展示了这些步骤:

  1. 用户在应用前端输入搜索查询,从而触发以下操作:

    1. 应用调用 fhir.search 方法以预加载搜索结果的第一页。

      _count 参数设置为 100,以使响应的页面大小保持相对较小,从而缩短响应时间。如果有更多结果要返回,响应中会包含分页网址 (Bundle.link.url)。响应还包含搜索总数 (Bundle.total)。如果有更多结果要返回,搜索总数是估算值。如需了解详情,请参阅分页和排序

    2. 应用将响应发送到缓存层。

  2. 在缓存层中,应用会将响应中的 100 个匹配项分组为 10 个应用页面,每个页面包含 10 个匹配项。应用页面是应用可向用户显示的一小组匹配项。

  3. 应用向用户显示应用页面 1。应用页面 1 包含指向应用页面 2-10 的链接和估算的搜索次数总和。

  4. 用户点击指向其他应用页面(在此示例中为应用页面 10)的链接,从而启动以下操作:

    1. 应用使用上次预提取时返回的分页网址调用 fhir.search 方法,以预提取下一页的搜索结果。

      _count 参数设置为 40 可预加载用户搜索查询中的下 40 个匹配项。40 个匹配项是指应用向用户提供的接下来 4 个应用页面。

    2. 应用将响应发送到缓存层。

  5. 在缓存层中,应用会将响应中的 40 个匹配项分组到四个应用页面中,每个页面包含 10 个匹配项。

  6. 应用向用户显示应用页面 10。应用页面 10 包含指向应用页面 5-9(从应用页面 10 向后的五个应用页面)和应用页面 11-14(从应用页面 10 向前的四个应用页面)的链接。应用页面 10 还包含估算的搜索次数总和。

只要用户想继续点击指向应用页面的链接,此过程便会持续进行。请注意,如果用户从当前应用页面返回,并且您已缓存附近的所有应用页面,则可以根据您的用例选择不进行新的预提取。

此解决方案快速高效,原因如下:

  • 系统会快速处理小型预提取内容,甚至更小的应用页面。
  • 缓存的搜索结果可减少对同一结果进行多次调用的需要。
  • 无论搜索结果数量有多大,该机制的速度都不会降低。

设计选项

以下是一些设计选项,请根据您的用例进行考虑:

  • 应用页面大小。如果适合您的用例,您的应用页面可以包含超过 10 个匹配项。请注意,网页越小,加载速度就越快,如果网页上的链接过多,用户可能难以管理。

  • 应用页面链接的数量。在此处建议的工作流程中,每个应用页面都会返回指向其他应用页面的 9 个链接:5 个链接指向当前应用页面直接向后的应用页面,4 个链接指向当前应用页面直接向前的应用页面。您可以根据自己的用例调整这些数字。

最佳做法

  • 仅在必要时使用缓存层。如果您设置了缓存层,请仅在您的用例需要时使用它。不需要缓存层的搜索应绕过该层。

  • 缩减缓存大小。为了节省资源,您可以清除旧搜索结果,同时保留用于获取这些结果的网页网址,从而减小缓存大小。然后,您可以通过调用网页网址来根据需要重新构建缓存。请注意,由于 FHIR 存储区中的资源会在后台创建、更新和删除,因此对同一分页网址的多次调用的结果可能会随时间而变化。是否要清除缓存、如何清除以及清除缓存的频率都是设计决策,具体取决于您的使用场景。

  • 清除指定搜索的缓存。为节省资源,您可以从缓存中彻底移除非活跃搜索的结果。建议先移除闲置时间最长的搜索记录。请注意,如果已清除的搜索再次变为有效状态,可能会导致错误状态,迫使缓存层重启搜索。

如果您希望用户能够前往搜索结果中的任何网页(而不仅仅是当前网页附近的网页),则可以使用类似于导航到附近的网页中所述的缓存层。不过,如果要允许用户前往搜索结果中的任何应用页面,您需要预提取并缓存所有搜索结果。如果搜索结果数量相对较少,则有可能实现。如果搜索结果非常多,预加载所有结果可能不切实际或根本不可能。即使搜索结果数量不多,预加载这些结果所需的时间也可能超出用户合理的等待时间。

工作流

设置与导航到附近的页面类似的工作流程,但有一个关键区别:应用会继续在后台预加载搜索结果,直到返回所有匹配项或达到某个其他预定义限制为止。

以下是此解决方案的工作流程示例:

  1. 应用调用 fhir.search 方法,以预加载用户搜索查询的首页搜索结果。如果有更多结果要返回,响应中会包含分页网址 (Bundle.link.url)。响应还包含搜索总数 (Bundle.total)。如果有更多结果要返回,则此值为估算值。

  2. 应用会将响应中的匹配项分组到每个包含 20 个匹配项的应用页面中,并将其存储在缓存中。应用页面是应用可向用户显示的一小组匹配项。

  3. 应用向用户显示第一个应用页面。应用页面包含指向缓存的应用页面和估算的搜索总次数的链接。

  4. 如果还有更多结果要返回,应用会执行以下操作:

    • 调用上一次预提取返回的分页网址,以获取下一页搜索结果。
    • 将响应中的匹配项分组到每个包含 20 个匹配项的应用页面中,并将其存储在缓存中。
    • 使用指向新预提取和缓存的应用页面的新链接刷新用户当前正在查看的应用页面。
  5. 应用会重复上一步,直到没有更多结果可返回或达到某个其他预定义限制为止。系统会在搜索结果的最后一页中返回准确的搜索总数。

在应用在后台预提取和缓存匹配项时,用户可以继续点击指向缓存网页的链接。

设计选项

以下是一些设计选项,请根据您的用例进行考虑:

  • 应用页面大小。如果适合您的用例,应用页面可以包含多于或少于 20 个匹配项。请注意,网页越小,加载速度就越快,如果网页上的链接过多,用户可能难以管理。

  • 刷新搜索次数总和。在应用后台预取和缓存搜索结果时,您可以向用户显示越来越准确的搜索总数。为此,请将您的应用配置为执行以下操作:

    • 在设定的间隔时间内,从缓存层中的最新预提取中获取搜索总次数(Bundle.total 媒体资源)。这是目前对搜索总次数的最佳估算值。向用户显示搜索总次数,并指明该次数是估算值。根据您的用例确定此刷新的频率。

    • 了解缓存层中的搜索总次数何时准确无误。也就是说,搜索总次数是指搜索结果的最后一页中的次数。当到达搜索结果的最后一页时,应用会显示搜索总数,并向用户指明搜索总数准确无误。然后,应用会停止从缓存层获取搜索总数。

    请注意,如果匹配项数量很多,在用户完成搜索会话之前,后台预提取和缓存可能无法达到搜索结果的最后一页(以及准确的搜索总数)。

最佳做法

  • 删除重复的包含资源。如果您在预提取和缓存搜索结果时使用 _include_revinclude 参数,我们建议您在每次预提取后删除缓存中的包含资源的重复项。这有助于通过减小缓存大小来节省内存。将匹配项分组到应用页面后,请向每个应用页面添加适当的包含资源。如需了解详情,请参阅在搜索结果中添加其他资源

  • 对预提取和缓存设置限制。如果搜索结果非常多,预提取所有结果可能不切实际或根本不可能。我们建议您设置要预加载的搜索结果数量上限。这样可以将缓存保持在可管理的大小,并有助于节省内存。例如,您可以将缓存大小限制为 10,000 或 20,000 个匹配项。或者,您也可以限制要预加载的网页数量,或设置预加载停止的时间限制。您要强加的限制类型和强加方式是设计决策,取决于您的用例。如果在返回所有搜索结果之前就达到了上限,不妨考虑向用户指明这一点,包括搜索总数仍是估算值。

前端缓存

应用前端(例如网络浏览器或移动应用)可以对搜索结果进行一些缓存,这是一种在架构中引入缓存层的替代方案。此方法可以利用 AJAX 调用并存储搜索结果和/或分页网址,从而提供前一个网页或导航记录中的任何网页的导航。下面列出了此方法的一些优势:

  • 与缓存层相比,它可能需要的资源更少。
  • 它会将缓存工作分布到多个客户端,因此具有更高的可伸缩性。
  • 更轻松地确定何时不再需要缓存的资源,例如,当用户关闭标签页或离开搜索界面时。

一般最佳实践

以下是适用于本文档中所有解决方案的一些最佳实践。

  • 为网页数量小于 _count 值的情况做好准备。在某些情况下,搜索可能会返回的网页中匹配项的数量少于您指定的 _count 值。例如,如果您指定的页面大小特别大,就可能会发生这种情况。如果搜索返回的网页小于 _count 值,并且您的应用使用了缓存层,您可能需要决定是(1)在应用页面上显示的结果少于预期,还是(2)提取更多结果以获取足够的结果来填充完整的应用页面。如需了解详情,请参阅分页和排序

  • 针对可重试的 HTTP 请求错误执行重试。您的应用应预期可重试的 HTTP 请求错误(例如 429500),并在收到这些错误后重试。

评估您的用例

实现导航到任意网页、获取准确的搜索总数以及更新估算总数等功能会增加应用的复杂性和开发成本。这些功能还可能会增加延迟时间,并增加使用 Google Cloud资源的费用。我们建议您仔细评估自己的使用情形,确保这些功能的价值与费用相称。请注意以下事项:

  • 导航到任意页面。用户通常不需要从当前网页导航到特定网页,而是需要导航到多个网页。在大多数情况下,导航到附近的网页就足够了。

  • 准确的搜索总次数。随着 FHIR 存储区中资源的创建、更新和删除,搜索总数可能会发生变化。因此,搜索总数在返回时(随搜索结果的最后一页一起返回)是准确的,但随着时间的推移,其准确性可能会不保。因此,准确的搜索总次数对您的应用而言可能没有太大价值,具体取决于您的使用场景。