【Azure AI Search】外部ソースのデータを活用した生成AIの回答生成の仕組み

# はじめに
生成AIで回答の品質を向上するためのナレッジを以下の軸でまとめています。今回は外部ソースのデータを使った回答生成の方法をAzure AI Searchを活用しながら解説していきたいと思います。他の記事もセットでぜひチェックしてみてください。

| カテゴリ | 記事の概要 | 公開予定日 |
|:-:|:-:|:-:|
| プロンプトエンジニアリング | [目的に合った回答を生成できるように制限を与える：システムメッセージエンジニアリング](https://qiita.com/k-kimino/items/368b053aafba665574a5) | 公開済み |
|  | [プロンプトエンジニアリングでよい回答を得るために意識したい方針](https://qiita.com/k-kimino/items/543e8292bac581db4108) | 公開済み |
| 外部ソースのデータを使った回答(RAG) | 【Azure AI Search】外部ソースのデータを活用した生成AIの回答生成の仕組み | 1/24 |
|  | 非構造型データを使った回答の作成 | 1/25 |
| 安全に生成AIを活用する | 避けるべき情報が含まれないかチェックする | 1/31 |
|  | モデルの信頼性を評価する | 2/1 |

# RAG（Retrieval-Augmented Generation）の必要性
LLMモデルは学習済みのデータに関する情報しか持っていません。信頼できるソースを基に生成AIによる回答を生成することで回答の品質を向上させると共に、ソースが存在しない場合は回答させないといった制限を設けさせるためには仕組み（RAG）が必要になります。Azureにおいてデータセットからの検索を実現するツールがAzure AI Searchになります。
本記事で紹介する外部データを基にした生成AIの回答を生成する仕組みの全体像は以下です。

![Structured.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322105/f7c6e8f6-e79f-d313-b4da-25ce6dca6022.jpeg)


# Azure AI Searchについて
Azure AI Search (旧称Azure Cognitive Search)は企業や開発者がデータを簡単に検索できるようにするためのツールです。主に以下の4つの手法が提供されており、データセットごとに手法を選択することができます。以下ではそれぞれメリット・デメリットを表に示します。一般的にはハイブリッド検索、セマンティンクランク付けが汎用的かつ高い精度が期待できます（Microsoftより評価結果が示されています）。
ただしすべてのケースで単一の手法が優位なわけではないため、RAGの精度向上を目指すためには文書ごとにそれぞれの手法をテストし、高い精度が期待できる手法をデータセットごとに選択できるようにすることが必要と思います。本記事では無償の範囲で使用可能なフルテキスト検索を使用します。

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

| 検索手法 | フルテキスト検索 | ベクトル検索 | ハイブリット検索 | セマンティンクランク付け |
|:-:|:-:|:-:|:-:|:-:|
| 概要 | キーワードやフレーズを検索し、一致頻度を見て関連度の高い結果を返す | キーワードの一致ではなく、コンテキスト内の単語とフレーズの意味をAIがベクトルで表現し、テキストの類似性の高い結果を返す | フルテキスト検索とベクトル検索の両方のスコアを加味し、総合スコアから結果を返す | フルテキスト検索、ハイブリッド検索に対してクエリの意図に沿う結果が上位になるように再度ランク付けを行い、結果を返す |
| メリット | ・検索クエリに固有名詞が多く含まれる場合に高い検索精度が期待できる | <p>・キーワードが完全に一致していない場合でもクエリへの類似性の高いドキュメントの検索が期待できる</p><p>・テキストだけでなく、画像やビデオなどマルチモーダルな検索に対応できる</p> | ・汎用的に使用でき、高い検索精度が期待できる | <p>・フルテキスト検索、ハイブリッド検索よりもさらに高い検索精度が期待できる</p><p>・検索クエリがより自然文に近づいた場合でもクエリの意図に沿ったドキュメントの回答が期待しやすい</p> |
| デメリット | <p>・単語の多様性やスペルミスが含まれる場合などに検索が困難になる</p><p>・検索文が自然文の場合に意図を解釈できず適切な結果が得られないことがある</p> | ・AI(LLMモデル)が学習していないような固有名詞が多く含まれる場合にうまくベクトル値を計算できず検索精度が低くなる可能性がある | ・フルテキスト検索とベクトル検索の結果がマージされたスコアで返ってくるため、どちらの検索が効いているのか分かりづらく、検索結果の考察が難しい | <p>・フルテキスト検索、ハイブリッド検索の検索結果の上位50に対してランク付けを行うため、キーワードの一致頻度が低すぎる場合には効果得られにくい</p><p>・追加料金が必要</p> |
| 参考URL | [フルテキスト検索 - Azure AI Search Microsoft Learn](https://learn.microsoft.com/ja-jp/azure/search/search-lucene-query-architecture) | [ベクトル検索 - Azure AI Search Microsoft Learn](https://learn.microsoft.com/ja-jp/azure/search/vector-search-overview) | [ハイブリッド検索 - Azure AI Search Microsoft Learn](https://learn.microsoft.com/ja-jp/azure/search/hybrid-search-overview) | [セマンティック ランク付け - Azure AI Search Microsoft Learn](https://learn.microsoft.com/ja-jp/azure/search/semantic-search-overview) |



# CNNのニュース記事が格納されたCSV形式のデータセットからユーザーのクエリに対する回答を作成する

以下のシナリオに基づいて実現方法を考えていきます。
> ## 想定シナリオ
> １万以上のCNNのニュース記事が格納されたCSV形式のデータセットをもとに、ユーザーからのクエリ（クリントン民主党指名候補の選挙キャンペーンについて知りたい）への回答を作成するユースケースを通じて、構造型データをもとに回答を生成する仕組みとコードの具体例を紹介します。データセットは[Hugging Face](https://huggingface.co/datasets/cnn_dailymail/viewer/3.0.0/test)から取得しています。
> 
> 1. 複数のドキュメントから関連するものを検索する
Azure AI Searchで構成したインデックスを活用し、ユーザーからのクエリに関連する情報を取得します。
> 2. ドキュメント内部の関連箇所を検索する
取得した情報（CNNの記事）の中で、ユーザーからのクエリに最も関連する部分を抽出します。
> 3. テキストを要約し、回答を生成する
抽出した情報を要約することでユーザーからのクエリに対する回答を生成します。

## 複数のドキュメントから関連するものを検索する

Azure PortalからBlobストレージなどからデータをインポートして簡単にインデックスを作成することができますが、今回使用するCSVファイルのサイズが約50MBに対し、Free TierでBlobストレージからデータをインポートしてインデックスを作成する際の最小サイズが16MBと足りないため、[azure-search-documents](https://learn.microsoft.com/ja-jp/python/api/overview/azure/search-documents-readme)ライブラリを使用してドキュメントの読み込みを行っていきます。

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

https://learn.microsoft.com/en-us/azure/search/search-limits-quotas-capacity#indexer-limits

以下のようなインデックスを作成します。
![newsindex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322105/b49c6449-3cb7-4540-2371-5ca253e4c999.png)

作成したインデックスに対しドキュメントをアップロードし読み込ませます。以下サンプルコードです。
```python
index_name = "news-index"
credential = AzureKeyCredential(key)

# read the CNN dailymail dataset in pandas dataframe
df = pd.read_csv('PASS TO DATA') 

# Convert the dataframe to a list of dictionaries
documents = df.to_dict('records')

search_client = SearchClient(endpoint=service_endpoint, index_name=index_name, credential=credential)
result = search_client.upload_documents(documents)  
print(f"Uploaded and Indexed {len(result)} documents")
```

作成したインデックスを使用して検索を行います。”Clinton Democratic nomination（クリントン民主党指名候補）”というキーワードに関連する記事を検索します。スティーブ・ジョブズ氏の妻ローレン氏がクリントン民主党指名候補への称賛を語った際の記事がヒットしました。

![searchresult.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322105/980b61a9-bcb1-481b-af23-fd0a8978877b.png)

## ドキュメント内部の関連箇所を検索する
ドキュメントの中でユーザーのクエリに関連する情報が記されている箇所をAzure OpenAI Embeddingsモデルを活用して検索していきます。

https://learn.microsoft.com/en-us/azure/ai-services/openai/tutorials/embeddings?tabs=python%2Ccommand-line&pivots=programming-language-python

検索して得たドキュメントを10文を一塊にした文章に分割します。以下サンプルコードです。
```python
def splitter(n, s):
    pieces = s.split(". ")
    list_out = [" ".join(pieces[i:i+n]) for i in range(0, len(pieces), n)]
    return list_out

# Perform light data cleaning (removing redudant whitespace and cleaning up punctuation)
def normalize_text(s, sep_token = " \n "):
    s = re.sub(r'\s+',  ' ', s).strip()
    s = re.sub(r". ,","",s)
    # remove all instances of multiple spaces
    s = s.replace("..",".")
    s = s.replace(". .",".")
    s = s.replace("\n", "")
    s = s.strip()    
    return s

document_chunks = splitter(10, normalize_text(document))
```

Azure OpenAI embeddingsモデルを使用して塊に対するベクトルを計算します。以下サンプルコードです。

https://github.com/MicrosoftDocs/azure-docs/blob/main/articles/ai-services/openai/how-to/embeddings.md#openai-python-0281

```python
def get_embedding(text: str, engine: str = embedding_model):
    embedding = openai.Embedding().create(input=[text], engine=engine)["data"][0]["embedding"]          
    return np.array(embedding).astype(np.float32)

embed_df = pd.DataFrame(document_chunks, columns = ["chunks"]) 

topic of that chunk
embed_df['embeddings'] = embed_df["chunks"].apply(lambda x : get_embedding(x, engine = embedding_model))
```

検索クエリ”trouble so far in clinton campaign（クリントンの選挙キャンペーンで今まで起きたトラブル）”に近い塊を検索できました。以下サンプルコードです。

```python
def search_docs(df, user_query, top_n=3, to_print=True):
    embedding = get_embedding(
        user_query,
        engine=embedding_model,
    )
    df["similarities"] = df['embeddings'].apply(lambda x: cosine_similarity(x, embedding))

    res = (
        df.sort_values("similarities", ascending=False)
        .head(top_n)
    )
    if to_print:
        display(res)
    return res

document_specific_query = "trouble so far in clinton campaign" 
res = search_docs(embed_df, document_specific_query, top_n=2)
```

## テキストを要約し、回答を生成する
検索クエリに特に関連の高い塊が抽出できたので、ユーザーからのクエリ（クリントン民主党指名候補の選挙キャンペーンについて知りたい）に対する回答を生成します。以下サンプルコードです。

```python
result_1 = res.chunks[0]
result_2 = res.chunks[1]
prompt_i = 'Summarize the content about the Clinton campaign given the text provided.\n\nText:\n'+" ".join([normalize_text(result_1)])+ '\n\nText:\n'+ " ".join([normalize_text(result_2)])+'\n\nSummary:\n'

get_completion(prompt_i, model=chat_model) # default temperature is set to 0
```

クリントンの選挙キャンペーンについて、”The Clinton campaign has been criticized for staging events and using party insiders instead of typical Iowans to meet with Hillary Clinton.（クリントンの政治キャンペーンは、会談に典型的なアイオワ州人の代わりに党関係者を起用し、イベントを演出したことで批判されている。）”と要約が生成されました。

# まとめ
生成AIを活用してサービスを実装する上での課題として、「回答の品質が保証できない」や「予期しない回答をしてしまう」などがあると思います。今回の方法がすべてではないですが、信頼のあるソースを基にした回答だけを生成したい時に考慮したい仕組みになると思います。