Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

TiDB RAG with Ollama Ruri

livebooks/tidb/tidb_rag.livemd

TiDB RAG with Ollama Ruri

Mix.install([
  {:myxql, "~> 0.7"},
  {:kino_db, "~> 0.3"},
  {:ollama, "~> 0.8"}
])

DB接続

opts = [
  hostname: "",
  port: 4000,
  username: "",
  password: System.fetch_env!("LB_TIDB_PASSWORD"),
  database: "test",
  ssl: [cacertfile: "/etc/ssl/certs/ca-certificates.crt"]
]

{:ok, conn} = Kino.start_child({MyXQL, opts})

テーブル作成

result = MyXQL.query!(conn, ~S"DROP TABLE IF EXISTS rag_index", [])
result2 =
  MyXQL.query!(
    conn,
    ~S"""
    CREATE TABLE rag_index (
      text VARCHAR(1000),
      embedding VECTOR(768),
      VECTOR INDEX idx_embedding ((VEC_COSINE_DISTANCE(embedding)))
    )
    """,
    [],
    timeout: 180_000
  )

チャットの実装

client = Ollama.init(base_url: "http://ollama:11434/api", receive_timeout: 300_000)
Ollama.pull_model(client, name: "phi4")
Ollama.preload(client, model: "phi4")
answer = fn input, frame ->
  {:ok, stream} =
    Ollama.completion(
      client,
      model: "phi4",
      prompt: input,
      stream: true
    )

  stream
  |> Stream.transform("AI: ", fn chunk, acc ->
    response = acc <> chunk["response"]

    markdown = Kino.Markdown.new(response)
    Kino.Frame.render(frame, markdown)

    {[chunk["response"]], response}
  end)
  |> Enum.join()
end
answer_frame = Kino.Frame.new()
answer.("やせうまについて100文字程度で説明して", answer_frame)

テキスト埋め込み

Ollama.pull_model(client, name: "kun432/cl-nagoya-ruri-base")
string_inputs = [
  "浦島太郎は日本の昔話の主人公で、亀を助けた礼として竜宮城に招かれます。帰郷時に渡された玉手箱を開けると老人になってしまう物語です。",
  "豆腐小僧は江戸時代の草双紙や錦絵に登場する妖怪で、笠をかぶり盆に乗せた豆腐を持つ子供の姿をしています。特に悪さをせず、愛嬌のある存在として描かれています。",
  "やせうまは大分県の郷土菓子で、茹でた小麦粉の生地にきな粉と砂糖をまぶしたものです。素朴な甘さともちもちした食感が特徴です。"
]
embed = fn input ->
  client
  |> Ollama.embed(
    model: "kun432/cl-nagoya-ruri-base",
    input: input
  )
  |> elem(1)
  |> Map.get("embeddings")
  |> hd()
end
embeddings = Enum.map(string_inputs, fn input -> embed.("文章: #{input}") end)
[string_inputs, embeddings]
|> Enum.zip()
|> Enum.map(fn {text, embedding} ->
  MyXQL.query!(
    conn,
    ~S"""
    INSERT INTO rag_index (text, embedding) VALUES (?, ?)
    """,
    [text, embedding]
  )
end)
result3 = MyXQL.query!(conn, ~S"SELECT * FROM rag_index", [])

テキスト検索

search = fn query ->
  embedding = embed.("クエリ: #{query}")

  MyXQL.query!(
    conn,
    ~S"""
    SELECT text FROM rag_index
    ORDER BY VEC_COSINE_DISTANCE(embedding, ?)
    LIMIT 1
    """,
    [embedding]
  )
end
search.("やせうまについて100文字程度で説明して")

RAGチャットの実装

rag = fn input, frame ->
  context =
    input
    |> search.()
    |> Map.get(:rows)
    |> hd()
    |> hd()

  answer.("context: #{context}\n\ncontext に基づいて質問に答えてください\n\n#{input}", frame)
end
# 出力用フレーム
output_frame = Kino.Frame.new()

# ストリーミング用フレーム
stream_frame = Kino.Frame.new()

# 入力用フォーム
input_form =
  Kino.Control.form(
    [
      input_text: Kino.Input.textarea("メッセージ")
    ],
    submit: "送信"
  )

Kino.Frame.render(output_frame, Kino.Markdown.new(""))
Kino.Frame.render(stream_frame, Kino.Markdown.new(""))

# フォーム送信時の処理
Kino.listen(input_form, fn %{data: %{input_text: input}} ->
  Kino.Frame.append(output_frame, Kino.Markdown.new("あなた: " <> input))
  full_response = rag.(input, stream_frame)
  Kino.Frame.render(stream_frame, Kino.Markdown.new(""))
  Kino.Frame.append(output_frame, Kino.Markdown.new("AI: " <> full_response))
end)

# 入出力を並べて表示
Kino.Layout.grid([output_frame, stream_frame, input_form], columns: 1)