🐥

SolrでもRAGできるもん!の裏話

2024/02/06に公開

はじめに

先日、Solr をバックエンドに RAG(Retrieval Augmented Generation)をするという記事を書きました。

https://zenn.dev/sashimimochi/articles/be1122c813d989

その記事では、順風満帆なように書きましたが、裏ではいろいろと躓きポイントがありました。
本記事では、その紆余曲折の過程をご紹介します。

また、サンプルコードを公開しました。
自身のお手元で試してみたいときの参考にどうぞ。

https://github.com/Sashimimochi/solr-rag

ココが苦労した!

eurelis-langchain-solr-vectorstore の制約が厳しい

Solr と LangChain をつなぐために eurelis-langchain-solr-vectorstore というライブラリを使わせてもらっています。
個のライブラリを使う最大の恩恵は、普段は面倒な機械学習部分の処理をブラックボックス化し、たった数行で実装できてしまうという点です。
反面、前回記事でも触れた通り Python3.11 でしかインストールできないなどいろいろと厳しい制約が付いています。

使用可能な型は限定されている

eurelis-langchain-solr-vectorstore を使って Solr にインデックスさせる場合、使用可能な型がかなり限定されます。
v0.0.1 現在使用できる型は以下の 4 つのみです。

  • str
  • int
  • float
  • bool

これ以外の型を Solr に POST しようとするとエラーが発生します。

ValueError: Expected metadata value to be a str, int, float or bool, got None which is a <class 'NoneType'>

Solr だと日付型(DatePointField)や緯度経度型(LatLonPointSpatialField)などを使いたいことがままあるのですが、これらが使えないのがなかなかもどかしい所です。

https://solr.apache.org/guide/solr/latest/indexing-guide/field-types-included-with-solr.html

今回は、作成日などの日付データはやむなく str 型でインデックスさせています。
また、ドキュメントによってはしばしば該当フィールドの値がなく、null 値を入れたくなることがありますが、それも許容されません。
事前に add_documents() 関数に渡す前に、そのドキュメントの該当フィールドを空文字で埋めておく、あるいは null になっているフィールドを削除するといった前処理が必要になります。

メタフィールドのフィールド名が固定されている

eurelis-langchain-solr-vectorstore から Solr にインデックスする場合、メタデータのフィールド名は metadata_{fieldname}_{type} で固定されています。

https://github.com/Eurelis/Eurelis-LangChain-SolR-VectorStore/blob/8750c0e53ea03a6a4e2ee8136ebf87fd0b9d3e86/src/eurelis_langchain_solr_vectorstore/solr_core.py#L54-L76

この仕様のうち、接頭文字と接尾文字の両方が固定されているというのがなかなか使いづらいです。

例えば、Python 上で tag というキー名で str 型のデータをインデックスさせた場合、metadata_tag_s というフィールド名で Solr に POST しようとします。
フィールド名が多くなってくると 1 つ 1 つスキーマを定義するのは煩雑になるので、ダイナミックフィールドで受け取りたくなります。

このとき、接頭文字と接尾文字の両方が固定されているとやっかいになります。
Solr のダイナミックフィールドの制約上、ワイルドカードは接頭か接尾のどちらか一方にしか指定できません。
どんな方のデータが来るかわからない以上、metadata_* のようなスキーマ定義は難しいでしょう。
となると必然的に接尾文字でダイナミックフィールドを定義することになるでしょう。

これは、メタデータ以外のフィールドを持つコレクションでは使いづらいと思います。
例えば、*_s でダイナミックフィールドを定義してしまうと、メタデータか否かに関わらず、すべて同じスキーマ定義になってしまいます。
せめて、metadata_{type}_{fieldname} であれば、メタデータかつ各型に応じたフィールドスキーマを定義できたのですが、そうはなっていません。

LangChain 経由でインデックスさせる場合は、*_s などで問題ないよう、返却テキスト以外のフィールド名は一律 metadata を接頭文字につける運用にすることがよさそうです。

ベクトル検索の結果として返却するフィールドはメタデータではないので、追加の接頭文字や接尾文字なしに POST されます。

ちなみに、eurelis-langchain-solr-vectorstore では、uniquekey を自動で付与して POST しますが、そのフィールド名は id で固定されています。
ですので、Solr 側に事前に id フィールドを用意しておく必要があります。
id フィールドにはハッシュ値のようなものが入っており、str 型で固定です。
これに合わせて、Solr のスキーマ定義でも文字列を受け取れる型にしておく必要があります。

こうしてみると、インデックス POST 部分は LangChain に頼らず自前の POST ツールを使う方が自由度は高そうです。
自作の場合、検索時に絞り込みフィルターが使えないことは注意が必要です。

例えば、

retriever = vector_store.as_retriever(
        search_kwargs = {
                "filter": {"tag": "solr"}
        }
)

で指定したフィルター条件は、metadata_tag_s で探しに行こうとします。
なので、このフィールド命名規則になっていないと絞り込みがうまく機能しません。

ということを考えると、eurelis-langchain-solr-vectorstore でベクトル検索をする際は、インデックスも検索もこのライブラリ経由で行うのが無難そうです。
実装が簡便になりますが、仕様や選択肢、カスタマイズ性が乏しくなってしまうのがもどかしい所です。
まあ、面倒くさがらずに Pull Request を送ればいいだけではあるのですが。

Embedding モデルの選定はこだわるべし

最初は Emebedding モデルは BERT 系ならなんでもいいかくらいに考えていました。
ですが、実際は Embedding モデルの選定は非常に重要でした。
特に、日本語のような形態素解析が必要な言語においては、tokenizer の振る舞いが変わります。
なので、真剣に選んだほうが良いと思います。

4 つほど試しましたが、これくらい違ってきます。

words = tokenizer.tokenize("もちっとカフェは検索エンジンやそれにまつわる自然言語処理を中心にサークル主である「さしみもち」が興味を持ったことを日々アウトプットしている同人サークルです。Word2VecやSolr関連の書籍の執筆や頒布活動を行っています。")
print(words)
# https://huggingface.co/sonoisa/sentence-bert-base-ja-mean-tokens-v2
['もち', 'っと', 'カフェ', 'は', '検索', 'エンジン', 'や', 'それ', 'にまつわる', '自然', '言語', '処理', 'を', '中心', 'に', 'サークル', '主', 'で', 'ある', '「', 'さ', 'しみ', 'もち', '」', 'が', '興味', 'を', '持っ', 'た', 'こと', 'を', '日々', 'アウト', '##プ', '##ット', 'し', 'て', 'いる', '同人', 'サークル', 'です', '。', 'Wor', '##d', '2', 'Ve', '##c', 'や', 'Sol', '##r', '関連', 'の', '書籍', 'の', '執筆', 'や', '頒', '##布', '活動', 'を', '行っ', 'て', 'い', 'ます', '。']
# https://huggingface.co/line-corporation/line-distilbert-base-japanese
['▁もち', 'っ', '▁と', '▁カフェ', '▁は', '▁検索', '▁エンジン', '▁や', '▁それ', '▁に', '▁まつ', 'わる', '▁自然', '▁言語', '▁処理', '▁を', '▁中心', '▁に', '▁サークル', '▁主', '▁で', '▁ある', '▁「', '▁さ', 'しみ', '▁もち', '▁」', '▁が', '▁興味', '▁を', '▁持っ', '▁た', '▁こと', '▁を', '▁日々', '▁アウト', 'プ', 'ット', '▁し', '▁て', '▁いる', '▁同', '人', '▁サークル', '▁です', '▁。', '▁wor', 'd', '▁2', '▁v', 'ec', '▁や', '▁sol', 'r', '▁関連', '▁の', '▁書籍', '▁の', '▁執筆', '▁や', '▁', '頒', '布', '▁活動', '▁を', '▁行っ', '▁て', '▁い', '▁ます', '▁。']
# https://huggingface.co/pkshatech/GLuCoSE-base-ja
['▁', 'もち', 'っと', 'カフェ', 'は', '検索エンジン', 'や', 'それに', 'まつわる', '自然', '言語', '処理', 'を中心に', 'サークル', '主', 'である', '「', 'さ', 'しみ', 'もち', '」', 'が', '興味', 'を持った', 'ことを', '日々', 'アウトプット', 'している', '同人', 'サークル', 'です', '。', 'Word', '2', 'V', 'ec', 'や', 'S', 'ol', 'r', '関連の', '書籍', 'の', '執筆', 'や', '頒', '布', '活動', 'を行っています', '。']
# https://huggingface.co/intfloat/multilingual-e5-large
['▁', 'も', 'ち', 'っと', 'カフェ', 'は', '検索', 'エンジン', 'や', 'それに', 'ま', 'つ', 'わ', 'る', '自然', '言語', '処理', 'を中心に', 'サークル', '主', 'である', '「', 'さ', 'し', 'み', 'も', 'ち', '」', 'が', '興味', 'を持った', 'ことを', '日々', 'アウト', 'プ', 'ット', 'している', '同', '人', 'サークル', 'です', '。', 'Word', '2', 'V', 'ec', 'や', 'Sol', 'r', '関連', 'の', '書籍', 'の', '執', '筆', 'や', '頒', '布', '活動', 'を行っています', '。']

形態素が異なるということは当然 Embedding 結果にも響いてきます。

例えば、日本語特化の検索を行いたい場合は、日本語のボキャブラリーが豊富なモデルが適しています。
日本語のコンテキストに特化しているため、日本語の検索や理解には有効です。

他方、日英検索を行いたい場合は、マルチリンガルモデルが非常に有効です。
日本語特化のモデルを使用して日英検索を行った場合、うまくヒットしない可能性があります。
例えば、「operating room」についてカタカナ語や日本語で類似度を計算してみます。
すると、それぞれのモデルでこんなにも違ってきます。

# sonoisa/sentence-bert-base-ja-mean-tokens-v2
オペレータールーム operating room {'score': 0.39732588073596975}
手術室 operating room {'score': 0.20133361228015667}
# pkshatech/GLuCoSE-base-ja
オペレータールーム operating room {'score': 0.6755412726006355}
手術室 operating room {'score': 0.44083859862786035}
# intfloat/multilingual-e5-large
オペレータールーム operating room {'score': 0.8819132635535043}
手術室 operating room {'score': 0.8826626988800004}

多言語モデルを使うと、カタカナ語も和訳語もほぼ同程度の類似性になります。

インデックスデータの選別も怠ることなかれ

モデルを適切に選ぶことで、より検索意図に合った Embedding 結果を得ることはできます。
とはいえ、その根本となるインデックスデータを適切にコレクション分けすることも非常に重要です。

関係のないノイズデータがインデックスに含まれると、ベクトル検索時に誤ったヒットが起こります。
特に SNS の投稿文のような短文は、ノイズとして影響を及ぼしやすいです。
クエリならまだしも、インデックス側が短いと非常に検索結果のノイズになります。

例えば、pkshatech/GLuCoSE-base-ja モデルで「Solr でベクトル検索をする方法を教えてください。」と検索してみた結果、以下のような結果が得られました。

    "docs":[{
      "id":"bf1ab34a-9cff-11ee-9c81-0242c0a80007",
      "body":"Solrでベクトル検索をする場合、検索時も簡単です。 もし、各種クエリの使い方がわからない場合は、第 2 章 全文検索エンジンとして動かしてみるで解説しています。 適宜戻って見返してみてください。さて、ベクトルをインデックスさせたフィールドを指定して検索します。 インデックス時と同じようにリスト形式でベクトルを渡してあげます。\n```&q={!knn f=vector topK=10}[1.0, 2.0, 3.0, 4.0]```\n f がベクトル検索対象フィールドで、topK が上位何件を取得するかの指定になります。 topK のデフォルトは 10 件です。上記の例だと、ベクトル `[1.0, 2.0, 3.0, 4.0]` に近い順にスコアが高くなり検索上位にヒットします。 スコアは `similarityFunction` で指定した類似度計算の方法を使って算出された値になります。シャード分割している場合は、それぞれのシャードから topK を集めてくるようです。 なので、2 シャード構成の場合は `topK=10` だと 20 件ヒットします。また、次のように filter cache や cost などのローカルパラメーターを使えば、類似度スコアを絞り込み条件に使用できます。\n```&q={!knn f=vector topK=10}[1.0, 2.0, 3.0, 4.0]&fq={!frange cache=false l=0.99}$q```\n",
      "score":0.8361846
    },{
      "id":"bf1b9e90-9cff-11ee-9c81-0242c0a80007",
      "body":"紹介URLはここ",
      "score":0.72576535
    },{
    },{
      "id":"bf1ab0d4-9cff-11ee-9c81-0242c0a80007",
      "body":"ベクトルデータベース(以下、ベクトル DB)とは、ひと言でいうとベクトル検索機能を備えたデータストアです。 あえて「データベース」ではなく、「データストア」と表現した理由は、2.7 従来型のデータベースとベクトルデータベースとの違いで触れます。ベクトル DB の提供各社のドキュメントをいくつか見てみると次のような説明がなされています。その名の通り、ベクトル形式のデータを効率的に貯蔵、抽出するためのデータストアです。 画像や音声、動画などの非構造化データあるいはテキストや地理空間データも埋め込み表現に変換して扱いたいという昨今のニーズに応える機能が盛り込まれています。 例えば、保存したベクトルデータに対する近似近傍探索機能などです。 これは従来のリレーショナルデータベース(以下、RDB)にはなかった機能です。 もちろん RDB と同様に、拡張性やバックアップ、セキュリティ機能も持ち合わせています。",
      "score":0.69493014
    },{

検索意図に合っていそうな 3 番目の文章より、関係のない文章が 2 番目とより上位に来てしまっています。

あと、これは Solr 固有の問題っぽいのですが、文章の取得件数である topK の数を極端に小さくすると、類似スコアが最大ではない文章がヒットしてしまうこともありました。

{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":10
    "params":{
      "q":"{!knn f=vector topK=2}[-0.4743734300136566, 0.2524718940258026, 0.24989350140094757, ...]",
      "df":"id",
      "echoParams":"all",
      "fl":"id body score",
      "q.op":"AND",
      "sort":"score desc",
      "rows":"10",
      "facet":"off",
      "wt":"json"
    }
  },
  "response":{
    "numFound":2,
    "start":0,
    "maxScore":0.7486509,
    "numFoundExact":true,
    "docs":[{
      "id":"bf1ddc96-9cff-11ee-9c81-0242c0a80007",
      "body":"教えてくださぃ!!!",
      "score":0.7486509
    },{
      "id":"bf1b9e90-9cff-11ee-9c81-0242c0a80007",
      "body":"紹介URLはここ",
      "score":0.72576535
    }]
  }
}

Solr の topK は、LangChain でいうところの k にあたります。

retriever = vector_store.as_retriever(
	search_type = "similarity_score_threshold", # スコア閾値を使用したいときに指定
        search_kwargs = {
		"k": k, # topK 件のドキュメントだけ取得する
                "filter": {"tag": "solr"}, # Solr の fq の条件を指定できる。使用できるフィールドはインデックス時に metadata で渡したフィールドのみ
                "score_threshold": 0.5 # score の閾値。これを上回ったドキュメントだけ取得する
        }
)

topK=3 以上の値を指定すると類似スコア最大のものから順に取得できています。

{
  "responseHeader": {
    "zkConnected": true,
    "status": 0,
    "QTime": 8,
    "params": {
      "q": "{!knn f=vector topK=3}[-0.4743734300136566, 0.2524718940258026, 0.24989350140094757,...]",
      "df": "id",
      "echoParams": "all",
      "fl": "id body score",
      "q.op": "AND",
      "sort": "score desc",
      "rows": "100",
      "facet": "off",
      "wt": "json",
      "rid": "null-29"
    }
  },
  "response": {
    "numFound": 3,
    "start": 0,
    "maxScore": 0.8361846,
    "numFoundExact": true,
    "docs": [
      {
        "id": "bf1ab34a-9cff-11ee-9c81-0242c0a80007",
        "body": "Solrでベクトル検索をする場合、検索時も簡単です。 もし、各種クエリの使い方がわからない場合は、第 2 章 全文検索エンジンとして動かしてみるで解説しています。 適宜戻って見返してみてください。さて、ベクトルをインデックスさせたフィールドを指定して検索します。 インデックス時と同じようにリスト形式でベクトルを渡してあげます。...",
        "score": 0.8361846
      },
      {
        "id": "bf1ab30e-9cff-11ee-9c81-0242c0a80007",
        "body": "\"ベクトル検索の基礎概念が理解できたところで、早速 Solr を使ってベクトル検索をやってみましょう。使い方は思いのほか簡単です。公式ドキュメント(https://solr.apache.org/guide/solr/latest/query-guide/dense-vector-search.html)でしっかりとチュートリアルがあります。...",
        "score": 0.8262049
      },
      {
        "id": "bf210e84-9cff-11ee-9c81-0242c0a80007",
        "body": "で、「サンガ」って検索するじゃないですか。",
        "score": 0.7650884
      }
    ]
  }
}

これがバグなのか HNSW アルゴリズムゆえの仕様なのかまでは調べ切れていませんが、検索結果に必要のない文章は検索結果を汚す原因になります。
ですので、インデックスから除く、少なくとも検索対象とは別のコレクションにしておくことをおすすめします[1]
あるいは、k の値をデフォルト値(k=4)以上で設定しておき、取得後に絞り込むという方法もありだと思います。

https://github.com/Eurelis/Eurelis-LangChain-SolR-VectorStore/blob/master/src/eurelis_langchain_solr_vectorstore/solr_core.py#L100

eurelis-langchain-solr-vectorstore 経由ではなく、Solr に直接 GET リクエストをかける場合は、topK=10 としておき、rows で絞り込みを行うことでも回避可能です。

どうしても同じコレクションに共存させる必要があり、極力ノイズを減らしたい場合は、フィルタークエリでキーワード検索(LangChain でいうところの filter)を使うのも有効です。
フィルタークエリはメインクエリのプレフィルターとして効くので、フィルタリングされたドキュメント内でだけベクトル検索をしてくれます。

https://solr.apache.org/guide/solr/latest/query-guide/dense-vector-search.html#usage-with-filter-queries

特に固有名詞を含むような具体的なユーザ入力である場合、混在コレクションに対して余計なドキュメントがヒットしないよう、キーワードフィルタリングをすることは効果的だと思います。

ベクトルデータのインデックスには DIH は使えないかも

Solr は検索エンジンであって、データベースではありません。
ですので、インデックスデータは別のプライマリストアに保存しておき、Solr へはそのストアからデータを取り込むという形式をしばしば取りたくなります。
プライマリストアと Solr の橋渡し役として DIH(Data Import Handler)がありますが、ベクトルデータのインデックスツールとしては厳しいかもしれません。
Solr9.0 のリリースと当時に 3rd party となった DIH ですが、分離を機にサポートが薄くなっている印象です。

Solr9.0 以降で DIH を使う方法についてはこちらをご覧ください。
https://qiita.com/Sashimimochi/items/7366f0eb11c6aafead80

DIH や Solr 自身には Emebedding 機能はありません。
そのため、事前にインデックス化したデータをデータベースに保存しておき、DIH ではそれを取り出して Solr に入れることになります。

しかし、MySQL ではリスト型のデータを保存できず、さらに SQL を使用してベクトルデータを取り出す際にリストに変換するのが難しいです。
Python から MySQL に格納する際に、 list を join して無理やり string 型として保存し、DIH で TransformerScript を使って split すればいいかとも思いましたが、上手くいきませんでした。
Solr(の Docker イメージ)の問題か、DIH の問題か、はたまた単なる私の設定ミスかわかりませんが、3rd party 製になって以降、JavaScript が正常に動作せず、TransformerScript が使えませんでした。
Java で自作のプラグインを作ることもできるでしょうが、それはそれで保守性が悪いです。
そこまでするくらいなら DIH 経由ではベクトルデータをインデックスさせないという方針になりそうです(というか私は自作プラグインを読み込ませることすらできませんでした)。

さらに、DIH のデータベースへのドライバーも MariaDB(MySQL) には対応していますが、他のデータベースには対応していません。
ベクトル検索ストアとして Solr を利用する場合は DIH からの脱却が求められそうです。

ベクトルデータの保存先として MongoDB は良さそう

先ほど、MySQL はリスト形式のデータを格納できないと書きましたが、MongoDB だとリスト型のデータをそのまま保存できます。
この点から、プライマリストアとして MongoDB を使用するというのは結構ありな選択肢なような気がします。

ちなみに、クラウド版である Atlas なら Atlas 上でベクトル検索も可能です(そうなると Solr いらなくなりますが...)。

https://www.mongodb.com/products/platform/atlas-vector-search

ローカル LLM の使いこなし

低スペック PC の場合、圧縮モデルを使うべし

今回、ローカル LLM を扱う上で一番苦労したのが、そもそもローカルで LLM を動かせるようにすることでした。
当初使おうとしていたのが、日本語対応のチャットモデルの中でも評判の良い CALM2-7B-Chat でした。

https://huggingface.co/cyberagent/calm2-7b-chat

有識者によると、このモデルを動かすには float16 で 14~15GB、float8 でも 10GB の GPU メモリが必要らしいです[2][3]
しかし、私の手元の環境はこんな感じで、8GB の GPU しかありませんでした[4]

Size
RAM(CPU Memory) 16GB
VRAM(GPU Memory) 8GB

Google Colaboratory であれば、無料版でも 12GB の GPU メモリが使えます。
ですが、Solr がローカルにあるため、LLM と Solr を通信させる以上、Google Colab 上で LLM を動かすわけにも行かず、あきらめかけていました[5]

そんなとき、より LLM を軽量化できるライブラリがあることを知りました。

https://github.com/OpenNMT/CTranslate2
https://github.com/ggerganov/llama.cpp

いずれも量子化と呼ばれる技術を使ってモデルを軽量化します。
量子化は、モデルの重みなどのパラメーターをより小さいビットで表現することで、モデルサイズを圧縮する手法です[6]

TensorFlow や PyTorch では、デフォルトだと float 型すなわち 32 ビットの浮動小数点精度を使用します。
それを 16 ビットや 8 ビットに減らすことで、モデルの構造を変えることなくメモリの使用量を抑えられます。

ただし、その分精度を落とすことになるので、軽量化と推論性能はトレードオフになることは注意が必要です。
最近の研究によれば、8 ビットの量子化であれば 1% 程度の性能低下で済むとも言われています[7]

実は量子化は 2016 年と LLM の登場以前から考えられていた手法ですが、モデルの巨大化と共に注目されています[8]
数理的な詳細はこちらの記事によくまとまっています[9]

上述の Ctranslate2(以下 CT2) と llama.cpp はいずれも量子化のためのライブラリです。
llama.cpp が基本的にデコーダーモデルのみに対応しているのに対し、CT2 はデコーダーモデルに加えてエンコーダーモデルにもいくらか対応しているという違いがあるようです。
つまり、BERT や T5 といったエンコーダー・デコーダーモデルの量子化には CT2 の方が向いているようです。
今回はどちらが良いというのは考えずに、使えるものを使いました。

今回は

  • 日本語対応している
  • チャット形式にファインチューニングされている
  • 私の手元のリソースで動かせる

の 3 点を満たしている次の 2 つを試してみました。

https://huggingface.co/rinna/japanese-gpt-neox-3.6b-instruction-ppo
https://huggingface.co/TheBloke/calm2-7B-chat-GGUF

1 つ目が Rinna 社のモデルで、これを自分で CT2 形式に圧縮して使用しました。

2 つ目がこのセクション冒頭で触れた CALM2-7B-Chat です。
llama.cpp 形式で圧縮済みのものが公開されていたので、これを使いました。
README にあるように、GPU がなくても、8GB 程度の CPU Memory があれば動きます。
ノート PC に積まれているようなスペックで動くのでありがたいです。

各形式の読み込み方法に関しては、私のサンプルコードおよび参考記事を貼っておくのでこれらをご覧ください[10][11][12]
また、十分な CPU メモリがあれば、自分でも CT2 や llama.cpp を使って圧縮することもできます。

モデルごとに適切なテンプレートを作る必要がある

前回の記事で触れたように、RAG をする場合には、LLM にテンプレートという形式で情報を投入します。
プロンプトチューニングの重要性が言われているので当たり前のような気がしますが、モデルごとに適したテンプレート(プロンプト)を書いてあげる必要があります。
ChatGPT は割と雑でも良しなに解釈してくれますが、ローカル LLM はテンプレートで結構結果が変わります。

例えば、ChatGPT では次のようなテンプレートでも十分望みの返答をしてくれます。

### 入力
あなたは対話エージェントです。
[参考]部分の情報を使って[ユーザ入力文]部分のユーザからの会話に回答してください。

[ユーザ入力文]
Solrでのベクトル検索の始め方を教えてください。
[参考]
ベクトル検索の基礎概念が理解できたところで、早速 Solr を使ってベクトル検索をやってみましょう。使い方は思いのほか簡単です。公式ドキュメント...

### 回答
Solrでのベクトル検索を始める手順について詳細に説明されていますね。基本的な手順を以下に要約します。

1. スキーマの定義: まず、managed-schema.xmlにベクトル検索用のフィールドを定義します。vectorDimensionやsimilarityFunctionなどのパラメータを指定します。
2. 高度な設定の追加: 必要に応じて、solrconfig.xmlにパラメータを指定し、パフォーマンスを微調整します。例えば、hnswMaxConnectionsやhnswBeamWidthなどを調整できます。
3. ベクトルをインデックス化: 定義したフィールドにベクトルデータをインデックス化します。
4. ベクトル検索の実行: 検索時には、`&q={!knn f=vector topK=10}[1.0, 2.0, 3.0, 4.0]`のような構文を使用します。fはベクトル検索対象フィールドを指定し、topKは上位何件を取得するかを指定します。

ただし、具体的なコードや設定についての質問がある場合は、それに応じて詳細な説明を提供できます。

ただし、Calm2 や Rinna 社モデルだと適当なテンプレートではまともな回答が生成されません。
例えば、同じテンプレートで Calm2 に入力してみると単なるオウム返しになってしまいました。

### 回答
# calm2
ベクトル検索の始め方を教えてください。

これを次のようなテンプレートに変えると、所望の結果が返ってくるようになりました。

### 入力
USER: 貴方はユーザーの質問に答えるAIアシスタントBotです。
ユーザーの質問に対して適切なアドバイスを答えます。
情報として、以下の内容を参考にしてください。
========
{context}
========
さて、「{query}」という質問に対して、上記の情報を元に、答えを考えてみましょう。
ASSISTANT:

{context} 部分には先ほどで言うところの参考情報を {query} にはユーザ入力文をそれぞれ挿入します。

そのほか、モデルの設定パラメーターでも回答は変化しますが、テンプレートの影響はかなり大きかったです。

テンプレートだけではカバーしきれない質の違いはありそう

やはりパラメーターサイズの関係か、モデルによって回答の質がだいぶ違ってきます。
Calm2 は Rinna 社モデルに比べて、倍近いパラメーター数を持つこともあってか、持っている知識量も全然違います。
例えば、「Solr という検索エンジンについて教えてください」と質問したところ、Calm2 は妥当な返答をしてきました。
これに対し、Rinna 社モデルだとそれっぽいが、実在するかすら疑問な何か別のものを返してきました。

### 入力
Solrという検索エンジンについて教えてください。

### 回答
# rinna/japanese-gpt-neox-3.6b-instruction-ppo
Googleなどの検索エンジンよりも、より柔軟なクエリ結果を生成することができます。また,オープンソースであるため無料で利用できています(ライセンスはMIT). Customersなどには良いですが,一般の利用者にはあまり馴染みがないかもしれません。 Web開発者やWebマーケティング担当者にとって有用であると同時に非常に強力なツールとなることがあります。

# calm2
solrは、オープンソースの検索エンジンです。
Apacheソフトウェア財団によって開発され、Luceneと呼ばれるJavaで書かれた全文検索ライブラリを使用しています。
Solrは、高速な検索性能と高い拡張性を兼ね備えており、様々なアプリケーションやWebサイトで利用されています。

RAG についても Rinna 社モデルだと説明や要約タスクはなかなか難しいです。
それぞれベストな回答を選びましたが、これだけの違いが出てしまいます。

### 入力
Solrでのベクトル検索の始め方を教えてください

### 回答
# rinna/japanese-gpt-neox-3.6b-instruction-ppo
もちろんです。
ベクトル検索の基本的な手順は、スキーマ定義で使用する関数を宣言し(solrconfig.xml)、インデックス作成とクエリ実行のためのコマンドラインユーティリティである Solaris Lucene Processing Tool (SALTAIR)-KernelPythonを使用します。
その後、要素を取得するために SSL/SSL v3-APIを使用して Webサーバーにリクエストを投げ込み、結果を受け取ります。
これにより、Google ScholarsやMicrosoft Online Serviceなどの高度なサービスを利用して大量のデータを分析できますが、これらのエンジンは効率化のためにかなり重い処理が必要になります。
また、「!knn f=vector」というキーワード引数を使用することで、類似度の計算方法を変更することができます。
これは、探索の結果に対してより柔軟なアプローチを取りたいときに有用であり、多くのアルゴリズムではこの手法が採用されています。
最後に重要なことは、ユーザーフレンドリーであることが非常に重要だということです。
そのためには、できるだけ簡単に学習できるようにすることが非常に役立ちました。
幸運にも素晴らしいチュートリアルがあります!
今後ともご活躍されること願っています。
:* * Numerical Algorithm "Best Practice" for Evaluation and Balance Analysis by

# calm2
ご質問にお答えします。

「Solrでのベクトル検索の始め方を教えてください」という質問に対しては、以下のように回答いたします。

Solr のスキーマ定義にベクトル検索用の型とフィールドを定義する。
<?xml version="1.0" encoding="UTF-8" ?>
<schema name="example" version="1.6">
     <field name="_version_" type="plong" indexed="false" stored="false" />
     <field name="_root_" type="int" indexed="true" stored="false" docValues="false" />
     <uniqueKey>id</uniqueKey>
     <field name="id" type="int" indexed="true" stored="true" required="true" />
     <field name="vector" type="knn_vector" indexed="true" stored="false" /> <!-- フィールド定義 -->
     <fieldType name="int" class="solr.TrieIntField" precisionStep="0" positionIncrementGap="0" />
     <fieldType name="plong" class="solr.LongPointField" docValues="true" />
     <fieldType name="knn_vector" class="solr.DenseVectorField" vectorDimension="4" similarityFunction="cosine" /> <!-- 型定義 -->
</schema>

1 番下の行がベクトル用のフィールド定義です。
vectorDimension はインデックスするベクトルの次元数です。
現状指定できるのは最大 1024 次元までです。
similarityFunction には類似度計算の方法を定義します。
デフォルトはユークリッド距離になっていますが、公式の推奨はコサイン類似度です。
コサイン類似度は角度情報だけを使って類似度計算を行うので、事前に単位ベクトル (ベクトルのノルムを 1) にしておくことが推奨されています。
2023/2/25 現在、使用できる計算手法は以下の 3 つです

Rinna 社モデルと Calm2 では、ベストな回答になるよう、別のテンプレートを使用しています。
それぞれ、次の記事のテンプレートを参考にさせてもらいました[13][14]
詳しくは本記事冒頭のサンプルコードを参考にしてください。

あれこれ試しましたが、Rinna 社モデルだと、要約のようなタスクは難しそうです。

一方、入力情報から短い回答を抜き出す QA タスクはうまくいきました。

例えば、次の情報からフシギダネのデザイナーを答えさせてみます。

https://wiki.ポケモン.com/wiki/フシギダネ

### 入力
フシキダネのデザインをしたのは誰ですか?

### 回答
# rinna/japanese-gpt-neox-3.6b-instruction-ppo
にしだあつこ
# calm2
フシギダネのデザインはにしだあつこが担当しました。

回答速度が速いのが軽量モデルの良いところですが、汎用性的にはある程度のモデルサイズが、今のところは必要そうです。

おわりに

苦戦したところを全部書き出したら長文「なってしまいましたが、参考になりそうな情報はあったでしょうか?
融合分野を扱うと各方面の知識が要求されて難しいです。
その分、できることの幅は広がるので、みなさんもぜひチャレンジしてみてください。

脚注
  1. 同じく HNSW を使用している faiss で試してみましたが、faiss では k=1 でも問題なく類似度最大の文章が得られました。 ↩︎

  2. https://medium.com/axinc/calm2-7b-chatのopenai互換サーバを立てる-8a62c2833bec ↩︎

  3. https://qiita.com/Yuki-Imajuku/items/c58638008c923ae295d9 ↩︎

  4. 余談ですが、恥ずかしながら専用 GPU メモリと共有 GPU メモリの違いがわかっておらず、「16GB あるはずなのに動かない!」としばらくもがいでいました。 ↩︎

  5. ngrok などで一時的にグローバル IP を付与すれば Colab からローカルの Solr にもつなげますが、そこまでする気にはなれず。 ↩︎

  6. https://laboro.ai/activity/column/engineer/ディープラーニングを軽量化するモデル圧縮/ ↩︎

  7. https://arxiv.org/abs/1712.08934 ↩︎

  8. https://arxiv.org/abs/1511.00363 ↩︎

  9. https://tech.retrieva.jp/entry/20220128 ↩︎

  10. https://secon.dev/entry/2023/11/23/220000-ctranslate2-embeddings/ ↩︎

  11. https://nowokay.hatenablog.com/entry/2023/06/15/065849 ↩︎

  12. https://developers.cyberagent.co.jp/blog/archives/45308/ ↩︎

  13. https://note.com/eurekachan/n/nfa51c49ab584 ↩︎

  14. https://note.com/npaka/n/n3bb60c61ef94 ↩︎

Discussion