💽

【RAG】PrismaがPythonで使えるからベクトルデータの管理に使おうとした話

に公開

LLMアプリのRAG実装において、メタデータとベクトルデータを同じDB(PostgreSQL + pgvector)で管理する場合、どのようなORMでシステムを構築するかは悩みのタネの一つです。

TypeScriptでのプロジェクトが多かった中、好んで使用していたORMとしてPrismaがあります。そのPrismaがなんとなんと、非公式ながら、Python向けにラップしたライブラリが存在することを知りました。

今回のPythonプロジェクトでは、Node.jsで使用できるオリジナルPrismaをラップしたPython用Prismaを使用しようと考えました。この記事は、その紆余曲折を記したものです。

勝手のわかるPrisma

Node.js環境でマイグレーションもORMも兼ねているPrismaを使用できるとなれば、話は簡単。さらに、既存のコードの移植も容易になります。そう考えて、Python版での実装を開始しました。
導入後発覚したのが、2026年現在でもPrismaの型システムはvector型をネイティブサポートしていないこと。
ただし、使用すること自体は可能であるため、Node.jsでのシステム開発では公式で記されている手法を用いるのが良いかもしれません。

Prismaでベクトルデータを保存する

Prismaでpgvectorを扱う場合、Unsupported型を利用して定義することになります。
https://www.prisma.io/docs/postgres/database/postgres-extensions

model Document {
  id        String   @id @default(cuid())
  content   String
  metadata  Json
  // vector型をUnsupportedとして定義
  embedding Unsupported("vector(1536)")?

  @@map("documents")
}

この方法でマイグレーションは通りますが、データの挿入や検索時が課題となります。
Prisma Clientがvector型を理解できないため、生のSQLを書かなければなりません
騙されましたね。なんとか、ベクトルデータが入れられる土壌が整ったとしても、今度はORMとしての恩恵にあずかることができないなんて、本末転倒です。PHPのPDOやそれ以前みたいにプログラムに生のSQLを書くのはもうやりたくない。

# Prismaでのデータ挿入
await db.execute_raw(
    'INSERT INTO documents (id, content, embedding) VALUES ($1, $2, $3::vector)',
    "1", "本文", str(embedding_list)
)

ORMとしてのメリットも無ければ、型における安全性も気になる。動作するもののこれを選択肢として用いるのは些か抵抗があります。
そのため、このプロジェクトでは、かわいいかわいいPrismaちゃんとの別れを告げ、Pythonで人気の高いSQLAlchemyへと転換しました。

SQLAlchemy + Alembic への転換

PythonにおけるORMのスタンダードであるSQLAlchemy と、マイグレーションツールの Alembic に切り替えました(PrismaだったらORMもマイグレーションも一つで解決したのに)。

この切り替え自体は、当然と言えば当然の帰結になるため、すでにPythonでの実装を行ったことある方にとっては、退屈な内容かもしれません。

公式にベクトルデータがサポートされている

pgvector-pythonというライブラリが存在し、SQLAlchemyのカラム定義として Vector型をそのまま扱えます。

from sqlalchemy.orm import DeclarativeBase, mapped_column
from pgvector.sqlalchemy import Vector

class Document(Base):
    __tablename__ = "documents"
    
    id = mapped_column(String, primary_key=True)
    content = mapped_column(String)
    # 完全に型として扱える
    embedding = mapped_column(Vector(1536))

LangChainとも親和性がある

LangChain の PGVector ベクトルストアは、内部的に SQLAlchemy の構造を想定しているため、テーブル名やカラム名を指定するだけでスムーズに連携できます。

vector_store = PGVector(
    connection=DATABASE_URL,
    embeddings=OpenAIEmbeddings(),
    table_name="documents",
    embedding_column="embedding",
)

Prismaで行っていな謎の試行錯誤の時間が無駄に思えるほど、あっというまに標準的な解決に至りました。なんでPrisma使ったんだよ。

まとめ

昔の人はいいことを言いました。郷に入っては郷に従え。

NodeベースのPrismaがPythonで転用されているから便利だ!というのは軽率すぎました。RAGかどうか、というのを除いても、Pythonの環境で培われた最適解を用いるべきでした。もとより最適な選択肢を選んでいたり、詳細な調査を実施しておくことで、余計な手戻りは発生しなかったはずです。

おそらく多言語での余計な成功体験がなければ、Pythonを使用する人にとっては、当然ではあるもののNode.jsの環境が長い人には、Prismaを使っちゃえという発想に至ることはあると思います。
PythonにおけるSQLAlchemyの使用は、RAGのベクトルデータを保存するための標準的な仕組みではありますが、今回のような手戻りの体験をしてしまう前に、検討している人にこの記事が届けばいいなと思っています。

そして、Prismaでベクトルデータが公式サポートされたら、Python環境でもPrismaを使用するかもしれないし、Node.js環境でもRAGシステムが構築しやすくなるので開発の選択肢が充実するかもしれません(Prisma諦めてない)。

株式会社メンバーズ AIフォーオールカンパニー

Discussion