生成AIが勢いを増している今、遅いかもしれませんが、会社用にオープンソースを使用した、ローカルで動くRAGを作成しました。
できるだけ費用を0で実施したかったため、オープンソースのOpenSearchやgemma2モデルを使用して作成しました。
しかしながら、良い使い道が見つからず、作ったのに使われないのはもったいないので、ここにまとめておきます。参考にしていただけると幸いです。
長くなるので、2回にわけて、今回はOpenSearchの導入まで、次回でそれを利用したRAGの作成を行います。
なお、Widnows11で開発しております。
そもそもRAGとは
この記事を見ている人は必要ないかもしれませんが、RAGについてChatGPTに聞きました。
つまり、なにか検索するのに、AIが検索の手助けしてくれる感じでしょうか。
メリットは社内の情報など汎用AIが知っていない情報も検索対象にできる点だと思います。
RAGを使えば、モデルを一から作ることなく、汎用AIで、社内の情報を検索、まとめてくれます。
さらにこれらをローカルで行えば、外部に情報が漏れることなく、閉鎖した環境で調べ物ができるというすぐれものです。
まさに散らかった社内の情報をまとめて検索する社内検索システムにぴったりな手法だと思いました。
例えば、ドライブ内に業務ごとにフォルダーは別れて入るけど、テキスト形式やdoc形式、csv形式など統一感のなく、検索しづらい社内情報を一つにまとめるいい機会だと思いました。
準備
どこかに作業ディレクトリを作成します。
私の場合は、"D:\codes\python\rag-opensearch"
にしました。
ここを拠点に開発していきます。
OpenSearch
OpenSearchはオープンソースの検索エンジンです。似たものにElasticsearchというものがありますが、ライセンス関係で何やら違うとか、今回はオープンソースのOpenSearchを使いました。
OpenSearchのダウンロード
OpenSearchはこちらからダウンロードできます。
作業ディレクトリに展開します(特にここにする必要はありません)。
パスワードの設定と接続確認
パスワードを設定します。
# コマンドプロンプト(パスワードは適宜変更してください)set OPENSEARCH_INITIAL_ADMIN_PASSWORD="tG7$9!5A"
でパスワードを変更できたら、"opensearch-windows-install.bat"
を起動します。
起動には少し時間がかかります。
起動できたら、以下のコマンドで接続できるか確認します。パスワードは特殊文字がある場合は、エスケープする必要がありました。
1 2 |
# コマンドプロンプト curl.exe -X GET https://localhost:9200 -u "admin:\"tG7$9!5A\"" --insecure |
以下のように表示されれば成功です。
1 2 3 4 5 6 |
{ "name":"....", "cluster_name":"....", "cluster_uuid":"....", .... } |
日本語トークナイザーのインストール
日本語を検索に使用するためには、別でトークナイザーが必要になります。
そこで、日本語のトークナイザーとして、analysis-kuromoji
をインストールします。
1 2 3 4 5 |
# コマンドプロンプト # opensearch-windows-install.batがあるディレクトリまで移動 cd D:\codes\python\rag-opensearch\opensearch-2.17.1-windows-x64\opensearch-2.17.1 # 拡張機能をインストール .\bin\opensearch-plugin install analysis-kuromoji |
-> Installed analysis-kuromoji with folder name analysis-kuromoji
となれば成功です。成功した場合は次の以下はスキップしてください。
Pythonのインストール
Pythonのダウンロードとインストール
Pythonをインストールします。今回はPython3.10.11をインストールしました。
仮想環境の作成
インストールしたPythonで仮想環境を作成します。
どこか作業ディレクトリを作成し、そこに移動して仮想環境を作成します。
1 2 3 4 |
# コマンドプロンプト # 作業ディレクトリへ移動 cd D:\codes\python\rag-opensearch py -3.10 -m venv .venv |
仮想環境をアクティブにします。
1 2 |
# コマンドプロンプト ./.venv/Scripts/activate |
アクティブになっているか確認します。
1 2 |
# コマンドプロンプト py --version |
Python 3.10.11
と表示されればOKです。
ログフォルダとモデル用フォルダと検索データ用フォルダの作成
ログファイルの保存用にディレクトリ"./log"
を作成しておきます。また、モデル保存用に"./llm/model"
と"./embeddings/models"
を作成しておきます。さらに、検索データ用のフォルダとして"./data"
を作成しておきます。
あとで気づきましたが、フォルダ名がややこしいので、適宜変更してもらって大丈夫です。その場合はこの後のプログラムで参照先も変更するのをお忘れなく。
1 2 3 4 5 6 |
# コマンドプロンプト # 作業ディレクトリで mkdir log mkdir llm¥model mkdir embeddings¥models mkdir data |
埋め込みモデルとLLMモデルのダウンロード
RAGに必要なモデルたちをダウンロードします。以下の2つです。
- 埋め込みモデル(intfloat/multilingual-e5-large)
これは下に記載するfilebulkinsert.py
でダウンロードするため、省略します。 - LLMモデル(gemma-2-2b-jpn-it)
こちらのリンクからダウンロードします。今回はQ4_K_Mのモデルを使います。下図のようにダウンロードします。 ダウンロードしたファイルは、今回は作業フォルダの"./llm/model"
の中に保存します。ちなみにサイトのcontext_length
がモデルのコンテキスト長です。
ちなみに、モデルのQ4とかQ8とかは量子化の方法の違いによるもので、数字が小さいほどサイズが小さいけど精度が低いみたいです。
OpenSearchにデータを格納する
検索データのダウンロード
最低限の準備は整ったのでここから、まずOpenSearchにデータを格納します。
実際に社内RAGとして使うなら、検索データは社内のいろんな情報であり、まず、それらをまとめる必要があります。ここが一番大変かもしれません。
今回はRAGの検索対象として、青空文庫にある2022アクセスランキングから上位10作品までを使用します。
ダウンロードリンクからテキストファイルを作業ディレクトリの"./data"
に格納します。"./zip_file"
は元ファイルを入れているだけなので、気にしないでください。
検索データ追加のプログラム
ダウンロードした作品たちをOpenSearchに追加していきます。
追加するプログラム(filebulkinsert.py)を以下にまとめて記載します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# filebulkinsert.py # Python from opensearchpy import OpenSearch from opensearchpy.helpers import bulk, parallel_bulk from setting import settings import glob import os import jsons from datetime import datetime as dt from tqdm import tqdm import requests from requests.packages.urllib3.exceptions import InsecureRequestWarning import logging import re # ログの設定 logger = logging.getLogger(__name__) fmt = "%(asctime)s %(levelname)s %(name)s :%(message)s" logging.basicConfig(filename="./log/filebulkinsert.log", encoding="utf-8", level=logging.INFO, format=fmt) requests.packages.urllib3.disable_warnings(InsecureRequestWarning) # SSLの警告を無視する index_name = "rag" # OpenSearchの作成するindex名 folderPath = "D:\\codes\\python\\rag-opensearch\\data" # テキストファイルが入っているフォルダのパス # hugingfaceを使用する from huggingface_hub import snapshot_download from langchain_huggingface.embeddings import HuggingFaceEmbeddings from langchain.text_splitter import RecursiveCharacterTextSplitter # テキストをチャンクに分割するためのクラス # model_name = "intfloat/multilingual-e5-base" # model_name = "intfloat/multilingual-e5-small" model_name = "intfloat/multilingual-e5-large" # 使用する埋め込みモデル model_path = f"D:/codes/python/rag-opensearch/embeddings/models/{model_name}" # ローカルの保存先 print("model_path: ", model_path) logging.info("model_path: " + model_path) # モデルのダウンロード download_path = snapshot_download( repo_id=model_name, local_dir=model_path, # モデルの保存先 local_dir_use_symlinks=False, # ※1 https://wonderhorn.net/programming/hfdownload.html ) print("download_path: ", download_path) logging.info("download_path: " + download_path) # 埋め込みモデルの読み込み embeddings = HuggingFaceEmbeddings( model_name=download_path, model_kwargs={"device": "cuda:0"} ) # GPUを使用する場合(cudaのインストールとpytorchのインストールが必要) # embeddings = HuggingFaceEmbeddings(model_name=download_path, model_kwargs={"device": "cpu"}) # CPUを使用する場合 # FileObjクラス class FileObj: def __init__( self, path, content, ): self.path = path # ファイルのパス self.content = content # ファイルの内容 def set_embedings(self, vector_field): self.vector_field = vector_field # ファイルの内容の埋め込みベクトル # OpenSsearchに挿入するでデータを作成する関数 def makeInsertFileList(file_path): fileContentList = [] # ファイルの内容を入れるリスト fileObjList = [] # FileObjを入れるリスト documents = [ filePath ] # 埋め込みを行うテキストを格納するリスト(はじめにpathを入れる。これはファイル名も検索対象にするため(必要なければいらない)) is_exist_file = os.path.isfile(filePath) # ファイルが存在するかどうか # ファイルが存在すればtextファイルを見に行く if is_exist_file: with open(file_path, "r", encoding="shift_jis") as txtfile: content = txtfile.read() # contentをUTF-8に変換 content = content.encode("utf-8").decode("utf-8") content = re.sub(r"[\n\s\u3000]", "", content) # 改行、空白、全角スペースを削除 # テキストが長い場合のためにをチャンクに分割(400文字ごとに分割) text_splitter = RecursiveCharacterTextSplitter( separators=[":", "。", "、", ",", "."], # 分割する文字 chunk_size=400, # チャンクの文字数 chunk_overlap=20, # チャンクオーバーラップの文字数 ) documents.extend(text_splitter.split_text(content)) # print("len(documents):", len(documents), documents) fileContentList.extend(documents) # リストにFileObjインスタンスを追加(この時点では埋め込みをセットしない) fileObjList.extend( [ FileObj( filePath, content, ) for content in documents ] ) print(file_path + ":変換終了 ファイルパス:" + filePath + "ドキュメント数:" + str(len(fileContentList))) logging.info(file_path + ":変換終了 ファイルパス:" + filePath + "ドキュメント数:" + str(len(fileContentList))) # データない場合はここで終了 if len(fileContentList) == 0: return [] print(filePath + ":embed開始") logging.info(filePath + ":embed開始") embeddingsList = embeddings.embed_documents(fileContentList) print(filePath + ":embed終了 次元数: " + "[" + str(len(embeddingsList)) + ", " + str(len(embeddingsList[0])) + "]") logging.info( filePath + ":embed終了 次元数: " + "[" + str(len(embeddingsList)) + ", " + str(len(embeddingsList[0])) + "]" ) # ファイルオブジェクトに埋め込みをセットしたリスト actions = [] for i, file in enumerate(fileObjList): # Fileオブジェクトをjsonに変換 fileDict = file.__dict__ # vector_fieldを追加(埋め込んだベクトル) fileDict["vector_field"] = tuple(embeddingsList[i]) # リストに追加する actions.append( { "_op_type": "index", "_index": index_name, # "_id": xxxx, # 必要であれば自分でidを設定する。なければ自動でふられる "_source": jsons.dump( fileDict ), # json.dumpsとかもあるけど、jsons.dumpでうまくいったので、これを使用している } ) return actions print("OpenSearchに接続します") logging.info("OpenSearchに接続します") # OpenSearchの接続先 host = "localhost" # ホスト名 port = 9200 # ポート番号 auth = ("admin", '"tG7$9!5A"') # ユーザー名とパスワード # OpenSearchの接続(参考:https://opensearch.org/docs/latest/clients/python-low-level/) client = OpenSearch( hosts=[{"host": host, "port": port}], http_compress=True, http_auth=auth, use_ssl=True, verify_certs=False, ) print("OpenSearchに接続しました") logging.info("OpenSearchに接続しました") # indexの削除 # response = client.indices.delete( # index = index_name # ) # indexがなければ、indexの作成とindexの中のデータに対しての設定 if not client.indices.exists(index=index_name): print("index作成") logging.info("index作成") client.indices.create(index=index_name, body=settings) print("index作成完了") logging.info("index作成完了") else: print("indexが存在します") logging.info("indexが存在します") # データの挿入を行う関数 def bulk_insert(data): succeeded = [] # 成功したデータ failed = [] # 失敗したデータ # データの並列挿入(参考:https://github.com/opensearch-project/opensearch-py/blob/main/guides/bulk.md) for success, item in parallel_bulk( client, actions=data, chunk_size=400, request_timeout=6000, ): # 成功時と失敗時でリストに追加 if success: succeeded.append(item) else: failed.append(item) # エラーがあればエラーを表示 if len(failed) > 0: print(f"There were {len(failed)} errors:") for item in failed: print(f"{item['index']['error']}: {item['index']['exception']}") # 成功したデータがあれば成功したデータを表示 if len(succeeded) > 0: print(f"Bulk-inserted {len(succeeded)} items.") print("folderPath: ", folderPath) logging.info("folderPath: " + folderPath) filePathList = glob.glob(folderPath + "/*.txt") print("ファイル数: ", len(filePathList)) logging.info("ファイル数: " + str(len(filePathList))) for filePath in tqdm(filePathList): print("データ挿入開始") logging.info("データ挿入開始") # OpenSearchに挿入するデータを作成する insertFileList = makeInsertFileList(filePath) # データがあるときだけ挿入 if len(insertFileList) > 0: bulk_insert(insertFileList) print("終了") logging.info("終了") |
足りないライブラリは適宜 pip install 〇〇
でインストールしてください。
opensearchpyは pip install opensearch-py
でインストールできると思います。
また、中で参照しているsetting.py
は以下です。OpenSearchのインデックスの設定を記載しています。インデックスの設定については、こちらなどを参考にしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# setting.py # Python settings = { "settings": { "index": { "knn": True, }, "number_of_shards": 1, "number_of_replicas": 0, "analysis": {"analyzer": {"default": {"type": "custom", "tokenizer": "kuromoji_tokenizer"}}}, }, "mappings": { "properties": { "path": {"type": "text", "analyzer": "kuromoji"}, "content": {"type": "text", "analyzer": "kuromoji"}, "vector_field": { "type": "knn_vector", "dimension": 1024, "method": { "name": "hnsw", "space_type": "l2", "engine": "faiss", "parameters": {"ef_construction": 512, "m": 40}, }, }, } }, } |
埋め込みモデルを変更する場合は、vector_field
のdimension
などを適切に設定しないとエラーとなりますので、モデルのパラメータをしっかり確認してから、設定しなければなりません。
プログラムの基本的な流れは、以下の通りです。
- 埋め込みモデルのダウンロード
- 対象ファイルの読み込みと、埋め込みベクトル化
- OpenSearchに追加
GPUを使用する方法
ここで、プログラムの始めの方の埋め込みモデルの読み込みでGPUを使用する場合とCPUを使用する場合に分けていますが、
体感でGPUを使用したときの方が埋め込み処理が10倍以上速かったと思いましたので、
GPUを持っている方はできれば使用したほうが良いと思います。
1 2 3 4 5 |
# 埋め込みモデルの読み込み embeddings = HuggingFaceEmbeddings( model_name=download_path, model_kwargs={"device": "cuda:0"} ) # GPUを使用する場合(cudaのインストールとpytorchのインストールが必要) # embeddings = HuggingFaceEmbeddings(model_name=download_path, model_kwargs={"device": "cpu"}) # CPUを使用する場合 |
ただGPUを使用するにはCUDAのツールキットとPyTorchをインストールする必要があります。
具体的な方法はこちらが参考になりますが、バージョンによって、うまくいかないことが結構ありますので、気を付けてインストールしてください。
例として私の環境は、以下のとおりです。
- Windows11Pro
- GeForce RTX 3060 Ti
- cuda 12.4
- cuDNN 9.3.0
- pytorch 2.5.1+cu124
- Python 3.10.11
以下に手順を記載します。
CUDA Toolkitのインストール
まずGPUの型を確認し、こちら(表:Compute Capability, GPU semiconductors and Nvidia GPU board products)でCompute Capabilityを確認します(下図参照)。
そして、表: Compute Capability (CUDA SDK support vs. Microarchitecture)で対応しているCUDA SDK Version(s)を確認します(下図参照)。
私の場合、Compute Capabilityが8.6で11.4-11.4のバージョンが対応しているようですが、緑背景のバージョンも対応しているようで、12.4でも大丈夫でした。
そして、対象のCUDA Toolkitをダウンロードして、インストールします。
cuDNNのインストール
次に、こちらから自分の環境を選択し、cuDNNをダウンロードして、インストールします。アーカイブはこちらから。
パスの確認
インストールできたらパスが通っているか確認したほうが良いです。
試しにCUDAのバージョン確認を実行します。
1 2 |
# コマンドプロンプト nvcc -V |
パスが通っていれば下図のように、CUDAのバージョンが表示されます。
もしパスが通っていなければ、
先程のこちらのように、設定>システム>バージョン情報>システムの詳細設定から環境変数をクリックし、
システム環境変数のPathをクリックし、編集します。下図のようにCUDAとcuDNNのパスを入力し、OKをクリックして終了です。
これでもう一度、 nvcc -V
を入力し、バージョン情報は表示されなければ再起動を試してみるといいかもしれません。
PyTorchのインストール
CUDAをインストールしたあとはPyTorchをインストールします。
こちらから、自分にあった環境の選択するとコマンドが表示されます(下図参照)。
私の場合は pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
です。
これをPythonのインストールで作成した、仮想環境の中で実行します。
GPUを使用するまでの流れは以上です。
cuDNNのバージョン確認方法
ちなみにcuDNNのバージョンの確認方法は、こちらに方法があります。以下に方法を記載します。
まず、コマンドプロンプトで where cudnn64_*.dll
と入力して、cuDNNの場所を特定します。
私の場合は、C:\tools\cuda\bin\cudnn64_9.dll
でした。そして、パスのbin以降(bin\cudnn64_9.dll
の部分)を変更して、以下のように入力します。
find "#define" "C:\tools\cuda\include\cudnn_version.h"
すると、下図のように結果が表示されるので、CUDNN_MOJOR 9
, CUDNN_MINOR 3
, CUDNN_PATCHLEVEL 0
の数字を取って9.3.0とわかるようです。
OpenSearchへの検索データの追加
プログラムが完成して、GPUを利用できる方はGPUが利用できるようになったら、filebukkinsert.py
を実行します。
実行する際は、作成した仮想環境に入ってから実行することをお忘れなく。
1 2 3 4 5 |
# コマンドプロンプト # 仮想環境に入る(すでに入っている場合は不要) ./.venv/Scripts/activate # 実行 py filebuilinsert.py |
1ファイルずつ追加していき、私の場合、一つ数秒~数十秒で追加できましたので、合計数分で追加完了しました。
CPUだけの場合は、もっと時間かかります。
OpenSearchの検索
データの格納が終わりましたので、OpenSearchで検索ができるか確認します。
以下のコマンドをコマンドプロンプトから実行します。このコマンドはキーワード”羅生門”が含まれる文章をすべて検索するものです。
1 2 3 |
# コマンドプロンプト # OpenSearchから全文検索 curl -XGET "https://localhost:9200/rag/_search" -u "admin:\"tG7$9!5A\"" --insecure -H "Content-Type: application/json" -d "{\"query\": {\"match\":{\"content\":\"羅生門\"}}}" |
以下のように結果が出力されます。
ヒットした項目がすべて表示されています。大量の数字の羅列は埋め込みベクトルです。少し見づらいので、戻り値をpathのみに絞ってhighlight機能で文中の“羅生門”の部分を抽出し強調表示してみます。
1 2 3 |
# コマンドプロンプト # OpenSearchから全文検索(path, contentのみ返却) curl -XGET "https://localhost:9200/rag/_search" -u "admin:\"tG7$9!5A\"" --insecure -H "Content-Type: application/json" -d "{\"query\": {\"match\":{\"content\":\"羅生門\"}}, \"_source\": [\"path\"], \"highlight\": { \"fields\": { \"content\": {}}}}" |
結果は下図の通り、
確かに、ヒットした項目に”羅生門”が含まれていることがわかります。
正直これだけでも、社内の文章を全文検索するデータベースとして機能しているので、ここからRAGを利用しなくても、このOpenSearchを社内のどこかのサーバに立ち上げとくだけで、簡単に社内の文章検索が可能です。highlight機能は検索フォームなどで検索結果を表示する際に使えそうですね。
OpenSearchにも検索方法として、この他にも様々なものがありますし、別にWebなどで検索フォームを作成すれば、より活用することができると思います。
まとめ
今回はRAGのための下準備とOpenSearchのインストール、データの追加を行いました。
次回は作成したOpenSearchを利用して、RAGを実施します。
参考サイト
- https://opensearch.org/docs/latest/install-and-configure/install-opensearch/windows/
- https://huggingface.co/bartowski/gemma-2-2b-jpn-it-GGUF
- https://www.aozora.gr.jp/access_ranking/2022_txt.html
- https://huggingface.co/intfloat/multilingual-e5-large
- https://qiita.com/kongo-jun/items/d7ea50a02d14d76766cb
- https://qiita.com/rikinumata/items/b3729ad5144a3e98c6b4
- https://shift101.hatenablog.com/entry/2022/02/27/200953
- https://zatoima.github.io/aws-elasticsearch-commands-lists.html
Pythonのライブラリ一覧
一応、今回と次回で私の環境でPythonにインストールしたライブラリの一覧を以下に載せておきます。
コメント