DifyでRAG(Text Embedding + Rerank)をセルフホスティングしたい! - Embeddingモデルプロバイダーを用意する
February 05, 2025
Text Embedding用モデルプロバイダーを用意したかった(text-embeddings-inference・ollama編)
Text Embeddingをおこなう際に重要となるのは、何をどのような基準でベクトル化するかである。 英文であれば、1単語ごとにスペースで区切ってあることがほとんどであるので、単語を基準にするのが手っ取り早い。 一方の日本語文では、単語の区切りが明確になっておらず、単語単位で取り出そうとすると形態素解析が必須となる。 これを文字単位で切り出してベクトル化すると、文字が一部分一致しているだけのまるで嚙み合わない結果が返ってくるようになってしまう。
セルフホスティングできるText Embeddingのモデルプロバイダーとしては、
huggingface/text-embeddings-inference、
モデルとしてはhttps://huggingface.co/cl-nagoya/ruri-largeなどがある。
しかしながら、このtext-embeddings-inference
と
日本語用のEmbeddingモデル(cl-nagoya/ruri-large)を
用いてAPIサーバーを建てようとすると、tokenizer.json
がないよ~と次のようなエラーが出力されてしまう。
$ docker run --rm \
--gpus all \
-p 8083:80 \
-v $PWD/data:/data \
--pull always \
ghcr.io/huggingface/text-embeddings-inference:89-1.6 \
--model-id cl-nagoya/ruri-large
...
...
HTTP status client error (404 Not Found) for url (https://huggingface.co/cl-nagoya/ruri-large/resolve/main/tokenizer.json)
tokenizer_config.json
を覗いてみると、word_tokenzer_type
がmecab
になっていたり、
mecab
の辞書にunidic_lite
が用いられていたり、
sudachi
っぽい記述があったりと、外部のtokenizerが用いられていることが分かる。
"clean_up_tokenization_spaces": true,
"cls_token": "[CLS]",
"do_lower_case": false,
"do_subword_tokenize": true,
"do_word_tokenize": true,
"jumanpp_kwargs": null,
"mask_token": "[MASK]",
"mecab_kwargs": {
"mecab_dic": "unidic_lite"
},
"model_max_length": 512,
"never_split": null,
"pad_token": "[PAD]",
"sep_token": "[SEP]",
"subword_tokenizer_type": "wordpiece",
"sudachi_kwargs": null,
"tokenizer_class": "BertJapaneseTokenizer",
"unk_token": "[UNK]",
"word_tokenizer_type": "mecab"
cl-nagoya/ruri-largeのページには、fugashi
をインストールせよとの記述もある。
# cl-nagoya/ruri-largeをsentence-transformersから利用する際の依存ライブラリ
sentence-transformers fugashi sentencepiece unidic-lite
残念ながらこれらはtext-embeddings-inference
では考慮されておらず、
text-embeddings-inference
は一般的なtokenizer.json
を用いようとするため、
先述のエラーが発生してしまう。
ではollamaのリポジトリにあるkun432/cl-nagoya-ruri-largeは 一体どうやってるんだろうと思い試してみたところ、精度が芳しくなく、的外れな検索結果が返ってくるケースが非常に多かった。 やはり適切なtokenizerを利用できる状態にしなければならない。
Text Embedding用モデルプロバイダーの実装
text-embeddings-inference
もollama
も利用できないのであれば他も同様であろうと推察される。
そこで今回は、OpenAIのEmbeddings API(/v1/embeddings
)と同じフォーマットで結果を返す、互換サーバーを実装してみることにした。
といっても実装はシンプルで、次のようなリクエストが/v1/embeddings
宛てにapplication/json
で来た際に、
{
"model": "cl-nagoya/ruri-large",
"input": [
"文章: てきとうなテキストだよ。",
"文章: てきとうなテキストです。"
]
}
レスポンスとして次のようなJSONを返すようにするだけである。
{
"object": "list",
"data": [
{
"object": "embedding",
"embedding": [
0.8186585903167725,
-0.47749972343444824,
-0.34532251954078674,
-0.7178557515144348,
...
-0.6007830500602722
],
"index": 0
},
{
"object": "embedding",
"embedding": [
0.958991289138794,
-0.322582870721817,
-0.35985010862350464,
-0.5917154550552368,
...
-0.6487450003623962
],
"index": 1
}
],
"model": "cl-nagoya/ruri-large",
"usage": {
"prompt_tokens": 0,
"total_tokens": 0
}
}
レスポンスのdata.embedding
には、リクエストの順にベクトル(cl-nagoya/ruri-large
の場合は1024個の浮動小数点数)が入ってくる。
実行環境と依存ライブラリの用意
Pythonの実行環境の用意には、uv
を用いる。
あらかじめ、uv
のInstallationを参考に、uv
をインストールしておく。
$ mkdir ~/embed-rerank-server
$ cd embed-rerank-server
$ uv init
必要なライブラリをインストールする。
$ uv add fastapi uvicorn fugashi pydantic sentence-transformers sentencepiece unidic-lite
うまく準備できれば、次のようなpyproject.toml
が生成されているはずである。
[project]
name = "embed-rerank-server"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"fastapi>=0.115.8",
"fugashi>=1.4.0",
"pydantic>=2.10.6",
"sentence-transformers>=3.4.1",
"sentencepiece>=0.2.0",
"unidic-lite>=1.0.8",
"uvicorn>=0.34.0",
]
APIサーバーの実装
Text Embedding APIサーバーを、embeddings-api-server.py
として、次のように実装する。
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Union
from sentence_transformers import SentenceTransformer
app = FastAPI()
model = SentenceTransformer("cl-nagoya/ruri-large")
class EmbeddingRequest(BaseModel):
model: str
input: Union[str, list[str]]
@app.post("/v1/embeddings")
def create_embedding(request: EmbeddingRequest):
texts = [request.input] if isinstance(request.input, str) else request.input
embeddings = model.encode(texts, convert_to_numpy=True).tolist()
data = []
for idx, emb in enumerate(embeddings):
data.append({"object": "embedding", "embedding": emb, "index": idx})
res = {
"object": "list",
"data": data,
"model": request.model,
"usage": {"prompt_tokens": 0, "total_tokens": 0},
}
return res
APIサーバーの起動
uvicorn
を通じて、APIサーバーを起動する。この際のポート番号はてきとうに空いているものを利用する。
当然ながら、Difyのインスタンスからネットワーク的に到達できるように、適宜変更が必要である。
$ uv run uvicorn embeddings-api-server:app --host 0.0.0.0 --port 8081
動作検証
実際に機能するか検証する。 curlで試す際は、次のようにリクエストを投げる。
$ curl -v http://127.0.0.1:8081/v1/embeddings -H 'Content-Type: application/json' -d '
{
"model": "cl-nagoya/ruri-large",
"input": [
"文章: てきとうなテキストだよ。",
"文章: てきとうなテキストです。"
]
}'
無事ながーいベクトル(cl-nagoya/ruri-largeの場合は1024要素)が返ってくれば成功である。