
Elasticsearch
イベント
該当するコンテンツが見つかりませんでした
マガジン
技術ブログ
Smart Data Platform (SDPF) クラウド/サーバーのネットワークオーケストレータ「ESI」の開発において、生成 AI を活用するために行ったログ基盤の整備や開発環境の工夫についてご紹介します。ログサーバーの新設や コーディング支援 AI(本事例では GitHub Copilot を使用)の導入により、エラー調査・開発の自動化を実現した事例です。 はじめに ESI について リアーキテクティングにおける課題と施策 ログ内容の精査 ― セキュリティ確保と安全なログ提供 ログサーバーの新設 ― サービス横断的なログ調査の実現 リポジトリ一元管理 ― コンテキスト不足の解消 展望 試験計画・品質検証の自動化 監視・障害対応への拡張 コンテキストの拡大 まとめ はじめに はじめまして。NTTドコモビジネスの SDPF クラウド/サーバー の内製開発に従事している堀岡勇杜と申します。 私が主に担当しているのは、「ESI(Elastic Service Infrastructure)」という名称の、クラウドのネットワークオーケストレータの開発です。ESI チームの業務内容については、 こちらの記事 でも紹介しています。 今回は、私が昨年度に取り組んだ ESI 開発における生成 AI 活用事例について、ご紹介したいと思います。 ESI について 具体的な活用事例について述べる前に、ESI の役割や構成について軽く触れたいと思います。 ESI は、SDPF クラウド/サーバーの SDN(Software Defined Networking)サービスや VM(Virtual Machine)サービスといった各種サービスと連携して、仮想ネットワークやゲートウェイ、VNF(Virtual Network Function)の提供・リソース監視を実施しているシステムです。SDPF クラウド/サーバーのネットワーク機能の中核を担っており、高信頼性と高可用性が要求されています。 下図は、ESI が Web ポータル や API 経由でのリクエストを受け付け、各種クラウドサービスと連携し、ネットワーク機能を提供する様子を示しています。 この ESI ですが、次のようなサービス群によって構成されています。 OpenStack Neutron と互換性のある API サービス オーケストレーションサービス リソース監視サービス これらのサービスは全て冗長化されており、高可用性を担保しています。 しかし、既存のオーケストレーションサービスには拡張性や管理面での課題が発生しつつあります。また ESI は10年を超える長寿システムであり、抜本的なアーキテクチャ改善が必要となってきています。 そこで私は昨年度からこのリアーキテクティングに、生成 AI を活用しつつ取り組んでいます。 リアーキテクティングにおける課題と施策 ESI のリアーキテクティングを進めていく中で、次のような課題が浮上しました。 生成 AI が各種サーバーからログを取得する構成にする場合のセキュリティ・権限・トークン消費量の問題 実装した内容がエラーを発生させた際のログ調査において、冗長化された複数サービスから出力されるログを横断的に調査することの難解さ 複数サービス結合によりシステムが成立していることによる、生成 AI 側のコンテキストやノウハウ不足 これらの課題を解決するために、以下の施策を実施しました。 ログ内容の精査 ― セキュリティ確保と安全なログ提供 生成 AI に渡されることがセキュリティ的に望ましくないログ出力の存在を入念に調査し、これらを全てログ出力から除去しました。地味な作業ですが、生成 AI にログを安全に提供するための前提として非常に重要です。 ログサーバーの新設 ― サービス横断的なログ調査の実現 冗長化された複数サービスから出力されるログを一元的に収集する、ログサーバーを新設しました。各サービスサーバーには Filebeat を配置してログを転送し、ログサーバー上の Elasticsearch に集約する構成としています。収集時にパイプラインを通すことで、サービス横断的に時系列順でのログが取得でき、かつ簡単にフィルタリングも可能です。 ログサーバーでは、API や MCP(Model Context Protocol)ツール経由でログを取得できるようにしました。 MCP ツールとしては以下のようなものを提供しています。 ツール 用途 get_error_summary エラーの全体像を把握する(パターン別集計) get_recent_errors 直近のエラーログを取得(日時範囲を指定可) get_error_trends エラーの時系列傾向を分析 search_logs キーワード・ID でログを横断検索 trace_request request_id からサービス横断でログチェーンを追跡 investigate_proxy_error リバースプロキシのエラーからバックエンドまで自動追跡 get_proxy_errors リバースプロキシの 4xx/5xx エラーを取得 count_logs サービス別のログ件数を確認 これらのツールを組み合わせることで、生成 AI が少ないトークン消費で段階的にログを調査できるようになっています。 この仕組みにより、ESI のリソース間リレーションも考慮したエラー調査が可能となりました。ESI のリソースは複数の関連リソースと密接に結びついており、あるリソースのエラー原因がその他のリソースにあることも多々あります。ログサーバーを通じてサービス横断的にログを参照できるようになったことで、エラーの真の要因に容易にたどり着くことができるようになりました。 また、コーディング支援 AI にはあくまでログサーバーへのアクセス権のみを与え、ログサーバーから各種 ESI システムへのリクエストは禁止しました。これにより、コーディング支援 AI が ESI システムに与える影響を最小限に抑えつつ、安全にログを参照できる環境を実現しました。 リポジトリ一元管理 ― コンテキスト不足の解消 開発サーバー上で ESI を構成するリポジトリ群を集約して配置することで、開発で使用しているコーディング支援 AI がこれらのコードを網羅的に調査・修正できるようにしました。下図は最終的に完成した構成です。 複数リポジトリを横断して参照できるようになったことで、ESI のコンテキスト不足問題についてはかなり解消されました。さらに、前述のログサーバーと組み合わせることで、例えばエラーが発生したリソースの ID を指定するだけで「ログからエラーの原因を特定 → ソースコードからエラー発生要因となるバグを特定 → バグを修正」といったことが自律的に可能になり、直近のバグ修正において調査・開発の時間が平均して従来比 約2割(つまり約80%減)に短縮できました。 実際の運用では、コーディング支援 AI が作成した差分を開発者がレビューしたうえでそのまま採用できるケースが多く、人手による追加修正が最小限で済む傾向にあります。ESI には既存開発で蓄積された大小さまざまなテスト群が存在しており、レビューとこれらのテストを前提に生成 AI を活用しつつ品質を確認しています。 展望 今回の取り組みで生成 AI による開発の基盤は整いましたが、まだまだ発展の余地があると考えています。 試験計画・品質検証の自動化 現在は「バグの特定→コード修正」までを生成 AI が担っていますが、試験は人間が計画しています。試験の計画、および生成される試験レポートのチェックも生成 AI が実施することで、コードの品質向上を自動的に進められていく環境を整えたいと考えています。 監視・障害対応への拡張 現在はあくまで開発時のエラー調査・修正に活用していますが、この仕組みは監視・障害対応にも応用できると考えています。例えばユーザーから不具合への対応を依頼された際に、生成 AI が自動的にログを分析して障害の影響範囲を特定し、既知のパターンであれば復旧手順を提案する、といった運用支援への発展が見込めます。この運用を見越し、既知の不具合パターンおよび対応方法を生成 AI が理解しやすい形で蓄積する営みを、現在進めています。 コンテキストの拡大 現状では、トークン上限の制約からログの出力内容を厳選し、必要最小限の情報のみを生成 AI に提供しています。将来的に AI がより長大なコンテキストを処理できるようになったり、ローカルで十分な性能を発揮できるようになったりした暁には、設定ファイルやデプロイ履歴など、より多くの情報を含んだログを提供することも検討できます。これにより、エラーの根本原因分析の精度がさらに向上することを期待できます。 今後の開発では、「どの程度の量のログ」を「どの程度の粒度」で「どのログレベルで出力するか」をより入念に設計していく必要があると考えています。 まとめ 今回は SDPF クラウド/サーバーのネットワークオーケストレータ開発における、生成 AI 活用の取り組みについて紹介させていただきました。今後も「品質の維持」と「生成 AI 活用による速度向上」の両輪で最適な開発を追求し、より便利で使いやすいクラウドの実現に向けて取り組んでいこうと思っています。 最後にインターンの宣伝です。本記事を通じて SDPF クラウド/サーバー開発や ESI 開発に興味を持っていただけた学生の方は、ぜひインターンのポスト「 【B19】エンタープライズ向け大規模クラウド/ネットワークサービスを支えるコントローラ開発 」にご応募ください。
想定読者:SOC アナリスト、脅威ハンター、検知エンジニア。SIEM の経験はあるが Elastic / EQL は初めて、あるいは復習したい方。 読了時間:約 15 分 セキュリティ運用の現場では、毎日とんでもない量のログを見ます。Elasticsearch は「ログを保存して検索する」のはとても得意です。でも、 脅威の検出 となると話が一段難しくなります。 なぜか。攻撃は単独のイベントではなく、 複数のステップが時間軸の上で連なる流れ だからです。 たとえば、次のような流れを「ひとかたまり」として検出したいとします。 ファイルが Temp フォルダに作成された その直後にそのファイルが実行された さらにそのプロセスが外部に通信した 普通のクエリでは、これを 3 回別々に検索して、ID やタイムスタンプで突き合わせる必要があります。これがしんどい。 EQL(Event Query Language)は、この「流れ全体」を一度のクエリで捕まえるために作られた言語 です。本記事では、セキュリティエンジニアが EQL を実務で使えるようになるための最短ルートを紹介します。 目次 EQL とは何か EQL を動かすための前提 通常の検索クエリ(KQL/Lucene)と何が違うか Splunk SPL を使っている人へ:EQL との対応表 同じ検出を SPL と EQL で書き比べる EQL の SPL に対する優位点 EQL の基本構文 最小単位の形 例 1:PowerShell 実行の検出 例 2:複数条件の組み合わせ EQL の演算子チートシート == と : の使い分け(ハマりやすい) ECS:Elastic Common Schema を理解する ECS とは何か セキュリティで頻出する ECS フィールド タスク文を EQL に変換するフレームワーク 5 ステップ翻訳法 実演:タスク文を 5 ステップで翻訳する キーワード → ECS フィールドの早見表 sequence と時間制約:流れの検出 構文 by の力:複数フィールドで相関 イベント間で値を引き継ぐ:per-event by maxspan の選び方 実例:セキュリティ検出パターン 5 選 パターン 1:暗号化されていない通信の検出(コンプライアンス) パターン 2:PowerShell ダウンロードからの C2 接続 パターン 3:Office アプリからのプロセス起動(Living off the Land) パターン 4:マルウェア配置 → 実行 → C2 通信 パターン 5:横展開の検出(ラテラルムーブメント) よくあるミス と正しい書き方 ミス 1:ECS でないフィールド名を使う ミス 2:大文字小文字の罠 ミス 3:event.action と event.type の混同 ミス 4:sequence の順序逆転 ミス 5:maxspan の単位忘れ ミス 6:EQL 関数の大文字小文字を間違える ミス 7:関数の引数の大文字小文字でヒットしない 実際に動かす方法 方法 1:Kibana の Dev Tools で試す 方法 2:Elastic Security の Timeline 方法 3:Detection Rule として登録 まとめ 参考資料 EQL とは何か EQL は Elastic が開発した、 イベントベースのクエリ言語 です。SQL や KQL に少し似ていますが、決定的に違うのは「 複数のイベントの順序と関係性を表現できる 」点です。 ひとことで言うと: EQL は「A が起きて、その後に B が起きて、さらに C が起きた」を 1 つのクエリで書ける言語。 これは脅威検出と相性が抜群です。なぜなら、攻撃チェーン(MITRE ATT&CK で言うところの Tactics の連鎖)は、まさにそういう構造をしているからです。 EQL を動かすための前提 ここで先に伝えておきたいことがあります。EQL は ECS(Elastic Common Schema)を前提に設計されているため、 検索対象のデータには @timestamp と event.category フィールドが必要 です。Elastic 公式ドキュメントにも、EQL はデフォルトでこの 2 つのフィールドを使うと明記されています。 つまり、 process where process.name == "powershell.exe" の process は、実際には「event.category が process のイベント」を意味します。 Elastic Agent や Elastic Defend、Beats から取り込んだデータは ECS に準拠しているのでそのまま EQL が動きます。独自データの場合は、最低限この 2 フィールドをマッピングしておく必要があります。 通常の検索クエリ(KQL/Lucene)と何が違うか 特性 通常のクエリ (KQL) EQL 1 つのイベント検索 ✅ 得意 ✅ 得意 集計・統計 ✅ 得意 ⚠️ 限定的 複数イベントの順序関係 ❌ 自前で組み立て ✅ ネイティブ対応 時間窓での相関 ❌ 困難 ✅ maxspan で簡単 ECS フィールドの活用 ✅ ✅ 「単一イベントを検索したい」なら KQL でも十分です。 「攻撃の流れを検出したい」なら EQL です。 Splunk SPL を使っている人へ:EQL との対応表 SOC 経験者の多くは Splunk SPL の知識があるはずです。両者の対応関係を理解しておくと、EQL の学習速度が一気に上がります。 やりたいこと Splunk SPL Elastic EQL 単一イベント検索 search index=… process=”…” process where process.name == “…” 複数イベントの相関 transaction host maxspan=30s sequence by host.name with maxspan=30s ストリーミング相関 streamstats sequence データモデル / 正規化 CIM (Common Information Model) ECS (Elastic Common Schema) フィールド指定 process_name(CIM) process.name(ECS) 否定 NOT not / != ワイルドカード * * (: または like 演算子) ⚠️ これは厳密な機能対応ではなく、考え方を理解するための対応表です。 Splunk の transaction はイベントをまとめて後処理するイメージが強く、EQL の sequence は「順序あるイベント列」をネイティブに表現する処理です。考え方として近い、というレベルで捉えてください。 同じ検出を SPL と EQL で書き比べる シナリオ: PowerShell が実行され、30 秒以内に外部 HTTPS 接続が発生 Splunk SPL: (index=endpoint sourcetype=process process_name="powershell.exe") OR (index=endpoint sourcetype=network dest_port=443) | transaction host maxspan=30s | where mvcount(sourcetype)>1 Elastic EQL: sequence by process.entity_id with maxspan=30s [process where process.name == "powershell.exe"] [network where destination.port == 443] EQL の方が「 意図がそのまま構文になっている 」のがわかります。「これが起きて、次にこれが起きる」と読める。(process.entityの代わりに広域な相関で host.name を使う事も可能です) EQL の SPL に対する優位点 実務でメリットになるのは、おおむね以下の点です。 構文が攻撃チェーンのメンタルモデルと一致 — sequence … [event A] [event B] という形が、そのままアナリストの思考順序になる 時間ウィンドウの指定が一行 — with maxspan=30s を足すだけ ECS により書いたクエリの再利用性が高い — process.name を含むデータであれば、Windows、Linux、各種 EDR を横断して同じクエリが使える 検知ルールにそのまま流用可能 — Elastic Security の Detection Engine が EQL をネイティブサポート オープンスタンダードに沿った設計 — ECS は OpenTelemetry の Semantic Conventions に統合される方向で進化している 逆に EQL が苦手なこと も正直に書いておきます。 大規模な統計集計(カウント、平均、グルーピング)→ ES|QL や Lens の方が向いている 複雑な条件分岐や後処理 → ES|QL を併用するのが現実的 フィールド同士の比較(例:「1 回目の host.name と 2 回目の host.name が違う」)→ EQL 単独では難しい。ES|QL での後続分析と組み合わせる EQL は「 順序ある脅威検出 」のためのツール。集計や複雑な比較が必要なら別のツールと組み合わせるのが Elastic 流です。 EQL の基本構文 ここから手を動かす段階に入ります。 最小単位の形 event_category where condition たったこれだけ。読むと: event_category:何のイベントを見るか(process、file、network、authentication、registry など。内部的には event.category の値) where:条件を続けますよ、という宣言 condition:実際の条件 例 1:PowerShell 実行の検出 process where process.name == "powershell.exe" 例 2:複数条件の組み合わせ process where process.name == "powershell.exe" and process.command_line : "*DownloadString*" ここで重要な演算子を整理しておきます。 EQL の演算子チートシート 演算子 意味 使いどころ == 厳密一致(大文字小文字を区別) プロセス名、ポート番号など正確に一致させたい時 != 一致しない 除外条件 : 大文字小文字を区別しない文字列一致 (ワイルドカード *、? 対応) パス、コマンドライン、拡張子(実体は like~ と等価) like ワイルドカード一致(大文字小文字を 区別する ) パターンマッチ like~ ワイルドカード一致(大文字小文字を 区別しない ) : と同じ in リストのいずれか(区別あり) process.name in (“cmd.exe”, “powershell.exe”) in~ リストのいずれか(区別なし) 大文字小文字が揺れるデータに not 否定 not process.name == “explorer.exe” and / or 論理演算子 条件の組み合わせ 💡 メモ: : は「ワイルドカードが使える ==」と覚えると間違いません。文字列の比較で、== より柔軟(大文字小文字も吸収、* ? も使える)。 == と : の使い分け(ハマりやすい) # ❌ ハマるパターン:ファイル拡張子に == を使うと小文字限定 file where file.extension == "exe" # .EXE は引っかからない # ✅ : を使えば大文字小文字の揺れを吸収 file where file.extension : "exe" # .exe も .EXE もマッチ Windows の拡張子は大文字小文字が混在することがよくあります。ファイルパスや拡張子は基本的に : を使う方が安全です。 ECS:Elastic Common Schema を理解する EQL を本気で使うには、 ECS の理解が欠かせません。これは Splunk の CIM に相当する概念です。 ECS とは何か ECS は「 Elasticsearch にイベントデータを保存するときの共通フィールド仕様 」です。 セキュリティログは多種多様なソースから来ます。 Windows Sysmon Linux Auditd ファイアウォール EDR クラウドサービスの監査ログ これらが全部バラバラのフィールド名(proc_name、process_name、pname、Image…)だと、検索のたびにスキーマを覚え直す羽目になります。 ECS はこれを process.name のような共通名に統一する仕様です。 ECS 対応のデータであれば、同じ意味の情報を同じフィールド名で扱える ようになります。 ⚠️ 注意: ECS は「共通化のルール」であって「すべてのデータソースが必ず全フィールドを持つ」保証ではありません。たとえばネットワーク機器のログには通常 process.name がありません。実際にどのフィールドが入っているかは、データソースやインテグレーションによって異なるため、Kibana の Discover や Data Views で確認してから書く習慣をつけましょう。 セキュリティで頻出する ECS フィールド 実務でほぼ毎日使うフィールドを覚えておきましょう。 プロセス フィールド 内容 process.name プロセス名(例:powershell.exe) process.executable フルパス(例:C:\Windows\System32\powershell.exe) process.command_line コマンドライン全文 process.pid プロセス ID(OS が割り当てる数値) process.entity_id プロセスの一意識別子(Elastic Defend などが付与。 推奨 ) process.parent.name 親プロセス名 process.parent.entity_id 親プロセスの entity_id 💡 process.pid と process.entity_id の違い: PID は OS のプロセス識別子ですが、プロセス終了後に 再利用される ため、長い時間ウィンドウのクエリでは別プロセスのイベントが混ざる可能性があります。Elastic Defend や Sysmon が付与する process.entity_id はプロセスごとに一意な値なので、sequence の by 句では process.entity_id を優先 します。 ファイル フィールド 内容 file.path フルパス file.name ファイル名 file.extension 拡張子(. なし) file.size バイト数 file.hash.sha256 SHA256 ハッシュ ネットワーク フィールド 内容 destination.ip 接続先 IP destination.port 接続先ポート source.ip 送信元 IP network.protocol プロトコル network.direction 通信の向き。 ホスト視点 なら ingress / egress 、 ネットワーク観測点視点 なら inbound / outbound / internal / external 💡 network.direction の値はデータソースによって違う: Elastic Defend や endpoint 系(Auditbeat、Sysmon 経由)は ingress / egress を使う傾向、Packetbeat や Zeek のような observer 系(ネットワーク監視目線)は inbound / outbound を使う傾向です。クエリを書く前に Discover で実際の値を確認しましょう。 イベント / 主体 フィールド 内容 event.category イベント大分類(process、file、network 等)。 EQL の必須フィールド event.type ECS 標準のサブタイプ(creation、start、end、connection 等) event.action データソース固有のアクション名(ソースによって値が異なる) event.outcome success / failure user.name ユーザー名 host.name ホスト名 @timestamp イベント時刻。 EQL の必須フィールド 💡 event.type と event.action の違い: event.type は ECS が定義する 標準化されたサブタイプ (creation、deletion、start、connection など決められた値)。event.action は データソース固有のアクション名 (Windows なら file-created、Sysmon なら FileCreated など値がバラバラ)。 ポータブルなクエリを書くなら event.type を使うのが基本 です。 タスク文を EQL に変換するフレームワーク 検知エンジニアリングで一番難しいのは、「日本語の脅威シナリオを、ECS フィールドと EQL に翻訳すること」です。これさえ身につければ、新しい脅威ハンティングのアイデアをすぐクエリにできるようになります。 5 ステップ翻訳法 どんな日本語のシナリオも、次の 5 つの問いに答えれば EQL になります。 Step 1. 何のイベント? → event.category (process / file / network …) Step 2. どんなアクション? → event.type (creation / start / connection …) Step 3. 何に対して? → file.* / process.* (オブジェクト) Step 4. 誰が引き起こした? → process.* / user.* (主体) Step 5. 順序や時間制約? → sequence / maxspan (流れ) 実演:タスク文を 5 ステップで翻訳する タスク: 「 実行ファイル が Word ドキュメント を 作成し 、30 秒以内に大量のデータ送信が起きたケースを検出したい」 誰が? → 実行ファイル 何を? → Word ドキュメント どうした? → 作成した EQL では、この「誰が・何を・どうした」を ECS フィールドに置き換えていきます。 誰が? → process.executable 何を? → file.extension どうした? → event.type つまり、今回の前半部分はこういう考え方になります。 .exe の実行ファイルが .doc / .docx の Word ドキュメントを 作成した file where event.type == "creation" and process.executable : "*.exe" and file.extension : ("doc", "docx") ここで大事なのは、 file.* と process.* の見方です。 file.* は、 作られたもの の情報です。 今回であれば、作られたものは Word ドキュメントなので、 file.extension : ("doc", "docx") になります。 一方で、 process.* は、 そのイベントを起こしたプログラム の情報です。 今回であれば、Word ドキュメントを作ったのは実行ファイルなので、 process.executable : "*.exe" になります。 つまり、ファイル作成イベントでは、次のように考えるとわかりやすいです。 process が file を event.type した 今回なら、こうです。 .exe が .doc/.docx を creation した 次に、後半の条件「30 秒以内に大量のデータ送信が起きた」を見ます。 これはネットワークイベントです。大量のデータ送信は、たとえば source.bytes で表現できます。 「作成」→ 「30 秒以内」→ 「大量送信」、流れの検出 → sequence + maxspan sequence with maxspan=30s [file where event.type == "creation" and process.executable : "*.exe" and file.extension : ("doc", "docx")] [network where source.bytes > 1000000] 完成です。日本語から EQL までの距離が、思ったより近いのが伝わったでしょうか。 キーワード → ECS フィールドの早見表 翻訳作業でつまずきやすい「日本語キーワード」と「対応する ECS フィールド」の対応表です。 日本語キーワード 条件フィルター よく組み合わせるフィールド 実行された / 起動された event.type == "start" process.name 、 process.executable 作成された / 保存された event.type == "creation" file.name 、 file.path 、 file.extension 削除された event.type == "deletion" file.* 変更された event.type == "change" file.* 、 registry.* 外部に通信 / 接続 event.type == "connection" destination.ip 、 destination.port ログインした event.category == "authentication" event.outcome 、 user.name 、 source.ip sequence と時間制約:流れの検出 EQL の真骨頂は sequence です。詳しく見ていきます。 構文 sequence [by FIELD] [with maxspan=TIME] [event_category where condition_1] [event_category where condition_2] [event_category where condition_3] by FIELD:イベント間を「何でつなぐか」(join key)。host.name、process.entity_id、user.name など with maxspan=TIME:「全体が何秒以内に起きたら検出するか」(s / m / h / d) […]:各ステップのイベント条件 by の力:複数フィールドで相関 sequence by host.name, user.name with maxspan=5m [authentication where event.outcome == "failure"] [authentication where event.outcome == "success"] 「同じホスト、同じユーザーで、ログイン失敗の後に成功」を 5 分以内で検出。 ブルートフォースの成功 を捕まえる典型パターンです。 イベント間で値を引き継ぐ:per-event by ここが少しトリッキーで、よく間違えるところです。 やりたいこと: ファイルが作られて、その 同じファイル名 のプロセスが起動した sequence by host.name with maxspan=1m [file where event.type == "creation" and file.extension : "exe"] by file.name [process where event.type == "start"] by process.name sequence by host.name で ホスト全体の相関 、各 […] の後の by file.name / by process.name で 値を引き継ぎ ます。これにより「file.name == process.name」という相関が成立。 ⚠️ 重要: by のキー数は 全イベントで揃える必要があります 。[eventA] には by を付けて [eventB] に付けない、という書き方はできません。全イベントに付けるか、付けないかのどちらかです。 maxspan の選び方 シナリオ 推奨 maxspan プロセス起動 → 即時通信 30s 〜 1m マルウェア配置 → 実行 1m 〜 5m ログイン → 横展開 5m 〜 30m 内部偵察 → 権限昇格 1h 〜 4h 持続化 → 後日の C2 1d 以上(要注意:誤検知も増える) 短すぎると正規の動作も誤検知、長すぎると無関係なイベントが混ざる。 ここは仮説に基づいてチューニングする 領域です。 実例:セキュリティ検出パターン 5 選 そのままルールとして登録できる形にしてあります。 パターン 1:暗号化されていない通信の検出(コンプライアンス) network where event.type == "connection" and ( (destination.port == 80 and network.protocol == "http") or (destination.port == 21 and network.protocol == "ftp") ) and network.direction in ("egress", "outbound") 社内から外向きの HTTP/FTP を可視化。PCI-DSS や社内ポリシー違反の検出に使えます。 パターン 2:PowerShell ダウンロードからの C2 接続 sequence by process.entity_id with maxspan=30s /* * Step 1: * PowerShell が起動されたイベントを探す。 * process.entity_id で後続の network イベントと同じプロセスに紐づける。 */ [process where event.type == "start" and process.name : "powershell.exe" /* * PowerShell のコマンドラインに、 * ダウンロードやコード実行でよく使われる文字列が含まれているかを見る。 * IEX / Invoke-Expression は、文字列として取得したコードを実行する時によく使われる。 * DownloadString / Net.WebClient は、外部からスクリプトやペイロードを取得する時によく使われる。 */ and ( process.command_line : "*IEX*" or process.command_line : "*DownloadString*" or process.command_line : "*Invoke-Expression*" or process.command_line : "*Net.WebClient*" ) ] /* * Step 2: * 同じプロセスが 30 秒以内に HTTPS 通信しているかを見る。 * destination.port == 443 は HTTPS 通信の代表的なポート。 */ [network where event.type == "connection" and destination.port == 443 /* * 接続先がプライベート IP ではないことを確認する。 * つまり、社内ネットワークではなく外部サーバーへ通信している可能性を見る。 */ and not cidrmatch( destination.ip, "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" ) ] PowerShell が「ダウンロード系」のコマンドを実行して、直後にプライベート IP 以外 へ HTTPS 接続したケース。C2 ビーコンの典型形です。 ✅ ポイント: by process.entity_id で 同一プロセス内の流れ に限定しています。process.pid は再利用されるため誤検知の原因になりますが、process.entity_id は一意なので安全。 パターン 3:Office アプリからのプロセス起動(Living off the Land) process where event.type == "start" and process.parent.name : ("winword.exe", "excel.exe", "powerpnt.exe", "outlook.exe") and process.name : ("cmd.exe", "powershell.exe", "wscript.exe", "cscript.exe", "mshta.exe", "rundll32.exe") Office アプリがシェル系プロセスを起動するのは、ほぼマクロ起点の攻撃。 現代の標的型攻撃で最頻出のパターン で、誤検知も少なめ。検知ルールとしてまず入れたい一本です。 パターン 4:マルウェア配置 → 実行 → C2 通信 sequence by host.name with maxspan=2m /* * Step 1: * 同じホスト上で、Temp フォルダに実行可能ファイルが作成されたイベントを探す。 * Temp フォルダは、マルウェアやドロッパーが一時的にファイルを置く場所としてよく使われる。 */ [file where event.type == "creation" and file.path : "*\\Temp\\*" /* * exe / dll / scr は、攻撃で使われやすい実行可能ファイルの拡張子。 * exe は通常の実行ファイル、dll はライブラリ、scr はスクリーンセーバー形式だが実行可能。 */ and file.extension : ("exe", "dll", "scr") ] by file.name /* * Step 2: * 直後に、作成されたファイルと同じ名前のプロセスが起動されたかを見る。 * ここでは by file.name と by process.name を使い、 * 「作られたファイル名」と「起動したプロセス名」を結びつけている。 */ [process where event.type == "start"] by process.name /* * Step 3: * さらに、そのプロセスが外部通信でよく使われるポートへ接続したかを見る。 * 80 / 443 は HTTP / HTTPS、 * 8080 は代替 HTTP、 * 4444 は攻撃ツールやリバースシェルで使われることがある代表的なポート。 */ [network where event.type == "connection" and destination.port in (80, 443, 8080, 4444) ] by process.name Temp に実行ファイルが落ちて → そのファイルが起動して → 外部通信、までを 2 分以内に検出。 ドロッパー型マルウェアの典型挙動 です。 パターン 5:横展開の検出(ラテラルムーブメント) sequence by user.name with maxspan=30m /* * Step 1: * あるユーザーでログイン成功イベントが発生したかを見る。 * 横展開では、攻撃者が盗んだ認証情報を使って、 * まずどこかの端末やサーバーへログインすることがある。 */ [authentication where event.outcome == "success"] /* * Step 2: * 同じユーザーの操作として、横展開で使われやすいツールが起動されたかを見る。 * * psexec.exe: * Windows 環境でリモート端末上にコマンドを実行するためによく使われる。 * * wmic.exe: * WMI を使って、リモート端末の情報取得やコマンド実行に使われることがある。 * * winrm.exe: * Windows Remote Management を使ったリモート操作に関係する。 */ [process where event.type == "start" and process.name : ("psexec.exe", "wmic.exe", "winrm.exe")] /* * Step 3: * その後、同じユーザーで再びログイン成功イベントが発生したかを見る。 * これは、別の端末やサーバーへ移動した可能性を確認するためのステップ。 */ [authentication where event.outcome == "success"] 「同じユーザーで、ログイン成功 → 横展開系ツール実行 → ログイン成功」という流れを 30 分以内で検出。 侵入後の活動段階 を捕まえます。 ⚠️ EQL の限界: 「 別ホスト へのログイン」まで厳密に判定したい(1 回目と 2 回目の host.name が違う、という条件)場合、EQL の by 句では「値が等しい」しか表現できないため、これだけでは難しいです。実務では次のどちらかで対応します。 EQL でこのクエリを実行して候補を絞り、Detection Rule の Investigation Guide で host.name の違いを確認する ES|QL で後続分析する(同じユーザーが複数ホストにログインしているかを集計) よくあるミス と正しい書き方 実務でハマる典型パターンです。 ミス 1:ECS でないフィールド名を使う # ❌ process where proc_name == "powershell.exe" # ✅ process where process.name == "powershell.exe" 対策: Kibana の Data Views でフィールド名を確認するクセをつける。SPL からの移行者がいちばんやりがちなミス。 ミス 2:大文字小文字の罠 # ❌ 大文字の .EXE は引っかからない file where file.extension == "exe" # ✅ file where file.extension : "exe" ミス 3:event.action と event.type の混同 # ⚠️ 動くかもしれないが、データソース依存(値が "file-created" や "FileCreated" など揺れる) file where event.action == "creation" # ✅ ECS 標準化された値を使う file where event.type == "creation" ミス 4:sequence の順序逆転 sequence は 書いた順 = 起きた順 です。 # ❌ 通信が先、プロセス起動が後…と書いている sequence by process.entity_id [network where destination.port == 443] [process where process.name : "powershell.exe"] # ✅ 「PowerShell 起動 → 通信」のシナリオなら sequence by process.entity_id [process where process.name : "powershell.exe"] [network where destination.port == 443] シナリオを先に 日本語で書き下す と、順序ミスが減ります。 ミス 5:maxspan の単位忘れ # ❌ 単位なしはエラー with maxspan=30 # ✅ with maxspan=30s s(秒)/ m(分)/ h(時間)/ d(日)を必ずつける。 ミス 6:EQL 関数の大文字小文字を間違える EQL の 関数名は大文字小文字を区別 します。これでハマる人は多いです。 # ❌ Unknown function エラーになる not cidrmatch(destination.ip, "10.0.0.0/8") # ✅ camelCase が正解 not cidrMatch(destination.ip, "10.0.0.0/8") よく使う関数の正しいスペル: cidrMatch 、 startsWith 、 endsWith 、 stringContains 、 indexOf 、 concat 。関数を大文字小文字を無視させたいときは ~ を付けます(例: endsWith~(file.path, ".exe") )。 ミス 7:関数の引数の大文字小文字でヒットしない 関数名そのものに加えて、 関数の引数(比較対象の文字列)もデフォルトで大文字小文字を区別 します。Windows のファイルパスのように大文字小文字が揺れるデータでは、これで取りこぼします。 # ❌ .EXE や .Exe は引っかからない(小文字 .exe のみマッチ) file where endsWith(file.path, ".exe") or endsWith(file.path, ".dll") # ✅ ~ を付けると大文字小文字を吸収 file where endsWith~(file.path, ".exe") or endsWith~(file.path, ".dll") ルール: 関数を大文字小文字を無視させたいときは、関数名の末尾に ~ を付けます。 endsWith~ 、 startsWith~ 、 stringContains~ 、 indexOf~ などすべての文字列関数で使えます。 💡 : と endsWith~ の使い分け: 単純な拡張子チェックなら file.extension : "exe" で十分。 endsWith~ はパス末尾や任意の文字列末尾を見たいとき(例: process.command_line の末尾)に便利です。 実際に動かす方法 方法 1:Kibana の Dev Tools で試す 最も手軽な方法。 POST logs-endpoint.*/_eql/search { "query": """ sequence by process.entity_id with maxspan=30s [process where process.name : "powershell.exe"] [network where destination.port == 443] """ } POST logs-endpoint.*/_eql/search 方法 2:Elastic Security の Timeline Elastic Security の「Timeline」では、UI から直接 EQL クエリを実行できます。脅威ハンティングのインタラクティブな探索に向いています。 方法 3:Detection Rule として登録 Elastic Security → Manage → Rules → Create new rule Rule type で「 Event Correlation 」を選択(これが EQL) クエリを貼り付け 重要度、MITRE ATT&CK タグ、Investigation Guide を設定 Save & enable 検証用のテストを必ず通してから本番有効化することをおすすめします。 まとめ EQL は「 順序ある脅威検出 」のために作られたクエリ言語 EQL を動かす前提は @timestamp と event.category の 2 フィールド Splunk SPL の transaction に近い考え方だが、構文が 攻撃チェーンのメンタルモデルに直結 している ECS は『同じ意味のデータには同じフィールド名を使う』というルール。だから 1 つのクエリで複数データソースに対応できる。ただし、すべてのデータソースが ECS の全フィールドを持っているわけではないので、Discover で実際の中身は必ず確認。 5 ステップ翻訳法(What → Action → Object → Actor → Sequence)で、日本語の脅威シナリオを EQL に変換できる sequence + maxspan + by が真骨頂。順序と時間ウィンドウを 1 つのクエリで表現 セキュリティ運用の効率は、「使えるクエリ言語」と「使える脅威モデル」の組み合わせで決まります。EQL は前者の強力な武器です。 Elastic 公式の Detection Rules リポジトリ をフォローして、新ルールをキャッチアップ 参考資料 EQL Syntax Reference (Elastic 公式) Event Query Language Overview Event correlation (EQL) r ules | Elastic Security Elastic Common Schema (ECS) Reference ECS Categorization Field: event.type ECS File Fields ECS Process Fields Elastic Security Detection Rules(GitHub) MITRE ATT&CK Framework ElasticsearchのためのLuceneクエリ入門ガイド The post セキュリティエンジニアのための EQL 入門:Elastic で脅威を見つけるクエリ言語 first appeared on Elastic Portal .
― マルチモーダル embedding の可能性と限界 ― サイオステクノロジー株式会社 Saman Elasticsearch のベクトル検索といえば、これまではテキストや画像が中心でした。 しかし最近は、テキスト・画像・動画・音声を同じ埋め込み空間で扱える「マルチモーダル embedding」が現実的な選択肢になってきています。 本記事は、Elastic Inference Service (以下 EIS) で利用できる .jina-embeddings-v5-omni-small を使い、音声ファイルを Elasticsearch に保存して kNN 検索でどこまで使えるかを検証した PoC のレポートです。 結論を先に書くと、次のとおりです。 音声を embedding 化して Elasticsearch に保存し、kNN で検索する 基本パイプラインは問題なく動いた 音声 → 音声(audio-to-audio) の類似検索は期待どおりに機能した 一方で テキスト → 音声(text-to-audio) の意味検索は、今回の条件では十分な精度が出なかった 技術ブログとして、うまくいった部分だけでなく「なぜうまくいかなかったか」も合わせて共有します。同じような検証を計画している方の参考になれば幸いです。 目次 マルチモーダル embedding と Jina v5 Omni embedding とは Jina Embeddings v5 Omni なぜ音声を検索可能にしたいか システム構成 インデックスの mapping 検証データ 音声を embedding 化して保存する Step 1: 音声を Base64 に変換する Step 2: EIS に送って embedding を生成してもらう Step 3: 返ってきた embedding を受け取る Step 4: メタデータと一緒に Elasticsearch に保存する 検証1: audio-to-audio 検索 結果 検証2: text-to-audio 検索 結果 なぜ text-to-audio は弱かったのか 1. 検証データが「音響的に似すぎている」 2. テキストと音声の意味空間が完全には揃っていない 3. 音声が短く、意味的な信号が少ない まとめ 改善方針: transcript と組み合わせた Hybrid Retrieval まとめ 参考資料 マルチモーダル embedding と Jina v5 Omni embedding とは embedding とは、データを数値ベクトルに変換したものです。たとえば「ログインできない」と「アカウントに入れない」は意味が近いので、embedding にすると近い位置のベクトルになります。 ログインできない → [0.12, -0.03, 0.45, …] アカウントに入れない → [0.11, -0.04, 0.46, …] ← 近い 今日の天気は晴れです → [0.92, 0.31, -0.20, …] ← 遠い これまでは、テキストはテキスト用モデル、画像は画像用モデルと、モダリティごとに別の embedding 空間を使うのが一般的でした。 マルチモーダル embedding は、テキストも画像も音声も「同じ空間」に埋め込みます。同じ意味を持つテキストと音声が、空間上で近い位置に置かれることが理想です。 Jina Embeddings v5 Omni 今回使ったのは Jina AI のマルチモーダル embedding モデルです。Elastic Search Labs でも、テキスト・画像・動画・音声を 1 つの Elasticsearch インデックスに保存して横断的に検索できるモデルとして紹介されています。 EIS では preconfigured な inference endpoint として以下が利用できます。 .jina-embeddings-v5-omni-small(出力次元: 1024) 利用可能なモデルは、Dev Tools で GET _inference を実行することで確認できます。 ここで重要なのは、今回使用するモデルは ChatGPT のような 回答を生成する LLM ではなく、ベクトル変換のための embedding モデル だという点です。ここを混同すると後段の評価がブレるので、最初に押さえておきます。 なぜ音声を検索可能にしたいか ビジネス的な動機は明確です。音声データの「中で何が話されているか」で検索したいというニーズは、現場にたくさんあります。 コールセンター音声から「ログインできない」と話している通話を見つけたい 問い合わせ音声を内容で分類して FAQ を改善したい 障害発生時に「画面が固まる」「決済できない」といった声が急増していないか確認したい 顧客との会話音声から、契約・解約に関する文脈を後から探したい 社内セミナーや会議音声から、必要な情報を探したい 医療の現場で、音声カルテから何かを検索したい これらは現在、文字起こし(transcript)してから text search で実現するのが普通です。マルチモーダル embedding が一定の精度で動くなら、文字起こしを介さずに 音声そのもの を検索対象に加えられる可能性があります。 また、音声データには機密情報が含まれることが多く、外部 API には投げにくいケースが少なくありません。Elastic 内で inference・indexing・検索・アクセス制御を統合できれば、データを外に出さずに音声検索基盤を構築できる点も大きなメリットです。 システム構成 今回の構成はシンプルです。 音声ファイル(.wav) │ Base64 エンコード ▼ Elastic Inference Service (.jina-embeddings-v5-omni-small) │ 1024 次元 embedding ▼ Elasticsearch (dense_vector field) │ kNN 検索 ▼ 類似音声 / 類似テキスト クライアント側(今回はローカル Mac の Python スクリプト)では、音声ファイルを Base64 化して EIS に送るだけです。embedding 生成自体は EIS 側で実行されるため、GPU やモデル管理をローカルに持つ必要はありません。 インデックスの mapping embedding を保存するインデックスの mapping は次のとおりです。 PUT audio-poc-jina-eis-v1 { "mappings": { "properties": { "audio_id": { "type": "keyword" }, "file_name": { "type": "keyword" }, "expected_topic": { "type": "keyword" }, "audio_url": { "type": "keyword" }, "embedding": { "type": "dense_vector", "dims": 1024, "index": true, "similarity": "cosine" }, "created_at": { "type": "date" }, "embedding_method": { "type": "keyword" } } } } 設計のポイントは以下です。 dense_vector の dims: 1024 は Jina v5 Omni small の出力次元に合わせる index: true を指定して kNN 検索の対象にする 類似度関数は cosine を選択。ベクトルの長さよりも方向で比較するため、テキスト/音声 embedding では一般的な選択 検証データ 検証用に、短い日本語の問い合わせ音声を5つ用意しました。 ファイル 想定トピック 話している内容 1.wav ログインできない 昨日から何度もログインしようとしていますが、正しいメールアドレスとパスワードを入力してもアカウントに入れません。 2.wav パスワード再設定 パスワードを忘れてしまったので再設定メールを送ったのですが、メールが届かず手続きが進められません。 3.wav クレジットカード決済エラー クレジットカードで支払いをしようとすると毎回エラーが出て、注文を完了できない状態です。 4.wav サービス画面の フリーズ サービスの画面が途中で固まってしまい、急ぎの作業ができないのでとても困っています。 5.wav 契約プランと 請求金額の確認 契約内容について確認したいことがあるので、現在のプランと次回の請求金額を教えてください。 すべて 同じ話者・同じ録音条件 で作成しています。 この条件が、後段で検索結果を解釈するうえで重要 になります。 音声を embedding 化して保存する 本記事のコードは GitHub で公開しています: https://github.com/SIOS-Technology-Inc/elastic-blogs/tree/main/2026-05-15-test-jina-audio ここは PoC の中核なので、少し丁寧にやったことの流れを追います。 Step 1: 音声を Base64 に変換する .wav は バイナリファイル です。HTTP の JSON ボディには通常バイナリをそのまま載せられないので、まず「テキスト」に変換する必要があります。そのために使うのが Base64 です。Base64 は、任意のバイナリデータを ASCII 文字列で表現する方式で、画像や音声を API に渡すときの定番テクニックです。 def audio_to_base64(file_path: Path) -> str: with open(file_path, "rb") as f: return base64.b64encode(f.read()).decode("utf-8") 1.wav を変換すると、こんな長い文字列になります。 UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAA... これで、音声を JSON に乗せられる形になりました。 Step 2: EIS に送って embedding を生成してもらう 次に、Base64 化した音声を Elasticsearch の inference API に送ります。EIS で公開されている embedding モデルは、以下の統一エンドポイントで呼び出します。 POST /_inference/embedding/{INFERENCE_ID} {INFERENCE_ID} には、今回は .jina-embeddings-v5-omni-small を指定します。リクエストボディは1行だけです。 { "input": ["data:audio/wav;base64,UklGRiQAAABXQVZF..."] } ここでひとつ重要なのが、Base64 文字列の前についている data:audio/wav;base64, という接頭辞です。これは データ URI と呼ばれる形式で、「この入力は wav 音声データだよ」とモデルに伝える役割を持ちます。同じエンドポイントはテキストも受け付けるので、入力がテキストなのか音声なのかをこの接頭辞で判別しています。 Python で書くとこうなります。 def get_audio_embedding(file_path: Path) -> list: audio_b64 = audio_to_base64(file_path) # data URI プレフィックスで「これは音声」だとモデルに伝える audio_input = f"data:audio/wav;base64,{audio_b64}" response = es.perform_request( "POST", f"/_inference/embedding/{INFERENCE_ID}", headers={ "Accept": "application/json", "Content-Type": "application/json", }, body={"input": [audio_input]}, ) return response["embeddings"][0]["embedding"] 実装で唯一ハマったポイントは、 Acceptと Content-Typeの両方を指定する必要があった ことです。片方しか指定しないと互換バージョンと衝突して 400 エラーになることがありました。気づくまで少し時間を使ったので、同じことを試す方は注意してください。 Step 3: 返ってきた embedding を受け取る EIS が返してくるレスポンスは次のような形です。 { "embeddings": [ { "embedding": [0.012, -0.028, 0.103, -0.057, /* ...計1024個の数値... */] } ] } embeddings[0].embedding を取り出すと、 長さ 1024 の float の配列 が手に入ります。これがその音声の「特徴を数値化したもの」です。人間には意味の分からない数値の羅列ですが、Elasticsearch にとってはこの 1024 個の数字が「似ているか」を判定する材料になります。 Step 4: メタデータと一緒に Elasticsearch に保存する 最後に、embedding をメタデータと組み合わせて、1つの ドキュメント として Elasticsearch に保存します。 indexed_doc = { ... } es.index( ... ) これで、audio-poc-jina-eis-v1 インデックスには次のような JSON ドキュメントが保存されます。 { "audio_id": "audio-001", "file_name": "1.wav", "expected_topic": "ログインできない", "embedding": [0.012, -0.028, 0.103, /* ...計1024個... */], "embedding_method": "elastic_inference_jina_omni_small", "created_at": "2026-..." } 5 つの音声に対してこの処理を繰り返すと、Elasticsearch には 5 件の音声 embedding が並びます。あとは kNN クエリで「近いベクトル」を探すだけ、という状態になりました。 検証1: audio-to-audio 検索 最初に、音声 → 音声の類似検索を試しました。1.wav を query として、最も近い音声を探します。 検索 body はシンプルな kNN クエリです。 { "knn": { "field": "embedding", "query_vector": [/* query 音声の 1024 次元 embedding */], "k": 5, "num_candidates": 10 }, "_source": ["audio_id", "file_name", "expected_topic"] } 結果 1.wav を query にした場合: 1位: 1.wav score: 0.99999 2位: 3.wav score: 0.99142 3位: 5.wav score: 0.99125 4位: 4.wav score: 0.98703 5位: 2.wav score: 0.98342 次、5.wav を query にした場合: 1位: 5.wav score: 1.0000001 ※ ※ cosine 類似度ベースの kNN スコアは (1 + cos) / 2 で計算されるため理論上の上限は 1.0 です。query ベクトルとインデックス側ベクトルが同一の場合に、浮動小数点演算の丸め誤差で形式上 1.0 をわずかに超えた値が返ることがあります。実質 1.0 と読み替えてください。 期待どおり、query にした音声自身が 1 位になりました。 音声 → 音声の類似検索は正しく動作している と判断できます。 ただし注目すべきは 2 位以下のスコアです。すべて 0.98〜0.99 という非常に高い値に集中しています。1 位は明確に分離できるものの、それ以外は ほぼ団子状態 です。この観察は、次の text-to-audio の議論の伏線になります。 検証2: text-to-audio 検索 次に、日本語テキスト query から音声を探します。クエリは同じく kNN ですが、query_vector を「テキストから生成した embedding」に差し替えます。クライアント側のコードはほぼ同じで、入力をテキストに切り替えるだけです。 結果 query: ログインできない (期待: 1.wav が1位) 1位: 5.wav (契約プランと請求金額の確認) score: 0.54343 2位: 1.wav (ログインできない) score: 0.54301 3位: 3.wav (クレジットカード決済エラー) score: 0.54214 少し長めの query でも試しました。 query: 正しいメールアドレスとパスワードを入力してもアカウントに入れません 1位: 5.wav 2位: 1.wav 期待した 1.wav は 1 位になりませんでした。さらに、上位 3 件のスコアが 0.543 前後に密集しており、意味的な分離がほとんど効いていないことが分かります。 つまり、 今回の条件では text-to-audio 検索の精度は実用レベルに届かなかった ということです。 なぜ text-to-audio は弱かったのか この PoC で最も重要なメッセージは、ここにあります。「マルチモーダルであれば何でも検索可能」というわけではない、という現実を共有できるかもしれません。 考えられる要因は複数あります。 1. 検証データが「音響的に似すぎている」 今回の 5 つの音声は、すべて以下の条件で作られています。 同じ話者 同じ口調 同じ録音条件(マイク、室内環境、無音区間の入り方) 同じくらいの長さ(10〜15 秒) 同じ「問い合わせ口調」の文体 audio embedding は、内容(何を話しているか)だけでなく、 話者の声質・録音条件・話すトーン・無音区間 といった音響的な特徴も拾います。今回のように音響条件が似すぎたデータでは、内容の違いより「音声としての雰囲気の共通性」が embedding を支配しやすくなります。audio-to-audio 検索で 2 位以下のスコアが 0.98 台に集中していたのは、まさにこの影響と整合します。 2. テキストと音声の意味空間が完全には揃っていない マルチモーダル embedding は「同じ意味のテキストと音声を近づける」ように学習されていますが、その整合性の強さは学習データやモデルサイズに依存します。 今回使ったのは omni-small(軽量モデル) です。日本語の短い問い合わせ音声で text-to-audio の対応関係を十分捉えられるかは、未知数の領域です。スコア差が 0.001 オーダーしかなくノイズに埋もれている状態は、まさにこの「整合性が弱い」状態と読めます。 3. 音声が短く、意味的な信号が少ない 各音声は 10〜15 秒程度です。短い音声ほど、テキスト query との意味的なマッチングに使える信号は少なくなります。数十秒〜数分の音声であれば、もう少し違う結果になった可能性があります。 まとめ audio-to-audio が動いて text-to-audio が弱かったのは「実装ミス」ではなく、 マルチモーダル embedding の現実的な特性と、検証データの条件が組み合わさった結果 だと考えています。 ここから引き出せる教訓はシンプルです。 「ベクトル検索の品質は、モデルだけでなく、データの性質と用途の組み合わせで決まる」 。これはマルチモーダルに限らず、テキスト embedding でも同じことが言えます。 改善方針: transcript と組み合わせた Hybrid Retrieval 実務で音声検索を本気で組むなら、音声 embedding 単体で頑張るより、 文字起こし(transcript)を併用する ほうが圧倒的に現実的です。 具体的にはこんな構成を考えています。 音声ファイル ├─ audio embedding (audio-to-audio 検索用) ├─ transcript text (BM25 / lexical 検索用) ├─ transcript embedding (semantic 検索用) └─ メタデータ (時間、顧客ID、カテゴリなど) │ ▼ Elasticsearch │ ▼ Hybrid Retrieval (RRF などで結果を統合) Elasticsearch には、複数の retriever の結果を統合する仕組みとして Reciprocal Rank Fusion (RRF) があります。これを使うと、以下を 1 リクエストで束ねられます。 音声 embedding による kNN(似た音声を探す) transcript embedding による kNN(意味の近い発話を探す) transcript への BM25(キーワード検索) メタデータでの filter(期間、顧客カテゴリなど) ユーザーが「ログインできない」と検索したとき、文字起こしテキスト側がしっかり 1.wav にマッチしてくれるはずです。音声 embedding はその上で「似た声質・トーンの問い合わせ」を補強する役割で使う、という役割分担が現実的です。 次のステップとして、この hybrid 構成を実装し、再度同じ query で評価してみる予定です。 まとめ 今回の PoC で確認できたことは次のとおりです。 EIS 経由で音声 embedding を生成できる — .jina-embeddings-v5-omni-small に Base64 音声を送ると 1024 次元の embedding が返る dense_vector に保存して kNN 検索できる — マルチモーダル検索の土台は問題なく動く audio-to-audio 検索は実用的に機能する — 同じ音声を query にすればその音声が 1 位に返る text-to-audio 検索は今回の条件では弱かった — 短く・音響条件が似た音声群では、テキスト query との意味的な分離が困難 ここから得た一番の学びは、 「マルチモーダル embedding は万能ではなく、ユースケースに合わせて他の検索手段と組み合わせて使うもの」 という現実です。 参考資料 Elastic Search Labs: Jina Embeddings v5 Omni — all media, one index https://www.elastic.co/search-labs/jp/blog/jina-embeddings-v5-omni-all-media-one-index Jina AI: jina-embeddings-v5-omni-small model card https://jina.ai/models/jina-embeddings-v5-omni-small/ Elasticsearch: dense_vector field type / kNN search / Reciprocal Rank Fusion (RRF) https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html Elastic Inference Service https://www.elastic.co/docs/explore-analyze/elastic-inference/eis The post Elastic Inference Service と Jina Embeddings v5 Omni で音声を検索する first appeared on Elastic Portal .










