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

# はじめに
こんにちは！ AI エンジニアのヤマゾーです。
近年、生成 AI の進化が目覚ましく、生成 AI を活用したシステムの開発が盛んに行われています。その中で最も有名なテクニックが **RAG** です。RAG というのは検索拡張生成 (Retrieval Augmented Generation) の略で、**質問の関連情報を検索し、質問と関連情報をセットで入力して回答させる技術**のことです。
![Slide11.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/defabc7d-c345-cbe9-3ab4-1714bffd549e.jpeg)


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

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

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

# サマリ
### 本記事の目次
- [1. Azure AI Search の概要](#1-azure-ai-search-の概要)
  - [1.1 Azure AI Search とは](#11-azure-ai-search-とは)
  - [1.2 検索エンジンの基本用語](#12-検索エンジンの基本用語)
  - [1.3 Azure AI Search 固有の用語](#13-azure-ai-search-固有の用語)
- [2. 検索エンジンの仕組み](#2-検索エンジンの仕組み)
  - [2.1 転置インデックスとは](#21-転置インデックスとは)
  - [2.2 アナライザーとは](#22-アナライザーとは)
- [3. Azure AI Search の基本操作](#3-azure-ai-search-の基本操作)
  - [3.1 サービスの利用開始](#31-サービスの利用開始)
  - [3.2 インデックスの作成](#32-インデックスの作成)
  - [3.3 ドキュメントの登録](#33-ドキュメントの登録)
  - [3.4 ドキュメントの検索](#34-ドキュメントの検索)
- [4. 基礎的な検索クエリの構文](#4-基礎的な検索クエリの構文)
  - [4.1 重みづけ (Boosting)](#41-重みづけ-boosting)
  - [4.2 絞り込み (Filtering)](#42-絞り込み-filtering)
  - [4.3 並び替え (Sorting)](#43-並び替え-sorting)
  - [4.4 グループ集計 (Faceting)](#44-グループ集計-faceting)


### 想定読者
- 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/a5c1f32f-4c04-f36a-bd79-011a3e5d8295.jpeg)

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


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

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

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

:::note warn
一部の機能は価格レベルによって制限されますが、本記事では割愛します。各価格レベルの詳細な違いについては [Azure 公式ドキュメント](https://learn.microsoft.com/ja-jp/azure/search/search-sku-tier#feature-availability-by-tier)をご参照ください。
::: 

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


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

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

例えば自己紹介のデータに対して`生成AI人間`と検索する場合を考えます。
自己紹介文がヒットするまでの全体像は以下のようになります。
![Slide3.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/e5ee2a3f-55da-b9f8-08f1-4efd6e1e0bcd.jpeg)
　
まず、`生成AI人間`という検索クエリをアナライザーが`生成`, `AI`, `人間`と単語レベルに分割します。次に、あらかじめ作成しておいた転置インデックスで`生成`, `AI`, `人間`の各単語が登場する文書 ID を問い合わせます。その結果、最終的に文書 **001** と **003** がヒットします。実際には更に単語の登場率や登場回数などでランク付けを行うケースが殆どですが、基本的な仕組みはこれだけです。文量が膨大になっても各単語のリストを参照するだけなので、高速に検索できるメリットがあります。

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


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

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

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

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

その他にもアナライザーの代表的な処理には以下のようなものがあります。
- 大文字と小文字、全角と半角の統一（例: `生成ＡＩ`, `生成AI` → `生成ai`）
- 活用形の統一 (例: `書か`, `書き`, `書く`, `書け` → `書く`)
- 特殊文字や HTML タグの除去 (例: `\<p>エンジニア!?\</p>` → `エンジニア`)
- 同義語辞書による拡張 (例: `猫` → `ねこ`, `ネコ`, `cat`)

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

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

![Slide1.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/fea241e8-e483-a8ca-f137-d04627ab24c7.jpeg)


### 3.1 サービスの利用開始
![Slide2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/6bea3115-196b-3864-67ab-ca2ef574f37a.jpeg)

事前準備として、AI Search のサービスを無料プランで作成しましょう。
まず、[Azure Portal](https://azure.microsoft.com/ja-jp/get-started/azure-portal/) 上で「AI Search」と検索し、そのまま選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/9b808dc2-f486-4d6d-f291-f1a5162e30e9.png)
　
「作成」ボタンを押下し、AI Search サービスを新規作成する画面に移動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/ce7cca20-5a03-363f-0db1-eb54f1ee0b7c.png)
　
サービス名やリージョン、価格レベルなどの情報を入力します。
この際、**価格レベルが Free になっていることを必ず確認してください。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/32f507bb-a637-d115-77b5-c304f1ef36c2.png)

無事にデプロイが完了したら準備完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/7abe4b3a-f9bf-389c-3195-3581e2f41203.png)

### 3.2 インデックスの作成
![Slide3.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/0fc4421d-a32e-dd48-a096-8892568372f5.jpeg)

次に、文書データを登録するためのインデックスを作成します。
先ほど作成した AI Search のサービスを開き、「インデックスの追加」を押下します。インデックスのスキーマは JSON 形式での設定も可能ですが、今回は Web 画面上で設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/eeab018f-43ca-a5d7-1ad6-21fb70b99474.png)
　
次に、作成するインデックスのフィールドを定義していきます。
例として飲食店の名前を格納する `restaurant_name` フィールドを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/ee79298a-9fa1-ce29-e8d5-585a09a0af21.png)


ここで種類とは[データ型](https://learn.microsoft.com/ja-jp/rest/api/searchservice/supported-data-types)のことで、`Edm.String` は文字列型のデータです。ちなみに先頭の `Edm.` は [Entity Data Model](https://learn.microsoft.com/ja-jp/dotnet/framework/data/adonet/entity-data-model) の略ですが意識する必要はありません。データ型は他にも整数 `Edm.Int32`、時刻 `Edm.DateTimeOffset`、文字列の配列 `Collection(Edm.String)` などがあります。

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

各フィールドで使わない項目、特に検索可能のチェックは外しておきましょう。
フィールドの追加が完了したら、インデックス名を入力して各フィールドを最終確認し、「作成」を押下します。これでインデックスの作成は完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/5594ff2f-92a2-a40f-2527-ca199f219445.png)


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

### 3.3 ドキュメントの登録
![Slide4.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/60286a23-f54e-3bb0-3a86-d0a36a7fdca6.jpeg)

インデックスを作成した後はドキュメントを登録しましょう。
[公式 Quickstart](https://learn.microsoft.com/ja-jp/azure/search/search-get-started-text) も参考にしながら Python SDK を使用します。

事前に「設定」から「キー」を選択し、プライマリ管理者キーの値をコピーして控えておきます。プログラムを実行する際必要になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/85ff723e-d711-388d-51cc-86016eb07604.png)
　
それでは実際にドキュメントを登録しましょう。
まずは必要なライブラリ群をインストールします。
```python: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: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:Python
documents = [
    {
    "@search.action": "upload",
    "id": "001",
    "restaurant_name": "炭火焼鳥 まるや",
    "category": ["焼き鳥", "焼酎", "日本酒"],
    "price": "5000",
    "description": "炭火でじっくり焼き上げた焼鳥と厳選された焼酎・日本酒が楽しめる焼き鳥専門店。和の風情を感じる居心地の良い空間です。",
    }
]
```

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

```python: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: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)
```
</details>

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

### 3.4 ドキュメントの検索
![Slide5.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/d7586976-f9b2-d9a3-2496-38976f0ec745.jpeg)

ドキュメントの登録が完了したら実際に検索できるか試してみましょう。飲食店の `description` フィールドに対し、`雰囲気`と検索してみます。
```python: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 クエリ構文](https://learn.microsoft.com/ja-jp/azure/search/query-lucene-syntax)で先ほどの検索クエリを エディターに入力すれば Python SDK と同じ結果が得られるはずです。

```json:JSON クエリ
{
  "search": "雰囲気",
  "searchFields": "description",
  "select": "restaurant_name"
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1348507/5359e308-882c-ef41-ca9c-dbecd59079a8.png)

:::note warn
JSON クエリは以下のような curl コマンドでも実行できます。
```bash: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 SDK](https://learn.microsoft.com/ja-jp/python/api/azure-search-documents/azure.search.documents.aio.searchclient?view=azure-python#azure-search-documents-aio-searchclient-search) と [Lucene クエリ構文](https://learn.microsoft.com/ja-jp/azure/search/query-lucene-syntax) (以下 JSON クエリ) それぞれの実装例を紹介します。

### 4.1 重みづけ (Boosting)
複数の検索キーワードを使う際、特定のキーワードを重視したい場合があります。その際に役に立つのが**重みづけ (Boosting)** です。
例えば飲食店名に`東京`、紹介文に`個室`を含む飲食店を検索し、一つ目の条件を 100 倍重みづけする検索クエリは以下のようになります。
```python: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:JSON クエリ
{
  "queryType": "full",
  "search": "restaurant_name:東京^100, description:個室",
  "select": "restaurant_name"
}
```

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

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

例えば平均単価が `3000` 以上 `8000` 未満で、お店の種類が`居酒屋`または`バー`の飲食店に絞り込む検索クエリは以下のようになります。
```python: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` の配列に`居酒屋`または`バー`のいずれかが含まれる条件を指定しています。[公式ガイド](https://learn.microsoft.com/ja-jp/azure/search/search-security-trimming-for-azure-search#about-the-security-filter-pattern)にもある通り、配列の要素数が数百から数千になる場合は性能の観点から `search.in` を使用することが推奨されます。

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

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

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

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

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

```python: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` と指定します。ちなみに `asc` と `desc` はそれぞれ ascending (昇順) と descending (降順) の略です。

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

```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"
}
```

:::note warn
並び替えに用いるフィールドはカンマ区切りで複数指定できます。
:::
### 4.4 グループ集計 (Faceting)
最後に**ファセット (Faceting)**、つまりグループ集計の機能を試してみましょう。例えば飲食店の種類 `category` 毎の件数を集計する場合、以下のようなクエリになります。
```python: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:JSON クエリ
{
  "queryType": "full",
  "facets": ["category"]
}
```
　
実際のプロジェクトではこれまで挙げた構文を組み合わせて使うことが多いです。
他にも AI Search では[あいまい一致](https://learn.microsoft.com/ja-jp/azure/search/search-query-lucene-examples#example-2-fuzzy-search)や[正規表現](https://learn.microsoft.com/ja-jp/azure/search/search-query-lucene-examples#example-5-regex)なども使用できますが、あまり使用頻度が高くないため本記事では割愛します。更に詳しく知りたい方は [Lucene クエリ構文の公式リファレンス](https://learn.microsoft.com/ja-jp/azure/search/query-lucene-syntax)をご参照ください。検索エンジンは直感と異なる挙動を示すことも多いので、実際に試して確認することが重要です。

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

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

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


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

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

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