はじめに
前編では環境構築から始め、ファイルを添付してRAGが問題なくできるところまでを確認しました。
今回はOpen WebUIでのRAGがどのように実行されているのか、コードを実際に見ていきます。
確認したバージョン
・Open WebUI Ver. 0.6.2
・Ollama Ver. 0.6.1
ファイルアップロードのコード実装
Open WebUIには事前に貯めておきたい知識を置いておくナレッジベースという機能があります。
ファイルを添付したときにどのように処理されているのかを確認していきます。
なお、公式のドキュメントにはドキュメントパース部分はget_loader
関数を見ろ、書いてあります。
ただしそのリンクは古くなっており、現在(v0.6.2)のコードではありません。
この部分を追うために、テストファイルを添付した際のログから確認していきます。
テスト用のナレッジベースを作り、テストファイルとしてはRFC3501のPDF版であるrfc3501.txt.pdf
を添付します。
アップロードの際にどのような処理が行われているのか、ログを確認します。
docker container logs open-webui
画像の1行目にopen_webui.routers.files:upload_file
という記載があります。
これはopen_webui/routers/files.py
のupload_file
関数が呼ばれていることを意味します。
GitHubのコードで言うとこの81行目です。
このupload_file
関数の一番重要なファイルを処理する部分は上記のリンク部分より少し進んだprocess_file
関数(125行目)です。
このファイルを処理していると思われるprocess_file
関数はopen_webui/routers/retrieval.py
で定義されています。
GitHubのコードはこの946行目です。
この関数を読み進めるとloader
を読み込み(1021行目)、そのload
関数を使ってドキュメントを読み込んでいること(1030行目)がわかります。
そこでさらにLoader
クラスを確認していきますと、load
関数の具体的な中身が見えてきます。
Loader
クラスを定義しているのはopen_webui/retrieval/loaders/main.py
であり、コードはこの169行目です。
_get_loader
関数では、self.engine
の値に分かれてその処理内容が記載されています。
デフォルトの設定だとself.engine
にはtika
やdocument_intelligence
などは入らないため、一番下のelse
に分岐します。
このself.engine
の値は管理者パネルの「ドキュメント」の「Content Extraction Engine」に当たります。
「Content Extraction Engine」に「デフォルト」を指定した場合は、一番下のelse
に行くため、コードにあるように拡張子ごとに処理が分岐されています。
例えば拡張子が「.pdf」になっているPDFファイルの場合を見てみますと、「PyPDFLoader」つまりLangChainのpyPDFLoaderを使っていること(245行目)がわかります。
公式のget_loader
関数のリンクは古くなっていると先程述べましたが、backend/open_webui/retriebal/loaders/main.pyの192行目の_get_loader
関数が2025年4月のVer0.6.2時点では最新の箇所ということになります。
話をprocess_file
関数に戻します。
添付したファイルをこのloader
関数に従ってテキストにした後の処理です。
次に大事な処理としてsave_docs_to_vector_db
関数があります。
GitHubのコードはこの1073行目です。
これは先ほどのログ(docker container logs open-webui
)の2行目でも表示されていた関数になります。
(open_webui.routers.retrieval:save_docs_to_vector_db
の部分)
このsave_docs_to_vector_db
関数が実際に定義されているのは、ログにもあるように
open_webui/routers/retrieval.py
です。
GitHubのコードだとこの785行目になります。
save_docs_to_vector_db
関数を読み進めていきますと、テキスト分割において、チャンクの設定(chunk_size
とchunk_overlap
)が使われています。
管理者パネルの「ドキュメント」内で設定できる「チャンクサイズ」や「チャンクオーバーラップ」がここで使われていることが確認できます。
単語 | 意味 |
---|---|
チャンクサイズ |
Text Spliter がCharactor の場合は、RecursiveCharacterTextSplitter を使っており、文字数と同値 |
チャンクオーバーラップ |
Text Spliter がCharactor の場合は、連続したチャンクで被る文字数。チャンクオーバーラップは文字列分割で大事な情報が落ちないように、文脈を保持するために使用します |
さらにsave_docs_to_vector_db
関数を読み進めていくとembeddings
にtexts
を与えています。
これは埋め込みモデル(embedding model)にチャンクサイズで分割したテキストを与えていることがわかります。
よく読むとreplace("¥n", " ")
も実行しているので、改行は半角空白に置き換わっていることもわかります。
つまり改行はOpen WebUIの埋め込みモデルでは空白と同じになるということがここからわかります。
このベクトル化した結果をDBに入れて、save_docs_to_vector_db
関数は終わります。
以上で、ファイルをアップロードした際にOpen WebUIでどのような処理が行われるのかを見てきました。
簡単にファイルアップロードの流れをまとめると
- 添付されたファイルを拡張子に沿ってパースする
- 埋め込みモデル(embedding model)を使ってベクトル化する
- ベクトル化したものをDBに保存する
という形になります。
RAGのコード実装
次にRAGを実施しているときのスクリプト部分を確認します。
「#」からすでに作成されたナレッジベース内のコレクションを選択し、RAGを動かした時のログを見ていきます。
例では「人物紹介」というコレクションを使っています。この中身は前編で解説しているので、詳細が気になる人はそちらを見てください。
ログは以下です。
docker container logs open-webui
大事なのは、ログの4行目に表示されているopen_webui.retrieval.utils:query_doc
です。
これはopen_webui/retrieval/utils.pyの76行目に当たります。
見るとわかりますが、この関数のメインの処理はVECTOR_DB_CLIENT.search
だけです。
この関数を追う前にquery_doc
関数がどこから来たのかを先に確認します。
これはopen_webui/routers/retrieval.py
のquery_doc_handler
関数から呼ばれています。
こちらのGitHubのコードはこの1535行目です。
このquery_doc_handler
関数で大事なことは2点です。
(1) 管理者パネルの「ドキュメント」の「ブリッジ検索」がONになっている場合は1541行目のif文がtrue
になるため、呼ばれる関数はquery_doc
関数ではなく、query_doc_with_hybrid_search
関数になります。
「ブリッジ検索」をONにした場合はBM25アルゴリズムを使ったり、リランキングしたりしてコレクションの検索をするようになります。記事が冗長になるのでこれ以上は踏み込みませんが、詳細が気になる人は下のquery_doc
と同じようにコードを読んでいけば分かると思います。
(2)query_doc
関数に渡しているのは以下の内容です。
変数 | 解説 |
---|---|
collection_name | これはナレッジベースで作られたコレクションの名前です |
query_embedding | 埋め込み関数(embedding function)にクエリを渡したもの、つまりクエリをベクトル化したものです |
k | これは管理者パネルの「ドキュメント」内の「トップK」で指定できる数字で、検索の上位K件を結果に用います |
user | これもそのままユーザです(一般ユーザと管理者ユーザでできることが異なるので、この値を渡していると思われますが要調査) |
この(1)(2)の情報を踏まえて、query_doc
関数内のVECTOR_DB_CLIENT.search
を追っていきます。
VECTOR_DB_CLIENT
はopen_webui/retrieval/vector/connector.py
です。
GitHubのコードだとここですが、そんなに中身があるわけではなく、VECTOR_DB
で値を分岐させているだけです。
VECTOR_DB
の値が大事ですが、これは一番上に書かれているようにopen_webui/config.py
に書かれているので、そちらにいきます。GitHubのコードはこの1616行目に書かれています。
これは記載の通りOSの環境変数のVECTOR_DB
の値を使います。
設定されていない場合は第二引数のchroma
が指定されるということなので、デフォルトだとDBとしては使うものはChroma
になります。
なお、この環境変数の値は公式ドキュメントにも記載があり、コードと同じ挙動が書かれていることが確認できます。
それではVECTOR_DB_CLIENT.search
に話を戻しましょう。
デフォルトではDBはChromaを用いるのでconnector.py
の記載に従い、open_webui/retrieval/vector/dbs/chroma.py
を見にいきます。
GitHubのコードだとこの66行目にsearch
関数が記載されています。
71行目では引数に与えられたコレクション名からDB内に保存されているコレクションを選び、73行目でそのコレクションの検索をしています。
Chroma DBのコードの詳細は別の記事に譲りますが、ここではクエリをベクトル化したものを渡して、近しい文書を検索しているだけです。
80行目にも記載がありますが、コサイン類似度を使ってクエリ(をベクトル化したもの)とコレクションに保存された文書(をベクトル化したもの)の類似度を出していることがわかります。1
以上で、Open WebUIでRAGがどのような処理が行われるのかを見てきました。
大きな流れとしては
- 埋め込み関数を使ってクエリをベクトル化する
- コサイン類似度を使って、クエリに近い文章を検索する
- 検索で得られた上位K件の文章を使い、LLMに渡して回答生成をする
という形になります。
終わりに
似たようなことをやっている方もいますが、UIが少し古くなっているのと個人的にはLoader
以外も気になったので色々調べてみました。
Open WebUI+OllamaでローカルLLMを構築し、RAGをやりたい人の参考になれば幸いです。
もし記事の内容で誤りなどがあれば、コメント欄でご指摘ください。