👌

Heroku+PostgresにLangChainで構築したRAGをデプロイする

2025/01/18に公開

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の価格ページをご確認ください。

https://jp.heroku.com/pricing

HerokuへのログインとCLIの利用

アプリケーションをデプロイする際には、Heroku CLIというコマンドラインインタフェースを利用します。また、このCLIを用いて、事前に必要な設定を済ませておきます。

Heroku CLI のインストール

まずは環境(OS)に応じたHeroku CLIをインストールしておきます。以下のHeroku CLIのページに、各OSでのインストール方法、ダウンロードのリンクがありますので、インストールしておいてください。

https://devcenter.heroku.com/articles/heroku-cli

インストール後に、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 タブで確認できます。

https://dashboard.heroku.com/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を指定しています。

app.py
# 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 を利用します。

runtime.txt
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
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_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_URLpostgres://から始まっていますが、これをpostgresql://に修正する必要があります。

具体的には、コード中で以下のように修正すればよいでしょう。

rag_sample.py
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に保存されました。

https://ja.wikipedia.org/wiki/北陸新幹線

Web API でRAGに問い合わせ

次に、app.pyに実装したWeb APIに問い合わせて、RAGの動作を確認してみます。

Web API への問い合わせには、VSCodeのREST Client拡張機能をインストールして利用します。

https://marketplace.visualstudio.com/items?itemName=humao.rest-client

REST Clientは、Web APIのテストを手軽にできる拡張機能です。この拡張機能をインストールし、拡張子が.httpのファイルをVSCodeで作成します。ここではtest.httpを以下のように作成します。

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として動作していることがわかります。

もう一つ試してみます。

test.http
### 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