FHIR 検索を使用したページ分けと検索の合計の実装

Cloud Healthcare API の FHIR 検索の実装は、REST と FHIR 仕様のガイドラインと制限に準拠しながら、高いスケーラビリティとパフォーマンスを実現しています。これを実現するために、FHIR 検索には次のプロパティがあります。

  • 検索の合計数は、最後のページが返されるまでの見積もり値です。fhir.search メソッドによって返される検索結果には、検索の合計(Bundle.total プロパティ)が含まれます。これは、検索で一致した合計数です。検索の合計は、検索結果の最終ページが返されるまでは概算です。検索結果の最後のページで返される検索合計は、検索内のすべての一致の正確な合計です。

  • 検索結果は、順番に前方ページネーションされます。返される検索結果が増えると、レスポンスには、次の結果ページを取得するためのページ分け URL(Bundle.link.url)が含まれます。

基本的なユースケース

FHIR 検索は、次のユースケースのソリューションを提供します。

これらのユースケースの考えられる解決策については、次のセクションをご覧ください。

順番にブラウジングする

ユーザーが目的の一致を見つけるまで、結果のページを順番にブラウジングできる低レイテンシ アプリケーションを構築できます。このソリューションは結果をスキップしないでページごとに閲覧するため、一致の数が小さい場合に有効です。ユーザーが一度に複数のページを前後に移動する必要があるユースケースの場合は、近隣のページに移動するをご覧ください。

このソリューションでは、検索結果の最後のページが返されるまで、正確な検索合計を提供できません。ただし、結果の各ページで、おおよその検索数を指定できます。バックグラウンド プロセスでは正確な合計検索数が必要になる場合がありますが、人間のユーザーにとっては通常、おおよその合計検索数で十分です。

ワークフロー

このソリューションのワークフローの例を次に示します。

  1. アプリが fhir.search メソッドを呼び出し、検索結果の最初のページを返します。返される結果が多い場合は、レスポンスにページ分け URL(Bundle.link.url)が含まれます。レスポンスには、検索の合計(Bundle.total)も含まれます。返される結果が最初のレスポンスよりも多い場合、検索の合計は概算になります。詳細については、ページ分けと並べ替えをご覧ください。

  2. アプリには、検索結果のページ、次のページへのリンク(ある場合)、検索の合計が表示されます。

  3. ユーザーが次のページの結果を表示したい場合は、リンクをクリックします。これにより、ページ分け URL が呼び出されます。返される結果が多い場合は、レスポンスに新しいページ分け URL が含まれます。レスポンスには、検索の合計も含まれます。返される結果がさらにある場合は、この値が更新されます。

  4. 検索結果の新しいページ、検索結果の次のページへのリンク(ある場合)、検索の合計が表示されます。

  5. ユーザーが検索を停止するか、結果の最後のページが返されるまで、上記の 2 つのステップが繰り返されます。

ベスト プラクティス

人間が読みやすく、適切なページサイズを選択します。ユースケースに応じて、ページあたり 10 ~ 20 件の一致を表示できます。ページサイズが小さいほど読み込みは速くなります。また、ページ上のリンクが多すぎると、ユーザーが管理するのが難しくなります。ページサイズは _count パラメータで制御します。

一連の検索結果を処理する

ページ分け URL を使用して fhir.search メソッドを連続して呼び出すことで、検索結果のセットを取得できます。検索結果の数が十分に少ない場合、返されるページがなくなるまで続行することで、完全な結果セットを取得できます。正確な検索結果の合計数は、結果の最後のページに表示されます。検索結果を取得した後、アプリは検索結果を読み取り、必要な処理、分析、集計を実行できます。

検索結果が非常に多い場合は、検索結果の最後のページ(および正確な検索合計)を現実的な時間内に取得できない可能性があります。

ワークフロー

このソリューションのワークフローの例を次に示します。

  1. アプリは fhir.search メソッドを呼び出し、検索結果の最初のページを返します。返される結果が多い場合は、レスポンスにページ分け URL(Bundle.link.url)が含まれます。

  2. 返される結果がさらにある場合、アプリは前の手順のページ分け URL を呼び出して、検索結果の次のページを取得します。

  3. アプリは、返す結果がなくなるか、事前定義された上限に達するまで、前の手順を繰り返します。検索結果の最終ページに到達した場合、検索の合計数は正確です。

  4. アプリが検索結果を処理します。

ユースケースに応じて、アプリは次のいずれかを行います。

  • すべての検索結果が受信されるまで待ってから、データを処理します。
  • fhir.search を連続して呼び出すたびに、受信したデータを処理します。
  • 返される一致数や経過時間など、なんらかの上限を設定します。上限に達した場合、データの処理、データの処理なし、またはその他のアクションを実行できます。

設計オプション

検索レイテンシを低減できる可能性がある設計オプションをいくつか紹介します。

  • 大きなページサイズを設定する。_count パラメータを使用して、ユースケースに応じて大きなページサイズ(500 ~ 1,000 など)を設定します。大きなページサイズを使用すると、各ページ取得のレイテンシが増加しますが、検索結果全体を取得するのに必要なページ数が少なくなるため、プロセス全体が高速化される可能性があります。

  • 検索結果を制限する。正確な検索の合計(リソース コンテンツを返す必要がない)だけが必要な場合、fhir.search メソッドの _elements パラメータを identifier に設定します。これにより、完全な FHIR リソースの返却をリクエストする場合と比較して、検索クエリのレイテンシが短縮される可能性があります。詳細については、検索結果に返されるフィールドの制限をご覧ください。

プリフェッチとキャッシュ保存が必要なユースケース

ページ分け URL を使用して fhir.search メソッドを連続して呼び出すという単純なメカニズムより進んだ機能が必要になる場合があります。1 つの可能性としては、アプリと FHIR ストアの間にキャッシュ レイヤを構築し、検索結果をプリフェッチしてキャッシュに保存しながらセッション状態を維持する方法があります。アプリは、検索結果を 10 件または 20 件の一致の小さな「アプリページ」にグループ化できます。アプリは、ユーザーの選択に基づいて、これらの小さなアプリページを直接、連続的でない方法でユーザーに迅速に提供できます。このタイプのワークフローの例については、近くのページに移動するをご覧ください。

ユーザーが目的の一致を見つけるまで多数の検索結果を移動できるように、低レイテンシのソリューションを構築できます。レイテンシを低く保ちつつリソース消費の増加を比較的低く抑えながら、実質的に無制限の一致にまでスケーリングできます。検索結果のページに直接移動できます。現在のページから前または逆方向に、事前に設定したページ数だけ移動できます。結果の各ページで、推定合計検索数を指定できます。なお、この設計は Google 検索の設計に似ています。

ワークフロー

図 1 に、このソリューションのワークフローの例を示します。このワークフローでは、ユーザーが表示する結果ページを選択するたびに、アプリは近隣のページへのリンクを表示します。この場合、アプリは選択したページから最大 5 ページ前に、選択したページから最大 4 ページ後にリンクします。4 ページ後のページを表示するため、アプリはユーザーが結果のページを選択すると 40 個の追加一致をプリフェッチします。

プリフェッチとキャッシュ

図 1.アプリは検索結果を「アプリページ」にグループ化し、キャッシュに保存してユーザーが利用できるようにします。

図 1 はこれらのステップを図示したものです。

  1. ユーザーがアプリのフロントエンドに検索クエリを入力すると、次のアクションが開始されます。

    1. アプリは fhir.search メソッドを呼び出して、検索結果の最初のページをプリフェッチします。

      _count パラメータを 100 に設定してレスポンスのページサイズを比較的小さく維持すると、レスポンス時間が比較的短くなります。返される結果が多い場合は、レスポンスにページ分け URL(Bundle.link.url)が含まれます。レスポンスには、検索の合計(Bundle.total)も含まれます。返される結果が他にもある場合、検索の合計は概算になります。詳細については、ページネーションと並べ替えをご覧ください。

    2. アプリがキャッシュ レイヤにレスポンスを送信します。

  2. キャッシュ レイヤでアプリは、100 件の一致レスポンスを、ページあたり 10 件の一致を示す 10 ページのアプリページにグループ化します。アプリページは、アプリがユーザーに表示できる一致の小さなグループです。

  3. アプリによってユーザーには、アプリページ 1 が表示されます。アプリページ 1 には、アプリページ 2 ~ 10 へのリンクと推定検索総数が表示されます。

  4. ユーザーが別のアプリページ(この例ではアプリページ 10)へのリンクをクリックすると、次のアクションが開始されます。

    1. アプリは、前のプリフェッチで返されたページ分け URL を使用して fhir.search メソッドを呼び出し、検索結果の次のページをプリフェッチします。

      _count パラメータは 40 に設定されており、ユーザーの検索クエリから次の 40 件の一致をプリフェッチします。この 40 件の一致数は、アプリでユーザーが使用できるようになる次の 4 つのアプリページに相当します。

    2. アプリがキャッシュ レイヤにレスポンスを送信します。

  5. キャッシュ レイヤでは、40 件の一致レスポンスを、ページあたり 10 件の一致を示す 4 ページのアプリページにグループ化します。

  6. アプリによってユーザーには、アプリページ 10 が表示されます。アプリページ 10 には、アプリページ 5 ~ 9(アプリページ 10 から 5 アプリページ前)とアプリページ 11 ~ 14(アプリページ 10 から 4 アプリページ後)へのリンクがあります。アプリページ 10 には、推定検索総数も表示されます。

ユーザーがアプリページへのリンクをクリックし続けている限り、この処理は続行されます。ユーザーが現在のアプリページから戻るときに、近くのアプリページがすべてキャッシュされている場合は、ユースケースに応じて新しいプリフェッチを行わない選択もできます。

このソリューションは、次の理由から迅速かつ効率的です。

  • 小さなプリフェッチや、さらに小さなアプリページは、すばやく処理されます。
  • 検索結果をキャッシュに保存すると、同じ結果を複数回呼び出す必要がなくなります。
  • このメカニズムは、検索結果の数がどれほど大きくなっても高速です。

設計オプション

ユースケースに応じて検討すべき設計オプションをいくつか紹介します。

  • アプリのページサイズ。ユースケースに適している場合、アプリページには 10 を超える一致を含めることができます。小規模なページは読み込みが速く、ページ上のリンクが多すぎると、ユーザーの管理が難しくなることがあります。

  • アプリページのリンク数。ここで推奨しているワークフローでは、各アプリページが 9 つのアプリページへのリンクを返します。つまり、現在のアプリページから直接 5 アプリページ前のアプリページに 5 つのリンク、また現在のアプリページから直接 5 アプリページ後のアプリページに 4 つのリンクを返します。これらの数値は、ユースケースに合わせて調整できます。

ベスト プラクティス

  • キャッシュ レイヤは必要な場合にのみ使用します。キャッシュ レイヤを設定する場合は、ユースケースで必要に応じてのみ使用します。キャッシュ レイヤを必要としない検索では使用しないようにします。

  • キャッシュのサイズを縮小する。リソースを節約するため、検索結果の検索結果の表示に使用していたページの URL を保持しながら、古い検索結果を消去することでキャッシュ サイズを小さくできます。その後、ページの URL を呼び出すことで、必要に応じてキャッシュを再構築できます。FHIR ストアのリソースはバックグラウンドで作成、更新、削除されるため、同じページ分け URL を複数回呼び出すと結果が変わる可能性があります。キャッシュをパージするかどうか、パージ方法、パージ頻度は、ユースケースに応じて設計する必要があります。

  • 特定の検索のキャッシュを削除します。 リソースを節約するため、非アクティブな検索の結果をキャッシュから完全に削除できます。最初に、最も長い期間使用していない検索を削除することを検討してください。削除された検索が再びアクティブになると、エラー状態となり、キャッシュ レイヤが検索を再開することに留意してください。

現在のページの近くのページだけでなく、検索結果の任意のページにユーザー移動できるようにするには、近くのページに移動で説明したものと同様のキャッシュ レイヤを使用します。ただし、ユーザーが検索結果の任意のアプリページに移動できるようにするには、すべての検索結果をプリフェッチしてキャッシュに保存する必要があります。検索結果が比較的少ない場合はこれが可能です。検索結果が非常に多い場合は、すべてをプリフェッチすることは現実的ではない、または不可能です。検索結果が非常に少ない場合でも、プリフェッチにかかる時間は、ユーザーが待つと予想される時間よりも長くなる可能性があります。

ワークフロー

近くのページに移動と同様のワークフローを設定しますが、次の重要な違いがあります。この場合アプリは、すべての一致が返されるか定義済みの上限に到達するまで、バックグラウンドで検索結果のプリフェッチを継続します。

このソリューションのワークフローの例を次に示します。

  1. アプリは fhir.search メソッドを呼び出して、ユーザーの検索クエリから検索結果の最初のページをプリフェッチします。返される結果が多い場合は、レスポンスにページ分け URL(Bundle.link.url)が含まれます。レスポンスには、検索の合計(Bundle.total)も含まれます。返される結果がさらにある場合は、これは概算です。

  2. アプリは、レスポンスから取得した一致を、20 件の一致を示すアプリページにグループ化してキャッシュに保存します。アプリページは、アプリがユーザーに表示できる一致の小さなグループです。

  3. アプリによって最初のアプリページが表示されます。アプリページには、キャッシュされたアプリページと推定検索総数へのリンクが含まれます。

  4. 返す結果が他にもある場合は、アプリは次の処理を行います。

    • 前のプリフェッチから返されたページ分け URL を呼び出して、検索結果の次のページを取得します。
    • レスポンスから取得した一致を、20 件の一致を示すアプリページにグループ化し、キャッシュに保存します。
    • ユーザーが現在表示しているアプリページを更新し、新しくプリフェッチしてキャッシュに保存したアプリページへの新しいリンクを表示します。
  5. アプリは、返す結果がなくなるか、事前定義された上限に達するまで、前の手順を繰り返します。正確な検索合計は、検索結果の最後のページとともに返されます。

アプリがバックグラウンドで一致をプリフェッチしてキャッシュに保存している間、ユーザーはキャッシュに保存されたページへのリンクをクリックし続けることができます。

設計オプション

ユースケースに応じて検討すべき設計オプションをいくつか紹介します。

  • アプリのページサイズ。ユースケースに適している場合、アプリページには 20 を超えたり、20 未満の一致を含めることができます。小規模なページは読み込みが速く、ページ上のリンクが多すぎると、ユーザーの管理が難しくなることがあります。

  • 検索結果の合計を更新します。アプリがバックグラウンドで検索結果をプリフェッチしてキャッシュに保存している間、より正確な検索結果の合計をユーザーに表示できます。これを行うには、以下を行うようアプリを構成します。

    • 設定された間隔で、キャッシュ レイヤの最新のプリフェッチから検索の合計(Bundle.total プロパティ)を取得します。これは、検索数の現在の最良の推定値です。検索の合計が表示され、それが見積もりであることを示します。この更新の頻度は、ユースケースに基づいて決定します。

    • キャッシュ レイヤからの検索結果の合計が正確な場合を認識します。つまり、検索結果の合計数は検索結果の最後のページのものです。検索結果の最後のページに到達すると、アプリは検索結果の合計数を表示し、検索結果の合計数が正しいことをユーザーに示します。その後、アプリはキャッシュ レイヤから検索の合計を取得しなくなります。

    一致が多数ある場合、バックグラウンドのプリフェッチとキャッシュは、ユーザーが検索セッションを完了する前に、検索結果の最後のページ(および正確な検索合計)に到達しない可能性があります。

ベスト プラクティス

  • 含まれるリソースの重複を排除する。検索結果のプリフェッチとキャッシュに _include パラメータと _revinclude パラメータを使用する場合は、プリフェッチのたびにキャッシュに含まれるリソースの重複除去を行うことをおすすめします。これにより、キャッシュのサイズが小さくなり、メモリを節約できます。一致をアプリページにグループ化する場合は、各アプリページに適切な含まれるリソースを追加します。詳細については、検索結果に追加のリソースを含めるをご覧ください。

  • プリフェッチとキャッシュに上限を設定します。検索結果が非常に多い場合は、すべてをプリフェッチすることは現実的ではない、または不可能です。プリフェッチする検索結果の数に上限を設けることをおすすめします。これにより、キャッシュのサイズを管理可能な状態に保ち、メモリを節約できます。たとえば、キャッシュサイズを 10,000 件または 20,000 件の一致に制限できます。また、プリフェッチするページ数を制限したり、プリフェッチを停止する時間制限を設定したりすることもできます。適用する上限のタイプと適用方法は、ユースケースに応じて設計上の決定事項です。すべての検索結果が返される前に上限に達した場合、検索の合計がまだ見積もり値であるなど、そのことをユーザーに知らせることを検討してください。

フロントエンド キャッシュ

アーキテクチャにキャッシュ レイヤを導入する代わりに、ウェブブラウザやモバイルアプリなどのアプリケーション フロントエンドで検索結果をキャッシュに保存することもできます。このアプローチでは、AJAX 呼び出しを利用し、検索結果やページ分け URL を保存することで、前のページまたはナビゲーション履歴の任意のページへのナビゲーションを提供します。このアプローチの利点は次のとおりです。

  • キャッシュ レイヤよりもリソースを消費しない場合があります。
  • キャッシュ処理を複数のクライアントに分散するため、よりスケーラブルです。
  • キャッシュに保存されたリソースが不要になったタイミング(ユーザーがタブを閉じたときや、検索インターフェースから離れたときなど)を簡単に判断できます。

一般的なおすすめの方法

このドキュメントのすべてのソリューションに適用されるベスト プラクティスを次に示します。

  • ページ数が _count の値より小さくなることを想定してプランを立てます。状況によっては、指定した _count 値よりも一致が少ないページが検索結果に返されることがあります。たとえば、特に大きなページサイズを指定すると、このような事態が発生する可能性があります。検索で _count 値より小さいページが返され、アプリでキャッシュ レイヤが使用されている場合は、次のことを行う必要があるかもしれません。(1)アプリページには想定される数よりも少ない結果を表示するか、(2)完全なアプリページ表示するためにさらにいくつかの結果を取得します。詳細については、ページ分けと並べ替えをご覧ください。

  • 再試行可能な HTTP リクエスト エラーを再試行する。アプリは、再試行可能な HTTP リクエスト エラー(429500 など)を想定し、エラーを受信した後に再試行する必要があります。

ユースケースを評価する

任意のページへの移動、正確な検索結果の合計の取得、推定合計の更新などの機能を実装すると、アプリの複雑さと開発コストが増加します。また、これらの機能により、レイテンシが増加し、Google Cloud リソースの使用コストも増加する可能性があります。これらの機能の価値が費用に見合っていることを確認するために、ユースケースを慎重に評価することをおすすめします。以下の点に注意してください。

  • 任意のページに移動する。通常、ユーザーは現在のページから特定のページや多くのページに移動する必要はありません。ほとんどの場合、近くのページに移動で十分です。

  • 正確な検索の合計数。検索合計数は、FHIR ストアのリソースが作成、更新、削除されると変更される可能性があります。そのため、正確な検索合計は、検索結果が表示された時点(検索結果の最後のページ)で正確ですが、時間の経過とともに正確性を維持しない場合があります。そのため、ユースケースによっては、正確な検索合計数に関するアプリの価値が制限される可能性があります。