Solrのベクトル検索がちょっとだけ楽になったらしい
2025/1/23にSolr9.8がリリースされましたね。
アップデート内容を見ていたら、ちょっと面白そうな機能がリリースされたので調べてみました。
結論としては、こんな感じです。
- クエリ次元限定でSolr上でEmbeddingが可能になった
- 軽微な設定だけでローカルモデルも外部LLMとも連携可能
- まだ試験的な機能でパフォーマンス面など課題がある
検証に使ったサンプルコードも公開しているのでよかったらどうぞ。
はじめに
ここ最近は個人的には目玉機能のアップデートがなかったので、今回もさほど期待はしていませんでした。
なので、何の気なしにSolrのアップデート情報を眺めていたのですが、ふと気になる文言が目に留まりました。
New knn_text_to_vector query parser that encodes query text into a vector (AKA "embedding") via external LLM services.
うん?どうやらSolr上でテキストのEmbeddingができるようになったらしいぞ。
そして、いつの間にかしれっとベクトル検索関連のドキュメントも更新されていました。
ベクトル検索をやるときにハードルとなるのが、Embeddingを検索エンジンの外部で行う必要があることでした。
それが内部で完結できるのであれば、サービス導入へのしやすさがグッと上がる可能性があります。
これは調査せねばなるまいとのことで、一人アマゾンの奥地に向かうことにしました。
できるようになったこと
確かに、キーワード検索と同様にテキストでリクエストを送るだけでベクトル検索ができてしまいました。
例えば、以下のようなクエリを投げると、Solr側でEmbeddingを行ってベクトル検索をしてくれています。
http://localhost:8983/solr/idcc/select?fl=body&indent=true&facet=false&q={!knn_text_to_vector model=mymodel f=vector topK=10}温かい鍋料理が食べたい
技術的にはLangChain4j
というLangChainのJavaバインディングを使っているようです。
とうとうJavaからもLangChainが使えるようになったか!これは便利だ!
と思っていたのですが、よくよく調べてみるとなんとも片手落ち感がある機能でした。
というのも、Solr上でEmbeddingが可能なのはクエリ時(リクエスト時)のみです。
インデックス時は引き続き外部でEmbeddingしておく必要があります。
IndexerにEmbedding処理を実装するのからは逃れられませんでした。
一応、インデックス時もSolr上でサポートしようよという話は上がったみたいです。
ただ、それまで含めると大きすぎるということで、今回はスコープ外として見送ったようです。
インデックス時/クエリ時フルサポートではありませんが、リアルタイム検索で専用APIを用意する必要がなくなっただけでも実装はしやすくなったと言えるでしょう。
今後のアップデートに期待しつつ、現状の機能を試してみたいと思います。
実際にやってみた
今回はEmbeddingモデルとして以下を使います。
そこそこのスペックがあれば動く、多言語対応に優れた埋め込みモデルです。
インデックスデータには、いつもお世話になっているデータセットを使用します。
まずは、スキーマを用意します。
ここは以前と変わりません。
詳しくは以前記事にしているので、こちらをご覧ください。
例えば以下のようにmanaged-schema
を書きます。
次元数はEmbeddingモデルに合わせて768次元としています。
<?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="string" indexed="true" stored="false" docValues="false"/>
<uniqueKey>id</uniqueKey>
<field name="id" type="string" indexed="true" stored="true" required="true"/>
<field name="_id" type="int" indexed="true" stored="true"/>
<field name="media" type="string" indexed="true" stored="false" docValues="true"/>
<field name="url" type="string" indexed="false" stored="false" docValues="true"/>
<field name="created_at" type="pdate" indexed="false" stored="false" docValues="true"/>
<field name="title" type="string" indexed="true" stored="true"/>
<field name="body" type="string" indexed="true" stored="true"/>
<field name="vector" type="knn_vector" indexed="true" stored="true"/>
<field name="metadata_title_s" type="tring" indexed="true" stored="true"/>
<field name="metadata_body_s" type="string" indexed="true" stored="false"/>
<dynamicField name="*_s" type="string" indexed="false" stored="false"/>
<dynamicField name="*_i" type="pint" indexed="false" stored="false"/>
<fieldType name="int" class="solr.TrieIntField" precisionStep="0" positionIncrementGap="0"/>
<fieldType name="pint" class="solr.IntPointField" docValues="true"/>
<fieldType name="plong" class="solr.LongPointField" docValues="true"/>
<fieldType name="string" class="solr.StrField" sortMissingLast="true" docValues="true"/>
<fieldType name="text_general" class="solr.TextField" positionIncrementGap="100"/>
<fieldType name="pdate" class="solr.DatePointField" docValues="true"/>
<fieldType name="knn_vector" class="solr.DenseVectorField" vectorDimension="768" similarityFunction="cosine"/>
</schema>
続いて、solrconfig.xml
を用意します。
ここが新たに設定が必要になっています。
と言っても、新設されたクエリパーサーを読み込むだけです。
<?xml version="1.0" encoding="UTF-8" ?>
<config>
<luceneMatchVersion>9.8.0</luceneMatchVersion>
<lib dir="/opt/solr-9.8.0/modules/extraction/lib" regex=".*\.jar" />
<lib dir="/opt/solr-9.8.0/modules/langid/lib/" regex=".*\.jar" />
<!--↓この行を追加する!-->
<lib dir="/opt/solr-9.8.0/modules/llm/lib/" regex=".*\.jar" />
<dataDir>${solr.data.dir:}</dataDir>
<!--↓この行を追加する!-->
<queryParser name="knn_text_to_vector" class="org.apache.solr.llm.texttovector.search.TextToVectorQParserPlugin"/>
<!--中略-->
</config>
ただし、ここで大きな罠が潜んでします。
というのも、Solr9.8からsolrconfig.xml
で<lib>
タグがデフォルトで無効にされるようになりました。
"<lib>" tags in solrconfig.xml are now quietly ignored by default unless explicitly enabled with the SOLR_CONFIG_LIB_ENABLED=true environment variable (or corresponding sysprop)
なので、solr.in.sh
などで環境変数を設定して、<lib>
タグを有効にしておく必要があります。
SOLR_CONFIG_LIB_ENABLED=true
また、起動オプションでLLMのモジュールを有効にしておく必要があります。
docker-compose.yml
では、command
を使って以下のように設定します。
x-solr-service: &solr-service
image: solr:9.8.0
ports:
- "8983:8983"
command:
- "-Denable.packages=true -Dsolr.modules=llm"
ここまでできたら、Solrを起動して、コレクションを作成します。
今回はidcc
という名前でコレクションを作成した想定で行きます。
コレクションの作成までできたら、モデルの設定ファイルを書いてSolrにアップロードします。
例えば、myModel.json
を作成して以下のように記載します。
{
"class": "dev.langchain4j.model.huggingface.HuggingFaceEmbeddingModel",
"name": "mymodel",
"params": {
"accessToken": "<YOUR_ACCESS_TOKEN>",
"modelId": "intfloat/multilingual-e5-base"
}
}
上記では、HuggingFaceにあるモデルをSolr上で動かすための設定になります。
name
はモデルを呼び出すときに指定する名前を書きます。
お好みの名前を設定してください。
accessToken
にはHuggingFaceのアクセストークンを設定します。
以下を参考にしながらアクセストークンを発行してください。
modelId
には使用するモデル名を入力します。
HuggingFace以外にもOpenAIやCohereなども使えます。
詳しくは、公式ドキュメントをご覧ください。
モデルの設定ファイルが作成出来たら、以下のようにしてSolrにアップロードします。
curl -XPUT 'http://localhost:8983/solr/idcc/schema/text-to-vector-model-store' --data-binary "@./solr/myModel.json" -H 'Content-type:application/json'
アップロードしたモデルの設定は以下のURLから確認できます。
http://localhost:8983/solr/idcc/schema/text-to-vector-model-store/currentModel
正常に登録できていれば、以下のようなレスポンスが返ってきます。
{
"responseHeader": {
"status": 0,
"QTime": 0
},
"models": [
{
"name": "mymodel",
"class": "dev.langchain4j.model.huggingface.HuggingFaceEmbeddingModel",
"params": {
"accessToken": "<YOUR_ACCESS_TOKEN>",
"modelId": "intfloat/multilingual-e5-base"
}
}
]
}
なお、同じ名前のモデルは複数登録できません。
その場合は、一度削除してから再アップロードする必要があります。
削除も以下のようにAPI経由でできます。
curl -XDELETE 'http://localhost:8983/solr/idcc/schema/text-to-vector-model-store/mymodel'
ここまででSolr側に必要な設定は以上です。
ここからはベクトル検索をやっていこうということになるのですが、まずは検索対象となるインデックスを用意する必要があります。
これはSolr上では行えないので、外部で頑張ってやる必要があります。
例えば私は最近こんな感じでやってます。
インデックスが投入出来たら、いよいよ検索してみましょう。
http://localhost:8983/solr/idcc/select?fl=body&indent=true&facet=false&q={!knn_text_to_vector model=mymodel f=vector topK=10}温かい鍋料理が食べたい
すると、こんな感じでAPIなしでもベクトル検索ができました!
{
"responseHeader": {
"zkConnected": true,
"status": 0,
"QTime": 248,
"params": {
"q": "{!knn_text_to_vector model=mymodel f=vector topK=10}温かい鍋料理が食べたい",
"df": "id",
"indent": "true",
"echoParams": "all",
"fl": "body",
"q.op": "AND",
"sort": "score desc",
"rows": "20",
"facet": "false",
"wt": "json",
"rid": "null-83"
}
},
"response": {
"numFound": 10,
"start": 0,
"numFoundExact": true,
"docs": [
{
"body": "[グルメ]鍋"
},
{
"body": "おいしく楽しく料理をしてみてはいかがでしょうか。"
},
{
"body": "熱々のおいしい鍋を家族でつつきながら食べるのは、一番の幸せだ。"
},
{
"body": "だれかの作った料理がいいのだ。"
},
{
"body": "ここは料理も美味しいんです。"
},
{
"body": "[グルメ]好きな食べもの"
},
{
"body": "冬、といえば鍋だ。"
},
{
"body": "料理がやってきました・・・。"
},
{
"body": "冬になり、寒くなるにつれて、煮込む系の料理がしたくなる。"
},
{
"body": "[グルメ]自炊"
}
]
}
}
注意点として、Solr上でEmbeddingモデルを動かすので、その分リソースを食います。
性能がいいからと言って高次元ベクトルを実行するとうっかりサーバが落ちちゃうこともあるかもなのでお気を付けください。
ちなみに先ほど話したように、OpenAI API likeな外部APIをモデルとして使用することもできます。
OpenAI APIはもちろん、Geminiなどほかのモデルでも指定可能です[1][2]。
// http://localhost:8983/solr/idcc/schema/text-to-vector-model-store/currentModel
{
"responseHeader": {
"status": 0,
"QTime": 0
},
"models": [
{
"name": "mymodel",
"class": "dev.langchain4j.model.huggingface.HuggingFaceEmbeddingModel",
"params": {
"accessToken": "<YOUR_API_KEY>",
"modelId": "intfloat/multilingual-e5-base"
}
},
{
"name": "gemini",
"class": "dev.langchain4j.model.openai.OpenAiEmbeddingModel",
"params": {
"baseUrl": "https://generativelanguage.googleapis.com/v1beta/openai",
"apiKey": "<YOUR_API_KEY>",
"modelName": "text-embedding-004",
"timeout": 60,
"logRequests": true,
"logResponses": true,
"maxRetries": 5
}
}
]
}
上記のように複数のモデルをSolr上に登録できます。
検索時には使用したいモデル名を指定すれば、リクエスト内容によってモデルを切り替えることも可能です。
http://localhost:8983/solr/idcc/select?fl=body&indent=true&facet=false&q={!knn_text_to_vector model=gemini f=vector topK=10}温かい鍋料理が食べたい
外部APIを呼び出すのでSolrサーバの負荷は下げられるでしょう。
外部APIの呼び出しをSolrの設定として組み込めるので、Solr側としてもAPI提供側としても余計なことを意識しなくて済みます。助かりますね。
まとめ
クエリ時限定ですが、Solr上でEmbeddingが可能になりました。
インデックス時の埋め込み処理は相変わらず頑張る必要がありますが、Solrでのベクトル検索がちょっとだけ楽になったと言えるでしょう。
なかなか面白い機能ですが、まだまだ発展途上な側面もあります。
インデックス時をサポートしていないほかに
Apache Solr uses LangChain4j to interact with Large Language Models. The integration is experimental and we are going to improve our stress-test and benchmarking coverage of this query parser in future iterations: if you care about raw performance you may prefer to encode the text outside of Solr
とあるようにまだ実験的な機能でパフォーマンス面などの課題が残っているようです。
それでもこれを機にSolrを使ったベクトル検索の事例が増えると嬉しい限りです。
それではまた。
-
GeminiをOpenAI互換で扱う場合は以下が参考になります。 https://ai.google.dev/gemini-api/docs/openai?hl=ja ↩︎
-
text-embedding-004
モデルの出力次元数は768次元です。 https://ai.google.dev/gemini-api/docs/models/gemini?hl=ja ↩︎
Discussion