168
169

RAGは検索エンジンが命!Azure AI Search初心者入門

Last updated at Posted at 2024-09-05

はじめに

こんにちは! AI エンジニアのヤマゾーです。
近年、生成 AI の進化が目覚ましく、生成 AI を活用したシステムの開発が盛んに行われています。その中で最も有名なテクニックが RAG です。RAG というのは検索拡張生成 (Retrieval Augmented Generation) の略で、質問の関連情報を検索し、質問と関連情報をセットで入力して回答させる技術のことです。
Slide11.JPG

各企業ではこの RAG システムを積極的に導入していますが、ほぼ確実に課題になるのが検索部分の精度です。そして検索精度を上げるためには検索エンジンの知識が必要不可欠です。

本記事では検索エンジンの筆頭サービスである Azure AI Search を題材に、検索エンジンの基本的な仕組みや検索クエリの書き方について初学者向けに解説します。

RAG の検索部分を "Retriever" と呼びますが、この語源はゴールデンレトリバーと同じです。レトリバーとは猟犬のことで、獲物を回収 (retrieve) する役割に由来します。

サマリ

本記事の目次

想定読者

  • AI Search の概念や基本操作について知りたい方
  • 検索エンジンの仕組みや注意点に興味がある方
  • 生成 AI エンジニアを目指している方

1. Azure AI Search の概要

1.1 Azure AI Search とは

Azure AI Search とは Microsoft Azure が提供する検索エンジンサービスです。よく勘違いされがちなのですが、Azure AI Search のメイン機能は AI ではなく検索エンジンの方です。類似サービスとしては Amazon OpenSearch Service や ElasticSearch などが挙げられます。

大量の文書を Azure AI Search 専用のストレージに格納しておくことで、テキスト検索やあいまい検索など様々な手法で情報を取得できるようになります。この検索過程の中に AI との統合機能が豊富に含まれています。念のためフォローしておきますが、この統合機能も Azure AI Search の大きな魅力の一つです。

1.2 検索エンジンの基本用語

Azure AI Search の理解を深めるために、まずは検索エンジンの基本的な用語を SQL と対比させながら簡単に見ていきましょう。一部完全に対応していない部分もありますが、まずは大まかに理解できれば充分です。詳細は第 3 章で解説します。

検索エンジン用語 SQL の対応 説明
インデックス テーブル 検索対象データの保管場所
ドキュメント 行 (レコード) インデックス内の個々のデータ単位
フィールド 列 (カラム) ドキュメント内の個々のデータ項目
スキーマ スキーマ インデックスの構造を定義する設計図
クエリ クエリ データベースに対するデータ検索要求
インデクサー ETL 処理 データの変換・インデックスへ取り込む機能
スコアリング - 検索結果の関連度合いを数値化すること
フィルタリング WHERE クエリ結果を特定の条件で絞り込むこと
ランキング ORDER BY 検索結果を順位付けて並び替えること

各要素のイメージを掴むために、検索エンジンに文書を登録してから実際に検索するまでの最終的な全体像を以下に示します。

Slide1.JPG

ドキュメントの登録は手動でも可能です。具体的な手順については第 3 章のハンズオンで解説します。

1.3 Azure AI Search 固有の用語

Azure AI Search 固有の用語についても簡単に整理しておきます。
実際のプロジェクトで可用性やスケーラビリティなど、リソースを設計するうえで重要になる概念です。本記事では無料で使える範囲の値を設定するので安心してください。

Azure 用語 説明
SKU Azure AI Search の価格レベル。Free や Basic、Standard などがあり、価格レベル表に従い性能やクォータが異なる。
パーティション 物理ストレージ単位。複数のパーティションを使うとインデックスデータが分割して保存され、容量を拡張できる。
レプリカ 並列インスタンス数。各レプリカには全く同じインデックスデータがコピーされ、クエリを負荷分散できる。
検索ユニット (SU) Azure AI Search の最小リソース単位。SU あたりの料金やスペックは価格レベルで異なり、SU の数で課金額が決まる。

例えば Azure AI Search を Basic プランで契約し、パーティション数を 3、レプリカ数を 2 に設定すると以下のようになります。
Slide4.JPG
 
この場合、検索ユニットの数は 3 × 2 = 6 になります。 Basic プランの料金は検索ユニット 1 個につき \$0.14/h なので、最終的な課金額は \$0.84/h になります。 他にも一部の拡張機能を使用すると課金される場合がありますが、基本的に Azure AI Search の課金額はこの検索ユニット数で機械的に決まります。検索ユニットを 10 倍にするとコストも 10 倍になって月額料金が数十万単位で増える場合があるため、実際のプロジェクトでは非機能要件とコストのバランスを見ながら慎重に設計しましょう。

一部の機能は価格レベルによって制限されますが、本記事では割愛します。各価格レベルの詳細な違いについては Azure 公式ドキュメントをご参照ください。

2. 検索エンジンの仕組み

Azure AI Search を使い始める前に、検索エンジンにおける全文検索の仕組みについて解説します。全文検索とは普段私たちがイメージするような、複数のファイルにまたがる文書全体を一度に検索する技術のことです。ファイル名検索や単一ファイル内検索と区別してこのように呼ばれます。

全文検索にはいくつかの方法がありますが、本記事ではその中でも最も一般的である転置インデックスを用いた検索方法について解説します。

2.1 転置インデックスとは

転置インデックスとは、全文検索において高速かつ効率的に文書を検索するためのデータ構造です。仕組みは至ってシンプルで、全ての単語がどの文書に登場するのか丸暗記させているだけのデータです。 なお、文章を単語レベルに分割する解析ツールのことをアナライザーと呼びます。

例えば自己紹介のデータに対して生成AI人間と検索する場合を考えます。
自己紹介文がヒットするまでの全体像は以下のようになります。
Slide3.JPG
 
まず、生成AI人間という検索クエリをアナライザーが生成, AI, 人間と単語レベルに分割します。次に、あらかじめ作成しておいた転置インデックスで生成, AI, 人間の各単語が登場する文書 ID を問い合わせます。その結果、最終的に文書 001003 がヒットします。実際には更に単語の登場率や登場回数などでランク付けを行うケースが殆どですが、基本的な仕組みはこれだけです。文量が膨大になっても各単語のリストを参照するだけなので、高速に検索できるメリットがあります。

一方、ここで注意すべきなのが AI という検索で DAILY という単語はヒットしていない点です。転置インデックスは飽くまで単語レベルのマッチングなので、例えばAIで検索しても文書 002 の DAILY はヒットせず、生成 人間で検索しても文書 004 に書かれた一生成人間食の部分はヒットしません。部分文字列検索とは挙動が全く異なるので、検索処理を設計する際には注意が必要です。

転置インデックスを用いた全文検索はベクトル検索と区別してキーワード検索と呼ばれることもあります。

2.2 アナライザーとは

前節で触れた通り、アナライザーとは文書や検索クエリを単語レベルに分割する解析ツールです。一見地味な存在に思えるかもしれませんが、実はこのアナライザーこそが検索精度を上げるうえで重要な役割を果たします。

例えば、技術ですという検索クエリを考えてみます。
仮に検索クエリをそのまま技術, ですに分解して検索した場合、日本語の文書に含まれるありとあらゆる **ですという表現が大量にヒットしてしまい、本当に検索したい単語が埋もれてしまいます。
このような事態を回避するため、典型的なアナライザーはですのような不要な単語(ストップワードと呼びます)を無視する機能を持っています。

次に、郵便番号フィールドから 123-4567 を検索する場合を考えます。
SQL だと Where 句で簡単に検索できますが、全文検索の場合は注意が必要です。例えばアナライザーがこの検索クエリを 123-, 45, 67 と分解してしまった場合、123-6745 などの郵便番号もヒットしてしまうことになります。
これを回避する一つの策として、郵便番号フィールドの 123-4567 を一単語とみなす方法があります。Azure AI Search の場合、keyword と呼ばれる組込みアナライザーをフィールドに指定することで実現できます。keyword を指定したフィールドはドキュメントの値と検索クエリが一つの単語としてみなされるようになります。電話番号や日付のフィールドなどでも有効です。

その他にもアナライザーの代表的な処理には以下のようなものがあります。

  • 大文字と小文字、全角と半角の統一(例: 生成AI, 生成AI生成ai
  • 活用形の統一 (例: 書か, 書き, 書く, 書け書く)
  • 特殊文字や HTML タグの除去 (例: \<p>エンジニア!?\</p>エンジニア)
  • 同義語辞書による拡張 (例: ねこ, ネコ, cat)

Azure AI Search で日本語を扱う場合、Apache Lucene またはマイクロソフトのアナライザーを使うか、カスタムアナライザーを作成できます。

3. Azure AI Search の基本操作

それでは実際に Azure AI Search (以下 AI Search) でイチから検索エンジンを構築してみましょう。大きく以下の 4 ステップに分けて解説していきます。

Slide1.JPG

3.1 サービスの利用開始

Slide2.JPG

事前準備として、AI Search のサービスを無料プランで作成しましょう。
まず、Azure Portal 上で「AI Search」と検索し、そのまま選択します。
image.png
 
「作成」ボタンを押下し、AI Search サービスを新規作成する画面に移動します。
image.png
 
サービス名やリージョン、価格レベルなどの情報を入力します。
この際、価格レベルが Free になっていることを必ず確認してください。
image.png

無事にデプロイが完了したら準備完了です。
image.png

3.2 インデックスの作成

Slide3.JPG

次に、文書データを登録するためのインデックスを作成します。
先ほど作成した AI Search のサービスを開き、「インデックスの追加」を押下します。インデックスのスキーマは JSON 形式での設定も可能ですが、今回は Web 画面上で設定します。
image.png
 
次に、作成するインデックスのフィールドを定義していきます。
例として飲食店の名前を格納する restaurant_name フィールドを追加します。
image.png

ここで種類とはデータ型のことで、Edm.String は文字列型のデータです。ちなみに先頭の Edm.Entity Data Model の略ですが意識する必要はありません。データ型は他にも整数 Edm.Int32、時刻 Edm.DateTimeOffset、文字列の配列 Collection(Edm.String) などがあります。

また、属性の構成にある各チェック項目については以下の通りです。

  • 取得可能(Retrievable):検索結果としてフィールドの値を取得する機能です。特定のフィールドの情報を表示したり、活用したい場合に使います。
  • フィルター可能(Filterable):検索結果を特定の条件で絞り込む機能です。条件に合致するデータだけを表示したい場合に役立ちます。
  • 並べ替え可能(Sortable):検索結果を特定のフィールドの値で並び替える機能です。例えば、日付順や価格順などで結果を整列させる場合に使用します。
  • Facetable:フィールドの値でグループ化し、集計する機能です。カテゴリ別の集計やフィルタリングに利用され、結果を視覚的に把握しやすくします。
  • 検索可能(Searchable):フィールド内のテキストやデータを検索できる機能です。ユーザーのクエリに一致するフィールドの内容を見つけ出す際に使用します。

各フィールドで使わない項目、特に検索可能のチェックは外しておきましょう。
フィールドの追加が完了したら、インデックス名を入力して各フィールドを最終確認し、「作成」を押下します。これでインデックスの作成は完了です。
image.png

一度作成したインデックスのフィールドは設定を変更できません。新しいフィールドとして作り直すか、インデックスを再作成する必要があります。

3.3 ドキュメントの登録

Slide4.JPG

インデックスを作成した後はドキュメントを登録しましょう。
公式 Quickstart も参考にしながら Python SDK を使用します。

事前に「設定」から「キー」を選択し、プライマリ管理者キーの値をコピーして控えておきます。プログラムを実行する際必要になります。
image.png
 
それでは実際にドキュメントを登録しましょう。
まずは必要なライブラリ群をインストールします。

Python
! pip install azure-search-documents==11.6.0b1 --quiet
! pip install azure-identity --quiet
! pip install python-dotenv --quiet

次に、AI Search のエンドポイント、API キー、インデックス名を定義します。API キーは先ほど控えた管理者キーの値を代入してください。

Python
search_endpoint: str = "https://yamazo-ai-001.search.windows.net"
search_api_key: str = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
index_name: str = "restaurant_index"

インデックスに登録する架空の飲食店データを作成しましょう。
restaurant_name に店名、category にお店の種類、price に平均単価、descriptionに紹介文を入力します。id はドキュメントを識別するユニークな値です。

Python
documents = [
    {
    "@search.action": "upload",
    "id": "001",
    "restaurant_name": "炭火焼鳥 まるや",
    "category": ["焼き鳥", "焼酎", "日本酒"],
    "price": "5000",
    "description": "炭火でじっくり焼き上げた焼鳥と厳選された焼酎・日本酒が楽しめる焼き鳥専門店。和の風情を感じる居心地の良い空間です。",
    }
]

今回は様々な検索処理を検証するためにサンプルデータを 10 件用意します。

Python
documents = [
    {
    "@search.action": "upload",
    "id": "001",
    "restaurant_name": "炭火焼鳥 まるや",
    "category": ["焼き鳥", "焼酎", "日本酒"],
    "price": "5000",
    "description": "炭火でじっくり焼き上げた焼鳥と厳選された焼酎・日本酒が楽しめる焼き鳥専門店。和の風情を感じる居心地の良い空間です。",
    },
    {
    "@search.action": "upload",
    "id": "002",
    "restaurant_name": "トラットリア・グラツィエ",
    "category": ["イタリアン", "レストラン"],
    "price": "4000",
    "description": "本格的なイタリア料理を提供するトラットリア。自家製パスタと石窯で焼き上げるピザが自慢です。ワインも豊富に取り揃えています。",
    },
    {
    "@search.action": "upload",
    "id": "003",
    "restaurant_name": "バル・エル・マール東京",
    "category": ["スペイン料理", "バー"],
    "price": "3000",
    "description": "スペインの伝統的なタパスと共に、おしゃれなバルで楽しいひとときを。カウンター席ではバーテンダーとの会話も楽しめます。",
    },
    {
    "@search.action": "upload",
    "id": "004",
    "restaurant_name": "神楽坂酒場 千鳥",
    "category": ["日本酒", "居酒屋"],
    "price": "6500",
    "description": "豊富な地酒と創作料理が楽しめる神楽坂の隠れ家的居酒屋。居心地の良い和モダンな個室で、ゆったりとしたひとときをお過ごしください。",
    },
    {
    "@search.action": "upload",
    "id": "005",
    "restaurant_name": "東京ベンガルの風",
    "category": ["インド料理", "レストラン"],
    "price": "2500",
    "description": "本場のシェフが手がけるスパイシーで豊かな完全個室のインド料理専門店。香り高いカレーとナンが人気で、ベジタリアンメニューも充実しています。",
    },
    {
    "@search.action": "upload",
    "id": "006",
    "restaurant_name": "創作料理 ふう",
    "category": ["創作料理", "居酒屋", "和食"],
    "price": "8000",
    "description": "季節の素材を生かした創作和食と落ち着いた雰囲気をお楽しみください。",
    },
    {
    "@search.action": "upload",
    "id": "007",
    "restaurant_name": "オイスタークラブ東京",
    "category": ["海鮮", "レストラン"],
    "price": "5000",
    "description": "新鮮な牡蠣をメインにした海鮮レストラン。世界中から取り寄せた厳選されたオイスターが楽しめます。ワインペアリングもお楽しみいただけます。",
    },
    {
    "@search.action": "upload",
    "id": "008",
    "restaurant_name": "パティスリー・ブラン",
    "category": ["デザート", "カフェ"],
    "price": "2000",
    "description": "フランスの伝統的なケーキや焼き菓子を提供するパティスリー。こだわりの素材を使ったスイーツで、ティータイムを特別なひとときに。",
    },
    {
    "@search.action": "upload",
    "id": "009",
    "restaurant_name": "和風ダイニング 花衣",
    "category": ["和食", "居酒屋"],
    "price": "6000",
    "description": "静かな雰囲気で、季節の素材を使った和食を楽しめるダイニング。個室も完備しており、プライベートな時間をお過ごしいただけます。",
    },
    {
    "@search.action": "upload",
    "id": "010",
    "restaurant_name": "寿司処 一心",
    "category": ["寿司", "レストラン"],
    "price": "20000",
    "description": "新鮮な海の幸を厳選したネタで提供する寿司屋。職人の技が光るお寿司と共に、贅沢なひとときをお楽しみください。",
    }
]

データの準備ができたらインデックスにドキュメントを登録しましょう。
処理が正常終了したら、次のステップで実際に検索できることを確認します。

Python
from azure.search.documents import SearchClient
from azure.core.credentials import AzureKeyCredential
from azure.search.documents.indexes import SearchIndexClient

credential = AzureKeyCredential(search_api_key)
search_client = SearchClient(endpoint=search_endpoint,
                      index_name=index_name,
                      credential=credential)
try:
    result = search_client.upload_documents(documents=documents)
    print("Upload of new document succeeded: {}".format(result[0].succeeded))
except Exception as ex:
    print (ex.message)

    index_client = SearchIndexClient(
    endpoint=search_endpoint, credential=credential)

同じ操作で既存ドキュメントの変更も可能です。@search.action: "upload" と対象ドキュメントの id を指定してドキュメントを上書きできます。

3.4 ドキュメントの検索

Slide5.JPG

ドキュメントの登録が完了したら実際に検索できるか試してみましょう。飲食店の description フィールドに対し、雰囲気と検索してみます。

Python
results = search_client.search(
    search_text="雰囲気", # 検索ワード
    search_fields=['description'], # 検索対象のフィールド
    select='restaurant_name'  # 出力結果に含めるフィールド
)

for result in results:
    print(result)

# {'restaurant_name': '創作料理 ふう', '@search.score': 1.7204494, '@search.reranker_score': None, '@search.highlights': None, '@search.captions': None}
# {'restaurant_name': '和風ダイニング 花衣', '@search.score': 1.4623072, '@search.reranker_score': None, '@search.highlights': None, '@search.captions': None}

description雰囲気の文字列が含まれる飲食店 2 件のみヒットすれば検索成功です。@search.score はクエリとの関連度を表すスコアを表しています。創作料理 ふうのスコアが少し高いですが、これは description の総単語数が少なく、相対的に雰囲気という単語の占める割合が大きいためです。

ちなみにドキュメントの検索は AI Search のコンソール画面上でも可能です。
インデックスの画面上で「表示」から「JSONビュー」に変更し、Lucene クエリ構文で先ほどの検索クエリを エディターに入力すれば Python SDK と同じ結果が得られるはずです。

JSON クエリ
{
  "search": "雰囲気",
  "searchFields": "description",
  "select": "restaurant_name"
}

image.png

JSON クエリは以下のような curl コマンドでも実行できます。

curl
curl -X POST \
  "https://yamazo-ai-001.search.windows.net/indexes/restaurant_index/docs/search?api-version=2024-05-01-preview" \
  -H "Content-Type: application/json" \
  -H "api-key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d '{
    "search": "雰囲気",
    "searchFields": "description",
    "select": "restaurant_name"
  }'

4. 基礎的な検索クエリの構文

無事にドキュメントを検索できたら、基礎的な検索クエリの構文を見ていきましょう。本章では Python SDKLucene クエリ構文 (以下 JSON クエリ) それぞれの実装例を紹介します。

4.1 重みづけ (Boosting)

複数の検索キーワードを使う際、特定のキーワードを重視したい場合があります。その際に役に立つのが重みづけ (Boosting) です。
例えば飲食店名に東京、紹介文に個室を含む飲食店を検索し、一つ目の条件を 100 倍重みづけする検索クエリは以下のようになります。

Python
results = search_client.search(
    query_type="full", # 完全な Lucene 構文を使用するモード
    search_text="restaurant_name:東京^100, description:個室", # 店名に"東京"、一言紹介に"個室"を含むドキュメントを検索
    select='restaurant_name' # 出力結果に含めるフィールド
)

for result in results:  
    print(result)

# {'restaurant_name': '東京ベンガルの風', '@search.score': 100.13319, '@search.reranker_score': None, '@search.highlights': None, '@search.captions': None}
# {'restaurant_name': 'バル・エル・マール東京', '@search.score': 99.13085, '@search.reranker_score': None, '@search.highlights': None, '@search.captions': None}
# {'restaurant_name': 'オイスタークラブ東京', '@search.score': 99.13085, '@search.reranker_score': None, '@search.highlights': None, '@search.captions': None}
# {'restaurant_name': '和風ダイニング 花衣', '@search.score': 1.1302174, '@search.reranker_score': None, '@search.highlights': None, '@search.captions': None}
# {'restaurant_name': '神楽坂酒場 千鳥', '@search.score': 1.0023319, '@search.reranker_score': None, '@search.highlights': None, '@search.captions': None}

ここで query_type="full" は完全な Lucene 構文を使用するモードで、きめ細かい検索条件を指定するユースケースに適しています。上位 3 件の店名に東京が含まれており、他の飲食店と比較してスコアが 100 倍程度高いことを確認できます。

また、上記のクエリを AI Search の画面上で実行する場合は以下のように記述します。

JSON クエリ
{
  "queryType": "full",
  "search": "restaurant_name:東京^100, description:個室",
  "select": "restaurant_name"
}

一つ目の条件を必須にしたい場合は先頭に + をつけて +restaurant_name:東京と指定することも可能です。

4.2 絞り込み (Filtering)

特定のカテゴリや参照権限の範囲内で情報を探したい場合、検索結果を絞り込み (Filtering) できます。実際のプロジェクトで最も使われる構文です。

例えば平均単価が 3000 以上 8000 未満で、お店の種類が居酒屋またはバーの飲食店に絞り込む検索クエリは以下のようになります。

Python
results = search_client.search(
    query_type="full", # 完全な Lucene 構文を使用するモード
    filter="category/any(c:search.in(c, '居酒屋, バー')) and (price ge 3000 and price lt 8000)", # お店の種類が"居酒屋"または"バー"で、平均単価が 3000 円以上 8000 円未満のドキュメントに絞込み
    select='restaurant_name' # 出力結果に含めるフィールド
)

for result in results:  
    print(result)

# {'restaurant_name': 'バル・エル・マール東京', '@search.score': 1.0, '@search.reranker_score': None, '@search.highlights': None, '@search.captions': None}
# {'restaurant_name': '神楽坂酒場 千鳥', '@search.score': 1.0, '@search.reranker_score': None, '@search.highlights': None, '@search.captions': None}
# {'restaurant_name': '和風ダイニング 花衣', '@search.score': 1.0, '@search.reranker_score': None, '@search.highlights': None, '@search.captions': None}

少し複雑な記述に見えますが、category/any(c:search.in(c, '居酒屋, バー'))の部分で category の配列に居酒屋またはバーのいずれかが含まれる条件を指定しています。公式ガイドにもある通り、配列の要素数が数百から数千になる場合は性能の観点から search.in を使用することが推奨されます。

また、後半の price ge 3000 and price lt 8000 の部分で price の範囲を指定しており、ge は greater than or equal の略、lt は less than の略です。

JSON クエリの場合は以下のようになります。

JSON クエリ
{
  "queryType": "full",
  "filter": "category/any(c:search.in(c, '居酒屋, バー')) and (price ge 3000 and price lt 8000)",
  "select": "restaurant_name"
}

比較演算子は他にも le (less than or equal)、gt (greater than)、eq (equal)、 ne (not equal) などを指定できます。

4.3 並び替え (Sorting)

検索結果を特定フィールドの昇順、降順にしたい場合、検索結果を並び替え (Sorting) できます。先ほど取得したドキュメントを平均単価 price が安い順に並び替える場合、以下のようなクエリになります。

Python
results = search_client.search(
    query_type="full", # 完全な Lucene 構文を使用するモード
    filter="category/any(c:search.in(c, '居酒屋, バー')) and (price ge 3000 and price lt 8000)", # お店の種類が"居酒屋"または"バー"で、平均単価が 3000 円以上 8000 円未満のドキュメントに絞込み
    select="restaurant_name, price" # 出力結果に含めるフィールド
    order_by="price asc" # 平均単価が安い順に並び替え
)
for result in results:  
    print(result)

# {'restaurant_name': 'バル・エル・マール東京', 'price': 3000, '@search.score': 1.0, '@search.reranker_score': None, '@search.highlights': None, '@search.captions': None}
# {'restaurant_name': '和風ダイニング 花衣', 'price': 6000, '@search.score': 1.0, '@search.reranker_score': None, '@search.highlights': None, '@search.captions': None}
# {'restaurant_name': '神楽坂酒場 千鳥', 'price': 6500, '@search.score': 1.0, '@search.reranker_score': None, '@search.highlights': None, '@search.captions': None}

ここで、price asc が平均単価の安い順に並び替えを指定している部分です。逆に平均単価の高い順、つまり降順に並び替える場合は price desc と指定します。ちなみに ascdesc はそれぞれ ascending (昇順) と descending (降順) の略です。

JSON クエリの場合は以下のようになります。

JSON クエリ
{
  "queryType": "full",
  "filter": "category/any(c:search.in(c, '居酒屋, バー')) and (price ge 3000 and price lt 8000)",
  "select": "restaurant_name, price",
  "orderby": "price asc"
}

並び替えに用いるフィールドはカンマ区切りで複数指定できます。

4.4 グループ集計 (Faceting)

最後にファセット (Faceting)、つまりグループ集計の機能を試してみましょう。例えば飲食店の種類 category 毎の件数を集計する場合、以下のようなクエリになります。

Python
results = search_client.search(
    query_type="full", # 完全な Lucene 構文を使用するモード
    facets=["category"], # "category"列でグループ集計
)

results.get_facets()

# {'category': [{'value': 'レストラン', 'count': 4},
#   {'value': '居酒屋', 'count': 3},
#   {'value': '和食', 'count': 2},
#   {'value': '日本酒', 'count': 2},
#   {'value': 'イタリアン', 'count': 1},
#   {'value': 'インド料理', 'count': 1},
#   {'value': 'カフェ', 'count': 1},
#   {'value': 'スペイン料理', 'count': 1},
#   {'value': 'デザート', 'count': 1},
#   {'value': 'バー', 'count': 1}]}

飲食店の種類ごとの件数が取得できました。ファセットの結果を取得する場合は results.get_facets() を使用する点に注意してください。

JSON クエリの場合は以下のようになります。

JSON クエリ
{
  "queryType": "full",
  "facets": ["category"]
}

 
実際のプロジェクトではこれまで挙げた構文を組み合わせて使うことが多いです。
他にも AI Search ではあいまい一致正規表現なども使用できますが、あまり使用頻度が高くないため本記事では割愛します。更に詳しく知りたい方は Lucene クエリ構文の公式リファレンスをご参照ください。検索エンジンは直感と異なる挙動を示すことも多いので、実際に試して確認することが重要です。

また、AI Search はベクトル類似度とのハイブリッド検索や検索結果を意味的に再ランク付けするセマンティックランカーなどの機能が用意されていますが、まずはキーワード検索の精度改善から始めることをお勧めします。

ベクトル検索は文書全体 (特に文頭) の大まかな傾向しか捉えられず、例えばピンポイントの記述や専門用語の検索はキーワード検索の方が有効です。ハイブリッド検索は両方の良い所取りと言われていますが、その実態は RRF と呼ばれる素朴な足し算でスコアを平均しているだけなので、元となるキーワード検索の精度が低いと当然精度は出ません。

また、セマンティックランカーもリランキング前の文書 (上位 50 件) にそもそもヒットしなければ無意味ですし、ベクトル検索やセマンティックランカーは運用コストもかかります。データの傾向にもよりますが、個人的には RAG を構築する際はまずキーワード検索の精度を上げるためのクエリ構文設計やクエリ拡張、ドキュメント拡張から着実に検討するべきだと考えています。

おわりに

生成 AI の普及が加速する中、その可能性を最大限に引き出すためには非構造データを扱う技術が必要不可欠です。その中でも特に検索エンジンの需要は今後急増すると予想されます。 本記事を通して日本の生成 AI 技術向上に少しでも貢献できれば幸いです。

これからも生成 AI やクラウド技術の最新情報を初学者に分かりやすく発信できるように精進しますので、引き続きよろしくお願いします。まだフォローされていない方は是非 X (Twitter) の方もよろしくお願いします!

最後まで読んでいただき、ありがとうございました。

168
169
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
168
169