Heroku+PostgresにLangChainで構築したRAGをデプロイする
PaaSとして有名なHerokuに、Python+LangChainで構築した簡易なRAGをデプロイしてみました。HerokuのPosgreSQLにPGVector拡張をインストールして、ベクトルDBとして利用できるようにした構成です。とても簡単にデプロイできたので、RAGアプリケーションやAIチャットボットのバックエンドとして使い勝手がよいと思います。
HerokuにRAGをデプロイする
Herokuは、アプリの構築、提供、監視、スケールを支援するクラウド型のプラットフォームです。いわゆるPaaSと呼ばれるサービスで、さまざまな言語に対応しています。
かつては無料プランがあり、個人開発などでも多用されていましたが、現在は有料プランしかありません。それでも、もっとも安いプランでは5ドル/月から始めらることができるため、コスパも良好です。
そんなHerokuに、RAGのアプリケーションをデプロイしてみました。RAGを実現するにあたってはベクトルDBが必要ですが、HerokuのPosgreSQLにPGVector拡張をインストールしてベクトルDBとして利用することができます。LangChainのサンプルがそのまま動作するなど、簡単にデプロイできました。
ということで、この記事では、Herokuに簡易なRAGをデプロイする方法をご紹介します。
今回利用したHerokuのサービス
HerokuではDynoと呼ばれるスマートコンテナ上でアプリケーションを実行します。このDynoには、コンピュート性能やメモリ量、各種機能の有無などでプランがいくつかあります。
とりあえず簡単なアプリをデプロイするのであれば、最も安いEco
(定額5ドル/月)、または、Basic
(0.01ドル/時、最大7ドル/月)で十分です。何も設定しないでデプロイすると、デフォルトでBasicとしてデプロイされます。
一方、データベースはHeroku Postgres
を利用します。こちらも、ストレージ容量や同時接続数、ロールバック機能の有無などで複数のプランがあります。今回はテスト用途ですので、最も安いEssential 0
(約$0.007/時間、最大5ドル/月)を利用します。
Herokuの各種サービスとその価格については、以下のHerokuの価格ページをご確認ください。
HerokuへのログインとCLIの利用
アプリケーションをデプロイする際には、Heroku CLI
というコマンドラインインタフェースを利用します。また、このCLIを用いて、事前に必要な設定を済ませておきます。
Heroku CLI のインストール
まずは環境(OS)に応じたHeroku CLIをインストールしておきます。以下のHeroku CLIのページに、各OSでのインストール方法、ダウンロードのリンクがありますので、インストールしておいてください。
インストール後に、PowerShell(Windows)やターミナル(Mac)で、以下のコマンドでパスが通っていることを確認しましょう。
> heroku --version
heroku/10.0.2 win32-x64 node-v20.17.0
Herokuへのログイン
Heroku CLI を用いて heroku login
でログインします。heroku login
コマンドではブラウザ画面が立ち上がり、ブラウザからログインすることになります。
ただし、以下のメッセージが出てログインできない場合があります。
IP address mismatch
理由はよくわかりませんが、私の環境でもこのメッセージが出ました。
ブラウザからログインできない場合は、heroku login -i
でCLIからログインします。
>heroku login -i
heroku: Enter your login credentials
Email: xxxxx
Password: ********
Logged in as xxxxx
このようにCLIからアカウントのEmailとパスワードを入力します。
パスワードは、MFA(Multi-Factor Authentication)を有効にしている場合は、ブラウザからログインする際のパスワードではなく、APIキーを入力する必要があります。APIキーは、Herokuの Account Settings (Manage Account)の Account タブで確認できます。
とりあえずログインまでできれば、Heroku CLIの準備は完了です。
Herokuにデプロイする準備
Herokuにデプロイするには、いくつかの準備が必要です。ここでは、Pythonで構築したWebアプリケーションをデプロイする前提で、各種準備の方法を紹介します。
Procfileの作成
Herokuがデプロイされたアプリケーションをどのように実行するのかを指示するProcfile
が必要となります。
Python+Flaskで構築したWebアプリケーションの場合は、プロジェクトディレクトリの直下にProcfile
を以下の内容で作成します。
web: gunicorn app:app --log-file=-
最初のweb
はプロセスタイプ、そのあとのgunicorn app:app --log-file=-
は起動コマンドとなります。
HerokuではWebアプリの動作にgunicorn
を利用します。また、app:app
の部分ですが、最初のapp
はアプリケーションのモジュール名(ファイル名)のapp.py
を示し、2番目のapp
は、Flaskのインスタンスを示しています。具体的には、app.py
内で以下のようにFlaskアプリのインスタンスapp
を指定しています。
# Flaskアプリ
app = Flask(__name__)
Pythonパッケージの指定
Heroku上にデプロイするPythonアプリケーションが利用するパッケージを指定します。
ローカルの開発環境で仮想環境を利用しているのであれば、仮想環境に入った状態で、以下のようにrequiremenets.txt
を作成して、プロジェクトディレクトリ直下に配置しておけばOKです。
pip freeze > requirements.txt
また、仮想環境にpipenv
を利用している場合は、プロジェクトディレクトリにPipfile
があるはずですので、上記のrequiremenets.txt
は不要です。
Herokuでは、requiremenets.txt
またはPipfile
を読み取ってくれるようですので、どちらかがあれば問題ありません。
Pythonランタイムバージョンの指定
runtime.txt
として、以下のようにPythonランタイムのバージョンを指定したファイルを作成し、プロジェクトディレクトリ直下に配置します。今回は、現時点でPython 3.12 系列の最新バージョンの Python 3.12.8 を利用します。
python-3.12.8
Herokuにデプロイするサンプルプログラム
今回Herokuにデプロイするサンプルプログラムは、Web APIとして動作し、Body部のquery
の質問内容を読み取り、RAGを利用して回答を返信するプログラムです。Webアプリというよりは、AIチャットボットのバックエンドを想定しています。
app.py
はFlaskアプリ本体、rag_sample.py
はRAG部分の処理をまとめたものです。rag_sample.py
では、DOCUMENT_URL
で指定したURLのWebページのテキストを取得して、ベクトルDBを作成しています。
RAGのEmbeddings function、LLMともにOpenAIのAPIを利用しています。
- Embeddings: "text-embedding-3-small"
- LLM: "gpt-4o-mini"
app.py
import json
from flask import Flask, Response, request
from rag_sample import query
app = Flask(__name__)
@app.route("/", methods=["POST"])
def rag_test():
# Body部のJSONを取得
body_json = request.json
# query内容を確認
query_content = body_json.get("query")
if not query_content:
return ({"error": "Query not found"}, 404)
# RAG
answer = query(query_content)
# Response
data = {
"answer": answer["response"]
}
response = Response(
response=json.dumps(data, ensure_ascii=False),
mimetype='application/json'
)
return response
rag_sample.py
"""
RAG with Heroku PsotgreSQL + PGVector sample
"""
import argparse
import os
import sys
from operator import itemgetter
from dotenv import load_dotenv
from langchain_community.document_loaders import WebBaseLoader
from langchain_core.documents.base import Document
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_postgres.vectorstores import PGVector
from langchain_text_splitters import TokenTextSplitter
# LLM model
LLM_MODEL_OPENAI = "gpt-4o-mini"
EMBEDDING_MODEL = "text-embedding-3-small"
# .env
load_dotenv()
# argparse
parser = argparse.ArgumentParser()
parser.add_argument('-m', '--make_index', action="store_true", help='Make index with embeddings functions')
parser.add_argument('-q', '--query', help='Query')
# Retriever options
TOP_K = 5
DOCUMENT_URL = "https://ja.wikipedia.org/wiki/%E5%8C%97%E9%99%B8%E6%96%B0%E5%B9%B9%E7%B7%9A"
COLLECTRION_NAME = "sample_docs"
DATABASE_URL = os.environ["DATABASE_URL"].replace("postgres://", "postgresql://") if os.environ["DATABASE_URL"].startswith("postgres://") else os.environ["DATABASE_URL"]
# Template
my_template_jp = """Please answer the [question] using only the following [information] in Japanese. If there is no [information] available to answer the question, do not force an answer.
Information: {context}
Question: {question}
Final answer:"""
def load_and_split_document(url: str) -> list[Document]:
"""Load and split document
Args:
url (str): Document URL
Returns:
list[Document]: splitted documents
"""
# Read the Wep documents from 'url'
raw_documents = WebBaseLoader(url).load()
# Define chunking strategy
text_splitter = TokenTextSplitter(chunk_size=2048, chunk_overlap=24)
# Split the documents
documents = text_splitter.split_documents(raw_documents)
# for TEST
print("Original document: ", len(documents), " docs")
return documents
def create_vectordb():
"""Create vector store
Returns:
Vector Store
"""
# PGVector
embeddings = OpenAIEmbeddings(model=EMBEDDING_MODEL)
vectordb = PGVector(
embeddings=embeddings,
collection_name=COLLECTRION_NAME,
connection = DATABASE_URL,
use_jsonb=True,
)
return vectordb
def make_index() -> None:
"""
Make index
"""
# load and split document
documents = load_and_split_document(DOCUMENT_URL)
# VectorDB
vectordb = create_vectordb()
# Add documents to vectordb
vectordb.add_documents(
documents,
)
def query(query: str):
"""
Query with vectordb
"""
# model
model = ChatOpenAI(
temperature=0,
model_name=LLM_MODEL_OPENAI)
# prompt
prompt = PromptTemplate(
template=my_template_jp,
input_variables=["context", "question"],
)
# retreiver
vectordb = create_vectordb()
retriever = vectordb.as_retriever(
search_type="similarity",
kwargs={"k": TOP_K}
)
# Query chain
chain = (
{
"context": itemgetter("question") | retriever,
"question": itemgetter("question")
}
| RunnablePassthrough.assign(
context=itemgetter("context")
)
| {
"response": prompt | model | StrOutputParser(),
"context": itemgetter("context"),
}
)
# execute chain
result = chain.invoke({"question": query})
return result
# main
def main():
# OpenAI API KEY
if os.environ.get("OPENAI_API_KEY") == "":
print("`OPENAI_API_KEY` is not set", file=sys.stderr)
sys.exit(1)
# args
args = parser.parse_args()
# query
if args.make_index:
make_index()
if args.query:
result = query(args.query)
else:
sys.exit(0)
# print answer
print('---\nAnswer:')
print(result['response'])
if __name__ == '__main__':
main()
Herokuへのデプロイ
それでは、いよいよHerokuにデプロイしてみます。手順を追って紹介します。
Herokuでのアプリケーションの作成
Heroku CLIでログインしたあと、以下のコマンドでアプリケーションを作成します。
> heroku create <app-name>
<app-name>にアプリケーションの名前を設定しますが、省略すると適当な文字列が付けられます。
なお、前述のように、特に事前の設定なしでアプリケーションを作成すると、デフォルトでBasic
のDynoが作成されます。
Herokuの環境変数設定
Herokuの環境変数設定
以下のコマンドで環境変数(ローカルで .env
に設定している環境変数)を設定する。
heroku config:set <環境変数名>=<環境変数の内容> -a <app-name>
通常は.env
に環境変数を設定していると思いますが、その内容を上記のようなコマンドで設定します。Heroku上で動作するアプリは、これらの環境変数を設定した状態で動作します。
今回のサンプルアプリでは、OpenAIのAPIキーを環境変数として設定していますので、以下のように設定します。
heroku config:set OPENAI_API_KEY=<APIキー> -a <app-name>
設定された環境変数は、以下のコマンドで確認できます。
heroku config:get <環境変数名>=<環境変数の内容> -a <app-name>
Heroku Postgres のプロビジョニング
Heroku Postgresのプロビジョニングも事前に実施しておきます。
1. アドオンからDBを追加する
まず、HerokuのWebサイトにログインし、アプリの管理画面からHeroku Postgres
のAdd-onsを追加してプロビジョニングします。
- アプリ管理画面の Resources > Add-ons で
Heroku Postgres
を検索 - Planを選択し、Submit Order Form を押下
- 今回は一番安い
Essential-0
を選択
- 今回は一番安い
- Provisioningが終わるまでしばらく待つ
2. DATABASE_URLの確認と修正
次に、アプリからHeroku PostgresにアクセスするためのURLを確認します。
- アプリ管理画面の Settings > Config Variables から
DATABASE_URL
を確認
環境変数として設定されているため、デプロイしたアプリから環境変数DATABASE_URL
を参照することで、Heroku Postgres にアクセスできるようになります。
ただし、Pythonから利用する場合、このままではエラーとなってしまいます。HerokuのDATABASE_URL
はpostgres://
から始まっていますが、これをpostgresql://
に修正する必要があります。
具体的には、コード中で以下のように修正すればよいでしょう。
POSTGRESQL_CONNECTION = os.environ["DATABASE_URL"].replace("postgres://", "postgresql://") if os.environ["DATABASE_URL"].startswith("postgres://") else os.environ["DATABASE_URL"]
3. PGVectorの設定
今回はPostgreSQLの拡張機能の一つPGVector
を利用します。PostgreSQLでベクトル検索をできるようにするための拡張機能です。
Heroku CLI から以下のコマンドを実行して、PostgreSQLにPGVectorの拡張機能をインストールします。
# psqlコマンドでPostgreSQLに接続
heroku pg:psql DATABASE_URL -a <app-name>
# PGVector 拡張機能をインストール
<app-name>::DATABASE=> CREATE EXTENSION vector;
なお、heroku pg:psql
コマンドを実行するには、ローカル環境にpsql
コマンドがインストールされている必要があります。
Herokuへのデプロイ
準備が完了しましたので、いよいよアプリケーションをHerokuへデプロイします。
Githubと連携してデプロイする方法もありますが、今回はお手軽にgitコマンドのみで実施します。gitコマンドを利用しますが、Herokuにデプロイしたくないファイル(.envなど)は、事前に.gitignore
に記載しておくのを忘れないようにしてください。
> git init
> git add .
> git commit -m "commnet"
> git push heroku main
通常のgitと同じです。最後のgit push heroku main
でHerokuにデプロイされます。ビルドなどでしばらく時間がかかります。
イメージとしては、Heroku側に簡易なリモートリポジトリがあり、そのリポジトリにローカルからpushするとデプロイまで自動的に実行される、といった感じです。
git push
コマンドを実行すると、Heroku側でデプロイの様子が表示されますので、正常に終了したことを確認します。
動作確認
それではHerokuにデプロイした簡易RAGアプリの動作確認をしてみます。
ベクトルDBの作成
まず Heroku にデプロイしたrag_sample.py
を用いて、Webから取得した情報をもとにベクトルDBを作成します。このベクトルDBは、先ほど設定したPGVector
拡張をインストールした Heroku Postgres に作成されます。
ローカル環境からは、heroku run
コマンドでHerokuのアプリケーションを実行できます。rag_sample.py
に-m
オプションを与えて起動すると、Web上から情報を取得し、ベクトルDBを作成します。
> heroku run "python rag_sample.py -m" -a <app_name>
Running python rag_sample.py -m on ⬢ <app_name>... up, run.5342
Original document: 70 docs
このように表示されます。今回は、Wikipediaの北陸新幹線のページを読み込ませています。チャンクサイズを2048バイトに設定していて、70個のチャンク(ドキュメント)に分割されてベクトルDBに保存されました。
Web API でRAGに問い合わせ
次に、app.py
に実装したWeb APIに問い合わせて、RAGの動作を確認してみます。
Web API への問い合わせには、VSCodeのREST Client
拡張機能をインストールして利用します。
REST Client
は、Web APIのテストを手軽にできる拡張機能です。この拡張機能をインストールし、拡張子が.http
のファイルをVSCodeで作成します。ここではtest.http
を以下のように作成します。
### POST Request
POST <HerokuアプリのURL>
Content-Type: application/json
{
"query": "北陸新幹線の雪対策は?"
}
POSTの後ろにある<HerokuアプリのURL>には、先ほどHerokuにデプロイしたアプリのURLを記入します。HerokuにデプロイしたアプリのURLは、以下のコマンドで表示されます。
> heroku domains
HTTPのBody部には、JSON形式でクエリの内容を記述します。"query"として質問内容を記述しています。
test.http
ファイルを作成したら、POST Request の下に小さく表示されているSend Request
をクリックしてみましょう。設定した内容のHTTPリクエストが送信され、しばらくするとレスポンスの内容がResponseウィンドウに表示されます。
HTTP/1.1 200 OK
...(省略)
Connection: keep-alive
Server: gunicorn
Date: Sat, 18 Jan 2025 02:55:16 GMT
Content-Type: application/json
Content-Length: 1213
Via: 1.1 vegur
{
"answer": "北陸新幹線の雪対策には、以下のような方法が採用されています。\n\n1. **貯雪方式**: 比較的積雪量が少ない長野までの区間では、高架橋の軌道下の路盤コンクリートを高くし、線路の両脇に雪を貯める貯雪方式が採用されています。\n\n2. **散水消雪方式**: 降雪量が多い区間ではスプリンクラーによる散水消雪方式が採用されています。飯山エリアでは、温水を循環させておく「循環方式」が採用されています。\n\n3. **消雪パネル**: 温水が流れるパイプを設置したパネルによって雪を融かす消雪パネルの開発が行われ、実施されています。\n\n4. **雪落とし作業**: 東京方面へ直通する列車に対して、雪落とし作業が行われています。AIを用いて着雪量の推定を行い、作業の要否を判断するツールも導入されています。\n\n5. **トンネル雪庇散水**: トンネル緩衝口端部より散水する設備が導入されています。\n\nこれらの対策により、北陸新幹線は冬季においても安定した輸送を維持しています。"
}
Body部には、"answer"として回答内容が返ってきています。Wikipediaの内容をもとにした回答がきちんと生成されていますので、RAGとして動作していることがわかります。
もう一つ試してみます。
### POST Request
POST <HerokuアプリのURL>
Content-Type: application/json
{
"query": "北陸新幹線の歴史を教えて"
}
回答は以下のとおりです。
{
"answer": "北陸新幹線の歴史は、1967年に北陸三県商工会議所会頭会議で新幹線の実現を目指すことが決議されたことから始まります。その後、1970年に全国新幹線鉄道整備法が公布され、経済発展や地域の振興を目的とした新幹線の建設が進められました。1972年には基本計画が決定され、東京都を起点に長野市、富山市を経由して大阪市を終点とするルートが示されました。\n\n1973年には整備計画が決定され、北陸新幹線は長野市、富山市、小浜市を主要な経過地とすることが示されました。1997年10月1日に高崎駅から長野駅間が開業し、2015年3月14日には長野駅から金沢駅間が開業しました。現在、北陸新幹線は高崎駅から敦賀駅までの路線が整備されています。"
}
こんな感じで、Wikipediaの知識を元にした回答が得られました。
まとめ
HerokuにRAGを利用したWebアプリ(Web API)をデプロイしてみました。
Pythonで生成AIを利用したアプリやシステムを作成するうえで欠かせないLangChainは問題なく動作します。また、ベクトルDBに関しても、Heroku Postgres に PGVector 拡張をインストールすることで動作することが確認できました。
2025年現在は、DynoやPostgresの無料プランはありませんが、DynoのBasicプランなら7ドル/月、PostgresのEssential-0なら5ドル/月から始められます。操作やデプロイ方法も簡単かつ便利で、小規模なアプリや、AIチャットボットのバックエンドを構築するには良いサービスだと思います。
Discussion