Pythonで超手軽に使えるベクトル検索 ― nlp4j-local-search 0.8.0.0
ベクトル検索を試してみたいと思っても、実際に動かそうとすると意外と準備が大変です。
- Embeddingモデルを用意する
- テキストをベクトル化する
- ベクトルを保存する
- ベクトル検索エンジンを用意する
- クエリも同じモデルでベクトル化する
- 類似ベクトルを検索する
ちょっと試したいだけなのに、検索するまでの準備だけで結構なコードになってしまいます。
そこで nlp4j-local-search 0.8.0.0 では、これらをできるだけ意識せず、普通のPythonコードからベクトル検索を使えるようにしました。
PyPI:
インストール
まず検索ライブラリをインストールします。
pip install nlp4j-local-search
テキストから自動的にEmbeddingを生成する場合は、Embedding用パッケージもインストールします。
pip install nlp4j-local-search-embedding
Embeddingにはデフォルトで intfloat/multilingual-e5-large を利用します。
初回実行時にはモデル(約2.2GB)がHugging Faceからダウンロードされるため、最初だけ少し時間がかかります。
とりあえずベクトル検索してみる
用意するデータは普通のテキストです。
from nlp4j_local_search import SearchEngine
documents = [
{"id": "1", "body": "京都は日本の古都です"},
{"id": "2", "body": "東京は日本最大の都市です"},
{"id": "3", "body": "奈良には歴史的な寺院があります"},
{"id": "4", "body": "大阪は食文化で有名な都市です"},
{"id": "5", "body": "北海道は広大な自然が広がっています"},
]
with SearchEngine("ja", embedding=True) as engine:
for doc in documents:
engine.add(doc["id"], doc["body"])
engine.commit()
for r in engine.search_semantic("日本の古い都", limit=3):
print(r.id, r.body, r.score)
ポイントは、
SearchEngine("ja", embedding=True)
です。
embedding=True を指定すると、Embeddingモデルがロードされ、add() したテキストが自動的にベクトル化されます。
つまり、
engine.add("1", "京都は日本の古都です")
と書くだけです。
自分でEmbeddingを計算して、
[0.0234, -0.0182, 0.0421, ...]
のようなベクトルを扱う必要はありません。
検索するときも同じです。
engine.search_semantic("日本の古い都")
これだけで検索文字列が自動的にEmbeddingされ、ベクトル検索が実行されます。
「京都」と書かなくても京都が見つかる
通常のキーワード検索では、基本的には文書に含まれる単語を使って検索します。
一方、ベクトル検索では文章の意味的な近さを利用できます。
例えば登録されている文章が、
京都は日本の古都です
だったとしても、
engine.search_semantic("日本の古い都")
のように検索できます。
サンプルではさらに、
queries = [
"日本の古い都",
"大きな街",
"お寺や神社",
]
のような検索を行っています。
for query in queries:
print(f"=== セマンティック検索: '{query}' ===")
for r in engine.search_semantic(query, limit=3):
print(f"[{r.id}] {r.body} (score: {r.score:.4f})")
例えば「お寺や神社」という検索語から、
奈良には歴史的な寺院があります
のような意味的に近い文章を探す、といった使い方ができます。
キーワード検索もそのまま使える
embedding=True にしたからといって、ベクトル検索専用になるわけではありません。
従来のキーワード検索も利用できます。
for r in engine.search("京都", limit=5):
print(r.id, r.body, r.score)
つまり同じ SearchEngine から、
engine.search("京都")
ならキーワード検索、
engine.search_semantic("日本の古い都")
ならセマンティック検索、
というように使い分けられます。
個人的には、
search()
search_semantic()
とAPIを明確に分けることで、コードを見ただけで「どの種類の検索をしているのか」が分かるようにしています。
フィルター付きベクトル検索もできる
実際の検索アプリでは、
全文書から意味的に近いものを検索
するだけでなく、
条件に一致する文書の中から意味的に近いものを検索
したいケースがよくあります。
search_semantic() では filter_query を指定できます。
for r in engine.search_semantic(
"都市",
limit=5,
filter_query="id:1 OR id:2",
):
print(r.id, r.body, r.score)
この場合、まず
id:1 OR id:2
という条件で検索対象を絞り、その中で「都市」という意味に近い文書をベクトル検索できます。
フィルター部分にはLucene Query Syntaxを利用できます。
そのため、ベクトル検索と通常のフィールド検索を組み合わせた検索も構築できます。
中ではLucene 10.xが動いている
0.8.0.0では、検索エンジンとしてApache Lucene 10.xを利用しています。
Pythonから見ると、
engine.add(...)
engine.search(...)
engine.search_semantic(...)
というシンプルなAPIですが、その裏側ではLuceneのインデックスとベクトル検索機能を利用しています。
そのため、別途Elasticsearch、OpenSearch、Solrなどの検索サーバーを立ち上げる必要はありません。
Dockerで検索サーバーを起動する必要もありません。
PythonプロセスからローカルのLuceneを直接利用します。
イメージとしては、
Python
|
+-- nlp4j-local-search
|
+-- Embedding
| |
| +-- multilingual-e5-large
|
+-- Apache Lucene 10.x
|
+-- Keyword Search
+-- Vector Search
という非常にシンプルな構成です。
ベクトルDBを準備しなくてもベクトル検索できる
今回やりたかったことの一つがこれです。
「ベクトル検索を試してみたい」
というだけなのに、
- ベクトルDBを選ぶ
- サーバーを構築する
- Dockerを準備する
- 接続設定をする
- Embeddingライブラリを選ぶ
- モデルをロードする
- ベクトルのdimensionを管理する
といった準備が必要になると、検索そのものを試すまでのハードルが上がります。
nlp4j-local-search では、
pip install nlp4j-local-search
pip install nlp4j-local-search-embedding
として、
with SearchEngine("ja", embedding=True) as engine:
と書けば、そこから普通のPythonコードとしてベクトル検索を始められます。
もちろんEmbeddingを直接扱うこともできる
embedding=True は「テキストを簡単にベクトル検索したい」場合の便利な機能です。
一方、0.8.0.0ではVectorフィールドそのものを扱うこともできます。
つまり、
テキスト
↓
nlp4j-local-searchがEmbedding
↓
Lucene Vector Search
という使い方だけでなく、
自分で用意したVector
↓
Lucene Vector Search
という使い方も可能です。
独自のEmbeddingモデルを使いたい場合や、すでにベクトル化済みのデータを持っている場合にも利用できます。
まとめ
nlp4j-local-search 0.8.0.0では、PythonからLucene 10.xのベクトル検索をかなり手軽に利用できるようになりました。
最小限覚える必要があるのは、ほぼこの3つです。
SearchEngine("ja", embedding=True)
engine.add(id, text)
engine.search_semantic(query)
Embeddingモデルのロード、文書のベクトル化、検索クエリのベクトル化、Luceneへのベクトル登録と検索はライブラリ側で処理します。
ベクトル検索を使うために、大掛かりな検索サーバーやベクトルDBを準備する必要もありません。
「まずPythonでベクトル検索を試してみたい」
という用途であれば、かなり小さなコードから始められるようになったと思います。
from nlp4j_local_search import SearchEngine
with SearchEngine("ja", embedding=True) as engine:
engine.add("1", "京都は日本の古都です")
engine.add("2", "東京は日本最大の都市です")
engine.commit()
for r in engine.search_semantic("日本の古い都"):
print(r.body)
ベクトル検索を、普通のPythonの検索処理くらい手軽に。
それが今回の nlp4j-local-search 0.8.0.0 の狙いです。