AQ Tech Blog

FAISSとAmazon Bedrockを使ったベクトル検索の実装

作成者: yasuhiro.kawai|2024年12月05日
本記事はアジアクエスト Advent Calendar 2024の記事です。

概要

クラウドインテグレーション部クラウドソリューション2課の川井です。

今回は、FAISS(Facebook AI Similarity Search)とAmazon Bedrockを活用し、ベクトル検索システムを構築します。

FAISSは、数百万から数十億の高次元ベクトルに対する近似最近傍探索を行うための強力なライブラリであり、大規模データの迅速な検索を実現します。
一方、Amazon Bedrockは、自然言語処理タスクを強化するための大規模言語モデルを提供し、テキストデータを埋め込みベクトルに変換します。

ベクトル検索システムの構築は、Amazon EC2をアプリケーション(API)として構築し、Amazon EC2に対しクエリ文字列を追加して接続した際に、期待するレスポンス(回答)を表示させていきます。

アプリケーション(API)の流れは以下となります。

1. データ収集と準備: 検索対象となるテキストデータを収集し、前処理を行います。

2. テキストの埋め込み: Amazon Bedrockを使用し、収集したテキストデータを数値ベクトルに変換します。
この埋め込みプロセスにより、テキストの意味を数値的に表現します。

3. ベクトルデータのインデックス化: FAISSを用いて、生成された埋め込みベクトルをインデックス化します。
これにより、データベース内のベクトルに迅速にアクセスできるようになります。

4. ベクトル検索の実行: ユーザーからのクエリを受け取り、同様に埋め込みモデルを使ってクエリをベクトル化します。
そして、FAISSを使用して、最も類似したベクトルを検索し、関連するテキスト情報を取得します。

5. 結果の返却: Amazon Bedrockを使用して、自然な言語応答を生成して、検索結果をユーザーに返却し、ブラウザを通じて結果を表示します。

 

構成

構成は以下のとおりとなります。

今回は、PythonでAPI開発を行っていきますので、WebアプリケーションやAPIの構築に適している「Flask」をAmazon EC2にインストールしていきます。そして、アプリケーション(API)として起動する際に、Amazon Bedrockの'Titan Text Embedding'モデルや'FAISS'を用いて下記プロセスを実行し、インデックスを作成していきます。

・ 分割器を使ってテキストをチャンクに分割
・ 各チャンクを埋め込みモデルに渡し、数値ベクトルに変換
・ 得られたベクトルをFAISSベクトルストアに格納

このインデックスは、クエリを実行する際に使用され、情報検索の基盤を提供します。

続いて、アプリケーション(API)及びベクトルストアとして作成したAmazon EC2に対して、ユーザーがクエリを実行します。そして、ベクトルストアから情報を検索できるように、テキストをベクトル化した時に使用したAmazon Bedrockの'Titan Text Embeddings'モデルを使用して、ベクトルストアに対して検索できるように検索クエリをベクトル化していきます。

また、ベクトルストアから返ってきたレスポンスに基づいて、Amazon Bedrockの'Claude'モデルを使用し、ユーザーに返すレスポンスの文章の生成・要約を行っていきます。

※補足
Amazon Bedrockに関しては、東京リージョンでは使用できるモデルに制限があるため、リージョンはバージニア北部を使用しています。

また、今回Amazon EC2を使っていますが、元々は「API GateWay + Lambda」のサーバレス構成で構成を考えていました。しかし、Lambdaの標準ライブラリ以外のライブラリを多々使用するため、Lambda Layerを追加することで実行できる環境を作成するつもりでしたが、Lambda Layerの下記の制限が少々手間だったので、Amazon EC2の中で環境を構築する事にしました。

・ 圧縮されたzipファイルのサイズが50MBを超えないこと
・ 展開後のファイルサイズが250MBを超えないこと

そもそも、Lambda Layerの読み込みには一定の時間がかかりますし、 容量の大きなLayerをいくつもアタッチすると、Lambda関数のコールドスタートに影響がありますので、今回のようなベクトル変換させるような軽微ではない作業には、Lambdaは適していなかったような気がします。

また、ベクトルストア(データベース)として使用しているのはFAISSです。
Amazon Bedrockでデフォルトのベクトルストアとして使用しているのはAmazon OpenSearch Serverlessですが、 検証にそこまでものはいらないと思ったため、無料で提供されているFAISSを使用しています。

環境構築(事前準備)

1. EC2インスタンス作成

Amazon EC2のインスタンス作成は省略します。 使用したイメージや必要な権限等を下記に記載します。

Image: Amazon Linux 2023
配置場所: Public Subnet(インターネットから通信を許可したいので)
セキュリティグループ
インバウンド: 接続させるIPアドレス(全インターネットからのアクセス許可でも問題はありませんが推奨はしません)
アウトバウンド: 全インターネットへのアクセス許可
Role: Amazon Bedrockの権限と、AWS SSMの権限(セッションマネージャーを使わない場合不要)


2. 仮想環境作成

ここのステップは、必ず必要という訳ではありません。

ただ、Amazon EC2内にpipコマンドを使ってPythonのパッケージをインストールしていきますが、既存でインストールされているパッケージの依存関係等でライブラリが上手くインストールされないことがあります。そのため、OS内に仮想環境を作成していきます。

仮想環境とは(ChatGPTより抜粋)

1. 依存関係の管理
パッケージの隔離: 仮想環境を使うことで、特定のプロジェクトの依存関係を他のプロジェクトやシステム全体の環境から分離することができます。
これにより、異なるプロジェクトが異なるバージョンのパッケージを使用することが可能になります。

2. 環境の再現性
安定した開発環境: 仮想環境を使用すると、開発中のプロジェクトの環境を他の開発者やサーバーに簡単に再現することができます。
これにより、環境に依存するバグを減らすことができます。

3. 簡単なインストールとアンインストール
手間のない管理: パッケージをインストールしたりアンインストールしたりする際に、仮想環境を利用すると、
システム全体に影響を及ぼすことなく、必要なものだけを追加できます。

4. システム全体への影響を回避
システムのクリーンさ: 仮想環境内でのパッケージのインストールやアップグレードは、システム全体に影響を与えません。
これにより、他のアプリケーションやプロジェクトが予期せぬ動作をするリスクが減少します。

5. 異なるプロジェクト間での互換性
異なる依存関係の共存: 複数のプロジェクトを同じマシン上で開発する際に、それぞれが異なるライブラリやライブラリのバージョンを必要とする場合でも、
仮想環境を使うことで共存が可能です。


では、作成したAmazon EC2内にセッションマネージャーを使って、インスタンス内にログインしていきます。

Amazon EC2ではデフォルトで「Python3」がインストールされていますので、下記のコマンドで確認します。

> python3 --version

続いて、下記のコマンドで仮想環境を作成していきます。

> python3 -m venv myenv

myenvは、仮想環境名前なのでなんでも大丈夫です。

下記画像のように、lsコマンドでディレクトリが作成されていれば成功です。

仮想環境を作成したら、下記のコマンドでその環境をアクティブにします。
これにより、インストールしたパッケージがその環境にのみ適用されるようになります。

> source myenv/bin/activate

アクティブになると、下記画像のように一番左に作成した仮想環境名が表示されます。

※仮想環境から抜けるには「deactivate」と入力します。

 

3. 各種インストール

3-1. Pythonパッケージ管理

Pythonのパッケージ管理をインストールしていくために、まず「python-pip」をインストールします。

> sudo yum install python-pip

下記コマンドでインストールされたかを確認します。
> pip --version

また、仮想環境にインストールされたパッケージは
/root/myenv/lib/python3.9/site-packages
で確認できます。


3-2. Flask

次は、Amazon EC2をアプリケーション(API)として使用するので、「Flask」をインストールします。

> pip install Flask

WARNING: You are using pip version 21.3.1; however, version 24.3.1 is available.
You should consider upgrading via the '/root/myenv/bin/python3 -m pip install --upgrade pip' command.
上記のように、アップグレードしてくださいという警告が出ましたので、下記コマンドを入力します。

> pip install --upgrade pip

一旦、アプリケーションとして動くか確認します。
下記の簡単なコードで動かしてみます。

from flask import Flask

app = Flask(__name__)

@app.route('/', methods=['GET'])
def home():
return '<h1>Hello, World!</h1><p>Welcome to my Flask app running on EC2!</p>'

if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)

ルート( / )に接続した際に、想定どおりに画面が開いているのでアプリケーションとして起動しています。


3-3. Pythonパッケージ(ライブラリ)

後は、必要なものをインストールしていきます。

1. langchain-community
> pip install langchain-community
用途: LangChainのコミュニティ拡張パッケージです。公式のLangChainライブラリに加えて、コミュニティメンバーが提供する追加の機能やコンポーネントが含まれています。
特にドキュメントローダーやカスタム機能、PyPDFLoaderなどのPDF処理機能がこのパッケージに含まれています。

2. langchain
> pip install langchain
用途: LangChainは、さまざまな自然言語処理(NLP)タスクを実行するためのフレームワークです。言語モデルやデータ処理を統合して、生成AIアプリケーションを構築するために使用されます。

3. langchain-aws
> pip install langchain-aws
用途: LangChainとAWSサービスを統合するためのパッケージです。Amazon Bedrockや他のAWSサービスを利用して、言語モデルを簡単に使用できるようにします。

4. faiss-cpu
> pip install faiss-cpu(または、pip install faiss-gpu)
用途: 大量のベクトルデータに対する高速検索を提供します。データのインデックス化と検索の効率化を行うために使用されます。
例: ベクトルデータベースとして、テキストの埋め込みや類似度検索に利用。

5. boto3
> pip install boto3
用途: Boto3は、AWSのPython SDKです。これを使うことで、PythonからAWSの各種サービス(S3、EC2、DynamoDBなど)を操作できます。


下記は必要と思いインストールしましたが、必要なかったパッケージです。
インストールするかどうかはお任せします。
6. pypdf
> pip install pypdf
用途: PDFファイルを操作するためのライブラリです。

 

環境構築(API構築)

1. インデックス作成

「インデックス」とは、PDFから取り出したテキストデータを検索や情報取得に最適化するために、ベクトル表現で整理・構造化したデータベースです。
以下の手順でインデックスを作成します。

1. データの読み込み
'PyPDFLoader'クラスを使って、指定したPDFファイルからテキストを抽出します。
※今回、PDFファイルはインターネットで誰でも見れるものを使いました。

2. テキストの分割
'RecursiveCharacterTextSplitter'クラスを用いて、テキストを小さなチャンク(塊)に分割します。
チャンクは検索時に効率良くアクセスできるように、サイズや重なりを設定して分割されます。

3. ベクトルへの変換(埋め込み)
'BedrockEmbeddings'クラスを使って、各チャンクを「ベクトル」と呼ばれる数値の並びに変換します。このベクトル表現により、類似度検索などが可能になります。
今回は'amazon.titan-embed-text-v2'モデルを利用してテキストをベクトル化しています。

4. ベクトルストア(インデックス)の作成
'VectorstoreIndexCreator'クラスが、FAISS(Facebook AI Similarity Search)ライブラリを使って、ベクトルを格納するインデックスを構築します。
このインデックスは、クエリに対して類似のテキストを高速に検索するためのデータ構造です。

この「インデックス」を使うことで、PDF内のテキストを効率よく検索し、関連性の高い情報を素早く取り出すことができます。

構成図内の赤枠箇所を実装します。では、インデックスを作成するためのコード書いていきます。
まず、Amazon EC2内にコードを記載するためのファイルを作成します。

> touch app.py

app.pyが作成されていればファイル作成は完了です。

以下がインデックスを作成するコードです。

from flask import Flask, request, jsonify
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_aws import BedrockEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.indexes import VectorstoreIndexCreator


# インデックスの作成
def create_index():
# PDFファイルを読み込む
data = PyPDFLoader('https://ここはインターネットにあるPDFファイルのURLを入力してください)

# テキスト分割器の準備
split = RecursiveCharacterTextSplitter(
separators=["\n\n", "\n", " ", ""],
chunk_size=100,
chunk_overlap=10
)

# 埋め込みの準備
embeddings = BedrockEmbeddings(
model_id='amazon.titan-embed-text-v2:0',
region_name='us-east-1'
)

# インデックス作成オブジェクトの作成
index = VectorstoreIndexCreator(
text_splitter=split,
embedding=embeddings,
vectorstore_cls=FAISS
)

# インデックスを作成
db_index = index.from_loaders([data])
return db_index

# グローバルにインデックスを作成
db_index = create_index()

軽く説明しますと、インデックスを作成する、create_index( )関数を定義します。

関数の中身は、'PyPDFLoader'クラスでインターネット上の指定URLからPDFファイルを読み込み、テキストデータを取得します。

'RecursiveCharacterTextSplitter'クラスにより、読み込んだテキストを100文字単位のチャンクに分割し、チャンク間に10文字の重なり(オーバーラップ)を持たせます。

'BedrockEmbeddings'クラスによって、テキストチャンクを数値ベクトルに変換します。この処理で、各チャンクの意味が「ベクトル空間」にマッピングされ、類似性検索が可能になります。ここでは、amazon.titan-embed-text-v2モデルを使用しています。

'VectorstoreIndexCreator'クラスを使って、分割されたテキストのベクトルをFAISSベクトルストアに保存し、検索インデックスを作成します。作成されたインデックスdb_indexが返却され、PDF内のテキストを効率よく検索する準備を整えます。

最後に、このインデックスをdb_indexとしてグローバル変数に保存します。これにより、他の部分のコードでdb_indexを使って類似度検索などを実行できます。

また、Amazon Bedrockで'titan-embed-text'モデルを使用できるように、Amazon Bedrockのコンソール画面でアクセス許可していきます。一緒に'claude'モデルもアクセス許可していきます。

Amazon Bedrockのコンソール画面で「モデルアクセス」をクリックします。
また、バージニア北部でしか使えないモデルあるため、リージョンは「バージニア北部」に変更します。今回で言えば'claude'モデルはここでしか使用できません。「titan」と検索して、「Titan Text Embeddings V2」に対してアクセス許可のリクエストを行ってください。
アクセスが許可されると下記画像のとおりに「アクセスが許可されました」と表示されます。続いて、「claude」モデルも同様にアクセス許可のリクエストを送ります。
アクセスが許可されると、下記画像のとおりに「アクセスが許可されました」と表示されます。また、モデルIDに関してはAmazon Bedrockのコンソール画面の「ベースモデル」の所から対象のモデルを確認すれば、モデルIDは確認できます。ドキュメントからも確認できますので、お好きな方から確認してください。

 

2. テキスト生成関数の定義

続いては、ベクトルストアからのレスポンスに基づいて、ユーザーに返すテキスト生成をするモデルを使用するための関数を定義していきます。
構成図の赤枠で示した部分です。

以下がテキスト生成関数を定義するコードとなります。

# LLMを接続する関数
def llm():
llm = BedrockLLM(
region_name='us-east-1',
model_id='anthropic.claude-v2:1',
model_kwargs={
"max_tokens_to_sample": 3000,
"temperature": 0.1,
"top_p": 0.9
}
)
return llm

こちらも解説します。

llm( ) 関数を定義し、モデルのインスタンスを作成して返します。この関数を呼び出すと、設定されたLLMオブジェクトが返され、API経由で利用できるようになります。

'BedrockLLM'クラスを使って、言語モデルのインスタンスを作成します。これは、Amazon Bedrock経由でAIモデルを呼び出すための設定です。モデルは、'anthropic.claude-v2:1'を使用します。

'model_kwargs'は、モデルの応答生成に関するパラメータを設定します。
'max_tokens_to_sample=3000'は、生成される応答の最大トークン数です。この例では、最大3000トークンまで生成するよう指定しています。
'temperature=0.1'は、出力のランダム性を制御するパラメータで、値が低いほど出力が安定します(より「確定的」な結果が得られる)。
'top_p=0.9'は、出力の確率分布の上位何パーセントまでをサンプリングに使うかを設定します。高い値ほど多様な出力が得られる可能性があります。

そして、設定したllmオブジェクトを関数の呼び出し元に返します。これにより、他のコードからこのllmオブジェクトを使って、テキスト生成などの言語モデル機能を利用できます。

 

3. APIエンドポイントの定義

最後に、APIエンドポイントとして動作するように定義をしてきます。以下がコードです。

app = Flask(__name__)

# APIエンドポイントの定義
@app.route('/search', methods=['GET'])
def search():
question = request.args.get('query') # クエリ文字列を取得
if not question:
return jsonify({'error': 'No query provided'}), 400

# LLMを取得し、クエリを実行
rag_llm = llm()
response = db_index.query(question=question, llm=rag_llm)

# 結果を返す
return jsonify({'response': response})

if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)

こちらも軽く説明すると、

クエリ処理の流れは、/searchに接続する際に追加したクエリ文字列を取得して、ベクトルストア db_index に対してquestionを使って検索を行います。db_indexは、PDFのテキストデータから生成されたベクトルインデックスで、クエリ内容に関連する情報を取得します。
クエリによって、db_indexは質問に最も関連性の高いテキストチャンクを返します。この段階で、質問に対する関連情報が選択されます。

そして、この関連情報がLLM(大規模言語モデル)'anthropic.claude-v2:1'に渡され、最終的な回答や補足情報が生成されます。LLMは、ベクトル検索から返された情報を参考にしながら、質問に答えるテキストを作成します。

環境構築(完成コード)

こちらがコードの全体像です。

from flask import Flask, request, jsonify
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_aws import BedrockEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.indexes import VectorstoreIndexCreator
from langchain_aws import BedrockLLM

app = Flask(__name__)

def create_index():
data = PyPDFLoader('https://PDFがあるURL')

split = RecursiveCharacterTextSplitter(
separators=["\n\n", "\n", " ", ""],
chunk_size=100,
chunk_overlap=10
)

embeddings = BedrockEmbeddings(
model_id='amazon.titan-embed-text-v2:0',
region_name='us-east-1'
)

index = VectorstoreIndexCreator(
text_splitter=split,
embedding=embeddings,
vectorstore_cls=FAISS
)

db_index = index.from_loaders([data])
return db_index

db_index = create_index()


def llm():
llm = BedrockLLM(
region_name='us-east-1',
model_id='anthropic.claude-v2:1',
model_kwargs={
"max_tokens_to_sample": 3000,
"temperature": 0.1,
"top_p": 0.9
}
)
return llm


@app.route('/search', methods=['GET'])
def search():
question = request.args.get('query')
if not question:
return jsonify({'error': 'No query provided'}), 400

rag_llm = llm()
response = db_index.query(question=question, llm=rag_llm)

return jsonify({'response': response})

if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)

 

API実行

では、APIを実行していきます。app.pyを作成したディレクトリで下記のコマンドを実行します。

> python3 app.py

実行が成功すれば下記のようになります。
警告が出ていますが、これは開発サーバーなので本番で使用しないでくださいと記載があるだけなので、動作に影響はありません。

では、今回インポートしたPDFの中に、病気休暇は9日あるという記載がありましたので、
これを作成したFAISSベクトルストア(データベース)から検索してみます。ブラウザから、パブリックIPでAmazon EC2に接続します。
その際に、クエリ文字列で「How many days of sick leave are available per year?(年間で病気休暇は何日ありますか)」とクエリ文字列に追記して接続します(PDFが英語で書かれているので、英語で質問文を記載しています)

http://パブリックIP/search?query='How many days of sick leave are available per year?'

以下の画像が返却された回答です。

英語
"response": " Based on the context provided, it says \"Nine days of sick leave is available to employees.\" Therefore, the number of days of sick leave available per year is 9 days."

日本語に変換
"response": " 提供されたコンテキストに基づくと、「従業員は 9 日間の病気休暇を利用できます。」と書かれています。したがって、1 年に利用できる病気休暇日数は 9 日です。"

ベクトルストアにクエリした所、レスポンス内容に基づいて、テキストが生成されてブラウザ上に回答が返却されてきました。

以上、FAISSとAmazon Bedrockを使ったベクトル検索の実装でした。

まとめ

今回は、ベクトル検索をAmazon BedrockのGUI上では行わず、自作APIを作成してベクトル検索を試しました。
各モデルは、Pythonのパッケージ(ライブラリ)をインストールして使えば、とても簡単に使用することができます。

また、ベクトルストアとしては、AWSが提供しているサービスではなく、無料で提供されているFAISSを使用してみましたが、こちらもPythonのパッケージ(ライブラリ)をインストールしてしまえば、とても簡単に利用可能です。

自作でベクトル検索の実装をしたことで、Amazon Bedrockの理解力も上がったのではないかと思います。
AWSのAI関係のサービスは、仕事上で関わる機会が余りありませんので、こういった検証を行う事で少しずつ学んでいけたらいいと思います。
では、また別の記事を執筆しますので、引き続きよろしくお願いいたします。

クラウドインテグレーション部
クラウドソリューション2課
川井 康敬