3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Livebook 上で日本語テキスト埋め込みモデル Ruri によるテキスト検索を実装する(Ollama 利用)

Last updated at Posted at 2025-02-19

はじめに

前回の記事で Livebook 上での Ruri による日本語テキスト検索を実装しました

前回記事の手法は Elixir で完結できる反面、モデルによっては Bumblebee が未対応で実行できないケースがあります

本記事では LLM の推論を Ollama に任せることで Ollama 対応のモデルであれば利用可能とします

実装したノートブックはこちら

コンテナの準備

本記事では Livebook と Ollam をコンテナで起動します

docker-compose.with-ollama.yml

---

services:
  livebook_with_ollama:
    image: ghcr.io/livebook-dev/livebook:0.14.7
    container_name: livebook_with_ollama
    ports:
      - '8080:8080'
      - '8081:8081'
    volumes:
      - ./tmp:/tmp

  ollama:
    image: ollama/ollama:0.5.11
    container_name: ollama_for_livebook
    ports:
      - '11434:11434'
    volumes:
      - ollama:/root/.ollama

volumes:
  ollama:

コンテナのビルド・起動

以下のコマンドでコンテナをビルド、起動します

docker compose --file docker-compose.with-ollama.yml up

両方のビルドが完了したら Livebook の URL にアクセスします

Livebook 上でのテキスト検索の実装

セットアップ

Livebook で新しいノートブックを開き、セットアップセルで以下のコードを実行します

Mix.install(
  [
    {:nx, "~> 0.9"},
    {:exla, "~> 0.9"},
    {:kino, "~> 0.14"},
    {:hnswlib, "~> 0.1"},
    {:ollama, "~> 0.8"}
  ],
  config: [nx: [default_backend: EXLA.Backend]]
)

モデルの用意

コンテナで起動している Ollama にアクセスするためのクライアントを用意します

URLでしていしている「ollama」の部分は docker-compose で指定したサービス名です

client = Ollama.init(base_url: "http://ollama:11434/api")

クライアントで Ruri base のモデルをダウンロードします

Ollama.pull_model(client, name: "kun432/cl-nagoya-ruri-base")

検索用インデックスの構築

検索インデックスに登録する文章を用意します

string_inputs = [
  "りんごはバラ科の落葉高木が実らせる果実で、世界中で広く栽培される。甘味と酸味のバランスが良く、生食のほかジュースや菓子など多彩な料理に利用される。ビタミンや食物繊維も豊富で、健康維持に役立つ。",
  "コンピューターは情報を高速かつ正確に処理する装置で、計算やデータ分析、通信など多様な分野で活用される。人工知能の発展とともに進化し、人々の生活や産業を大きく支えている。モバイルなど形態も多様化している。",
  "クジラは海洋に生息する巨大な哺乳類で、ヒゲクジラ類とハクジラ類に大別される。水中で呼吸を行うために定期的に海面に浮上し、高度な社会性やコミュニケーション能力を持つ。歌と呼ばれる鳴き声で意思疎通する種もいる。",
  "ラーメンは中国の麺料理を起源とする日本の国民食のひとつ。小麦粉の麺とスープが主体で、醤油・味噌・塩・豚骨など多様な味が楽しめる。具材もチャーシューやメンマ、ネギなど豊富で、地域ごとに特色ある進化を遂げている。ラーメンは中国の麺料理を起源とする日本の国民食のひとつ。小麦粉の麺とスープが主体で、醤油・味噌・塩・豚骨など多様な味が楽しめる。具材もチャーシューやメンマ、ネギなど豊富で、地域ごとに特色ある進化を遂げている。",
]

Ollama のコンテナで起動している API にアクセスし、各文章を埋め込み表現に変換します

embeddings =
  string_inputs
  |> Enum.map(fn input ->
    client
    |> Ollama.embed(
      model: "kun432/cl-nagoya-ruri-base",
      input: "文章: #{input}"
    )
    |> elem(1)
    |> Map.get("embeddings")
    |> hd()
    |> Nx.tensor()
  end)

実行結果

[
  #Nx.Tensor<
    f32[768]
    EXLA.Backend<host:0, 0.1579428613.2092826643.8739>
    [-0.009650768712162971, -7.209923351183534e-4, 0.056412581354379654, 0.031258221715688705, -0.02891393005847931, 0.001248478190973401, -0.006854886654764414, -0.017830533906817436, 0.010209999047219753, 0.04102960601449013, -0.014581399038434029, 0.008022818714380264, -0.011640564538538456, -0.037401340901851654, 0.009960242547094822, 0.0075966059230268, 0.030639519914984703, 0.0505681075155735, -0.05894075334072113, 0.023118672892451286, -0.04658573865890503, -0.047506626695394516, 0.050927288830280304, -0.028459692373871803, 0.012689411640167236, 0.021842215210199356, -0.017471879720687866, 0.010441550984978676, -0.043585631996393204, -0.04328896477818489, -0.03866329416632652, -0.0036312465090304613, 0.030375514179468155, -0.31801554560661316, -0.012979279272258282, -0.011597209610044956, -0.011567975394427776, -0.008289889432489872, -0.002796049462631345, 0.04218113049864769, 0.0018834196962416172, 0.002252012025564909, -0.04593528434634209, 0.052265871316194534, -0.025113524869084358, 0.04063776880502701, -0.022994544357061386, 0.013590161688625813, -0.02652803063392639, ...]
  >,
  #Nx.Tensor<
    f32[768]
    EXLA.Backend<host:0, 0.1579428613.2092826643.8740>
    [7.430978002958e-4, 0.0024165534414350986, 0.03719779849052429, 0.030217332765460014, -0.042501192539930344, -0.03368866443634033, -0.04792698472738266, -0.027135584503412247, 0.011622343212366104, 0.014928818680346012, -0.06277810782194138, 0.006294114049524069, -0.03015064261853695, -0.02395399659872055, 0.04785580560564995, 0.020249366760253906, 0.004230509977787733, 0.028682591393589973, -0.06140254810452461, 0.04606116563081741, -0.047766223549842834, -0.048105765134096146, 0.01467564981430769, -0.02284586988389492, 0.011559163220226765, -0.0066442182287573814, -0.02507941983640194, -0.003980780951678753, -0.025192061439156532, -0.011535302735865116, -0.05567653104662895, 0.02823845110833645, 0.008767286315560341, -0.3226644992828369, -0.014426269568502903, -0.01496464665979147, -0.02780456654727459, -0.011774307116866112, -0.016577530652284622, 0.03974340856075287, 0.03476350009441376, -0.01270400732755661, -0.02696702629327774, 0.01057896576821804, -0.0497346967458725, 0.041787199676036835, -0.024432798847556114, 0.021398447453975677, ...]
  >,
  #Nx.Tensor<
    f32[768]
    EXLA.Backend<host:0, 0.1579428613.2092826643.8741>
    [-0.0037725092843174934, 0.009813826531171799, 0.04746600612998009, 0.02799217216670513, -0.030641354620456696, -0.01203999761492014, -0.03386930748820305, -0.026255717501044273, 0.015185598284006119, 0.0211894903331995, -0.05401001125574112, 0.007819395512342453, -0.03497139364480972, -0.049362048506736755, 0.021169910207390785, -0.0021432156208902597, -0.022702250629663467, 0.03560576215386391, -0.06463878601789474, 0.049745798110961914, -0.014675949700176716, -0.049862559884786606, 0.04261210188269615, -0.022480115294456482, 0.025639444589614868, 0.03358863666653633, -0.04429693520069122, -0.0012138234451413155, -0.00893046148121357, -0.007472609169781208, -0.048195697367191315, 0.00833874847739935, -6.405961466953158e-4, -0.30959367752075195, -0.0012716944329440594, -0.02358010783791542, -0.010365098714828491, -0.03349795937538147, -0.011194777674973011, 0.03586369752883911, 0.037120141088962555, -0.012263430282473564, -0.06374254822731018, 0.014464219100773335, -0.05330660194158554, 0.03239421173930168, -0.0075212931260466576, ...]
  >,
  #Nx.Tensor<
    f32[768]
    EXLA.Backend<host:0, 0.1579428613.2092826643.8742>
    [0.002915355609729886, -0.01485569030046463, 0.04422079399228096, 0.04044019430875778, -0.0294900294393301, -0.04009227082133293, -0.05244863033294678, -0.007077774964272976, 0.002654271200299263, 0.03258080407977104, -0.010410468094050884, 0.017785947769880295, -0.032656338065862656, -0.04024633765220642, 0.028866376727819443, -0.004638455808162689, 0.006641987711191177, 0.03163190931081772, -0.07984863221645355, 0.050591617822647095, -0.04670245200395584, -0.07729766517877579, 0.04100413620471954, -0.028829488903284073, -0.014615902677178383, 0.0353550910949707, -0.03836218640208244, 0.04410500451922417, -0.003009211737662554, -0.049569156020879745, -0.03594614937901497, 0.012286822311580181, 0.005709735210984945, -0.2990419864654541, 8.963511209003627e-4, -0.022411122918128967, 0.003975078463554382, -0.01938365027308464, -0.009976733475923538, 0.042709674686193466, 0.023439226672053337, -0.006377306766808033, -0.01614820398390293, 0.04979158192873001, 0.006295747589319944, 0.038036104291677475, ...]
  >
]

埋め込み表現をインデックスに登録します

{:ok, index} = HNSWLib.Index.new(:cosine, 768, 100)

for embedding <- embeddings do
  HNSWLib.Index.add_items(index, embedding)
end

HNSWLib.Index.get_current_count(index)

テキスト検索

検索するクエリも同様に埋め込み表現に変換し、インデックスから類似文書を検索します

search = fn query ->
  query_embedding =
    client
    |> Ollama.embed(
      model: "kun432/cl-nagoya-ruri-base",
      input: "クエリ: #{query}"
    )
    |> elem(1)
    |> Map.get("embeddings")
    |> hd()
    |> Nx.tensor()

  {:ok, labels, _dist} = HNSWLib.Index.knn_query(index, query_embedding, k: 1)

  labels
  |> Nx.to_flat_list()
  |> hd()
  |> then(&Enum.at(string_inputs, &1))
end

鯨について教えて というクエリで検索を実行してみます

search.("鯨について教えて")

実行結果

"文章: クジラは海洋に生息する巨大な哺乳類で、ヒゲクジラ類とハクジラ類に大別される。水中で呼吸を行うために定期的に海面に浮上し、高度な社会性やコミュニケーション能力を持つ。歌と呼ばれる鳴き声で意思疎通する種もいる。"

文章内では「クジラ」とカタカナで書いていますが、クエリ内の漢字表記「鯨」で検索できています

お腹が空いた というクエリで検索を実行してみます

search.("お腹が空いた")

実行結果

"文章: ラーメンは中国の麺料理を起源とする日本の国民食のひとつ。小麦粉の麺とスープが主体で、醤油・味噌・塩・豚骨など多様な味が楽しめる。具材もチャーシューやメンマ、ネギなど豊富で、地域ごとに特色ある進化を遂げている。ラーメンは中国の麺料理を起源とする日本の国民食のひとつ。小麦粉の麺とスープが主体で、醤油・味噌・塩・豚骨など多様な味が楽しめる。具材もチャーシューやメンマ、ネギなど豊富で、地域ごとに特色ある進化を遂げている。"

直接関係する言葉は含んでいませんが、食べ物に関する文章が返ってきました

植物 というキーワードだけで検索してみます

search.("植物")

実行結果

"文章: りんごはバラ科の落葉高木が実らせる果実で、世界中で広く栽培される。甘味と酸味のバランスが良く、生食のほかジュースや菓子など多彩な料理に利用される。ビタミンや食物繊維も豊富で、健康維持に役立つ。"

りんごに関する文章が返ってきました

確かに日本語として関係する文章が取得できています

まとめ

Ollama を利用することで、 Livebook 上で日本語テキスト検索が実装できました

Ollama 対応のモデルであればローカルで完結できるので、 OpenAI API などの利用料もかからなくて良いですね

実はこちらの手段の方が先に実装できていたのですが、なんとか Bumblebee で推論させたくて前回記事の手法(トークナイザーの変換)を実装しました

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?