TECH PLAY

HTML

イベント

マガジン

技術ブログ

ウェブサイトのパフォーマンス問題はよくあることですが、根本原因の特定は困難な作業となります。この投稿では、 Server-Timing ヘッダー の潜在能力を引き出すことで、パフォーマンスに関するトラブルシューティングのプロセスをシンプルにする方法を学びます。 Server-Timing ヘッダーは、バックエンドのコンポーネントがユーザーリクエストへのレスポンスにおいて、タイミングメトリクスやパフォーマンスモニタリングに関するインサイトを伝達できるようにします。 ウェブサイトのアクセスでは、画像変換などのコンテンツ最適化やデータベースからの動的なデータ取得を含んだ、複雑なサーバーサイドのプロセスが関与しています。遅いリクエストの原因となるサーバーやプロセスを特定するには、複数のログを突き合わせて分析する必要があり、時間がかかってしまいます。このプロセスをシンプルにすることで迅速に問題を解決できます。具体的には、ユーザー体験の品質シグナルとサーバーサイドのパフォーマンス指標とを直接関連付けて、単一のログ行内にカプセル化することで実現します。この方法は、広範なデータクエリや相関分析が不要であり、パフォーマンス問題を素早く特定し、原因となるサーバーコンポーネントまで追跡することを可能にします。このアプローチの実例として Common Media Client Data(CMCD) が挙げられます。 CMCD は動画ストリーミング業界で最近生まれた革新的な技術で、クライアントおよびサーバー両方のオブサーバビリティデータを単一のリクエストログ行にシームレスに統合するものです。ウェブサイトにおいては、 Server-Timing ヘッダーを実装することで同様の方式を採用できます。サーバーサイドのメトリクスとクライアントサイドで利用可能なメトリクスとを効果的に統合し、特定のリクエスト-レスポンスサイクルのパフォーマンスを包括的に把握するのです。 私たちが提案するソリューションは 2 つのパートで構成されます。第一に、エンドユーザーのレイテンシを測定してパフォーマンス問題を特定すること、第二に、そうした問題が発生した際にサーバーのインサイトに即座にアクセスすることです。 まず前者を取り上げてから、 Server-Timing の実装について掘り下げていきましょう。 パフォーマンス問題の検出 ウェブサイトのパフォーマンスはレイテンシに大きく依存します。レイテンシとは、ユーザーアクション(リンクのクリックやフォームの送信など)とサーバーからのレスポンスとの間の時間遅延を指します。ウェブサイトにおけるレイテンシは、通常 Time to First Byte ( TTFB )、別名 First Byte Latency ( FBL )の形式で測定されます。これは、ウェブサイトのコンテンツがユーザーの画面に描画され始めるまでの速さを測定したもので、 First Contentful Paint ( FCP )や Largest Contentful Paint ( LCP )などの Core Web Vitals シグナルに直接影響します。シームレスなユーザー体験を確保するには、 TTFB を 800 ミリ秒以下に維持することが 推奨されています 。このベンチマークは、遅いリクエストを特定するための有用な閾値として機能します。 Amazon CloudFront のようなサービスを活用することで、静的および 動的コンテンツ 両方の TTFB の改善に役立ちます。 クライアントサイドの視点で TTFB を測定する際は、ユーザーのリクエスト開始時点から、サーバーからのレスポンスの最初のバイト受信時点までの時間を対象範囲とします。この計算には、ネットワーク伝送時間やサーバーサイドでのすべての処理時間が含まれており、ウェブサイトのアーキテクチャに応じて、コンテンツ配信ネットワーク( CDN )の処理、オリジンサーバーの処理、データベースのクエリ、およびその他のリクエスト処理タスクなどが含まれます。サーバーサイドの視点で TTFB を測定する場合は、 サーバーがリクエストを受信してから、レスポンスの最初のバイトをネットワーク層に送出する時点までの時間を対象範囲とします。このとき、ネットワーク転送時間は含まれず、 TTFB はレスポンスを開始する前のサーバーの処理時間を本質的に示します。さらに、リクエストフローの途中にサーバーが位置するシナリオでは、サーバーは二重の役割を果たします。一つはダウンストリームからのリクエストを受信するサーバーとして、もう一つはアップストリームの他のサーバーにリクエストを転送するクライアントとして機能するのです。この動作モデルは、 Amazon CloudFront のような CDN 内のサーバーにおいて一般的であり、そのようなサーバーではクライアントサイドとサーバーサイドの両方の TTFB メトリクスが存在することになります。 CloudFront とエッジ関数、 Application Load Balancer 、ウェブサーバー、データベースなどのコンポーネントを含む典型的なウェブサイトアーキテクチャでは、リクエストからレスポンスまでのサイクルは図 1 に示すように進行します。 図 1. 典型的なウェブサイトアーキテクチャにおけるリクエスト-レスポンスサイクルのタイミング 図 1 では、リクエストとレスポンスの開始と終了のそれぞれのタイムスタンプを T を用いて表しています。これらのタイムスタンプを使用して、様々な TTFB を以下のように計算します: ユーザー TTFB は T1 から T18 までの時間間隔です。ユーザーエクスペリエンスをモニタリングし、推奨値を超えた時の問題特定をするために測定すべき指標です。ユーザー TTFB が短いほど、レスポンスが速く、良いユーザーエクスペリエンスであることを示しています。 CloudFront ダウンストリーム TTFB は T2 から T17 までの時間間隔です。キャッシュヒット、つまり、オリジンでの処理を必要とせず CloudFront キャッシュからリクエストが処理される場合には、 TTFB は CloudFront がリクエストを処理してレスポンスを準備するのにかかった時間のみを示します。エッジ関数を使用するのであれば、その実行時間も含まれます。ただし、キャッシュミスの場合には、オリジンがリクエストを処理してレスポンスを準備するまでにかかった時間と、オリジンから CloudFront へレスポンスを転送する時間が追加されます。 CloudFront アップストリーム TTFB は T3 から T14 までの時間間隔です。これは、CloudFront がリクエストをオリジンに送信し、レスポンスを受信するまでのキャッシュミスの場合を表しています。 CloudFront と同様に、オリジン側のシステム内のすべてのサーバーも独自の TTFB を持っています。たとえば、HTML ページを生成するためにデータベースクエリを実行する場合に、T7 から T10 までの時間間隔として、データベース処理時間と伝送時間の両方を測定します。 アップストリームのコンポーネントからダウンストリームへの伝送時間は、ダウンストリーム TTFB からアップストリーム TTFB を引いた値で推定できます。たとえば、CloudFront からユーザーへの最初のバイトの伝送時間は、ユーザー TTFB から CloudFront ダウンストリーム TTFB を引いて計算できます。伝送時間が短いほど、ネットワーク状態が良好で、距離が短いことを示します。 ブラウザ内の JavaScript を使用してユーザー TTFB を測定するには、 Resource Timing API が使用できます。この API では、リクエストの開始時刻、DNS 解決時間、TCP および TLS ハンドシェイク、レスポンスの最初のバイトの受信といったリソースの読み込みに関わるさまざまな段階のタイムスタンプを取得できます。これにより、TTFB の計算やリソースの読み込みに関連するその他の有用なタイミング情報の取得が容易になります。 const timings = {}; new PerformanceObserver((entryList) => { const entries = entryList.getEntries(); entries.forEach(entry => { if (entry.responseStart > 0) { timings.userDNS = (entry.domainLookupEnd - entry.domainLookupStart).toFixed(2); timings.userTCP = (entry.connectEnd - entry.connectStart).toFixed(2); timings.userTLS = (entry.requestStart - entry.secureConnectionStart).toFixed(2); timings.userTTFB = (entry.responseStart - entry.requestStart).toFixed(2); } }); }).observe({ type: 'resource', buffered: true }); このコードスニペットは、ウェブページから読み込まれた各リソースの DNS、TCP、TLS、および TTFB のタイミングを取得しています。同様に、 Navigation Timing API を使用して、ブラウザ内のナビゲーションリクエストに対してこれらのタイミングを取得できます。このデータを使用すると、レイテンシが許容範囲内かどうかを判断できるだけでなく、リクエストの DNS、TCP、TLS 各段階の所要時間を分析することもできます。これらのメトリクスは、パケットが移動することになるユーザーとフロントエンドサーバー間のネットワーク距離や、ネットワークの輻輳状態に影響を受けます。これらの値が大きく、800 ミリ秒のベンチマークに近づいている場合は、よりスムーズなユーザーエクスペリエンスのためにネットワーク状態を改善する必要があることを示しています。 CloudFront は、エッジロケーションでユーザーに近い場所でリクエストを終端することにより、ネットワークパフォーマンスを大幅に向上させることができます。 しかし、サーバーサイドが原因のパフォーマンス問題はこのデータでは可視化できません。そこで Server-Timing が役立ちます。 Server-Timing の実装 あらゆるウェブサーバーでは HTTP レスポンスに Server-Timing ヘッダーを含めることができ、サーバーメトリクスを提供します。このヘッダーはすべてのモダンブラウザでサポートされており、 PerformanceServerTiming インターフェースを使用してメトリクスを簡単に解析および取得できます。 CloudFront はすでに Server-Timing をサポートしており、処理に関連するメトリクスを伝達できます。たとえば、cdn-downstream-fbl メトリクスは前述した CloudFront ダウンストリーム TTFB であり、 cdn-upstream-fbl は CloudFront アップストリーム TTFB です。その他の利用可能なメトリクスとその説明については、 開発者ガイド で確認できます。 CloudFront で Server-Timing を有効化するには、 レスポンスヘッダーポリシー を作成します。 Server-Timing ヘッダーパネルの「有効」オプションを切り替え、サンプリングレートを指定します。他のレスポンスヘッダーの追加または削除も必要に応じて設定します。CloudFront の Server-Timing 機能により、CloudFront がリクエストを十分な速さで処理できているかを評価できます。キャッシュヒットの場合、 cdn-downstream-fbl メトリクスの値は小さくなり、これは CloudFront が迅速にレスポンスを開始したことを示します。逆に、このメトリクスの値が大きい場合は、処理が遅いことを示唆し、CloudFront 側に問題があることを示します。キャッシュミスの場合には、 cdn-upstream-connect と cdn-upstream-dns メトリクスを確認して CloudFront からオリジンへの接続時間の値も評価します。これらのメトリクスの値が小さい場合は、リクエストフローにおける次のサーバー(図 1 に示されている Application Load Balancer など)が正常に稼働しており、接続を素早く確立し、CloudFront のオリジン向けサーバーの近くに配置されていることを示唆しています。たいていの場合、 cdn-upstream-connect や cdn-upstream-dns の値は 0 になります。なぜならば、CloudFront の 持続的接続 機能が以前に確立された接続を再利用しているからです。 cdn-upstream-fbl メトリクスは、オリジンからのレスポンスの最初のバイトが CloudFront に到達する速さを示します。このメトリクスの値が大きく、 cdn-upstream-connect と cdn-upstream-dns の値が小さい場合は、 Application Load Balancer の後段にあるオリジン側のシステムに問題が発生し、速いレスポンスを提供できていないことを示します。理想的には、これらのメトリクスがユーザーの経験するレイテンシ(ユーザー TTFB )に大きく影響を与えてはいけません。 CloudFront の Server-Timing ヘッダーは、 CloudFront のダウンストリーム・アップストリーム両方のパフォーマンスに関するインサイトを提供しますが、リクエスト中にオリジンで何が起こったかを直接教えてはくれません。複数の異なるコンポーネントやテクノロジーで構成される現代のオリジンアーキテクチャの多様性を鑑みれば、包括的な理解のためには、それぞれのパフォーマンスタイミング情報を組み込むことが不可欠です。オリジンサーバーからインサイトを抽出するには、オリジンからの CloudFront へのレスポンスに Server-Timing ヘッダーを独自に実装して含めることができます。 CloudFront がこのヘッダーを置き換えることはありません。代わりに、オリジンから受信した Server-Timing ヘッダーに CloudFront が自身のメトリクスを追加します。独自で実装する Server-Timing ヘッダーに含めるタイミングメトリクスとしては、画像の最適化、 API 呼び出し、データベースのクエリ、エッジコンピューティングなどの重要なバックエンドプロセスの測定値が考えられます。たとえば、 PHP を使用してデータベースクエリを実行している場合、次のようにしてクエリの所要時間を測定できます。 $dbReadStartTime = hrtime(true); // Database query goes here $dbReadEndTime = hrtime(true); $dbReadTotalTime = ($dbReadEndTime - $dbReadStartTime) / 1000000; header('Server-Timing: my-query;dur=' . $dbReadTotalTime); こちらのコードスニペットでは、データベース操作の完了にかかった時間を取得し、 Server-Timing ヘッダー内で my-query メトリクスとして伝達しています。データベースは時に過負荷状態になり、パフォーマンスのボトルネックとなることがあるため、このデータはそのようなシナリオを明らかにするのに役立ちます。 Node.js を使用している場合は、 PerformanceServerTiming インターフェース仕様の 例 を参考にして、 Server-Timing ヘッダーを実装してください。 エッジ関数 によって追加になるレイテンシも Node.js を使用している場合と同様の実装で測定を行います。ネットワーク呼び出しを含んだ複雑な処理を行う Lambda@Edge 関数では特に有益です。以下の例では、オリジンレスポンスイベントにアタッチされた Lambda@Edge 関数で Server-Timing ヘッダーの実装をしています: import json import time # CF headers are available in request object for Lambda@Edge functions attached to origin response event only def lambda_handler(event, context): # Get function's start timestamp handler_start_time = time.time() response = event['Records'][0]['cf']['response'] request = event['Records'][0]['cf']['request'] server_timing_value = [] # List of CloudFront headers to include in server timing for additional inisghts cf_headers = ['cloudfront-viewer-country', 'cloudfront-viewer-city', 'cloudfront-viewer-asn'] # Iterate over each header name and construct the value for the Server-Timing header for header_name in cf_headers: if header_name in request['headers']: header_value = request['headers'][header_name][0]['value'] server_timing_value.append('{}; desc="{}"'.format(header_name, header_value)) # Function's logic goes here # Get function's stop timestamp handler_stop_time = time.time() handler_duration = round((handler_stop_time - handler_start_time) * 1000, 2) server_timing_value.append('{}; dur={}'.format("my-function", handler_duration)) if server_timing_value: # Construct the Server-Timing header server_timing = [{ "key": "Server-Timing", "value": ', '.join(server_timing_value) }] # Add or append the Server-Timing header if 'server-timing' in response['headers']: response['headers']['server-timing'][0]['value'] += ', ' + ', '.join(server_timing_value) else: response['headers']['server-timing'] = server_timing print("Server-Timing:", response['headers']['server-timing']) return response 注目すべき点として、このコードで追加されるメトリクスは、ハンドラーコードの実行時間のみであり、その他の Lambda のタイミングは除外されています。なお、オリジンアーキテクチャの情報が悪意のある攻撃者に悪用されるおそれがあるため、メトリクスには意図的に抽象的な名前を使用しています。 また、ユーザーの地理的位置情報と ASN 番号に関するインサイトを Server-Timing ヘッダーに追加したことにも注目すべき点です。 そして、 クライアントサイドでも Server Timing を取得できるように serverTiming プロパティを使用してコードを拡張します。以下が修正されたコードスニペットとなります。 // Creating a new PerformanceObserver to monitor performance entries new PerformanceObserver((entryList) => { const entries = entryList.getEntries(); for (const entry of entries) { // Object to store timings for various stages const timings = { userDNS: null, // User DNS resolution time userTCP: null, // User TCP handshake time userTLS: null, // User TLS handshake time CFDNS: null, // CDN DNS resolution time CFUpstreamHandshake: null, // CDN upstream TCP handshake time MyQuery: null, // Query time CFUpstreamTTFB: null, // CDN upstream Time To First Byte (TTFB) MyFunction: null, // Function execution time CFDownstreamTTFB: null, // CDN downstream TTFB userTTFB: null, // User Time To First Byte (TTFB) CFRID: null, // CDN Request ID CFCacheStatus: null, // CDN Cache status (Hit or Miss) UserASN: null // User Autonomous System Number (ASN) }; // Iterating through server timing entries for the current performance entry entry.serverTiming.forEach((serverEntry) => { switch (serverEntry.name) { case 'cdn-rid': timings.CFRID = serverEntry.description; break; case 'cdn-cache-miss': timings.CFCacheStatus = "Miss"; break; case 'cdn-cache-hit': timings.CFCacheStatus = "Hit"; break; case 'cdn-upstream-connect': timings.CFUpstreamHandshake = serverEntry.duration; break; case 'cdn-downstream-fbl': timings.CFDownstreamTTFB = serverEntry.duration; break; case 'cdn-upstream-dns': timings.CFDNS = serverEntry.duration; break; case 'cdn-upstream-fbl': timings.CFUpstreamTTFB = serverEntry.duration; break; case 'my-query': timings.MyQuery = serverEntry.duration; break; case 'my-function': timings.MyFunction = serverEntry.duration; break; case 'cloudfront-viewer-asn': timings.UserASN = serverEntry.description; break; } }); // Calculating user-specific timings if the response not served from the local cache if (entry.responseStart > 0) { timings.userDNS = (entry.domainLookupEnd - entry.domainLookupStart).toFixed(2); timings.userTCP = (entry.connectEnd - entry.connectStart).toFixed(2); timings.userTLS = (entry.requestStart - entry.secureConnectionStart).toFixed(2); timings.userTTFB = (entry.responseStart - entry.requestStart).toFixed(2); // Logging metrics for the current entry console.log("Metrics for:", entry.name); console.log("userDNS:", timings.userDNS); console.log("userTCP:", timings.userTCP); console.log("userTLS:", timings.userTLS); console.log("CFDNS:", timings.CFDNS); console.log("CFUpstreamHandshake:", timings.CFUpstreamHandshake); console.log("DBQuery:", timings.MyQuery); console.log("CFUpstreamTTFB:", timings.CFUpstreamTTFB); console.log("lambdaEdge:", timings.MyFunction); console.log("CFDownstreamTTFB:", timings.CFDownstreamTTFB); console.log("userTTFB:", timings.userTTFB); console.log("CFRID:", timings.CFRID); console.log("CFCacheStatus:", timings.CFCacheStatus); console.log("UserASN:", timings.UserASN); console.log("------------------------------------------------------"); } } }).observe({ type: 'resource', // Observing resource-related performance entries buffered: true }); この改良されたコードスニペットでは、Server Timing を取得したのちに、クライアントメトリクスと共に timings オブジェクトに統合しています。これにより、クライアントサイドとサーバーサイドの両方のリクエスト – レスポンスサイクルにおける包括的なパフォーマンスのインサイトを一箇所にまとめることができました。以下は console.log の出力例です。 Metrics for: https://d1234.cloudfront.net/script.php userDNS: 0.00 userTCP: 0.00 userTLS: 5.00 CFDNS: 0 CFUpstreamHandshake: 88 DBQuery: 0.538685 CFUpstreamTTFB: 178 lambdaEdge: 0.09 CFDownstreamTTFB: 229 userTTFB: 233.10 CFRID: mRq-Uvr__3OBDo0IX9ELV5Lrk3lF-bOp4eOIqTEXlFkFn0wIWPKgpA== CFCacheStatus: Miss UserASN: 1257 この例を見てみましょう。 CloudFront の Lambda@Edge 関数の実行と、オリジンサーバーのデータベースクエリの両方を合わせて、最初のバイトをネットワークに送信するまでに 229 ミリ秒かかりました( CFDownstreamTTFB )。最初のバイトは 233 ミリ秒後にクライアントデバイスに到達しているので( userTTFB )、伝送時間は 4 ミリ秒であることを示しています。クライアントデバイスは、以前に確立された TCP および TLS 接続を再利用しており( userTCP 、 userTLS )、 CloudFront の IP アドレスをキャッシュしていました( userDNS )。 CloudFront はオリジンに向けて新しい TCP 接続を確立する際( CFUpstreamHandshake )、 88 ミリ秒かかりました。オリジンはリクエストを 90 ミリ秒以内( CFUpstreamTTFB – CFUpstreamHandshake )で処理しており、素早くレスポンスの最初のバイトを返していることがわかります。結論として、エンドユーザーの全体的なレイテンシは推奨値の 800 ミリ秒を下回っており、満足のいくものであると言えます。 Server-Timing を他のデータで拡充する Server-Timing ヘッダーは、サーバーサイドの処理時間を伝達するために設計されたものですが、その構文は単にその用途に限定されたものではありません。たとえば、CloudFront ではキャッシュのステータスや内部のユニークなリクエスト ID をメトリクスに含んでいます。これらのデータは CloudFront の処理を正確に分析するために不可欠なものです。同様にして、リクエストの経路に関してインサイトを提供するメトリクスを加えて、 Server-Timing ヘッダーを独自に拡充することができます。たとえば、ログを見つけやすくするために、クラスター内のサーバーの内部 ID を追加することもできます。ユーザーの地理的位置情報やデバイスタイプも追加はできますが、 CloudFront のヘッダー の使用で実現できます。先述の Lambda@Edge 関数で使用方法を示した通りです。これらのヘッダーは、オリジンレスポンスイベントに関連付けられた Lambda@Edge 関数、もしくは、ビューワーレスポンスイベントやビューワーリクエストイベントに関連付けられた CloudFront 関数のリクエストオブジェクトで利用できます。オリジンリクエストポリシーでこれらのヘッダーを有効化すると、 オリジンウェブサーバーが CloudFront からのリクエストに含まれるこれらのヘッダーを取り扱えるようになります。こうして、拡充されたメトリクスを Server-Timing ヘッダーに統合するのです。 Amazon CloudWatch で結果を分析する Server-Timing ヘッダーは、パフォーマンス問題を特定し、根本原因を突き止めるのに有用ですが、ウェブサイトパフォーマンスの他の重要な側面に関するインサイトは提供しません。たとえば、JavaScript 実行に関連するエラーや、累積レイアウトシフト( Cumulative Layout Shift )などの特定の Web Vitals メトリクスは、このソリューションでは直接キャプチャされません。もしすでにリアルユーザーモニタリング( RUM ) ベースのウェブサイトモニタリングソリューションを利用しているのであれば、 Server-Timing ヘッダーを統合することで、 既存の手法を置き換えたり、パフォーマンスモニタリングを Server-Timing のみに限定したりするのではなく、既存の手法を補完できます。包括的なウェブサイトモニタリングソリューションの一例として Amazon CloudWatch RUM があります。 CloudWatch RUM のインサイトを前述の手法で拡張するには、 Server-Timing ヘッダーから抽出したメトリクスを取得する カスタムイベント を作成し、 CloudWatch RUM クライアント経由で CloudWatch に送信します。このアプローチにより、すべての CloudWatch RUM のインサイトと Server-Timing を同じサービス内に統合し、両方のデータセットをシームレスに分析できるようになります。 前述のコードスニペットについて、 Server-Timing ヘッダーから抽出したデータとクライアントサイドの測定値を使用して、CloudWatch RUM クライアントを介してカスタムイベントを記録する方法の例を以下に示します: // Sending performance data to a remote server cwr('recordEvent', { type: 'my-server-timing', data: { current_url: entry.name, ...timings // Spread operator to include all timings } }); この例では、 timings オブジェクトのすべてのプロパティと値を、 cwr 関数 に送信される data オブジェクトに含めています。これは、特定のエントリに対してキャプチャされたすべてのタイミングが、 current_url と共に送信されることを意味します。 カスタムイベントは CloudWatch Logs に記録され、CloudWatch Logs Insights を使用してクエリを実行できます。さらに、 メトリクスフィルター を使用して CloudWatch メトリクスを作成して、モニタリング目的のメトリクスアラームを設定することができます。 上記のコードで収集しているタイミングのカスタムメトリクス実装の例を以下に示します: { "event_timestamp": 1710929230000, "event_type": "my-server-timing", "event_id": "9ae82980-4bfb-47f5-8183-b241379e09e1", "event_version": "1.0.0", "log_stream": "2024-03-20T03", "application_id": "c27d1cef-e531-45ad-9bc4-8e03a716c775", "application_version": "1.0.0", "metadata": { "version": "1.0.0", "browserLanguage": "en", "browserName": "Chrome", "browserVersion": "123.0.0.0", "osName": "Mac OS", "osVersion": "10.15.7", "deviceType": "desktop", "platformType": "web", "pageId": "/", "interaction": 0, "title": "TTFB Demo", "domain": "d1234.cloudfront.net", "aws:client": "arw-script", "aws:clientVersion": "1.17.0", "countryCode": "SE", "subdivisionCode": "AB" }, "user_details": { "sessionId": "c9d2514a-8884-4b32-aec0-25203f213f84", "userId": "0f7f2bf3-c9b7-46ab-bc9e-2ff53864ea74" }, "event_details": { "current_url": "https://d1234.cloudfront.net/getmeal.php", "userDNS": "0.00", "userTCP": "0.00", "userTLS": "9.50", "CFDNS": 0, "CFUpstreamHandshake": 90, "MyQuery": 0.517874, "CFUpstreamTTFB": 180, "MyFunction": 0.12, "CFDownstreamTTFB": 233, "userTTFB": "239.30", "CFRID": "ujYncZYVJeIOk6fI7ApFuNt-mJoh8hfL3nZPgAj77z7RdtSzNMTcqQ==", "CFCacheStatus": "Miss", "UserASN": "1257" } } このメトリクスに基づいて、ユーザー TTFB のメトリクス用に以下のフィルターパターンを作成できます: { $.event_details.userTTFB= * && $.event_details.CFCacheStatus= * && $.event_details.UserASN= * && $.metadata.countryCode=*} これにより、国コード、 ASN 番号、 CloudFront キャッシュステータスなどのディメンションを持つユーザー TTFB のメトリクスを作成できます。その後、このメトリクスに対してアラートを作成し、推奨される 800 ミリ秒などの事前定義された静的な閾値を超えた場合に通知を受け取ることができます。また、 CloudWatch 異常検出 を利用することもできます。 最適化とコスト 重要なこととして、Server-Timing ヘッダーがレスポンスサイズを増加させる点を認識してください。これは、 CloudFront のデータ転送アウトに関するコストや、分析システム内でのデータの保存や処理に影響を与える可能性があります。たとえば、前述の Server-Timing ヘッダーの値は約 350 バイトに相当しますが、仮に 100 万リクエストを仮定した場合、追加で 0.325 ギガバイトのデータを転送することになります。ウェブサイトが受信するリクエスト数によっては、これが大きなコストになる場合とならない場合があります。ただし、 Server-Timing に必須情報、特にアクション可能なデータのみを含めることで、このコストを削減できます。たとえば、主にパフォーマンス低下の検出に Server-Timing が必要な場合は、推奨しきい値である 800 ミリ秒を超えるリクエストにのみ追加することを選択できます。さらに、インタラクティブフォームや API 呼び出しなど、ウェブサイト上の重要なリソースの読み込みにのみ適用することで、使用量を最小限に抑えることもできます。これには、クライアントサイドの JavaScript コードで該当するメトリクスに必要なフィルターを実装することで実現できます。 まとめ この投稿では、ウェブサイトパフォーマンスモニタリングにおける TTFB の重要性を探求し、リクエスト-レスポンスサイクルにおいて詳細なインサイトを提供する Server-Timing ヘッダーの活用方法を実証しました。レイテンシの測定とサーバーサイドメトリクス( CloudFront の処理時間、オリジンサーバーのレスポンス時間など)の取得により、ウェブサイトの所有者はパフォーマンス問題の根本原因を特定し、ウェブサイトの最適化に向けた積極的な対策を講じることができます。 本記事は「 How to identify website performance bottlenecks by measuring time to first byte latency and using Server-Timing header 」と題された記事の翻訳となります。 翻訳はプロフェッショナルサービスの 鈴木(隆) が担当しました。
この記事は、合併前の旧ブログに掲載していた記事(初出:2022年1月19日)を、現在のブログへ移管したものです。内容は初出時点のものです。こんにちは、LINE フロントエンド開発センターの玉田です。突...
こんにちは!SCSKの野口です。 前回の記事では、RAGの全体像(Indexing / Retrieval / Augmentation / Generation)と、「LLMの性能そのものより、前段の設計で品質が決まる」ことを整理しました。 (シリーズ1:RAGの基本情報 / 第1回)RAGとは:全体像、なぜ必要か、基本フローと設計の勘所 RAG(検索拡張生成)の定義、なぜ必要か、基本フロー(Indexing/検索/補強/生成)を整理します。 blog.usize-tech.com 2026.01.27 今回はシリーズ1(RAGの基本要素)の第2回として、 「チャンキング(チャンク化)」 を扱います。 早速ですが皆さんに質問です。 「検索結果は返ってくるのに、回答が噛み合わない/断片的になる」こと、ありませんか? 現場でよく起きるこの状況、Retrieval(検索)の問題に見えますが、実は Indexing時に“根拠をどう切り出して保存したか” が原因になっているケースが少なくありません。 というのも、RAGは「検索したチャンク(断片)」をコンテキストとしてLLMに渡す仕組みなので、 そもそもチャンクの単位が悪ければ、検索が当たっていても“回答に必要な情報が揃わない” 状態になります。 RAGからの情報検索自体は成功しているのに取得した情報の品質が低い——これはRAGの“あるある”です。 そこで本記事では、まず RAG全体像の中でチャンキングがどこに位置し、どのような役割を果たしているのか を図で押さえたうえで、サイズ・オーバーラップ・戦略の選び方、そして簡単な検証デモまで一気に整理します。 本記事で扱う範囲 チャンキングの位置づけ :RAGのIndexing工程の中で、チャンキングが検索品質にどう効くか 設計パラメータと戦略 :chunk size / overlap の勘所と、代表的なチャンキング戦略の使い分け 検証の進め方 :LangChain + Vertex AI Embeddings(Google)で、戦略差を“取得チャンク”として見える化するデモ ※評価(Ragasなどの定量評価)は重要なので触れますが、詳細は次回(評価編)で扱います。 RAGのIndexing工程 チャンキングは、RAGのIndexing(インデックス作成)工程の中核です。ここでの設計が、後続のRetrieval品質に直結します。 Indexingの基本フロー 文書を取り込む(Parsing / 整形) 文書をチャンクに分割する(Chunking) チャンクを埋め込みに変換する(Embeddings) ベクトルDB(または検索基盤)に保存する(Indexing) 基本フローに関しては、私が発表した下記資料「RAGの全体像とチャンキングの位置付け」でまとめているので一読ください。 ※Parsing部分については表現を省いた図を載せています。 2026年1月 豊洲会(発表資料)     また、下記AWSブログでもRAGの流れが記載されています。 Evaluate the reliability of Retrieval Augmented Generation applications using Amazon Bedrock | Amazon Web Services In this post, we show you how to evaluate the performance, trustworthiness, and potential biases of your RAG pipelines a... aws.amazon.com チャンキング(チャンク化)とは チャンキング(Chunking)とは、長いドキュメントを 検索と生成に扱いやすい単位 へ分割し、各チャンクを埋め込み(Embedding)に変換して保存する工程です。 ポイントは、チャンキングが単なる「文章を切る」作業ではなく、 検索精度・文脈保持・コスト・レイテンシを制御する重要な作業 だという点です。極端に言えば、LLMがどれだけ高性能でも、 “拾う根拠がズレていれば、ズレたまま賢く答える” だけです。 先程の発表資料内でも触れていますが、「 不適切なチャンクは、ゴミを入れてゴミを出す(Garbage In, Gargabe Out) 」と言い換えることができます。   まず押さえる:サイズとオーバーラップ(最重要パラメータ) チャンキング設計の基本は、 chunk size(サイズ) と chunk overlap(オーバーラップ) です。ここを外すと、後段の「戦略(splitter)の種類」をどれだけ工夫しても、Retrieval品質が安定しません。 用語整理:chunk / chunk size / chunk overlapについて ここでいう chunk は「検索・生成で扱うために分割したテキストのひとかたまり」を指します。 そのひとかたまりの上限長が chunk size 、隣り合うチャンク同士で 重複させる長さ が chunk overlap です。 chunk size :1チャンクに含めるテキスト量(上限)。単位は トークン (推奨)または文字数。 chunk overlap :隣接チャンク間で重複させる量。境界で情報が欠けるのを緩和する役割を持つ。 図解:size=500, overlap=100 のとき何が起きる? 例えば chunk size = 500 、 overlap = 100 なら、 1つ目のチャンクが 0〜500、2つ目は 400〜900 のように 100分だけ重なります 。 (※開始位置は (n-1) × (size - overlap) のスライディングウィンドウになります) 図 例)サイズとオーバーラップの関係 精度・文脈・コストへの影響について chunk size と overlap は、 検索精度(ノイズ) 、 文脈保持(断片化耐性) 、 コスト/レイテンシ に影響を与えます。 ここでは「 回答がどう崩れているか 」の感覚が掴めるように、ポイントだけ整理します。 1) chunk size が影響を与えるもの(ノイズ ↔ 文脈) 大きすぎる :1チャンクに関係ない情報が混ざりやすく、検索でノイズが乗る(ベクトルが“平均化”され、クエリとの整合が甘くなる)。生成側も入力トークンが増え、コスト・レイテンシが増える。 小さすぎる :条件・例外・参照(主語、前提)がチャンク境界で別れやすくなり、回答が断片的になりやすい。チャンク数が増えるため、検索(Top-k / rerank)負荷も増えやすい。 2) chunk overlap が影響を与えるもの(境界欠落 ↔ 冗長) 固定長分割では、文の途中や「ただし〜」などの条件節が境界で切れやすく、取得はできても「例外条件が落ちる」「主語が消える」といった形で回答が崩れることがあります。 overlap はこの“境界欠落”を緩和 します。 overlap を増やす :断片化に強くなる(必要な根拠が同じチャンクに残りやすい)。 overlap を増やしすぎる :同じ内容が複数チャンクに入って検索結果が冗長になり、コストも増える(インデックスサイズ・取得チャンク重複)。 3) chunk size / overlapの調整 まず 固定長 + overlap をベースラインにして、 回答がどう崩れているか(断片化/ノイズ混入など) を見ながら調整するのが堅実です。 回答が断片的  → overlap を増やす、または size を少し大きくする 関係ない文が混ざる(ノイズ) → overlap を減らす、size を小さくする、必要なら構造認識・メタデータを活用する 目安としては、まず overlap を chunk size の 10〜20% 程度から始めると、境界問題を抑えつつコストもそこまで増えることはないかと思います。 トークン基準で考えることの重要性 チャンクサイズを文字数で切ると、モデル側のトークナイザ差分で 想定以上にトークンが膨らむ ことがあります。 そのため、文字数を基準にチャンクサイズを選択するのではなく、「トークンベースでサイズを管理」する事が重要となります(特に日本語は差が出やすい)。 下記の公式情報は参考になるので、ご確認ください。 ・Azure AI Search:チャンキングの考え方/推奨の出発点(例:512 tokens + 25% overlap) Chunk documents - Azure AI Search Learn strategies for chunking PDFs, HTML files, and other large documents for agentic retrieval and vector search. learn.microsoft.com ・Google Cloud:取り込み時の chunk_size / chunk_overlap、レイアウト解析の統合(RAG Engine) Use Document AI layout parser with Vertex AI RAG Engine  |  Generative AI on Vertex AI  |  Google Cloud Documentation cloud.google.com ・Weaviate:chunkingのベースラインと発展手法の整理(overlap目安含む) Chunking Strategies to Improve Your RAG Performance | Weaviate Learn how chunking strategies can help improve your RAG performance and explore different chunking methods. weaviate.io チャンキング戦略の全体像:代表6パターン(+発展2) ここからは、チャンキング戦略の手法を整理します。 チャンキング戦略を選択する際は、いきなり高度な戦略に飛ぶのではなく、 固定長 or 再帰でベースラインを作る 回答の崩れ方(断片化/ノイズ/表崩れ) から原因を推定する 必要なところだけチャンキング戦略変更(構造認識/セマンティック/階層/コンテキスト付与) の順が、検証コストが小さくなるかと思います。 それぞれのチャンキング戦略の説明とLangChainでの実装コードについて簡単に説明します。 (1) 固定長(トークン)+オーバーラップ 位置づけ :最初に作るべき ベースライン 。チューニング(size/overlap)とログ観察がしやすく、改善サイクルの起点になります。 強み :実装が簡単。速度・コスト見積もりがしやすい。比較実験(A/B)で差分を取りやすい。 弱み :文の途中で切れたり、表・コード・章節構造を無視して分割しがち(=構造がある文書では品質が低くなりやすい)。   LangChain最小実装 from langchain_text_splitters import CharacterTextSplitter splitter = CharacterTextSplitter.from_tiktoken_encoder( chunk_sise=512, chunk_overlap=128, separator="", keep_separator=False, ) chunks = splitter.split_text(text) # text: str (2) 再帰的分割(段落→改行→空白…の優先順位) 仕組み :自然な区切り(段落・改行)を優先しつつ上限サイズに収める。 強み :固定長より「読みやすいチャンク」になりやすく、検索が安定しやすい。 弱み :表やコードなど“構造を持つデータ”では崩れることがある(前処理が重要)。 向く文書 :議事録、ブログ、一般ドキュメント、自然言語中心の資料。   LangChain最小実装 from langchain_text_splitters import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter(chunk_sise=1200, chunk_overlap=100) chunks = splitter.split_text(text) # 必要であれば、下記のように「優先する区切り」を明示する splitter = RecursiveCharacterTextSplitter( chunk_size=1200, chunk_overlap=100, separators=["\n\n", "\n", "。", " ", ""] ) chunks = splitter.split_text(text) (3) 構造認識(見出し・表・リスト・レイアウト) 仕組み :見出し階層、箇条書き、表、HTMLタグ、PDFレイアウト等を解析して「論理単位」で分割。 強み :仕様書やPDFで起こりがちな「表崩れ」「章節の断絶」を抑えやすい。メタデータ(章タイトルなど)も付けやすい。 弱み :前処理(パース)の品質がボトルネック。導入コストも上がりやすい。 向く文書 :Markdown/HTML/PDF/Office文書(特に表が多い資料)。   LangChain最小実装 「構造認識」は入力形式で実装が分かれます。 ここでは、 HTML / Markdownの見出しをメタデータ化して分割 する例を示します。 HTML(タグ単位で分割) from langchain_text_splitters import HTMLHeaderTextSplitter headers_to_split_on = [ ("h1", "Header 1"), ("h2", "Header 2"), ("h3", "Header 3") ] splitter = HTMLHeaderTextSplitter(headers_to_split_on) docs = splitter.split_text(html_text) # html_text: str   Markdown(見出しで分割) from langchain_text_splitters import MarkdownHeaderTextSplitter headers_to_split_on = [ ("#", "Header 1"), ("##", "Header 2"), ("###", "Header 3") ] splitter = MarkdownHeaderTextSplitter(headers_to_split_on=[("#","h1"), ("##","h2"), ("###","h3")]) docs = splitter.split_text(markdown_text) (4) セマンティック分割(意味の変わり目で切る) 仕組み :隣接文の埋め込み類似度が落ちる地点をbreakpointとして分割。 強み :トピック境界を捉えやすく、長文・論文で“概念の連続性”を保ちやすい。 弱み :前処理コストが増える。閾値(どこで切るか)のチューニングが必要。 向く文書 :長文記事、論文、説明書(話題が頻繁に切り替わる資料)。   LangChain最小実装 ここでは、埋め込み類似度でbreakpointを打つことでセマンティック分割を実装する例を示します。 import numpy as np from langchain_google_vertexai import VertexAIEmbeddings emb = VertexAIEmbeddings(model_name="gemini-embedding-001") sents = text.split("。") # 例:粗めの文分割(実際はもっと丁寧に分割) vecs = np.array(emb.embed_documents(sents)) sim = (vecs[:-1] * vecs[1:]).sum(axis=1) / (np.linalg.norm(vecs[:-1],axis=1)*np.linalg.norm(vecs[1:],axis=1)) breaks = np.where(sim < 0.75)[0] # 閾値は要調整 # breaks を境界にチャンクを組み立てる(ここは数行では割愛) 上記の例では、「VertexAIEmbeddings」を利用しています。 しかし、LangChainの公式ドキュメントを確認すると、「VertexAIEmbeddings」は 非推奨(将来リリースで削除) となっています。 VertexAIEmbeddings - Docs by LangChain docs.langchain.com                    公式ドキュメントに記載のとおり、「GoogleGenerativeAIEmbeddings」で代替してください。 https://docs.langchain.com/oss/python/integrations/text_embedding/google_generative_ai (5) 階層(Hierarchical) 仕組み :検索は小チャンクで行い、生成の際は親チャンク(より大きい文脈)を渡す。 強み :条件・例外・前提などの“背景”が回答に乗りやすく、断片化に強い。 弱み :親サイズを大きくしすぎるとコスト増。親子の設計(サイズ比・親サイズの選び方)が要点。 向く文書 :規約・設計書・仕様書・研究資料(参照関係が強い資料)。   LangChain最小実装 「子で検索し、親を渡す」までの一連の流れを最小構成で示します。 from langchain.retrievers import ParentDocumentRetriever from langchain.storage import InMemoryStore from langchain_text_splitters import RecursiveCharacterTextSplitter child = RecursiveCharacterTextSplitter(chunk_size=400, chunk_overlap=50) # 子: 小さい単位 parent = RecursiveCharacterTextSplitter(chunk_size=1500, chunk_overlap=100) # 親: 大きい単位 store = InMemoryStore() retriever = ParentDocumentRetriever( vectorstore=vs, docstore=store, child_splitter=child, parent_splitter=parent ) retriever.add_documents(docs) # docs: List[Document]   (6) メタデータ駆動(フィルタ/分割/並べ替え) 仕組み :章節、日付、システム名、部品名などのメタデータを付け、検索時にフィルタや優先順位付けに活用する。 強み :専門用語が多い領域で、誤ヒットやノイズを抑えやすい。運用の“説明責任”にも効く。 弱み :付与設計が雑だと逆効果(フィルタが効かない、メタデータが不整合など)。 向く文書 :社内ドキュメント全般(AP基盤ドキュメントは特に相性が良い)。   LangChain最小実装 分割自体は再帰的分割・構造認識を利用し、 metadataを付けて検索時にフィルタ するのがポイントです(これはVectorStore側の機能に依存します)。 from langchain_core.documents import Document docs = [ Document(page_content="...", metadata={"system":"AP基盤", "version":"v1"}), Document(page_content="...", metadata={"system":"AP基盤", "version":"v2"}), ] vectorstore.add_documents(docs) retriever = vectorstore.as_retriever(search_kwargs={"k": 5, "filter": {"system": "AP基盤"}}) hits = retriever.invoke("デフォルト設定値は?") 上記では、 filter= でフィルタリングを行っています。このフィルタリングが効くかどうかはVectorStore実装依存です。 (例: Pinecone / Weaviate 等は強い、FAISSは弱い) [発展] コンテキスト付与(チャンクに“位置づけ説明”を足す) チャンク単体では主語や前提が抜けがちな場合、チャンクに短い説明(文書内での位置づけ)を付与してから埋め込む、という発展的アプローチがあります。主に「指示代名詞が多い」「前提が多い」文書で効きますが、索引コストは増えます。   LangChain最小実装 チャンク本文に短い前置き(タイトル / 章 /目的など)をつけて埋め込む例を示します。 from langchain_core.documents import Document enriched = [] for d in docs: # docs: Document[] prefix = f"[{d.metadata.get('h2','')}/{d.metadata.get('h3','')}] " enriched.append(Document(page_content=prefix + d.page_content, metadata=d.metadata)) vectorstore.add_documents(enriched) [発展] Late Chunking(先に文書全体でエンコード→後で分割) 通常は「chunk→embed」ですが、先に文書全体を通して文脈を持たせたベクトル表現を得てから分割する、という発展的な考え方です。文書全体の文脈が効く一方、適用条件やコスト面の検討が必要です。 参考 ・LangChain:Text Splitters(概念と実装) LangChain overview - Docs by LangChain LangChain is an open source framework with a pre-built agent architecture and integrations for any model or tool — so yo... python.langchain.com ・Google Cloud:layout parser統合(構造認識の入口として有用) Use Document AI layout parser with Vertex AI RAG Engine  |  Generative AI on Vertex AI  |  Google Cloud Documentation cloud.google.com ・Pinecone:semantic/contextual chunking を含む戦略整理 Chunking Strategies for LLM Applications | Pinecone In the context of building LLM-related applications, chunking is the process of breaking down large pieces of text into ... www.pinecone.io ・Weaviate:chunking戦略(+発展手法)整理 Chunking Strategies to Improve Your RAG Performance | Weaviate Learn how chunking strategies can help improve your RAG performance and explore different chunking methods. weaviate.io ・IBM watsonx:LangChain互換Chunker/隣接チャンク拡張(window search) RAG - IBM watsonx.ai ibm.github.io 戦略別比較表:精度・コスト・実装難度のトレードオフ 各戦略は万能ではありません。 精度(Precision)/ノイズ耐性 / 実装難度 / コスト / レイテンシ のトレードオフを確認し、どの戦略を利用するかを判断する必要があります。 下記表に各チャンキング戦略の特徴をまとめています。 表. チャンキング戦略比較 戦略 精度 ノイズ耐性 実装難度 コスト レイテンシ 固定長 + overlap 低〜中 低 低 低 低 再帰的分割 中 中 低 低 低 構造認識 中〜高 高 中 中 中 セマンティック 高 高 高 高 高 階層(small-to-big) 中〜高 中 中 中 中 コンテキスト付与/発展 中〜高 高 中〜高 中〜高 中〜高   この表は「どれが最強か」を決めるものではありません。各チャンキング戦略に得意な文章構造などがあるため、事前にその内容を加味して選択する必要があります。また、最初に選んだ戦略であまり精度が出なかった場合は、他のチャンキング戦略を採用してみるなどの トライ&エラー も必要になります。 チャンキング戦略 選び方 一度採用した戦略で思うような精度が出ない場合は 「回答パターン」 を確認するとよいです。 回答パターンとその原因・対策の一例を示します。下記が正解ではありませんが、参考にしていただければと思います。 表. 回答パターンの原因とその対策 回答の崩れ方(よくあるパターン) ありがちな原因 優先して試す対策 回答が断片的(例外条件が落ちる) サイズ小さすぎ / overlap不足 overlap増 / 階層(small-to-big) 関係ない文が混ざる(ノイズ多い) サイズ大きすぎ / 前処理不足 サイズ削減 / 構造認識 / メタデータフィルタ 表の数値が崩れる PDF/表のパース崩れ 構造認識(layout parser等)/ 取り込み前処理の改善 同じ用語でも別文書がヒットする メタデータ不足 / フィルタ無し メタデータ付与(システム/部品/版数)+ フィルタ 検索は当たるのに主語が不明 参照が多い / 文脈が抜ける overlap増 / コンテキスト付与 検証デモ:LangChain + Vertex AI ここからはデモパートです。今回は「チャンキング戦略によって、検索で拾える根拠がどう変わるか」を、LangChainでサクッと比較できる形にします。 なお、本デモの内容をもう少し詳しくした内容についてはGitHubで公開しているので、ぜひ確認してみてください。 GitHub - HiaHia1969/chunking_demo_public Contribute to HiaHia1969/chunking_demo_public development by creating an account on GitHub. github.com 構成 :TextSplitter(戦略) → Embeddings(Vertex AI) → VectorStore(ローカル) → Retriever → 取得チャンクの比較 前提:環境構築 今回は uv を利用して環境構築を行います。 # 作業ディレクトリ準備 mkdir langchain_demo && cd langchain_demo # uv初期化 uv init # ライブラリ準備 uv add langchain \ langchain-community \ langchain-text-splitters \ langchain-google-genai \ faiss-cpu \ python-dotenv \ numpy \ tiktoken # GitHubリポジトリを参考にする場合は、下記コマンドで依存関係を解決できます。 uv sync 図 ディレクトリ構造 図 pyproject.tomlの内容 環境変数 .env ファイルにVertexAI経由でGoogleモデルを呼び出すための設定を行います。 APIキーは事前に発行しておく必要があります。 GOOGLE_API_KEY=<取得したAPIキー> GOOGLE_CLOUD_PROJECT=<Google Cloudのプロジェクト名> GOOGLE_CLOUD_LOCATION=<リージョン名> GOOGLE_GENAI_USE_VERTEXAI=true EMBEDDING_MODEL=gemini-embedding-001 図 環境変数の設定   共通:ベクトル化と検索のユーティリティ import os from dataclasses import dataclass from typing import List, Tuple from dotenv import load_dotenv from langchain_google_genai import GoogleGenerativeAIEmbeddings from langchain_community.vectorstores import FAISS # LangChain splitters from langchain_text_splitters import RecursiveCharacterTextSplitter # 環境変数の読み込み load_dotenv() @dataclass class SearchResult: label: str docs: List[str] def build_vs(chunks: List[str], embeddings: GoogleGenerativeAIEmbeddings) -> FAISS: """Build a local FAISS vector store from plain text chunks.""" return FAISS.from_texts(chunks, embedding=embeddings) def topk_texts(vs: FAISS, query: str, k: int = 3) -> List[str]: docs = vs.similarity_search(query, k=k) return [d.page_content for d in docs] def show(title: str, texts: List[str]) -> None: print(f"\n===== {title} =====") for i, t in enumerate(texts, 1): print(f"\n--- top{i} ---\n{t}") # Embeddings(Google Generative AI) # 本記事では、gemini-embedding-001を利用します。利用できるモデルは下記を確認してください embeddings = GoogleGenerativeAIEmbeddings( model=os.getenv("EMBEDDING_MODEL", "gemini-embedding-001"), api_key=os.getenv("GOOGLE_API_KEY"), project=os.getenv("GOOGLE_CLOUD_PROJECT"), location=os.getenv("GOOGLE_CLOUD_LOCATION"), vertexai=os.getenv("GOOGLE_GENAI_USE_VERTEXAI", "true").lower() == "true", ) デモ1:overlapの有無で「例外条件が落ちる」を再現 対応ソース : demos/demo1_overlap_effect.py 目的:単発ケースだけでなく複数ケースでも、overlap が Top1 の根拠取得に与える影響を確認します。 このデモで確認すること 目的 :境界分断が起きたとき、overlap が Top1 の根拠欠落をどこまで緩和できるかを確認する 設定 : chunk_size=120 、 overlap=0 と overlap=20 、検索は k=1 (Top1)で比較 期待される差分 :overlap ありの方が「基本 + 例外」が同一チャンクに残りやすく、Top1 欠落が減る 読み方 :`判定` 行と `Top1で基本+例外を同時取得できた件数`(再現率)を見る from langchain_text_splitters import RecursiveCharacterTextSplitter # 共通ユーティリティ(build_vs / topk_texts / show)と embeddings は前節を利用 def make_doc(noise_repeat: int) -> str: return ( "背景説明。" * noise_repeat + "A部品の設定方針は次の通り。基本はX=ONとする。" + "ただしBモード時のみ例外でX=OFFとする。" ) query = "A部品の設定方針を教えてください。基本設定(X=ON)と例外設定(X=OFF)を両方含めてください。" # チャンク化:境界でX=ONが分断される設定 chunk_size = 120 overlap0 = 0 overlap1 = 20 split0 = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=overlap0) split1 = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=overlap1) # 代表ケース(noise_repeat=20) doc = make_doc(20) chunks0 = split0.split_text(doc) chunks1 = split1.split_text(doc) show("overlap=0(境界で例外が落ちやすい)", topk_texts(build_vs(chunks0, embeddings), query, k=1)) show("overlap=20(例外が同居しやすい)", topk_texts(build_vs(chunks1, embeddings), query, k=1)) # 複数ケース for r in [16, 18, 20, 22, 24]: d = make_doc(r) c0 = split0.split_text(d) c1 = split1.split_text(d) t0 = topk_texts(build_vs(c0, embeddings), query, k=1) t1 = topk_texts(build_vs(c1, embeddings), query, k=1) ok0 = any("X=ON" in t and "X=OFF" in t for t in t0) ok1 = any("X=ON" in t and "X=OFF" in t for t in t1) print(f"noise_repeat={r}: overlap=0 -> {'○' if ok0 else '×'}, overlap=20 -> {'○' if ok1 else '×'}") 実行結果 実行コマンド 出力結果(要約) [設定] chunk_size=120 【代表ケース】noise_repeat=20 overlap=0 : 判定 × 例外設定(X=OFF)が欠落 overlap=20 : 判定 ○ 基本設定と例外設定の両方が含まれる 【追加検証】複数ケースでの再現率(Top1) noise_repeat=16: overlap=0 -> ×, overlap=20 -> × noise_repeat=18: overlap=0 -> ×, overlap=20 -> × noise_repeat=20: overlap=0 -> ×, overlap=20 -> ○ noise_repeat=22: overlap=0 -> ○, overlap=20 -> ○ noise_repeat=24: overlap=0 -> ○, overlap=20 -> ○ Top1で基本+例外を同時取得できた件数 overlap=0: 2/5 overlap=20: 3/5 考察 代表ケースでは overlap=0 で取りこぼし、overlap=20 で回収できることを再現しました。 複数ケースでも overlap=20 の方が Top1 で根拠が揃う件数が多く(3/5 vs 2/5)、改善傾向を確認できました。 差分は境界位置に依存するため、実務では overlap 単体ではなく chunk_size と k を合わせて調整するのが妥当です。 今回のミニデモでは差分は限定的ですが、実務プロジェクトの長文・多条件文書では境界分断が増えるため、overlapの効き目は一般に大きくなります。 観察ポイント overlapは常に効く魔法ではなく、境界依存の問題を緩和する手段 Top1運用では、境界情報を残す保険として有効に働きやすい デモ2:固定長(token) vs 再帰分割で「読みやすいチャンク」を比較 対応ソース : demos/demo2_token_vs_recursive.py 目的:固定長だと文がブツ切れになり、人間が読んでも意味が取りづらい(=LLMにも厳しい)ことを示します。 ※token側は日本語で文字化けしにくい `token_splitter()` を使います。langchaignの「CharacterTextSplitter」を利用しています。 今回のデモを作成するにあたり、当初は「TokenTextSplitter」を利用していました。 しかし、日本語のチャンキング時にチャンク文字列が文字化けしてしまうという事象が発生していました。 下記のような感じです。         ...制御する� �計判断です。 どうやら「TokenTextSplitter」では、日本語などのマルチバイト文字を含む文字列を分割すると、分割後に文字化けが発生する可能性があるようです。 そのため、今回は「TokenTextSplitter」ではなく、「CharacterTextSplitter」を採用しています。 langchain公式ドキュメント Text splitter integrations - Docs by LangChain Integrate with text splitters using LangChain. docs.langchain.com   このデモで確認すること 目的 :固定長分割と再帰分割で、チャンクの可読性と意味まとまりがどう変わるかを比較する 設定 :Token側は chunk_size=25 、Recursive側は chunk_size=120 、どちらも overlap=0 期待される差分 :Token分割は文途中で切れやすく、Recursive分割は自然な文境界を保ちやすい 読み方 :Token側の `[NG] 文の途中で切断` と、Recursive側の `[OK] 自然な区切り` を比較する from langchain_text_splitters import RecursiveCharacterTextSplitter from src.splitters import token_splitter text = """ RAGのチャンキングは単なる分割ではありません。 検索精度と文脈保持、さらにコストとレイテンシのトレードオフを制御する設計判断です。 例えば、条件・例外・参照が多い仕様書では、文脈の断片化が致命的になります。 """ token_split = token_splitter(chunk_size=25, chunk_overlap=0) rec_split = RecursiveCharacterTextSplitter(chunk_size=120, chunk_overlap=0) token_chunks = token_split.split_text(text) rec_chunks = rec_split.split_text(text) print("\n===== token split(固定長のイメージ) =====") for c in token_chunks: print("-", c) print("\n===== recursive split(自然なまとまり) =====") for c in rec_chunks: print("-", c) 実行結果 実行コマンド 出力結果(要約) 【パターン1】Token分割 (chunk_size=25トークン) 結果: 7個のチャンクに分割 例: - 『RAGのチャンキングは単なる分割ではありま』 - 『せん。検索精度と文脈保』 【パターン2】Recursive分割 (chunk_size=120文字) 結果: 1個のチャンクに分割 例: - 『RAGのチャンキングは単なる分割ではありません。...(全文)』 考察 Token分割は長さ制御には強い一方、文の途中切断が連続し、意味まとまりが崩れやすいことが確認できました。 Recursive分割は今回のテキストでは1チャンクに収まり、文脈の一貫性を保持できています。 日本語では「文字化けしないtoken分割」を使っても、 文脈保持の観点ではRecursive優位 になりやすい、という位置づけが妥当です。 デモ3:構造認識(レイアウト解析)に寄せると何が嬉しいか 対応ソース : demos/demo3_semantic_breakpoints.py 目的:構造なしの分割と、見出し構造を使った分割で、チャンクの意味的まとまりがどう変わるかを比較します。 このデモで確認すること 目的 :平文分割と見出し分割で、トピック完結性と検索向けメタデータの有無を比較する 設定 :平文は RecursiveCharacterTextSplitter 、構造ありは MarkdownHeaderTextSplitter (Header 1〜3) 期待される差分 :見出し分割の方が章単位でまとまり、Headerメタデータが付与される 読み方 :`メタデータ` 行と、平文側の「トピック混在」有無を確認する from src.splitters import markdown_header_splitter, recursive_splitter plain_doc = """ システム設定ガイド A部品の設定 基本設定 A部品の設定方針は次の通りです。 基本は「X=ON」とする。 例外設定 ただし、Bモードの場合は例外で、X=OFFとする。 """ markdown_doc = """ # システム設定ガイド ## 1. A部品の設定 ### 基本設定 A部品の設定方針は次の通りです。 基本は「X=ON」とする。 ### 例外設定 ただし、Bモードの場合は例外で、X=OFFとする。 """ # パターン1: 構造なし(Recursive) plain_chunks = recursive_splitter(chunk_size=100, chunk_overlap=0).split_text(plain_doc) # パターン2: 構造認識(Markdown Header) headers_to_split_on = [("#", "Header 1"), ("##", "Header 2"), ("###", "Header 3")] md_docs = markdown_header_splitter(headers_to_split_on).split_text(markdown_doc) print("plain chunks:", len(plain_chunks)) print("markdown header chunks:", len(md_docs)) for d in md_docs: print(d.metadata, d.page_content[:40]) 実行結果 実行コマンド 出力結果(要約) 【パターン1】構造なし(Recursive) 結果: 3個のチャンク - Chunk 2 に「例外設定」と「認証設定」が同居し、トピックが混在 【パターン2】Markdown Header分割 結果: 4個のチャンク(見出し単位) - Chunk 1 metadata: {'Header 1': 'システム設定ガイド', 'Header 2': '1. A部品の設定', 'Header 3': '基本設定'} - Chunk 2 metadata: {'Header 1': 'システム設定ガイド', 'Header 2': '1. A部品の設定', 'Header 3': '例外設定'} 考察 構造なし分割では「見出しだけ残る」「異なる章が同居する」状態が発生し、検索時の解釈が不安定になります。 見出し分割ではチャンク境界が文書構造と一致し、 トピック完結性とメタデータ活用性 が大きく向上します。 仕様書・手順書・運用ドキュメントのような構造化文書では、まずHeader分割を優先するのが実践的です。 観察ポイント 構造なし分割では、見出しと本文が混在しやすく、トピックが分散しやすい 見出し分割では、Headerメタデータ付きでトピック単位にまとまりやすい 参考 ・LangChain:Text Splitters(概念と実装) LangChain overview - Docs by LangChain LangChain is an open source framework with a pre-built agent architecture and integrations for any model or tool — so yo... python.langchain.com ・LangChain:Vertex AI embeddings integration Google Vertex AI integration - Docs by LangChain Integrate with the Google Vertex AI embedding model using LangChain Python. python.langchain.com ・Google Cloud:layout parser統合(構造認識の入口として有用) Use Document AI layout parser with Vertex AI RAG Engine  |  Generative AI on Vertex AI  |  Google Cloud Documentation cloud.google.com 評価(次回記事):チャンキング改善はどう測る? チャンキングは“それっぽく”改善できてしまう一方で、主観評価に寄ると迷走しがちです。最低限、次の指標で定量的に「良くなった/悪くなった」を測れる状態にしておくのが安全です(詳細は次回で扱います)。 Context Recall :正解に必要な根拠がTop-kに入っているか Context Precision :Top-kがノイズだらけになっていないか Faithfulness :回答が取得した根拠に接地しているか Answer Relevancy :質問にちゃんと答えているか おすすめの評価・改善ループは、 代表クエリ50件(ファクト系/分析系/手順系を混ぜる) ベースライン(固定長+overlap or 再帰)でTop-kログを保存 1つだけ条件を変えて比較(サイズだけ、overlapだけ、構造認識だけ…) です。これで“改善の方向性”が掴めます。 (補足)Amazon Bedrock Knowledge Basesで考える場合 シリーズ2以降で本格的に検証予定ですが、「マネージドサービスで楽をしたい」場合の整理も置いておきます。 AWSでは、Amazon Bedrock Knowledge Basesというマネージドサービスが提供されており、RAG環境を簡単に構築することが可能です。2026年2月時点で利用できるAmazon Bedrock Knowledge Bases(Bedrock KB)で利用できるチャンキング戦略は下記となります。 これまで説明してきたチャンキング戦略と対応付けると、ざっくり次のイメージです(詳細はTipsシリーズで検証します)。 表. Amazon Bedrock Knowledge Bases で利用可能なチャンキング戦略 Bedrock KB 一般戦略の読み替え 一言 Default ベースライン 迷ったらまずこれ Fixed-size 固定長 + overlap 速度・コスト優先 Hierarchical 階層(Hierarchical) 複雑文脈向け Semantic セマンティック 高精度寄り(コスト増に注意) None 分割なし 前処理済み/FAQ向け まとめ 本記事では、RAGにおけるチャンキング戦略について説明してきました。 まずは固定長 + overlap/再帰分割でベースラインを作る 断片化・ノイズ・表崩れなど、 回答がどう崩れているか から原因を推定し、必要なところだけ高度化する デモのように、取得チャンクを比較して「どこが壊れているか」を観察する 改善は評価指標(Recall/Precision/Faithfulness等)で“定量的に測れる状態”にして進める 次回は、この改善が本当に効いているかを判断するために、 RAGの評価(定量評価) を扱います。Ragasなどの評価指標で「良くなった/悪くなった」を測れる状態にしていきましょう。 次回もぜひご覧ください。 「その質問、ドキュメントに書いてある」問題を終わらせたい:RAG連載を始めます 社内ナレッジをRAGで活用し、膨大なドキュメントから必要情報を素早く見つける仕組みを目指します。本記事では連載開始の背景と、RAG基礎〜Bedrock実装・アプリ/エージェント構築までの構成を紹介します。 blog.usize-tech.com 2026.01.27 (シリーズ1:RAGの基本情報 / 第1回)RAGとは:全体像、なぜ必要か、基本フローと設計の勘所 RAG(検索拡張生成)の定義、なぜ必要か、基本フロー(Indexing/検索/補強/生成)を整理します。 blog.usize-tech.com 2026.01.27

動画

書籍