Ollama サーバーを立ててベクトル生成とベクトル検索を試してみた

## このブログは何でしょう
Oracle Database 23ai が Ollama に対応ということで、ローカルのPCやクラウド上の仮想サーバー上に Ollama を使ってテキストのベクトル（エンベディング、埋め込み）を生成するサーバを構築する手順を確かめてみました。

ベクトル検索や、それを活用した RAG を構成する際には、ドキュメントとクエリーのテキストをベクトル（エンベディング、埋め込み）に変換する必要があります。クラウドサービスを使えば、Cohere Embed V3 などの高性能な埋め込みモデルを API で呼び出して利用することができますが、セキュリティポリシーなどの制約でクラウドのマルチテナントなAPIサービスの利用が難しい場合もあります。

そんな場合には Ollama を使えば自社のテナンシ（アカウント）の中に簡単に埋め込みサーバを構築できます。

Oracle Database 23ai の AI Vector Search では、他のデータベースと同様に外部で生成したベクトル（エンベディング、埋め込み）をSQLのINSERT文やUPDATE文、SQL*Loader、Oracle Data Pumpを使って登録することができるのはもちろんですがデータベースの中に埋め込みモデルをインポートしてベクトル生成したり、UTL_TO_EMBEDDING() という関数を呼び出すことでデータベースの中から外部の埋め込みモデルを呼び出してテーブル上のデータをベクトルに変換することもできます。

このとき呼び出せる埋め込みモデルは、これまで、Cohere, Google AI, Hugging Face, OCI Generative AI, OpenAI, Vertex AI が提供する REST API でアクセスできるモデルだけでした。しかし、Release Update 23.6 からは、ローカル REST API として Ollama にも対応しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74534/aed2cb9e-0379-3dc6-575d-0fa700463cac.png)

[Supported Third-Party Provider Operations and Endpoints](https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/supported-third-party-provider-operations-and-endpoints.html)

今後、この機能は Autonomous Database でも利用可能になると思いますが、Autonomous Database では、データベースが稼働しているサーバーにユーザーが Ollama をインストールすることはできませんので、別途、サーバーを立てる必要があります。そこで、今回は、この ネットワーク越しにアクセスできるOllama サーバーの構築手順を確認してみました。

このブログでは、OCIの仮想マシン上に Ollama サーバを起動する形態を取っていますが本格的に利用する場合には、OKEなどのコンテナ環境にデプロイしてロードバランサで可用性と性能を確保することもできます。

なお、このブログの手順はOCIに固有のものではありませんので、ローカルのPCや他のクラウドでも同様の手順で埋め込みサーバを構築できます。

また、このブログではベクトル検索を実行して正しくベクトル生成できていることを確かめていますがデータベースには Oracle Database は使用していません（ChromaDBを使っています）。

## 埋め込みサーバの構築
### Ollama のインストール
Linux 環境の場合、以下のコマンドで Ollama をインストールします。
```bash
curl -fsSL https://ollama.com/install.sh | sh
```
#### 出力例
```bash
$ curl -fsSL https://ollama.com/install.sh | sh
>>> Installing ollama to /usr/local
>>> Downloading Linux amd64 bundle
######################################################################## 100.0%
>>> Creating ollama user...
>>> Adding ollama user to render group...
>>> Adding ollama user to video group...
>>> Adding current user to ollama group...
>>> Creating ollama systemd service...
>>> Enabling and starting ollama service...
Created symlink /etc/systemd/system/default.target.wants/ollama.service → /etc/systemd/system/ollama.service.
>>> The Ollama API is now available at 127.0.0.1:11434.
>>> Install complete. Run "ollama" from the command line.
WARNING: No NVIDIA/AMD GPU detected. Ollama will run in CPU-only mode.
```
### Ollama が起動したことを確認
```bash
ollama ps
```

#### 出力例
```bash
$ ollama ps
NAME    ID    SIZE    PROCESSOR    UNTIL
```
### Ollama のバインドアドレスの変更
次のコマンドで ollma を停止します
```bash
sudo systemctl stop ollama.service
```
以下のように環境変数 `OLLAMA_HOST` を設定します
```bash
export OLLAMA_HOST=0.0.0.0:11434
```
次のコマンドで、ollama を再び起動します
```bash
ollama serve
```
ollama のサーバーがフォアグラウンドで起動されます。バックグランドで起動させたい場合は、`systemd`を使用して[デーモンとして起動](https://github.com/ollama/ollama/blob/main/docs/linux.md#adding-ollama-as-a-startup-service-recommended)します。このとき、`/etc/systemd/system/ollama.service` の `Environment` に `OLLAMA_HOST` を指定するのを忘れないようにしましょう。
フォアグラウンドで起動させた場合は、以降の作業は別のターミナルで実行します

### 埋め込みモデル（embedding model）の準備
今回は、日本語にも対応している "mxbai-embed-large" を試してみました。
次のコマンドで、埋め込みモデル "mxbai-embed-large" をダウンロードします。
```bash
ollama pull mxbai-embed-large
```

#### 出力例
```bash
$ ollama pull mxbai-embed-large
pulling manifest
pulling 819c2adf5ce6... 100% ▕██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 669 MB
pulling c71d239df917... 100% ▕██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏  11 KB
pulling b837481ff855... 100% ▕██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏   16 B
pulling 38badd946f91... 100% ▕██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏  408 B
verifying sha256 digest
writing manifest 
success
```

### ベクトル生成ができることの確認
```bash
curl http://localhost:11434/api/embeddings -d '{
  "model": "mxbai-embed-large",
  "prompt": "テキストを埋め込みベクトルに変換"
}'
```
#### 出力例
```bash
$ curl http://localhost:11434/api/embeddings -d '{
>   "model": "mxbai-embed-large",
>   "prompt": "テキストを埋め込みベクトルに変換"
> }'
{"embedding":[-0.8212569952011108,-0.1353001445531845,-0.13522231578826904,0.445271760225296,-0.2829969525337219,
（省略）,-0.07719031721353531]}
```

### リモートから ollama を呼び出せることの確認
以下の `curl` コマンドの"サーバーのIPアドレス"を ollama を稼働しているサーバーの IPアドレスに変更して、インターネットを経由した別のサーバーやPCからリモートでベクトルを生成できることを確認します
```bash
curl http://サーバーのIPアドレス:11434/api/embeddings -d '{
  "model": "mxbai-embed-large",
  "prompt": "テキストを埋め込みベクトルに変換"
}'
```
接続できない場合は、ollama を稼働しているサーバー上の Firewall や、クラウドであればセキュリティグループ、セキュリティリストなどの ACL を確認します。OCIの場合は、この記事の文末の「[補足](https://qiita.com/yuji-arakawa/items/1f6ab3f68b98ff56de12#%E8%A3%9C%E8%B6%B3)」をご参照ください。

## 埋め込みサーバを使ったベクトル検索のテスト
構築した Ollama による埋め込みサーバーと ChromaDB を使ってベクトル検索ができることを確認します。
### ライブラリのインストール
```bash
pip install ollama chromadb
```
### コード例
下記コードの `os.environ['OLLAMA_HOST'] = 'http://xxx.xxx.xxx.xxx:11434'` と`requests.get('http://xxx.xxx.xxx.xxx:11434/api/version')`を適切なURLに書き換えます。
```python:ollama_vector_search.py
import sys
import os
import requests
import chromadb

# Ollamaサーバーのエンドポイントを設定（import ollama より前に設定）
os.environ['OLLAMA_HOST'] = 'http://xxx.xxx.xxx.xxx:11434'
import ollama

# Ollamaサーバー接続テスト
try:
    response = requests.get('http://xxx.xxx.xxx.xxx:11434/api/version')
    print(f"Ollamaへの接続に成功しました: {response.status_code}")
except requests.exceptions.RequestException as e:
    print(f"Ollamaへの接続エラー: {e}")
    sys.exit(1)

# ベクトルデータベースへ格納するドキュメントのリスト
documents = [
    "鹿目まどかは、魔法少女まどか☆マギカの主人公です。彼女は、見滝原中学校に通う平凡な中学２年生でしたが白い猫のようなうさぎのようなマスコット的な外見をした地球外生命体キュゥべえから魔法少女になるよう勧誘を受け過酷な運命を背負うこととなります。彼女は、暁美ほむら、巴マミ、美樹さやか、佐倉杏子という4人の魔法少女とともに魔女と戦います。",
    "環いろはは、マギアレコード 魔法少女まどか☆マギカ外伝の主人公で宝崎市立第一中学校に通う中学３年生。不思議な夢の真相を探るべく、神浜市にやってきた魔法少女。キュゥべえと契約した時の願いは「妹の病気を治すこと」であったが、物語開始時点ではこの妹がいたことも妹のために魔法少女になったことも覚えていない。",
    "高町なのはは、魔法少女リリカルなのはの主人公です。彼女は、海鳴市に住む小学3年生でひょんなことからフェレットに似た動物の姿をしていたユーノと出会い、インテリジェントデバイスである「レイジングハート」を託されたことで時空管理局の魔導士となります。彼女は、時空管理局武装隊戦技教導官となり15歳で二等空尉、19歳で一等空尉となります。その戦闘能力の高さからエースオブエースと呼ばれています。彼女の白いバリアジャケット（魔法服）と圧倒的な戦闘力からファンの間では管理局の白い悪魔とも呼ばれています。",
    "加藤恵は冴えない彼女の育てかたのメインヒロインです。私立豊ヶ崎学園に通う高校2年生で、同人ゲームサークルBlessing softwareのメインヒロイン担当であり、スクリプト作成も一部担当し、2作目では副代表も務めている。なお、メインヒロイン担当が何を意味するかは明確にされていないが、主人公の安芸倫也の彼女という意味ではなく、あくまでメインヒロインのモデル、元ネタなどの意味。",
    "木之本桜は、カードキャプターさくらの主人公で、友枝小学校に通う小学4年生。父と兄との3人で暮らしている。ある日さくらは、父の書庫で不思議な本を発見する。その本に入っていたのは魔術師クロウ・リードが作った魔法のカード、この世に災いをもたらすという｢クロウカード｣だった。 世界に解き放たれてしまったクロウカードを回収するため、クロウカードの本を守る「封印の獣」ケルベロス（ケロちゃん）と共にさくらは｢カードキャプター｣として活躍する。さくらが封印の杖・星の杖・夢の杖を使う際に唱える呪文は、「封印解除（レリーズ）」",
]

# ChromaDBクライアントの作成
client = chromadb.Client()
# 'docs'コレクションが存在する場合は削除
if "docs" in [col.name for col in client.list_collections()]:
    client.delete_collection(name="docs")
collection = client.create_collection(name="docs")

# 各ドキュメントに対して埋め込みを生成し、コレクションに追加
for i, d in enumerate(documents):
    response = ollama.embeddings(model="mxbai-embed-large", prompt=d)
    embedding = response["embedding"]
    collection.add(
        ids=[str(i)],
        embeddings=[embedding],
        documents=[d]
    )

# クエリー
#prompt = "What animals are llamas related to?"
#prompt = "魔法少女が登場する作品は？"
#prompt = "魔法少女が戦う作品は？"
#prompt = "白い悪魔って誰？"
#prompt = "時空管理局に所属している魔法少女は？"
#prompt = "冴えない彼女の育てかたのヒロインは？"
#prompt = "カードを集めるお話しは？"
#prompt = "軍隊のような階級が出てくる作品は？"
prompt = "妹のために魔法少女になったのは誰？"

# クエリーテキストに対して埋め込みを生成
response = ollama.embeddings(prompt=prompt, model="mxbai-embed-large")

#最も関連性の高いドキュメントを取得
results = collection.query(
    query_embeddings=[response["embedding"]],
    n_results=3
)

# 結果をdistancesでソートして表示
documents = results['documents'][0]
distances = results['distances'][0]

# (distance, document)のペアを作成してソート
sorted_results = sorted(zip(distances, documents))

print("検索結果 (ベクトル距離が近い順):")
for distance, doc in sorted_results:
    print(f"\nベクトル距離: {distance:.2f}")
    print(f"ドキュメント: {doc}")

```
#### 実行例
```bash
$ python ollama_vector_search.py
Ollamaへの接続に成功しました: 200
検索結果 (ベクトル距離が近い順):

ベクトル距離: 103.24
ドキュメント: 環いろはは、マギアレコード 魔法少女まどか☆マギカ外伝の主人公で宝崎市立第一中学校に通う中学３年生。不思議な夢の真相を探るべく、神浜市にやってきた魔法少女。キュゥべえと契約した時の願いは「妹の病気を治すこと」であったが、物語開始時点ではこの妹がいたことも妹のために魔法少女になったことも覚えていない。

ベクトル距離: 117.66
ドキュメント: 鹿目まどかは、魔法少女まどか☆マギカの主人公です。彼女は、見滝原中学校に通う平凡な中学２年生でしたが白い猫のようなうさぎのようなマスコット的な外見をした地球外生命体キュゥべ えから魔法少女になるよう勧誘を受け過酷な運命を背負うこととなります。彼女は、暁美ほむら、巴マミ、美樹さやか、佐倉杏子という4人の魔法少女とともに魔女と戦います。

ベクトル距離: 144.49
ドキュメント: 高町なのはは、魔法少女リリカルなのはの主人公です。彼女は、海鳴市に住む小学3年生でひょんなことからフェレットに似た動物の姿をしていたユーノと出会い、インテリジェントデバイス である「レイジングハート」を託されたことで時空管理局の魔導士となります。彼女は、時空管理局武装隊戦技教導官となり15歳で二等空尉、19歳で一等空尉となります。その戦闘能力の高さからエースオブエースと呼ばれています。彼女の白いバリアジャケット（魔法服）と圧倒的な戦闘力からファンの間では管理局の白い悪魔とも呼ばれています。

```

## 補足
OCI上のOracle Linux8 コンピュートの場合は、以下のような設定が必要です。
### firewalld の設定
リモートから Ollama へのアクセスを firewalld で許可します
sudo firewall-cmd --permanent --add-port=11434/tcp
sudo firewall-cmd --reload
sudo firewall-cmd --list-all

### ネットワーク・セキュリティ・グループの作成
コンソール左上のハンバーガーメニューからネットワーキング >> 仮想クラウド・ネットワーク >> 仮想クラウド・ネットワークの詳細 >> ネットワーク・セキュリティ・グループ と辿り、"ネットワーク・セキュリティ・グループの作成" をクリックします。表示された画面で下記を設定します。
- "名前"に適当な名前を設定
- "コンパートメントに作成"で適切なコンパートメントを選択
- "ルール"の方向で"__イングレス__"を選択
- "ソースタイプ"で"__CIDR__"を選択
- "ソースCIDR" で Ollama サーバへアクセスする __アプリケーションを実行するホストの CIDR__ を指定
- "IPプロトコル"で"__TCP__"を選択
- "宛先ポート範囲" で "__11434__" を指定
"作成"ボタンをクリックします。


### ネットワーク・セキュリティ・グループのアタッチ
- コンピュート >> インスタンス で、Ollama をデプロイしたインスタンスを選択します
- インスタンスの詳細で"ネットワーキング"タブを選択します
- "ネットワーク・セキュリティ・グループ"の "Edit"をクリックします
- プルダウンから作成したネットワーク・セキュリティ・グループを選択します
- "変更の保存"をクリックします

### セキュリティ・リストの設定
- コンソール左上のハンバーガーメニューからネットワーキング >> 仮想クラウド・ネットワーク　と辿り、仮想マシンをデプロイした VCN を選択します
- "サブネット"で仮想マシンを配置したパブリックサブネットを選択
- "セキュリティ・リスト"に表示されているセキュリティリストをクリック
- "イングレス・ルールの追加"をクリック
-  ネットワーク・セキュリティ・グループのときと同様に設定
-  "イングレス・ルールの追加"をクリック