
形態素解析
イベント
該当するコンテンツが見つかりませんでした
マガジン

技術ブログ
はじめに コーディングエージェントは、コードやドキュメントを grep などで探して読めます。設計書・仕様書・運用手順などのテキスト文書で、設定キーや関数名のように探す文字列が分かっているときには有効な方法です。 一方、設計や運用について調べるときには、こんな場面もあります。 知りたい内容は説明できるが、資料で使われている用語が分からない 質問と資料とで表現が違い、文字列の一致だけでは見つけにくい リポジトリ外の設計 Wiki、PDF、Office 文書もまとめて調べたい そこで、用語による全文検索と意味検索を組み合わせ、文書形式をまたいで必要な箇所を探せるローカル知識ベースを作
意味で碑を探し、場所と災害種別で避難場所を絞る。その検索を支えるES|QLツール、現在地の受け渡し、トレースで分かったことを紹介します。 目次 はじめに 先に用語を 5 つだけ 全体の流れ 3 つのツール 3 つに共通する型(ツール 3 で見る):現在地を点にして、距離を測り、半径で絞る ツール 2(避難場所):災害種別で厳密に絞る ツール 1(意味検索):伝承内容が近い碑を「候補」として出す つまずきやすい所:任意の空欄には既定値が要る 意味検索(semantic)をどこで、どう使っているか データを入れる側:説明文を 2 つの形で保存する 検索する側:MATCH で「意味が近い順」に候補を出す 意味検索が「しないこと」 候補は意味の点数順、回答は距離順 エージェントのルール(指示文) 現在地の扱い 聞き返しのルール ツールの選び方 結果の見せ方 安全についての言い方と、やらないこと 現在地をどう渡しているか なぜ Chrome 拡張なのか 流れ 現在地は保存されるのか 7 分で何が消えるのか 地図に現在地を描くには PoC の限界 時間はどこで使われているか 限界と、今後やること まとめ 出典・データの加工 技術資料 はじめに 第1回 では、防災のAIエージェントを例に、意味検索で見つけた碑の災害種別を次の避難場所検索に使う「属性の引き継ぎ」を紹介しました。今回は、その仕組みを次の順番で見ていきます。 エージェントが持っている 3 つのツールは、何をしているのか 意味検索とその点数を、どこまで使うのか エージェントは、どんなルールで動いているのか 検索の中心になる現在地を、どうやって安全に渡しているのか コードは検索の流れを追うために載せています。構文を覚えるより、Elasticsearchが確かめる条件と、LLMが判断する部分に注目してください。検証環境はElastic Cloud Serverless(Kibana 9.6.0、2026年9月確認)です。 先に用語を 5 つだけ 用語 一言でいうと Elasticsearch 大量のデータを入れて、速く検索するためのデータベース。全文検索と地理検索が得意 インデックス Elasticsearch に登録したデータのまとまり。今回は伝承碑2,469件と避難場所115,829件を使う フィールドと型 表の「列」と、その列に入るデータの種類。文字( keyword )、文章( text )、位置( geo_point )、意味( semantic_text )など ES|QL Elasticsearch の問い合わせ言語。 FROM 表 | WHERE 条件 | SORT 並び順 のように、縦棒(パイプ)で処理をつなげる Agent Builder Kibana の中で AI エージェントを作る機能。エージェントに「ツール」を持たせ、日本語の質問に答えさせる 全体の流れ 利用者の質問(日本語) ↓ エージェント(LLM: Anthropic Claude Sonnet 5、Elastic 経由) ├─ ① 会話に添付された現在地を読む(attachments.read)。期限を確かめる ├─ ② 質問に足りないもの(半径、災害種別など)があれば聞き返す ├─ ③ 3 つのツールから選び、空欄(引数)を埋めて呼ぶ ↓ ES|QL ツール(Elasticsearch が距離・種別・意味を計算) ↓ エージェント ├─ ④ 結果を選び、並べ、表にする └─ ⑤ 注意書きを添えて回答する 距離と登録値の照合はElasticsearchが行います。LLMは質問を解釈してツールと引数を選び、返った説明文を読んで回答を組み立てます。つまり、LLMにも判断する仕事があります。 決めること 担当 理由 現在地からの距離、半径の中か外か Elasticsearch( ST_DISTANCE 、 WHERE ) 指定した座標と条件に基づいて計算する 災害種別の一致 Elasticsearch( MV_CONTAINS ) 登録済みの種別と、渡された値が一致するか確認する 碑文の意味が質問に近いか(候補出し) Elasticsearch( semantic_text + MATCH ) 2,469 件から候補を選ぶのは検索エンジンの仕事 候補のうち質問に合いそうな碑、次に使う災害種別 LLM 質問と説明文、登録された種別を読んで選ぶ。判断が常に正しいとは限らない 質問の理解、聞き返し、回答文 LLM 自然な日本語のやり取り 指定のない半径や災害種別 LLMに推測させない 必要な値が無ければ聞き返す。ただし、碑に複数の登録種別がある場合は、LLMが質問と合う登録値を選ぶ 3 つのツール 第1回で紹介した3つの検索ツールは、筆者がES|QLで用意した検索文を実行します。 ?latitude のように ? で始まる名前は、実行時に値を入れる場所(引数)です。LLMはツールと引数を選びますが、保存済みの検索文は書き換えません。 番号は第 1 回と同じです。 番号 ツール 何を探す 必須の空欄 任意の空欄 並び順 返す件数 ツール 1 geoai.search_disaster_lore_by_meaning 伝承内容が質問に近い伝承碑 緯度、経度、半径、検索文 災害種別 意味の近さ順(候補) 最大10 ツール 2 geoai.search_emergency_shelters 指定した災害に対応する避難場所 緯度、経度、災害種別 半径 近い順 最大10 ツール 3 geoai.search_disaster_lore 伝承碑 緯度、経度 半径 近い順 最大10 このほかに、エージェントは attachments.read というツールも使います。会話に添付された現在地を読むためのツールで、Agent Builder に最初から入っています。筆者が書いたツールではないので、この 3 つには数えていません。 説明は、一番簡単なツール 3 から始めます。 3 つに共通する型(ツール 3 で見る):現在地を点にして、距離を測り、半径で絞る ツール3(伝承碑を近い順に探す)のES|QL本文です。Agent Builderで使うには、このほかに引数の型と既定値の設定が必要です。 FROM geoai-disaster-lore-v1 | EVAL search_point = TO_GEOPOINT(CONCAT("POINT(", TO_STRING(?longitude), " ", TO_STRING(?latitude), ")")) | EVAL distance_m = ST_DISTANCE(location, search_point) | WHERE (?radius_km <= 0 OR distance_m <= ?radius_km * 1000) | EVAL distance_km = ROUND(distance_m / 1000.0, 3) | SORT distance_m ASC | EVAL monument_lat = ROUND(ST_Y(location), 5), monument_lon = ROUND(ST_X(location), 5) | KEEP name, address, disaster_types, disaster_name, description, distance_km, monument_lat, monument_lon | LIMIT 10 行 何をしているか FROM geoai-disaster-lore-v1 伝承碑の表から始める EVAL search_point = TO_GEOPOINT(...) EVAL は「新しい列を作る」命令。空欄の ?longitude ?latitude (現在地)を、地図上の 1 点に変える EVAL distance_m = ST_DISTANCE(...) 碑の位置( location 列、 geo_point 型)と現在地の距離をメートルで出す。地球の丸みを考えた直線距離 WHERE (?radius_km <= 0 OR ...) 半径の外を捨てる。半径の空欄が省略されたとき(0)は絞らない EVAL distance_km = ROUND(...) メートルを km に直す SORT distance_m ASC 近い順に並べる EVAL monument_lat = ... 碑の緯度・経度も返します。ただし、このPoCでは検索の中心を利用者の現在地に限定しており、碑の座標を次の検索の中心には使いません。 KEEP ... / LIMIT 10 返す列を選び、10 件に絞る 半径を省略できるようにしたのは、「一番近い碑は?」のような質問のためです。一番近いものを答えるのに、半径は要りません。 Agent Builderのツール作成画面 ツール 2(避難場所):災害種別で厳密に絞る 距離を計算する流れに、災害種別の一致条件を加えます。次はツール2の条件部分です。 | WHERE MV_CONTAINS(disaster_types, ?disaster_type) AND (?radius_km <= 0 OR distance_m <= ?radius_km * 1000) 避難場所のデータには、1 施設に「内水氾濫、土砂災害、洪水」のように複数の災害種別が付いています。Elasticsearch では 1 つの列に複数の値を入れられ、これを マルチバリュー と呼びます。 MV_CONTAINS は「その列に、この値とまったく同じものが入っているか」を見る関数です。 災害種別の引数は必須です。避難場所だけを求める質問で、どの災害に対応する場所か分からなければ聞き返します。第1回の連鎖検索では、先に見つけた碑に登録された災害種別から、質問に合う値をLLMが選んで渡しました。条件が一致するかの判定はElasticsearchが行います。 Agent Builderのツール作成画面 ツール 1(意味検索):伝承内容が近い碑を「候補」として出す FROM geoai-disaster-lore-v1 METADATA _score | WHERE MATCH(description_semantic, ?query) AND (?disaster_type IS NULL OR ?disaster_type == "" OR MV_CONTAINS(disaster_types, ?disaster_type)) | EVAL search_point = TO_GEOPOINT(CONCAT("POINT(", TO_STRING(?longitude), " ", TO_STRING(?latitude), ")")) | EVAL distance_m = ST_DISTANCE(location, search_point) | WHERE distance_m <= ?radius_km * 1000 | EVAL distance_km = ROUND(distance_m / 1000.0, 3) | EVAL relevance = ROUND(_score, 3) | SORT _score DESC | EVAL monument_lat = ROUND(ST_Y(location), 5), monument_lon = ROUND(ST_X(location), 5) | KEEP name, address, disaster_types, disaster_name, description, distance_km, monument_lat, monument_lon, relevance | LIMIT 10 ツール1のES|QL本文です。Agent Builderの引数設定は別に必要です。ツール3との主な違いは次の4か所です。 METADATA _score :検索の「点数」を列として使えるようにする。 _score は「どれくらい一致したか」を表す数字です。 WHERE MATCH(description_semantic, ?query) :これが意味検索です。 ?query には利用者の言葉をなるべくそのまま入れます。 AND (?disaster_type IS NULL OR ?disaster_type == "" OR ...) :災害種別を指定したときだけ、その登録値で絞ります。省略時は災害種別で絞りません。 SORT _score DESC :意味が近い順に、最大 10 件の 候補 を返します。 このツールで0行だった場合でも、「地域内に該当する碑が登録されていない」とは断定できません。意味検索を使わず、災害種別と距離だけで数えた結果とは区別します。この違いは第1回の5-6で扱いました。 また、 距離順に並べるのは、意味検索で選ばれた最大10件の中だけ です。半径内の全碑から「質問に合うものを距離順で10件」探す処理ではありません。候補から漏れた碑は、LLMも評価できません。 つまずきやすい所:任意の空欄には既定値が要る Agent Builder の ES|QL ツールでは、空欄に「任意」の印を付けられます。任意の空欄に既定値(省略されたときに入る値)が要るかどうかは、ツールの作り方で違います。 公式資料の説明 :Kibana の画面(UI)で任意の空欄を作るときは、既定値が必須です。API で作るときは既定値がなくてもよく、省略された空欄は null(値なし)になります。ただし null はクエリの失敗につながりやすいので、既定値を付けることが強く勧められています。 今回の実測 :今回のツール設定では、既定値を与えずに空欄を省略すると、実行時に Unknown query parameter [disaster_type] (知らない引数)というエラーになりました。 そこで、任意の文字引数には空文字 "" 、任意の数値引数には 0 を既定値にしました。 ?disaster_type が空なら種別で絞らず、ツール2・3では ?radius_km が0なら半径で絞りません。ツール1では半径は必須です。設定の詳細は ElasticのES|QLツールの資料 にもあります。 意味検索(semantic)をどこで、どう使っているか データを入れる側:説明文を 2 つの形で保存する 伝承碑の説明文は、1 つのインデックスの中に 2 つのフィールドとして入っています。 フィールド 型 何のため description text (日本語の形態素解析 kuromoji) 言葉で探す。「堤防」という単語が入っている碑 description_semantic semantic_text 意味で探す。「堤防が壊れた記録」に近い内容の碑 形態素解析 は、日本語の文章を単語に切る処理です。日本語は単語の間に空白がないので、「堤防が壊れた」を「堤防 / が / 壊れ / た」と切る必要があります。kuromoji はそのための道具です。 semantic_text は、文章を検索用の数値表現(ベクトル)に変換して扱うフィールド型です。今回のインデックスではElastic Inference Serviceの .jina-embeddings-v5-text-small を指定しました。日本語を含む多言語に対応し、既定では1,024次元のベクトルを作ります。モデルを自分のサーバーに配置せずに使える構成です。詳しくは Elasticの設定例 を参照してください。 検索する側:MATCH で「意味が近い順」に候補を出す MATCH(description_semantic, "川の堤防が切れて町が水につかった記録") と書くと、Elasticsearch は質問文も同じモデルでベクトルにして、意味が近い碑を探します。 筆者が川口市内から聞いた例です(距離は筆者の現在地からの値を、1km 単位に丸めています)。 碑 説明文の一部 距離 瓦曽根溜井(かわらぞねためい)防水記念碑(越谷市) 明治 23 年、行田市で利根川の堤防が決壊し、越谷市域は大洪水に見舞われた 約 8km 倉松落大口逆除之碑(春日部市) 明治 23 年、利根川の堤防決壊と古利根川の氾濫の記録 約 17km 水害伝承碑(川島町) 輪中の里、川島町は度重なる大水害に見舞われた 約 29km 「堤防が切れた」「町が水につかった」という日常の言い方から、「決壊」「氾濫」と書かれた碑を探せました。言葉が違っても関連する候補を見つけられるのが意味検索の利点です。 意味検索が「しないこと」 ここでいう意味検索はElasticsearchが質問文と碑の説明文を比べ、内容が近いものを候補として返す処理です。そのあとに、LLMが候補の説明文を読んで、質問に合うかを判断します。 例えば「川の堤防が切れて町が水につかった記録」を探したとき、半径10km内に登録されている碑は6件でした。この6件が意味検索の候補に入り、そのうち4件は災害種別が「地震」の碑でした。今回は災害種別で絞っていないため、地震の碑も候補から除かれません。 候補に出たことと、質問に合うことは別です。 エージェントは、意味検索で返った最大10件をLLMに読ませて、回答に使う碑を選びます。その10件に合う碑がなくても、地域内のすべての碑に該当するものがないとは言えません。「地域内の登録が0件か」を確かめたいときは、意味検索とは別に、災害種別と距離の条件で登録件数を数えます。 候補は意味の点数順、回答は距離順 r elevance は、Elasticsearchの検索スコア _score を小数点以下3桁に丸めて返す値です。質問文と碑の説明文がどれくらい似ているかを表す検索の点数です。ツール1では、Elasticsearchの _score をこの名前で返しています。 使う場所は、検索と回答で違います。 段階 何をするか 候補を探す SORT _score DESC で意味が近い順に並べ、最大10件をツールから返す。ここではrelevanceを使う 回答を作る LLMが候補の説明文を読んで質問に合うか判断し、合うと判断した碑を現在地から近い順に表示する。ここではrelevanceで並べない 近くの碑を知りたい質問なので、回答では距離を優先しました。ただし、 距離順に並べられるのは、意味検索が返した最大10件だけ です。候補に入らなかった碑が、後から距離順で追加されるわけではありません。また、relevanceは正答率や安全性を表す点数ではなく、一定以上なら必ず質問に合うという基準にも使っていません。 エージェントのルール(指示文) エージェントの動き方のルールは「指示文」に書きます。これはエージェントを作るときに設定する文章で、LLMはツールの説明と一緒に読みます。実際の動きが常に指示どおりになるとは限らないため、検索結果も確認します。 現在地の扱い 現在地は、会話の非表示の添付 geoai-current-location からだけ取る。 添付に緯度、経度、取得時刻、有効期限がそろっていて、期限内のときだけ使う。 2 回目以降の質問でも、検索の前に 毎回 添付を読み直して期限を確かめる。期限切れなら検索しない。 利用者に緯度・経度の入力を求めない。緯度・経度を回答に表示しない。 ここまで書くのは、LLM が親切だからです。放っておくと「緯度と経度を教えてください」と聞いたり、前の質問で読んだ座標を期限切れでも使い続けたりします。それを止めるルールです。 聞き返しのルール 検索 必須 足りないとき 避難場所 災害種別。半径は「範囲を求める質問」のときだけ 種別が無ければ検索せず、聞き返す 伝承碑(近い順) 半径は「範囲を求める質問」のときだけ 範囲を求めているのに半径が無ければ聞き返す 伝承碑(意味) 半径と検索文 検索せず、聞き返す 基本は「半径を推測しない。既定値で補わない」です。ただし「一番近い」「最寄り」のような質問には、半径そのものが要りません。そのときは聞き返さず、半径で絞らずに近い順で検索し、「半径の指定なしで、現在地から近い順に返しました」と一言添えます。 たとえば、筆者が川口市内から「最も近い自然災害伝承碑を教えて」と聞くと、聞き返しなしで「八丁の水神社再建記念碑」(地震、約 4km)が返りました(14 秒)。「推測しない」と「不要なものを聞かない」は両立できます。 ツールの選び方 質問に「碑の内容・教訓・テーマ」の指定がある? ├─ ある → ツール 1(意味検索) │ 災害種別がはっきり言われていれば、種別の空欄にも入れる └─ ない → 「一番近い」「周辺の一覧」→ ツール 3(近い順) 避難場所の質問 → ツール 2(避難場所) 碑と避難場所の両方を求められた → ツール 1 か 3 → ツール 2(第 1 回の「属性の引き継ぎ」) ツール 1 に渡す検索文は、利用者の言葉をなるべくそのまま使います。「川の堤防が切れて町が水につかった記録」を「洪水」に置き換えてはいけない、と書いてあります。「洪水」という一語に置き換えても意味検索ですが、「堤防が切れた」「町が水につかった」という具体的な条件が失われ、探したい内容が広がってしまうからです。 結果の見せ方 ツール 1 の結果について、LLM に 3 つの仕事をさせています。 選ぶ。 最大10件の候補の説明文を質問と照らし、「内容が一致する碑」と「参考」に分ける。一致が0件なら「今回返った候補には一致する碑がない」と言う。地域内の全碑について0件とは言わない。 並べる。 どちらも現在地から近い順。relevance では並べない。 見せる。 一致する碑は表に、参考の碑は 1 行ずつ。 説明文の要約にもルールがあります。説明文にある事実だけを短くし、数字・年・地名は原文のままにします。一致した1件目は原文を短く引用して、LLMが「それらしい教訓」を付け足していないか読者が確かめられるようにします。 安全についての言い方と、やらないこと この PoC を安全判定システムとして扱わない。 距離は道路距離でも徒歩距離でもなく、直線距離だと書く。 避難場所の開設状況、収容の余力、経路の安全は分からないと書く。 伝承碑データは全国の災害を網羅していない。0 件でも「災害がなかった」「今は安全」と言わない。 住所から座標への変換、登山情報、現在の危険度や避難が必要かどうかの判定はしない。 たとえば「神戸の市役所に一番近い碑を教えて」と聞くと、エージェントは「任意の住所を中心にした検索はできません」と断ります。これは、指示文でわざとそうしている動きです(理由は第 1 回の 1 節)。 現在地をどう渡しているか ここからは、検索の中心になる現在地の話です。作り方の細かい話は省き、設計の考え方だけを書きます。 なぜ Chrome 拡張なのか Kibana の Agent Builder のチャット画面には、「現在地を使う」ボタンがありません。位置を渡す方法は、大きく 3 つ考えられます。 方法 このPoCでの判断 利用者が緯度・経度を打ち込む 現実的ではない。打ち間違えると、別の場所を検索してしまう 住所を打ち込み、座標に変える ジオコーディングは今回入れていない(第 1 回の 1 節) ブラウザに現在地を聞く 利用者が許可したときだけ取得できるため、今回採用した 小さなChrome拡張を作り、利用者がボタンを押したときだけ、ブラウザのGeolocation APIで現在地を取得しました。拡張はKibanaの画面を読み書きせず、独立した小さな窓で動きます。 拡張機能の画面 流れ [利用者] ボタンを押す ↓ [Chrome 拡張] ブラウザから緯度・経度を取る ↓ 緯度・経度・取得時刻の 3 つだけ送る [自分のPCで動くローカルサーバー](ElasticのAPIキーを保持) ├─ Agent Builder APIで新しい会話を作り、現在地を非表示で添付する ├─ 地図用の一時インデックスに現在地を1件書く ├─ 7分後に会話と地図用の現在地の削除を試みる(サーバー稼働中のみ) └─ 会話のURLを拡張へ返す [Chrome 拡張] 新しいタブでその会話を開く ↓ [利用者] 日本語で質問する → エージェントが添付から現在地を読む ポイントは 2 つです。 APIキーを拡張に持たせない。 ブラウザで動く拡張にキーを入れず、ローカルサーバーだけが保持します。 現在地を添付した新しい会話を開く。 拡張は既存のKibana画面を書き換えません。新しい会話を作って位置を添付し、そのURLを開きます。 現在地は保存されるのか 現在地は保存されます。保存先ごとの扱いを分けて書きます。 場所 現在地は入るか Agent Builderの会話(非表示の添付とツール呼び出しの記録) 入る。7分後、サーバーが動いていれば会話の削除を試みる 地図用の一時インデックス map-session-current-location-v1 現在地1件を保存する。会話と同じタイマーで削除を試みる 伝承碑・避難場所のインデックス( geoai-* ) 入らない Agent Builder の動作記録(トレース) 入らないとは言えない(下で説明) 自分のPCのサーバー 今回の実装では位置をファイルやログへ書かない。削除タイマーはメモリ上にある トレースについて確かめたのは、データストリーム traces-agent_builder.otel-default を、今回の緯度の値で検索して0件だったことだけです。これは「位置が記録されない」証明にはなりません。 一方で、Kibana のチャット画面では、トレースのスパン(1 つ 1 つの処理)の詳細に、その処理の入力と出力が表示されることがあります。ツールの空欄に入れた緯度・経度や、 attachments.read が読んだ添付の中身が、スパンの詳細に記録されるかどうかは、確かめていません。記録の設定によって変わる可能性もあります。そのため、この記事では「トレースに位置は残らない」とは言いません。本番で使うなら、ツールの引数、 attachments.read の入力と出力、トレースの収集設定の 3 つを確認する必要があります。 また、エージェントが位置を読んで引数を選ぶ過程では、LLMにも位置情報が渡ります。このPoCは「現在地がElasticのどこにも残らない」という設計ではありません。 7 分で何が消えるのか 「7 分」は、位置を取得した時刻から数えます。 何が どうなるか 添付の有効期限 期限を過ぎた現在地は検索に使わず、「有効な現在地を取得できません」と答えるよう、エージェントに指示しています 会話そのもの サーバー稼働中は7分後に削除APIを呼ぶ。成功すれば会話の添付と記録も削除される 地図用の現在地 会話とは別の一時インデックスにある。サーバーが削除を試み、地図側でも期限切れを表示しない条件を使う 1 行目は「使わせない」ための期限で、2 行目と 3 行目が「実際に消す」処理です。この 2 つは別のものです。期限が切れても、それだけで会話が消えるわけではありません。実際に消えるのは、タイマーが動き、削除 API の呼び出しが成功したときです。 なぜ7分なのか。位置が古くなっても今の場所として検索しないよう、PoCで決めた時間です。1回の質問と短い聞き直しを想定しています。 7分で自動的に完全消去されるという意味ではありません。 地図に現在地を描くには このPoCのKibana Mapsでは、現在地・碑・避難場所を3つのレイヤーで重ねます。碑と避難場所は既存のインデックスから読み込みます。現在地の点を描くために、地図用の一時インデックス( map-session-current-location-v1 )を別に用意しました。伝承碑や避難場所のデータには混ぜません。 中身は現在地 1 件だけ。新しい会話を作るとき、古いものを先に消す。 会話の削除と同じタイマーで、削除を試みる。サーバーを起動したときにも、期限切れを掃除する。 地図のレイヤーにも「有効期限が今より後のものだけ描く」という条件を入れています。削除に失敗しても、地図を再検索・更新した際には、期限切れの位置を表示対象から除外します。 この地図はチャットの回答と自動連動しません。碑と避難場所のレイヤーは、それぞれのインデックスから点を読み込みます。災害種別で表示を絞る場合は、Kibana Mapsの検索欄で筆者が手動で指定します。表示される点は、ツールが返した最大10件とは別です。 地図上の伝承碑と避難場所の点は、国土地理院の各データを加工して筆者が表示したものです(出典は末尾に記載)。 PoC の限界 削除のタイマーはサーバーのメモリにあります。7分前にサーバーを止めたり、削除APIが失敗したりすると、会話が残ります。その場合はAgent Builderの画面かAPIで手動削除が必要です。地図用の現在地は、次にサーバーを起動したときに期限切れを掃除します。ただし、サーバーが止まっている間は削除されません。本番では削除の再試行と成功確認が必要です。 時間はどこで使われているか Kibana のトレース(処理の内訳を時間の流れで見る画面)で、第 1 回の連鎖検索 1 問(堤防決壊の碑 → 洪水の避難場所)を見ました。トレースには 6 つのスパン(処理のまとまり)が表示され、トレース全体の長さは 32.0 秒でした。 下の表は、画面に表示された順番のままです。時間も、画面に出た値をそのまま書いています。 順番 画面の表示 中身 時間 LLM への入力 / 出力トークン 1 LLM(chat) LLM の呼び出し 3.2 秒 23,953 / 88 2 TOOL(attachments.read) 現在地の添付を読む 1 ミリ秒未満 ― 3 LLM(chat) LLM の呼び出し 2.4 秒 24,304 / 191 4 TOOL(custom) ツール 1(碑の意味検索) 84 ミリ秒 ― 5 LLM(chat) LLM の呼び出し 25.6 秒 28,454 / 2,165 6 TOOL(custom) ツール 2(避難場所) 画面上で読み取れない ― なお、表の時間を足しても、32.0 秒にはなりません。32.0 秒はトレースの始まりから終わりまでの長さで、各スパンの時間の合計ではありません。スパンどうしが重なったり、間があいたりすることもあります。 この画面からは、分からないこともありました。 5 番目の LLM のあとに、6 番目の避難場所ツールが並んでいます。回答の文章をどの LLM 呼び出しが書いたのかは、この画面からは分かりません。 同じ質問で、チャット画面に表示された回答時間は 51 秒でした。トレースの 32.0 秒との差がどこから来るのかは、確かめていません。 分かったのは、観測できたスパンの中ではLLMのスパンが最長で、ツール1のスパンは84ミリ秒だったことです。この84ミリ秒をElasticsearch単体の実行時間とはみなしません。回答を書いた処理や、画面の51秒との差は見えていないため、待ち時間全体の内訳とは言えません。 観測した3回のLLM呼び出しでは、入力が約2.4万~2.8万トークンでした。入力の内訳を調べていないため、どの文章が時間や費用にどれだけ効いたかは分かりません。次に改善するなら、入力の内訳とツール実行後のスパンも確認します。 限界と、今後やること 項目 今回確認した限界 次に確かめること 回答の待ち時間 連鎖検索のチャット表示は51~69秒。観測したスパンではLLMが最長だが、待ち時間全体の内訳は不明 トレースに出ていない処理、画面表示時間との差、入力の内訳を確認する 候補の漏れとLLMの選別 上位10件以外は読めず、抽象的な質問では一致判定もぶれる 検索評価を増やし、候補数や判断基準を比較する 災害種別の言い換え LLMが指示文の対応表を使って登録済みの種別を選ぶ 言い換え表をデータとして管理する方法を検討する 現在地の削除 期限切れの位置を検索に使わないよう指示しているが、サーバー停止時には会話が残り得る 再試行できる削除処理と成功確認を設ける まとめ このエージェントでは、Elasticsearchが距離と登録種別を照合し、意味が近い碑を候補として返します。LLMは質問を解釈してツールと値を選び、候補の説明文を読んで回答します。 ES|QLのツールは3つ。距離や登録種別の条件は、保存済みの検索文に書きました。 semantic_text と MATCH は、質問に関連する碑の候補を選ぶために使います。回答の距離順は、 選ばれた候補の中での順番 です。 災害種別が必要なときは、登録済みの値を選びます。質問に必要な半径がなければ聞き返します。 7分を過ぎた位置を検索に使わないよう指示し、サーバー稼働中に削除を試みます。 地理情報をAIにつなぐと、自然な質問から場所に結び付いたデータを探せます。その答えを評価するには、 検索条件、候補の範囲、LLMの判断、処理の記録 をそれぞれ見えるようにする必要があります。このPoCは、その作り方と残る課題を示しました。 出典・データの加工 出典:国土地理院「 自然災害伝承碑データの提供について 」(2026年8月27日版を2026年9月5日に取得) 出典:国土地理院「 指定緊急避難場所データ 」(2026年9月5日に取得) 本記事の検索結果と地図は、上記データをもとに筆者が災害種別の整理、Elasticsearchへの登録、検索、可視化を行って作成しました。国土地理院が本記事の検索結果や地図を作成したものではありません。 指定緊急避難場所は災害種別ごとに指定されます。公開データには更新前の情報や未掲載の施設がある場合があります。実際の避難には自治体などの最新情報を確認してください。 技術資料 Elastic「 ES|QL tools in Elastic Agent Builder 」 Elastic「 Semantic text field type reference 」 Elastic「 ES|QL MATCH function 」 The post GeoAIエージェントの中身【第2回】ES|QLツールと現在地の渡し方 first appeared on Elastic Portal .
はじめに こんにちは、WEAR開発部バックエンドブロックの小山です。普段は弊社サービスである WEAR のバックエンド開発を担当しています。 WEARの投稿検索(フリーワード検索)では、 OpenSearch を検索基盤として利用しています。従来の検索方法では、ユーザーが入力するクエリの表記ゆれや誤字・略称により、適切な検索結果を返せないケースがありました。この課題を解決するため、全文検索とベクトル検索を組み合わせたハイブリッド検索を導入しました。しかし、導入の過程ではストレージの肥大化やレイテンシの増加、検索精度のチューニングといった技術的課題に直面しました。本記事では、これらの課題と解決に至るまでの取り組みをご紹介します。 なお、ハイブリッド検索の導入にあたって行った検索インデクサーの刷新(ベクトルデータのOpenSearchへの連携基盤)については、以下の記事で紹介しています。本記事では、検索処理を担うアプリケーション側で直面した課題と解決策に焦点を当てます。 techblog.zozo.com 目次 はじめに 目次 背景・課題 従来のフリーワード検索の限界 ハイブリッド検索とは hybrid queryの構造 normalization-processorによるスコアの統合 ベクトル検索の導入で直面した課題 1. ストレージの肥大化 2. レイテンシの問題 3. 検索精度の課題 課題を解決したアプローチ 1. ベクトルデータの量子化と_source最適化によるストレージ削減 スカラー量子化(fp16)の適用 derived_sourceによる_sourceからのベクトル除外 施策の効果 2. warmup APIの定期実行による暖気機構の導入 スパイクの原因はコールドスタート warmup APIを実行するだけでは解決しなかった 導入した暖気機構 3. スコアの組み合わせ比率の最適化 search pipelineの設定 全文検索の重みを大きくした理由 精度検証の進め方 スコア閾値による関連性の低い結果の除外 あいまい検索を実行する条件の制御 効果 今後の改善 まとめ 背景・課題 従来のフリーワード検索の限界 WEARの投稿検索では、ユーザーが入力したクエリをルールベースでDBのレコードと突き合わせ、性別・カラー・カテゴリー・ブランド・タグなどの検索条件に変換して検索結果を返しています。しかし、この方式だけでは以下のようなケースで適切な検索結果を返せませんでした。 表記ゆれ:「Tシャツ」「Tシャツ」「ティーシャツ」のように同じ意味でも表記が異なるクエリ 誤字・略称:ルールベースのマッピングではカバーしきれない入力 意味的な類似検索:クエリと字面は異なるが意味的に近い投稿をヒットさせられない たとえば「平成レトロ」で検索した場合、従来は「#平成レトロ」というタグが付いた投稿しかヒットしませんでした。「夏フェス 野外」のような複数キーワードの検索は、両方のタグに完全一致する投稿しか返せず、ほとんどヒットしない状態でした。 これらのケースをカバーするには、タグの完全一致とは別の検索手段が必要でした。一般的に、キーワードが厳密に一致しない検索には次のような手法があります。 全文検索:タイトルや説明文などのテキストを対象に、キーワードと字面が一致する投稿を広く拾える ベクトル検索:キーワードの字面ではなく「意味」の近さで検索でき、別表記や言い換えにも対応できる WEARの課題は、「夏フェス 野外」のように字面の部分一致で解決できるケースと、「ティーシャツ」のように字面が異なるケースの両方にまたがっています。そのため、どちらか一方ではなく両者を組み合わせるハイブリッド検索の導入を決定しました。 なお、ハイブリッド検索は従来の検索を置き換えるものではありません。ルールベースのマッピングには、クエリの意図を性別やカラーなどの構造化された検索条件へ正確に落とし込めるという強みがあります。そのため従来のマッピングの機構は残したまま、タグ検索のヒット件数が少ない場合に検索結果の量を補助する位置づけでハイブリッド検索を適用しています(適用条件の詳細は後述します)。WEARでは、このハイブリッド検索を利用した検索機能を「あいまい検索」と呼んでいます。 ハイブリッド検索とは ハイブリッド検索とは、キーワードマッチによる全文検索と、意味的な類似度で検索するベクトル検索を組み合わせ、双方のスコアを統合して検索結果を返す検索手法のことです。OpenSearchでは、バージョン2.10で導入された hybrid query と normalization-processor を利用することで実現できます。 それぞれの検索手法には以下のような長所・短所があり、互いに弱点を補完し合う関係にあります。 検索手法 長所 短所 全文検索(BM25) キーワードの完全一致に強い、高速 同義語・言い換えに弱い、意味を理解しない ベクトル検索(k-NN) 意味的な類似性を捉えられる、表記ゆれ・言い換えに強い 計算コストが高い、キーワードの完全一致に弱い hybrid queryの構造 ハイブリッド検索のクエリは、hybrid queryの中に全文検索クエリとベクトル検索クエリを並べた構造になります。WEARでは、全文検索にはmulti_matchクエリを、ベクトル検索には検索キーワードをOpenSearch側でベクトル化して検索するneuralクエリを利用しています。 { " query ": { " hybrid ": { " queries ": [ { " multi_match ": { " query ": " 夏フェス 野外 ", " fields ": [ " title.sudachi ", " content.sudachi ", " ... " ] } } , { " neural ": { " text_embedding ": { " query_text ": " 夏フェス 野外 ", " model_id ": " <モデルID> " } } } ] } } } なお、フィールド名の .sudachi は形態素解析器(Analyzer)を表しています。全文検索における日本語の形態素解析にはSudachiを利用しており、kuromojiで解析したフィールドも併用できる構成にしています。 normalization-processorによるスコアの統合 ここで問題になるのが、2つの検索のスコアを単純に比較できないことです。BM25のスコアには上限がなく、マッチした単語数や出現頻度によって数十以上の値も取る一方、ベクトル検索の類似度スコアは0〜2程度の狭い範囲に収まります。そのまま足し合わせると、値の大きい全文検索のスコアに結果が引きずられてしまいます。 normalization-processorは、このようにスケールの異なるスコアを揃えて統合するための仕組みです。search pipelineに設定すると、検索結果へ次の2段階の処理を適用します。 正規化(normalization):各クエリのスコアを同じ土俵で比較できるように変換する。min_max方式では (スコア - 最小値) / (最大値 - 最小値) で0〜1の範囲に正規化する 結合(combination):正規化したスコアを重み付きで結合し、最終スコアを算出する なお、WEARの検索では、ハイブリッド検索のスコアを並び順に使っていません。スコアは、関連性の低い投稿を除外する足切りに使っています。足切りを通過した投稿は、人気順スコアで並べ替えて返します。関連度だけで並べると、検索結果に含まれる投稿の質がばらつきやすいためです。関連度は足切りで担保し、その上で人気の高い投稿から表示して検索結果の質を保つ設計です。 検索リクエストからレスポンスまでの流れは以下のとおりです。 ベクトル検索の導入で直面した課題 ハイブリッド検索を実現するためにベクトル検索を導入したところ、リリース前に解消すべき以下の技術的課題が発生しました。 1. ストレージの肥大化 投稿ごとに1024次元の埋め込みベクトルを保持するため、インデックスのストレージサイズが大幅に増加しました。ベクトルの各要素を32ビットの浮動小数点(fp32、4バイト)で保持すると、1投稿あたり約4KB(1024次元 × 4バイト)のベクトルデータが追加されます。対象の投稿は膨大なため、全量を投入するとベクトルデータだけで数十GB規模の追加になる見積もりでした。実際に、一部の投稿データで構築した検証用インデックスを実測すると、fp32のベクトルを含むサイズは13.2GBに達していました。 ベクトルデータの増加は、ストレージコストだけの問題ではありません。後述のとおり、k-NN検索ではHNSWグラフを検索時にメモリへロードしておく必要があるため、ベクトルデータが増えるほど必要なメモリ容量も増えます。ディスクは増設で対応できますが、メモリの確保はインスタンスの増強が必要になり、コストへの影響が大きくなります。 2. レイテンシの問題 ベクトル検索(k-NN検索)では、近いベクトル同士をつないだグラフ構造(HNSW)をインデックスとして構築し、検索時にそれをたどることで高速な近傍探索を実現しています。このグラフをメモリにロードしていない状態で検索すると、レイテンシが大きく悪化する問題を抱えていました。実際に、通常時は200ms程度で応答する検索が、不定期に20秒程度まで悪化するスパイクを起こしていました。 3. 検索精度の課題 ハイブリッド検索を導入しただけでは期待する検索精度が得られず、全文検索とベクトル検索のスコアをどのような比率で組み合わせるかのチューニングが必要でした。 たとえば、ベクトル検索の重みを大きくしすぎると、ブランド名で検索した際に意味的に近い別ブランドの投稿が混入してしまいます。逆に全文検索の重みを大きくしすぎると、表記ゆれや言い換えをカバーするというベクトル検索導入の狙いが薄れてしまいます。 課題を解決したアプローチ 1. ベクトルデータの量子化と_source最適化によるストレージ削減 ストレージの肥大化に対しては、ベクトルデータの量子化(fp16化)と、 _source フィールドからのベクトルデータ除外(derived_source)の2つの施策で対応しました。 スカラー量子化(fp16)の適用 量子化手法にはFaissエンジンのスカラー量子化(SQfp16)を採用しました。ベクトルの各要素をfp32から16ビットの浮動小数点(fp16)に変換して保持する手法で、ベクトルデータのサイズを50%削減できます。プロダクト量子化やバイナリ量子化ほどの圧縮率はないものの、精度への影響が小さく、事前学習も不要でマッピングの encoder 設定を追加するだけで導入できます。精度と導入の手軽さのバランスを重視して、この手法を選びました。 knn_vector フィールドの定義は以下のとおりです。 { " text_embedding ": { " type ": " knn_vector ", " dimension ": 1024 , " method ": { " name ": " hnsw ", " space_type ": " innerproduct ", " engine ": " faiss ", " parameters ": { " ef_construction ": 128 , " m ": 16 , " encoder ": { " name ": " sq ", " parameters ": { " type ": " fp16 " } } } } } } ただし、fp16化のみを適用した場合、インデックスサイズは13.2GBから12.2GBへと7.6%の削減にとどまりました。k-NN検索用のデータ構造は半減しても、 _source フィールドに元のベクトル値がそのまま保存されているためです。 derived_sourceによる_sourceからのベクトル除外 そこで、OpenSearch 2.19で実験的機能(experimental)として導入された knn.derived_source を有効化しました。この設定を有効にすると、ベクトルデータが _source に重複して保存されなくなり、必要な場合はk-NN検索用のデータ構造から導出されるようになります。 { " settings ": { " index ": { " knn ": true , " knn.derived_source.enabled ": true } } } なお、derived_sourceの有効化で検索時に20ms程度のオーバーヘッドを確認しましたが、許容範囲と判断しています。 施策の効果 2つの施策によるストレージ削減効果は以下のとおりです(検証用インデックスで実測)。 インデックス設定 ストレージサイズ 削減率 ベースライン(fp32) 13.2GB - fp16のみ 12.2GB 7.6% fp16 + derived_source 5.2GB 60.6% fp16化により検索精度への影響が懸念されたため、fp32のインデックスとfp16のインデックスのそれぞれに同一クエリで検索をかけ、結果を比較して検証しました。上位20件の重複率はおおむね95%以上で、スコアの差も小数点以下6桁目程度の違い(例:1.5960883と1.5960877)にとどまりました。結果が異なる投稿は実際に目視で確認し、検索クエリの意図から外れていないことを確かめた上で採用しています。 なお、この施策は投稿数がもっとも多いコーディネートのインデックスのみに導入しています。データ量の少ないインデックスでは削減効果が限定的なためです。 2. warmup APIの定期実行による暖気機構の導入 レイテンシの問題に対しては、OpenSearchの warmup API を定期実行する暖気機構を導入しました。 スパイクの原因はコールドスタート まず、k-NN統計API( GET /_plugins/_knn/stats )でレイテンシ悪化時の状態を分析しました。すると、メモリ不足によるグラフ破棄(Eviction)は発生しておらず、キャッシュミス(Miss Count)が多発していることがわかりました。つまり、スパイクの原因はメモリ不足ではなく、以下のメカニズムによるコールドスタートでした。 インデックス更新に伴いバックグラウンドでmerge/refreshが発生し、新しいHNSWグラフが作成される 新しいグラフはまだネイティブメモリにロードされていない そこに検索リクエストが到達すると、ディスクからのグラフロードが発生し、完了まで数秒〜数十秒待たされる warmup APIを実行するだけでは解決しなかった コールドスタート対策として、グラフを事前にメモリへロードするwarmup APIをデータ投入後に実行しましたが、それでもスパイクは解消しませんでした。調査の結果、原因はデータ投入とwarmupのタイミングにありました。 処理 同期/非同期 Bulk APIによるデータ投入 同期(ただし検索可能になるのはrefresh後) HNSWグラフ構築 非同期(Bulk完了後もバックグラウンドで継続) flush/merge 非同期(グラフデータのディスク永続化) warmup APIの対象になるのは、flush/mergeが完了しディスクに永続化済みのグラフのみです。HNSWグラフ構築とflush/mergeはBulk APIの完了後も非同期で続くため、データ投入直後にwarmupを実行しても、永続化前のグラフには適用されません。つまり「Bulk APIの完了 ≠ warmup実行可能なタイミング」だったのです。 導入した暖気機構 この調査を踏まえ、インデックスの洗い替え時と日常の差分更新のそれぞれに対して暖気を組み込みました。 フィールドの追加やマッピングの変更の際は、新しいインデックスを作成してデータを入れ直す洗い替え(リインデックス)が必要になります。洗い替えの際は以下の順に処理し、暖気が完了したインデックスだけがユーザーからの検索を受けるようにしています。 新インデックスを作成し、データを投入する refreshを実行し、投入したデータを検索可能にする warmup APIを実行し、HNSWグラフをネイティブメモリにロードする エイリアスを新インデックスに切り替える 一方、日常運用では差分更新によって新しいHNSWグラフが継続的に作られるため、洗い替え時の暖気だけではカバーできません。WEARでは、差分データを10分間隔で投入するワークフローが動いています。このワークフローの最終ステップにwarmup APIの実行を組み込み、差分投入で作られた新しいグラフを毎回暖気しています。ワークフローの流れは以下のとおりです。 差分データを抽出する(BigQuery) データをGCSからS3へ転送する ベクトル化してOpenSearchへ投入する(詳細は前述のインデクサー刷新の記事を参照) 投入完了を確認する 削除済みの投稿をインデックスから除去する refreshを実行し、投入したデータを検索可能にする warmup APIを実行し、新しいHNSWグラフをネイティブメモリにロードする なお、暖気を組み込んでもスパイクを完全には防げません。そのため、あいまい検索専用のOpenSearchクライアントに0.5秒の短いタイムアウトを設定しています。タイムアウトや一時的なエラーが発生した場合は、従来のタグ検索の結果を返すフォールバックを実装しています。あいまい検索が失敗してもユーザーには正常なレスポンスが返る多層的な構えとし、フォールバックの発生は監視基盤に記録して検知できるようにしています。 3. スコアの組み合わせ比率の最適化 検索精度の課題に対しては、ハイブリッド検索の実行結果を確認しながら、全文検索とベクトル検索のスコアを組み合わせる際の最適な重みを探りました。 search pipelineの設定 スコアの正規化と結合は、以下のsearch pipelineで行っています。正規化手法にはmin_maxを、結合手法には重み付き算術平均(arithmetic_mean)を採用し、最終的に全文検索を重視した重みに落ち着きました。 PUT /_search/ pipeline / hybrid - search - pipeline { " description ": " Post processor for hybrid search ", " phase_results_processors ": [ { " normalization-processor ": { " normalization ": { " technique ": " min_max " } , " combination ": { " technique ": " arithmetic_mean ", " parameters ": { " weights ": [ <全文検索の重み>, <ベクトル検索の重み> ] } } } } ] } 全文検索の重みを大きくした理由 検索方式 重み 理由 全文検索 大 ユーザーが入力したキーワードへの一致を重視。ファッション検索ではブランド名・アイテム名などの正確な一致が重要なため ベクトル検索 小 セマンティックな関連性を補完。「秋コーデ」に対する「紅葉」「ニット」のような関連概念の発見を支援するため 前述のとおり、ベクトル検索の重みを大きくしすぎるとキーワードと一致しない投稿が混入します。ファッションの検索ではブランド名やアイテム名の正確な一致が重要なため、キーワード一致を主、意味的な類似を従と位置づけてこの比率を選びました。 精度検証の進め方 精度検証は、nDCGのような定量指標による自動評価ではなく、目視による定性評価で進めました。評価用のクエリを用意し、検索結果をスプレッドシートへ並べて、キーワードとの関連性や表示順の違和感をチームで確認していきました。 調整は以下の手順で進めました。 ベクトル検索単体の足切りスコア(min_score)を見極める 全文検索の単体での足切りスコアを決める 重みづけを設定した上でハイブリッド検索を試し、ハイブリッド検索としての足切りスコアを決める その結果を定性評価する 実際に調整を進める中で、ベクトル検索・全文検索の単体では良い結果が得られているのに、ハイブリッド検索として組み合わせると期待どおりの結果にならないケースがありました。このようなケースを1件ずつ確認しながら、重みは固定したまま、足切りスコアの調整で精度を追い込んでいきました。 スコア閾値による関連性の低い結果の除外 重みの調整とあわせて、全文検索・ベクトル検索のそれぞれにスコアの下限値(min_score)を設定しています。ベクトル検索はどんなクエリに対しても「もっとも近い」ドキュメントを返してしまうため、閾値を設けないと関連性の低い投稿が検索結果に含まれてしまいます。閾値は、複数の評価用クエリで検索結果を定性評価し、クエリごとに求めた「関連性の低い結果を除外できるスコア」の平均値として決定しました。 なお、スコアの分布は固定ではない点に注意が必要です。実際、リリース後にOpenSearchをバージョンアップした際、全文検索のスコアが全体的に低く出るようになり、正常な検索結果が閾値で除外される事象が発生しました。この事象は、検索結果からの遷移率などのKPIモニタリングがきっかけで検知しました。その際は、リリース判定時と同じ検索語句でスコア順TOP1000件の分布を確認し、閾値を引き下げて対応しました。min_scoreを利用する場合は、バージョンアップなどによるスコア分布の変化まで含めた運用を想定しておくことをおすすめします。 あいまい検索を実行する条件の制御 背景で述べたとおり、あいまい検索は従来の検索を補助する位置づけであり、すべてのキーワードに対して実行しているわけではありません。検索キーワードがタグにマッピングされ、タグ検索で一定の件数以上の結果を得られる場合は、タグ検索の結果をそのまま返します。タグに完全一致する検索結果は精度が高く、十分な件数があればあいまい検索で補完する必要がないためです。 効果 これらの取り組みにより、ハイブリッド検索を本番運用できる状態でリリースできました。 ストレージ:fp16量子化とderived_sourceにより、インデックスサイズを13.2GBから5.2GBへ約60%削減しました レイテンシ:暖気機構により、コールドスタートに起因する最大20秒のレイテンシスパイクを大幅に抑制しました。リリース時のあいまい検索のレイテンシはp99平均369msで、事前の負荷試験と同水準の結果でした 検索精度:スコアの重みと閾値の調整により、キーワード一致を保ちつつ、表記ゆれや言い換えを含むクエリでも意図した投稿がヒットするようになりました たとえば「平成レトロ」と検索すると、従来はタグ「#平成レトロ」が付いた投稿しかヒットしませんでした。ハイブリッド検索の導入後は「平成」「平成ギャル」をタグや説明文に含む投稿も返せています。「秋っぽいコーデ」のようなあいまいな言い回しのクエリや「夏フェス 野外」のような複数キーワードのクエリでも、意味の近い投稿をヒットさせられています。 今後の改善 ハイブリッド検索の導入で表記ゆれや言い換えへの対応は進みましたが、運用する中で新たな課題も見えてきています。 部分一致によるノイズ:検索キーワード「白シャツ」に対して、「白Tシャツ」や「黒シャツ」を含む投稿まで高い類似度でヒットしてしまう 複数キーワードの検索意図の解釈:「ロングスカート 低身長」のような検索では、どちらか一方しか含まない投稿もヒットしてしまい、クエリ全体の意図を汲み取れない こうしたノイズを除外するチューニングとあわせて、コーディネート画像の活用も検討しています。社内ではすでに別部署が 画像からの特徴量の抽出・テキスト化 に取り組んでいます。その成果を検索データとして取り込む案と、画像そのものをベクトル化する案の両方が選択肢です。テキスト情報だけでは表現しきれないコーディネートの特徴から検索できるようになれば、あいまい検索の適用範囲をさらに広げられると考えています。 また、全文検索側では辞書の拡充も検討しています。語彙数の多い辞書や独自のユーザー辞書を導入すると、「夏フェス」のような複合語やトレンドから生まれる新語も1語として扱えるようになります。意図しない単語分割が減り、全文検索のノイズの低減にもつながると考えています。 さらに、あいまい検索の手前にあるクエリ理解の改善も検討しています。キーワードから検索条件へのマッピングの精度を高めれば、あいまい検索に頼る前の段階で、より適切な検索結果を返せるようになります。 まとめ 本記事では、WEARのフリーワード検索にハイブリッド検索を導入した際の課題と、その解決のための技術的工夫をご紹介しました。 ベクトル検索導入によるストレージ肥大化には、fp16へのスカラー量子化とderived_sourceによる _source 最適化で対応しました k-NN検索のレイテンシ問題には、リインデックス時の暖気完了後のエイリアス切り替えと、差分更新ワークフローへのwarmup組み込みによる暖気機構で対応しました 検索精度の課題には、全文検索とベクトル検索のスコアを組み合わせる重みとスコア閾値の最適化で対応しました OpenSearchでのハイブリッド検索の運用を検討している方や、精度検証の進め方に悩んでいる方の参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
動画
該当するコンテンツが見つかりませんでした










