【Azure AI Search】フルテキスト検索のクイックスタートをやってみる

Azure AI Search (旧:Azure Cognitive Search)はMicrosoft社が提供している検索サービスです。
インデックスに登録したドキュメントに対して「フルテキスト検索」「ベクトル検索」「ハイブリッド検索」といったアプローチでクエリを実行することが出来ます。
詳細は下記の公式ドキュメントをご参照ください。

https://learn.microsoft.com/ja-jp/azure/search/search-what-is-azure-search

https://learn.microsoft.com/ja-jp/azure/search/search-query-overview

検索サービスの用途としては、生成AIをRAG構成で使用する際の外部ソース管理とかに役立ちそうです。
まずはフルテキスト検索のアプローチについて、公式サイトのクイックスタートをベースに確認していこうと思います。

https://learn.microsoft.com/ja-jp/azure/search/search-get-started-text?tabs=python

# 前提
本記事は下記が完了していることを前提とします。
公式サイトのクイックスタートだとJupyterノートブックを前提としていますが、今回はVSCodeでPythonを実行する形で進めます。

- Azure
    - アカウントの作成
    - リソースグループ作成
- ソフトウェアのインストール
    - Visual Studio Code
    - Python3.9
    - GitBash

# Azure AI Searchの作成
## 1. Azureの検索枠から「AI Search」を開く。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736004/7971741c-1cce-6b16-df78-408295211ed3.png)

## 2.「作成」をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736004/880ab044-18a2-fbd8-3d60-8146687aaa7b.png)

## 3. 基本の各項目を入力する。
リソースグループ、サービス名、リージョン、価格レベルを設定する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736004/b9a1263c-abd7-76b8-b373-a7461545f9a6.png)

**(補足)** サービス名はアカウント内外問わずユニークな必要がある。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736004/a16f4f27-ed87-114e-8835-fd253ec642fe.png)

**(補足)** 価格レベルはグレードによって料金が大きく異なるので要確認。
アカウント内で1つのみ料金の掛からないFreeプランが利用可能。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736004/248225bd-ae2d-7b51-8490-381489a42969.png)

## 4. エンドポイントを公開に設定する。
今回はインターネット経由で接続できるように設定。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736004/ff6b9e4e-073f-75e6-204f-cb261e24473b.png)

## 5.「作成」をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736004/40ee5489-49f3-48b1-9d3b-9b60d978373e.png)

Azure AI servicesの一覧に作成したサービスが表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736004/99085dae-727d-75ce-333d-b33bdafd8e18.png)

## 6. エンドポイントとAPIKeyをメモする。
サービスのエンドポイントとAPIKeyが後続の手順で必要となるため、取得してメモ帳などに貼り付けておく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736004/456de429-adae-a2ac-ab0e-02b6a36c4696.png)

# 開発環境の構築
VSCodeの中でGitBashを起動して下記コマンドを実行していく。
## 1. 仮想環境作成、アクティベート
```shell
■仮想環境作成
py -3.9 -m venv .venv

■アクティベート
. .venv/Scripts/activate
```

## 2. パッケージのインストール
```shell
pip install azure-search-documents==11.6.0b1
pip install azure-identity
pip install python-dotenv
```

# インデックスの作成
AISearchのインデックスを作成する。
下記のPythonプログラムの一部を書き換えて実行する。
- PUT-YOUR-SEARCH-SERVICE-ENDPOINT-HERE
→サービスのエンドポイントに修正
- PUT-YOUR-SEARCH-SERVICE-ADMIN-API-KEY-HERE
→APIKeyに修正

※本項以降のプログラムでも同様の書き換えを行ってください。

```python:create_index.py
from azure.core.credentials import AzureKeyCredential
from azure.search.documents.indexes import SearchIndexClient
from azure.search.documents import SearchClient
from azure.search.documents.indexes.models import (
    ComplexField,
    SimpleField,
    SearchFieldDataType,
    SearchableField,
    SearchIndex
)

# Set Credentials
search_endpoint = "PUT-YOUR-SEARCH-SERVICE-ENDPOINT-HERE"
search_api_key = "PUT-YOUR-SEARCH-SERVICE-ADMIN-API-KEY-HERE"
index_name = "hotels-quickstart"
credential = AzureKeyCredential(search_api_key)

# Create a search schema
index_client = SearchIndexClient(
    endpoint=search_endpoint, credential=credential)
fields = [
        SimpleField(name="HotelId", type=SearchFieldDataType.String, key=True),
        SearchableField(name="HotelName", type=SearchFieldDataType.String, sortable=True),
        SearchableField(name="Description", type=SearchFieldDataType.String, analyzer_name="en.lucene"),
        SearchableField(name="Description_fr", type=SearchFieldDataType.String, analyzer_name="fr.lucene"),
        SearchableField(name="Category", type=SearchFieldDataType.String, facetable=True, filterable=True, sortable=True),

        SearchableField(name="Tags", collection=True, type=SearchFieldDataType.String, facetable=True, filterable=True),

        SimpleField(name="ParkingIncluded", type=SearchFieldDataType.Boolean, facetable=True, filterable=True, sortable=True),
        SimpleField(name="LastRenovationDate", type=SearchFieldDataType.DateTimeOffset, facetable=True, filterable=True, sortable=True),
        SimpleField(name="Rating", type=SearchFieldDataType.Double, facetable=True, filterable=True, sortable=True),

        ComplexField(name="Address", fields=[
            SearchableField(name="StreetAddress", type=SearchFieldDataType.String),
            SearchableField(name="City", type=SearchFieldDataType.String, facetable=True, filterable=True, sortable=True),
            SearchableField(name="StateProvince", type=SearchFieldDataType.String, facetable=True, filterable=True, sortable=True),
            SearchableField(name="PostalCode", type=SearchFieldDataType.String, facetable=True, filterable=True, sortable=True),
            SearchableField(name="Country", type=SearchFieldDataType.String, facetable=True, filterable=True, sortable=True),
        ])
    ]

scoring_profiles = []
suggester = [{'name': 'sg', 'source_fields': ['Tags', 'Address/City', 'Address/Country']}]

# Create the search index=
index = SearchIndex(name=index_name, fields=fields, suggesters=suggester, scoring_profiles=scoring_profiles)
result = index_client.create_or_update_index(index)
print(f' {result.name} created')
```

実行が完了するとコンソール画面でhotels-quickstartというインデックスが作られていることを確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736004/19832148-a169-0d1b-9dfb-2ed9004b90bb.png)

各フィールドの定義は下記のようになっている。
用途によって`SimpleField`や`SearchableField`などの使い分けが必要。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736004/3d0a0724-1422-ae26-f52d-77bd681bb5f9.png)

https://learn.microsoft.com/ja-jp/dotnet/api/azure.search.documents.indexes.models?view=azure-dotnet

# ドキュメントの登録
先ほど作成したインデックスにドキュメントを登録する。
下記のPythonを実行する。

```python:create_documents.py
from azure.core.credentials import AzureKeyCredential
from azure.search.documents.indexes import SearchIndexClient
from azure.search.documents import SearchClient

# Set Credentials
search_endpoint = "PUT-YOUR-SEARCH-SERVICE-ENDPOINT-HERE"
search_api_key = "PUT-YOUR-SEARCH-SERVICE-ADMIN-API-KEY-HERE"
index_name = "hotels-quickstart"
credential = AzureKeyCredential(search_api_key)

# Create a documents payload
documents = [
    {
    "@search.action": "upload",
    "HotelId": "1",
    "HotelName": "Secret Point Motel",
    "Description": "The hotel is ideally located on the main commercial artery of the city in the heart of New York. A few minutes away is Time's Square and the historic centre of the city, as well as other places of interest that make New York one of America's most attractive and cosmopolitan cities.",
    "Description_fr": "L'hôtel est idéalement situé sur la principale artère commerciale de la ville en plein cœur de New York. A quelques minutes se trouve la place du temps et le centre historique de la ville, ainsi que d'autres lieux d'intérêt qui font de New York l'une des villes les plus attractives et cosmopolites de l'Amérique.",
    "Category": "Boutique",
    "Tags": [ "pool", "air conditioning", "concierge" ],
    "ParkingIncluded": "false",
    "LastRenovationDate": "1970-01-18T00:00:00Z",
    "Rating": 3.60,
    "Address": {
        "StreetAddress": "677 5th Ave",
        "City": "New York",
        "StateProvince": "NY",
        "PostalCode": "10022",
        "Country": "USA"
        }
    },
    {
    "@search.action": "upload",
    "HotelId": "2",
    "HotelName": "Twin Dome Motel",
    "Description": "The hotel is situated in a  nineteenth century plaza, which has been expanded and renovated to the highest architectural standards to create a modern, functional and first-class hotel in which art and unique historical elements coexist with the most modern comforts.",
    "Description_fr": "L'hôtel est situé dans une place du XIXe siècle, qui a été agrandie et rénovée aux plus hautes normes architecturales pour créer un hôtel moderne, fonctionnel et de première classe dans lequel l'art et les éléments historiques uniques coexistent avec le confort le plus moderne.",
    "Category": "Boutique",
    "Tags": [ "pool", "free wifi", "concierge" ],
    "ParkingIncluded": "false",
    "LastRenovationDate": "1979-02-18T00:00:00Z",
    "Rating": 3.60,
    "Address": {
        "StreetAddress": "140 University Town Center Dr",
        "City": "Sarasota",
        "StateProvince": "FL",
        "PostalCode": "34243",
        "Country": "USA"
        }
    },
    {
    "@search.action": "upload",
    "HotelId": "3",
    "HotelName": "Triple Landscape Hotel",
    "Description": "The Hotel stands out for its gastronomic excellence under the management of William Dough, who advises on and oversees all of the Hotel's restaurant services.",
    "Description_fr": "L'hôtel est situé dans une place du XIXe siècle, qui a été agrandie et rénovée aux plus hautes normes architecturales pour créer un hôtel moderne, fonctionnel et de première classe dans lequel l'art et les éléments historiques uniques coexistent avec le confort le plus moderne.",
    "Category": "Resort and Spa",
    "Tags": [ "air conditioning", "bar", "continental breakfast" ],
    "ParkingIncluded": "true",
    "LastRenovationDate": "2015-09-20T00:00:00Z",
    "Rating": 4.80,
    "Address": {
        "StreetAddress": "3393 Peachtree Rd",
        "City": "Atlanta",
        "StateProvince": "GA",
        "PostalCode": "30326",
        "Country": "USA"
        }
    },
    {
    "@search.action": "upload",
    "HotelId": "4",
    "HotelName": "Sublime Cliff Hotel",
    "Description": "Sublime Cliff Hotel is located in the heart of the historic center of Sublime in an extremely vibrant and lively area within short walking distance to the sites and landmarks of the city and is surrounded by the extraordinary beauty of churches, buildings, shops and monuments. Sublime Cliff is part of a lovingly restored 1800 palace.",
    "Description_fr": "Le sublime Cliff Hotel est situé au coeur du centre historique de sublime dans un quartier extrêmement animé et vivant, à courte distance de marche des sites et monuments de la ville et est entouré par l'extraordinaire beauté des églises, des bâtiments, des commerces et Monuments. Sublime Cliff fait partie d'un Palace 1800 restauré avec amour.",
    "Category": "Boutique",
    "Tags": [ "concierge", "view", "24-hour front desk service" ],
    "ParkingIncluded": "true",
    "LastRenovationDate": "1960-02-06T00:00:00Z",
    "Rating": 4.60,
    "Address": {
        "StreetAddress": "7400 San Pedro Ave",
        "City": "San Antonio",
        "StateProvince": "TX",
        "PostalCode": "78216",
        "Country": "USA"
        }
    }
]

# Upload documents to the index
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)
```

実行が完了するとコンソール画面から登録データの検索が可能となる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736004/113b4f18-22d3-e35d-465f-12538457c701.png)

今回はアクションとしてデータを登録しているが、`@search.action`のプロパティを変更することによって削除やマージなどのアクションに切り替えることが可能。

https://learn.microsoft.com/ja-jp/rest/api/searchservice/addupdate-or-delete-documents#request-body

# クエリの実行
プログラムベースでクエリを実行する。
クイックスタートの中でも気になった項目を試してみる。

SearchClient.Searchメソッドのドキュメント :pencil: 

https://learn.microsoft.com/ja-jp/dotnet/api/azure.search.documents.searchclient.search?view=azure-dotnet#azure-search-documents-searchclient-search-1(system-string-azure-search-documents-searchoptions-system-threading-cancellationtoken)

## 全件検索
検索文字列に`search_text="*"`を指定することで、全てのドキュメントが取得できる。
下記のPythonを実行する。

```python:search_fulltext.py
from azure.core.credentials import AzureKeyCredential
from azure.search.documents.indexes import SearchIndexClient
from azure.search.documents import SearchClient

# Set Credentials
search_endpoint = "PUT-YOUR-SEARCH-SERVICE-ENDPOINT-HERE"
search_api_key = "PUT-YOUR-SEARCH-SERVICE-ADMIN-API-KEY-HERE"
index_name = "hotels-quickstart"
credential = AzureKeyCredential(search_api_key)

# Run an empty query (returns selected fields, all documents)
search_client = SearchClient(endpoint=search_endpoint,
                      index_name=index_name,
                      credential=credential)
results =  search_client.search(query_type='simple',
    search_text="*" ,
    select='HotelName,Description',
    include_total_count=True)

print ('Total Documents Matching Query:', results.get_count())
for result in results:
    print(result["@search.score"])
    print(result["HotelName"])
    print(f"Description: {result['Description']}")
```

実行結果は下記のようになる。
```
$ python search_fulltext.py 
Total Documents Matching Query: 4
1.0
Triple Landscape Hotel
Description: The Hotel stands out for its gastronomic excellence under the management of William Dough, who advises on and oversees all of the Hotel's restaurant services.
1.0
Twin Dome Motel
Description: The hotel is situated in a  nineteenth century plaza, which has been expanded and renovated to the highest architectural standards to create a modern, functional and first-class hotel in which art and unique historical elements coexist with the most modern comforts.
1.0
Sublime Cliff Hotel
Description: Sublime Cliff Hotel is located in the heart of the historic center of Sublime in an extremely vibrant and lively area within short walking distance to the sites and landmarks of the city and is surrounded by the extraordinary beauty of churches, buildings, shops and monuments. Sublime Cliff is part of a lovingly restored 1800 palace.
1.0
Secret Point Motel
Description: The hotel is ideally located on the main commercial artery of the city in the heart of New York. A few minutes away is Time's Square and the historic centre of the city, as well as other places of interest that make New York one of America's most attractive and cosmopolitan cities.
```

## 用語検索
search_textに検索したい文字列を指定することで用語検索が可能となる。
下記のPythonを実行する。
```python:search_fulltext.py
from azure.core.credentials import AzureKeyCredential
from azure.search.documents.indexes import SearchIndexClient
from azure.search.documents import SearchClient

# Set Credentials
search_endpoint = "PUT-YOUR-SEARCH-SERVICE-ENDPOINT-HERE"
search_api_key = "PUT-YOUR-SEARCH-SERVICE-ADMIN-API-KEY-HERE"
index_name = "hotels-quickstart"
credential = AzureKeyCredential(search_api_key)

# Run an empty query (returns selected fields, all documents)
search_client = SearchClient(endpoint=search_endpoint,
                      index_name=index_name,
                      credential=credential)
results =  search_client.search(query_type='simple',
    search_text="wifi" ,
    select='HotelName,Description,Tags',
    include_total_count=True)

print ('Total Documents Matching Query:', results.get_count())
for result in results:
    print(result["@search.score"])
    print(result["HotelName"])
    print(f"Description: {result['Description']}")
```

実行結果としては下記のようになる。
```
$ python search_fulltext.py 
Total Documents Matching Query: 1
0.78019357
Twin Dome Motel
Description: The hotel is situated in a  nineteenth century plaza, which has been expanded and renovated to the highest architectural standards to create a modern, functional and first-class hotel in which art and unique historical elements coexist with the most modern comforts.
```

TagsもSearchableFieldなので、ここにヒットしたと思われる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736004/4c0b93f8-0217-de3b-6551-a6ddb5b2e996.png)

検索範囲を特定のフィールドに制限する場合は、下記のように`search_fields`プロパティにフィールド名を指定する。
```python
results = search_client.search(
    search_text="wifi", 
    search_fields=['Tags'], 
    select='HotelName,Description,Tags')
```

## フィルター
フィールドに対してフィルターを掛けることが可能。
例として評価が4を超えるホテルを降順で出力するプログラムを以下に示す。

```python:search_fulltext.py
from azure.core.credentials import AzureKeyCredential
from azure.search.documents.indexes import SearchIndexClient
from azure.search.documents import SearchClient

# Set Credentials
search_endpoint = "PUT-YOUR-SEARCH-SERVICE-ENDPOINT-HERE"
search_api_key = "PUT-YOUR-SEARCH-SERVICE-ADMIN-API-KEY-HERE"
index_name = "hotels-quickstart"
credential = AzureKeyCredential(search_api_key)

# Run an empty query (returns selected fields, all documents)
search_client = SearchClient(endpoint=search_endpoint,
                      index_name=index_name,
                      credential=credential)
# Add a filter
results = search_client.search(
    search_text="hotels", 
    select='HotelId,HotelName,Rating', 
    filter='Rating gt 4', 
    order_by='Rating desc')

for result in results:
    print("{}: {} - {} rating".format(result["HotelId"], result["HotelName"], result["Rating"]))
```

実行結果としては下記のようになる。
```
$ python search_fulltext.py 
3: Triple Landscape Hotel - 4.8 rating
4: Sublime Cliff Hotel - 4.6 rating
```

# 最後に

AISearchを使用することで検索サービスを楽々構築できることが分かりました。
今回はフルテキスト検索を試しましたが、ベクトル検索やハイブリッド検索などのアプローチも試してみようと思います。

下記の検証結果によると検索の精度は、
**フルテキスト検索 < ベクトル検索 < ハイブリッド検索 < ハイブリッド検索+セマンティック**
の順になるようです。
(ケースバイケースかもしれないですが…。)

https://techcommunity.microsoft.com/t5/ai-azure-ai-services-blog/azure-ai-search-outperforming-vector-search-with-hybrid/ba-p/3929167

検索精度の検証も追々やっていく予定です。