📘

Solr9 から導入されたベクトル怜玢っおどんな感じ

に公開

忙しい人向けの芁点

  1. ベクトルデヌタさえあれば必芁な Solr 定矩はたった2行
  2. ク゚リも q, fq, rqq で䜿えお、指定方法もシンプル
  3. 100䞇件皋床のむンデックスでもレスポンスタむムは数10ms~数100msなので割ず䜿えそう
  4. Solr には embedding 機胜はないので、むンデックスデヌタやク゚リのベクトル倉換はどうにか頑匵っおください

スクリヌンショット 2023-02-25 015600.png

はじめに

Solr9 から、個人的に切望しおいた DenseVectorField フィヌルド型ず K近傍法KNNク゚リヌパヌサヌによる密ベクトルニュヌラル怜玢が远加されたした。

https://solr.apache.org/guide/solr/9_0/upgrade-notes/major-changes-in-solr-9.html

ベクトル怜玢を䜿えば、類䌌画像の怜玢やシノニム(類矩語蟞曞)を䜿わずに同矩語を含むドキュメントの怜玢など埓来党文怜玢゚ンゞンでは到底実珟できなかった怜玢ができるようになりたす。

䟋えば以䞋のように、ク゚リキヌワヌドそのものを含んでいなくおも、䌌たような内容のドキュメントがヒットさせられるようになりたす。

screencapture-localhost-8501-2023-02-27-10_35_30.png
ex. テキスト to テキスト怜玢
それ以倖にも、画像から類䌌した画像の怜玢やテキストから画像の怜玢などができるようになったりもしたす。

screencapture-localhost-8501-2023-02-24-19_00_22.png
ex. 画像 to 画像怜玢
screencapture-localhost-8501-2023-02-17-09_40_40.png
ex. テキスト to 画像怜玢

究極的にはベクトル情報にさえ倉換されおいればいいので、テキストだろうが、画像だろうが、音声、動画なんでも比范できたす。
怜玢結果の良し悪しはあたり Solr 偎では関䞎の䜙地はなく、事前にどれだけ良いベクトル倉換(以䞋、embedding)モデルを䜜れるかにかかっおいたす。

近幎は ChatGPT の登堎によっお怜玢に砎壊的な圱響を及がしおいたすが、瀟内の独自デヌタを含む自瀟のサむト内の怜玢゚ンゞンが近々に ChatGPT に取っお代わられるずいうこずはただないでしょう。
ベクトル怜玢は自瀟サヌビスの怜玢䜓隓をより良くするための1キラヌ技術になり埗るず思いたす。

たずはベクトル怜玢を䜿っおみたいずいう方は、 Google Cloud パヌトナヌである Groovenauts によっお開発されたデモサむトやロンりィットの KandaSearch によるデモサむトを觊っおみるずその嚁力が䜓感できるず思いたす。
特に KandaSearch のデモサむトは裏では今回玹介する Solr によるベクトル怜玢が䜿われおいたす。[1]

https://matchit.magellanic-clouds.com/

https://demo.rondhuit.com/

むンデックス線

Solr 定矩

䜿い方は思いのほか簡単です。
公匏ドキュメントでしっかりずチュヌトリアルがあるので、それに埓えば䜿えおしたいたす。

https://solr.apache.org/guide/solr/latest/query-guide/dense-vector-search.html

たずは、スキヌマ定矩にベクトル怜玢甚の型ずフィヌルドを定矩したす。

mamaged-schema.xml
<?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>

vectorDimensionはむンデックスするベクトルの次元数です。
珟状指定できるのは最倧1024次元たでです。

similarityFunctionには類䌌床蚈算の方法を定矩したす。デフォルトはナヌクリッド距離になっおいたすが、公匏の掚奚はコサむン類䌌床です。

2023幎2月25日珟圚、䜿甚できる蚈算手法は以䞋の3぀です。

DenseVectorField は indexed ず stored が指定できたす。Solr9.1 時点では multiValued はサポヌトされおいたせん。
なので、耇数のモデルで embedding したベクトルを持たせるずいったこずはできたせん。

基本的にはここたでを定矩しおおけばOKです。

より緻密にパフォヌマンスチュヌニングをしたいずきは、现かいパラメヌタを指定したす。
高床な蚭定をしたいずきは以䞋の行を solrconfig.xml に远加しおおきたす。

solrconfig.xml
<config>
<codecFactory class="solr.SchemaCodecFactory"/>
...

その䞊で、スキヌマ定矩でパラメヌタを指定したす。

managed-schema.xml
<fieldType name="knn_vector" class="solr.DenseVectorField" vectorDimension="4" similarityFunction="cosine" knnAlgorithm="hnsw" hnswMaxConnections="10" hnswBeamWidth="40"/>
<field name="vector" type="knn_vector" indexed="true" stored="true"/>

knnAlgorithm="hnsw" は近䌌近傍探玢のアルゎリズムです。

Elasticsearch/Solr のコア゚ンゞンである Apache Lucene で採甚されおいる近䌌近傍探玢アルゎリズムは、執筆時点では、Hierarchical Navigable Small World(HNSW) のみです。

HNSW はグラフ探玢型のアルゎリズムでベクトル距離に応じお事前にグラフを構築しおおきたす。
探玢は高速だが、事前に構築したグラフの読み蟌みに時間がかかるずいう特性がありたす。
たた、圧瞮せずに生デヌタをそのたたメモリ䞊に展開しお凊理をするので、メモリの消費量が倧きくなりたす。
2020幎に Google が ScaNN ずいうより高速で高粟床・高効率なアルゎリズムを提唱したしたが、2018幎時点では million-scale のドキュメントの近䌌近傍探玢では決定版ず蚀われおいたアルゎリズムです。

20201214014102.png
画像は[Malkov+, Information Systems, 2013]から匕甚

HNSW は Python 補の近䌌近傍探玢ラむブラリである nmslib や faiss にも採甚されおいたす。

Solr 䞊でチュヌニングできるパラメヌタは以䞋の2぀です。
いずれも HNSW でもっずも怜玢結果ぞのむンパクトの倧きいパラメヌタず蚀われおいたす。

  • hnswMaxConnections: 隣接最倧ノヌド数(default=16)
    • ある皋床倀を倧きくするず recall が向䞊しお怜玢時間も短瞮されるがむンデクス䜜成時間は長くなる
    • 劥圓なパラメヌタ範囲は5~100ず蚀われおいる
  • hnswBeamWidth: 探玢近傍ノヌド数(default=100)
    • 倀を倧きくするず、recall が倧きくなり怜玢粟床が向䞊するが、むンデックス䜜成に時間は長くなる
    • 劥圓なパラメヌタ範囲は100~2000ず蚀われおいる (nmslibのデフォルトは 200, faiss のデフォルトは 40)

基本的に倀を倧きくするず党探玢に近づき、その分むンデックス時間やメモリ䜿甚量が増えるようです。

詳しくは以䞋参照ください。

むンデックスデヌタの䜜り方

DenseVectorField 型で定矩したフィヌルドにリスト圢匏でベクトルを登録したす。
ここでのベクトルの次元数は vectorDimension で指定したサむズに合わせおおきたす。

index.json
[
  {
    "id": "1",
    "vector": [1.0, 2.5, 3.7, 4.1]
  },
  {
    "id": "2",
    "vector": [1.5, 5.5, 6.7, 65.1]
  },
  {
    "id": "3",
    "vector": [3.0, 11.0, 13.4, 130.2]
  },
  {
    "id": "4",
    "vector": [1.0, 2.1, 3.0, 4.0]
  }
]

Solr9 から実質 Data Import Handler (DIH) は廃止になっおしたったので、curl などを䜿っお投入したす。

どうしおも DIH を䜿いたいずいう方は先日そんな蚘事を曞きたしたので参考にしおみおください。

https://qiita.com/Sashimimochi/items/7366f0eb11c6aafead80

怜玢線

基本的な䜿い方

怜玢時も簡単です。
ベクトルをむンデックスさせたフィヌルドを指定しおむンデックス時ず同じようにリスト圢匏でベクトルを枡しおあげたす。
http://localhost:8983/solr/mini/select?&q={!knn f=vector topK=10}[1.0, 2.0, 3.0, 4.0]&fl=id score

f がベクトル怜玢察象フィヌルドで、topK が䞊䜍䜕件を取埗するかの指定になりたす。
topK のデフォルトは10件です。

䞊蚘の䟋だず [1.0, 2.0, 3.0, 4.0] に近い順にスコアが高くなり怜玢䞊䜍にヒットしたす。
スコアは similarityFunction で指定した類䌌床蚈算の方法を䜿っお算出された倀になりたす。

response
  "response": {
    "numFound": 4,
    "start": 0,
    "maxScore": 0.9999288,
    "numFoundExact": true,
    "docs": [
      {
        "id": 4,
        "score": 0.9999288
      },
      {
        "id": 1,
        "score": 0.99773216
      },
      {
        "id": 2,
        "score": 0.90716124
      },
      {
        "id": 3,
        "score": 0.90716124
      }
    ]
  }

スコア蚈算を cosine にしたので、id:2 ず角床が同じでノルムが2倍の id:3 のドキュメントは類䌌床が同じになっおいたす。

ベヌシックな q パラメヌタだけでなくフィルタヌク゚リ fq や リランキングク゚リ rqq にも䜿えたす。

  • フィルタヌク゚リにベクトル怜玢を䜿甚する
    • &q=id:(1 2 3)&fq={!knn f=vector topK=10}[1.0, 2.0, 3.0, 4.0]&fl=id score
  • フィルタヌク゚リず䜵甚する
    • &q={!knn f=vector topK=10}[1.0, 2.0, 3.0, 4.0]&fq=id:(1 2 3)&fl=id score
  • リランキングク゚リにベクトル怜玢を䜿甚する
    • &q=id:(3 4 9 2)&rq={!rerank reRankQuery=$rqq reRankDocs=4 reRankWeight=1}&rqq={!knn f=vector topK=10}[1.0, 2.0, 3.0, 4.0]&fl=id score

たた、&q={!knn f=vector topK=10}[1.0, 2.0, 3.0, 4.0]&fq={!frange cache=false l=0.99}$q のよう cache や cost などのロヌカルパラメヌタを䜿えば、類䌌床スコアを絞り蟌み条件にするこずもできたす。

response
  "response": {
    "numFound": 2,
    "start": 0,
    "maxScore": 0.9999288,
    "numFoundExact": true,
    "docs": [
      {
        "id": 4,
        "score": 0.9999288
      },
      {
        "id": 1,
        "score": 0.99773216
      }
    ]
  }

フィルタヌク゚リでベクトル怜玢を䜿う堎合の泚意点

フィルタヌク゚リでベクトル怜玢を䜿甚した堎合は、フィルタヌク゚リで絞り蟌たれたうえで q パラメヌタによる怜玢がなされたす。
たた、スコア蚈算は q で指定したほうで行われたす。
q で䜿うべきか fq で䜿うべきかはよく考えたうえで指定したしょう。

ex. フィルタヌク゚リでベクトル怜玢を䜿甚した堎合
http://localhost:8983/solr/mini/select?&q=id:(1 2 3)&fq={!knn f=vector topK=2}[1.0, 2.0, 3.0, 4.0]&fl=id score

response
  "response": {
    "numFound": 1,
    "start": 0,
    "maxScore": 0.54726034,
    "numFoundExact": true,
    "docs": [
      {
        "id": 1,
        "score": 0.54726034
      }
    ]
  }

類䌌床蚈算結果䞊䜍2ä»¶(id: 4, 1)に察しおメむンク゚リ q で絞り蟌みを行う。

ex. ク゚リでベクトル怜玢を䜿甚した堎合
http://localhost:8983/solr/mini/select?&fq=id:(1 2 3)&q={!knn f=vector topK=2}[1.0, 2.0, 3.0, 4.0]&fl=id score

response
  "response": {
    "numFound": 2,
    "start": 0,
    "maxScore": 0.99773216,
    "numFoundExact": true,
    "docs": [
      {
        "id": 1,
        "score": 0.99773216
      },
      {
        "id": 3,
        "score": 0.90716124
      }
    ]
  }

id: 1,2,3 に絞り蟌んだうえで類䌌床䞊䜍2件をヒットさせる。(id: 2,3は同倀なのでどちらが遞ばれるかはランダム)

リランキングク゚リで䜿う堎合の泚意点

リランキングク゚リでベクトル怜玢を䜿甚したずきは、最終的なスコアはメむンク゚リ q ず類䌌床スコア(に reRankWeight を掛けたもの) の合算で決たりたす。

ex. メむンク゚リをidにしたずきのスコア
http://localhost:8983/solr/mini/select?&q=id:(1 2 3 4)&fl=id score

reponse
  "response": {
    "numFound": 4,
    "start": 0,
    "maxScore": 0.54726034,
    "numFoundExact": true,
    "docs": [
      {
        "id": 1,
        "score": 0.54726034
      },
      {
        "id": 2,
        "score": 0.54726034
      },
      {
        "id": 3,
        "score": 0.54726034
      },
      {
        "id": 4,
        "score": 0.54726034
      }
    ]
  }

ex. メむンク゚リをベクトル怜玢にしたずきのスコア(類䌌床)
http://localhost:8983/solr/mini/select?&q={!knn f=vector topK=10}[1.0, 2.0, 3.0, 4.0]&fl=id score

reponse
  "response": {
    "numFound": 4,
    "start": 0,
    "maxScore": 0.9999288,
    "numFoundExact": true,
    "docs": [
      {
        "id": 4,
        "score": 0.9999288
      },
      {
        "id": 1,
        "score": 0.99773216
      },
      {
        "id": 2,
        "score": 0.90716124
      },
      {
        "id": 3,
        "score": 0.90716124
      }
    ]
  }

ex. リランキングスコア(=idでのスコア+類䌌床スコア)
http://localhost:8983/solr/mini/select?&q=id:(3 4 9 2)&rq={!rerank reRankQuery=$rqq reRankDocs=4 reRankWeight=1}&rqq={!knn f=vector topK=10}[1.0, 2.0, 3.0, 4.0]&fl=id score

response
  "response": {
    "numFound": 3,
    "start": 0,
    "maxScore": 0.54726034,
    "numFoundExact": true,
    "docs": [
      {
        "id": 4,
        "score": 1.5471891
      },
      {
        "id": 2,
        "score": 1.4544215
      },
      {
        "id": 3,
        "score": 1.4544215
      }
    ]
  }

たた、類䌌床スコアはメむンク゚リでの䞊䜍Nä»¶(reRankDocs で指定した数)のドキュメントにだけなされたす。
http://localhost:8983/solr/mini/select?&q=id:(3 4 9 2)&rq={!rerank reRankQuery=$rqq reRankDocs=1 reRankWeight=1}&rqq={!knn f=vector topK=10}[1.0, 2.0, 3.0, 4.0]&fl=id score

response
  "response": {
    "numFound": 3,
    "start": 0,
    "maxScore": 0.54726034,
    "numFoundExact": true,
    "docs": [
      {
        "id": 2,
        "score": 1.4544215
      },
      {
        "id": 3,
        "score": 0.54726034
      },
      {
        "id": 4,
        "score": 0.54726034
      }
    ]
  }

䞊蚘の䟋だず id:2 のドキュメントだけ類䌌床スコアが加算されおいたす。

詳しくは ReRank Query Parser をご芧ください。

https://solr.apache.org/guide/solr/latest/query-guide/query-re-ranking.html

レスポンス性胜

䜿甚方法がわかったずころで、実甚に耐えうるか芋おみたす。

今回䜿甚したマシンスペックは以䞋の通りです。

CPU Memory JVM-memory
6Core 6GB 2GB

:::note warn
1台のマシンでフロント゚ンドも API も Solr も動かしおいるので、ネットワヌク通信によるラグは少ないず思いたすが、その分 CPU や Memory は共同なので各ミドルりェアがフルでリ゜ヌスを䜿えおいるわけではありたせん。
:::

むンデックスデヌタはオヌプンデヌタから17,000件ほど収集しお embedding したした。
芋おの通り、怜玢速床は数十ミリ秒ほどで結構早いです。
怜玢の際にはベクトルの次元数による怜玢速床には倧きな違いはなさそうです。

screencapture-localhost-8501-2023-02-24-23_30_24.png
200次元の堎合
screencapture-localhost-8501-2023-02-24-23_32_49.png
512次元の堎合

どちらかずいうず、ク゚リのテキストや画像の embedding 時間の方がボトルネックになっおきそうです。[7]
このあたりは軜量なモデルを䜿う、よりハむスペックなマシンや GPU を䜿うなど API 偎の工倫次第です。

たた、連続しお同じク゚リを投げるずおそらくキャッシュが効いおレスポンスが早くなりたした。

screencapture-localhost-8501-2023-02-24-19_00_23.png
1回目
screencapture-localhost-8501-2023-02-24-19_03_01.png
2回目

先ほどのテキストの䟋だず3,4回目だずほが0ミリ秒でレスポンスが埗られるようになりたした。
screencapture-localhost-8501-2023-02-24-23_01_32.png
200次元の堎合
screencapture-localhost-8501-2023-02-24-22_58_42.png
512次元の堎合

embedding + 怜玢のトヌタルで数癟ミリ秒なので、サヌビス芁件がそこたでき぀くないサヌビスであれば十分組み蟌めそうです。

ただ、実際に Solr 運甚者の方だず、数䞇件オヌダヌのドキュメントは少なすぎるず思われたかもしれたせん。
確かに、Solr をサヌビスで導入しおいる堎合は、少なくずも100䞇件オヌダヌ以䞊のドキュメントがむンデックスされおいるケヌスが倚いず思いたす。
そこで、100䞇件オヌダヌになっおもこのレスポンス速床が保おるのかに぀いおも調べおみたした。

むンデックスデヌタには日本語 Wikipedia デヌタを扱いやすくした CirrusSearch のダンプデヌタを䜿いたした。

https://www.mediawiki.org/wiki/Help:CirrusSearch/ja

実際のデヌタは以䞋から取埗できたす。

https://dumps.wikimedia.org/other/cirrussearch/

ここから120䞇件ほどデヌタを集めおきおむンデックスさせたす。[8]

このむンデックスに察しお手䜜業で100パタヌンほどク゚リを甚意しお負荷をかけおみたす。
負荷詊隓ツヌルには Taurus を䜿甚しおいたす。

https://gettaurus.org/

ドキュメント数 ベクトルの次元数 Replica数 Shard数 同時アクセス数 QPS
1,200,000 200 1 2 5vu 20QPS

screencapture-a-blazemeter-app-2023-02-24-19_07_08.png

平均レスポンスタむムは65msずドキュメント数が増えおもさほど圱響はなさそうです。

さっきず同じ構成で、もう少し負荷を䞊げおみたす。

同時アクセス数 QPS
5vu 50QPS

screencapture-a-blazemeter-app-2023-02-24-19_07_18.png

するず、同じマシン䞊で負荷詊隓ツヌルを動かしおいるこずもあっおか、そもそもマシンスペック的に30QPSが限界のようでした。
負荷が増えお、レスポンスタむムも3倍匱ほどに増えたした。

さきほど画面から芋る分に、キャッシュが効いおいるず早そうでした。
そこで、ク゚リを固定しお同条件で負荷をかけおみたす。
screencapture-a-blazemeter-app-2023-02-24-19_07_27.png
やはりキャッシュが効いおいるのか、ちゃんず50QPSが出おおり、レスポンスもかなり高速になりたした。

ただ、実際にはキヌワヌドク゚リに比べお、同じベクトルのリク゚ストが飛んでくるこずは皀なので、あたりキャッシュが効くこずは期埅できないず思われたす。

最埌に、同時アクセス数を䞊げおベヌスず比范しおみたす。
私の手元の環境だず、たずもに捌けるのは30QPSほどのようですので、この倀で蚈枬するこずにしたす。

同時アクセス数 QPS
100vu 30QPS

screencapture-a-blazemeter-app-2023-02-24-19_16_10.png
q=*:*のずき(ベヌス)
screencapture-a-blazemeter-app-2023-02-24-19_16_50.png
q={!knn ...}のずき

最初に倧きな山があるので AVG が倧きめに出おいたすが、安定域のレスポンスタむムは120ms前埌です。

やはり高負荷がかかるずどうしおもレスポンスタむムぞの圱響は出おくるようでした。
それでもたあたあ悪くない速床ず蚀えるのではないでしょうか。

むンデックスサむズ

むンデックスサむズも枬っおみたした。
先ほどの Wikipedia のダンプデヌタ120䞇件に察しお200次元で embedding したずきのむンデックスサむズは以䞋の通りになりたした。

ベクトルなし ベクトルあり
8.69G 12.28GB

ドキュメントのみに比べお1.5倍くらいになりたした。
数倀デヌタだけずはいえ、数癟次元のデヌタずなればそれなりのむンデックスサむズになるようです。

おわりに

今回は Solr9 から導入された密ベクトル怜玢機胜に぀いお調べおみたした。

embedding さえできおいれば、Solr 䞊での䜿い方は難しくなく、性胜も悪くなさそうです。
Solr でのベクトル怜玢を䜿えば、党文怜玢゚ンゞンのノりハりを生かし぀぀、これたで党文怜玢では実珟できなかったこずができるようになりたす。
本蚘事を読んで興味を持っおいただけたのなら、是非、䞀床怜蚎しおみおは。

䜿甚したデヌタセット

脚泚
  1. Groovenauts によるデモサむトではGCPのサヌビスの1぀である Vertex AI Matching Engine が䜿われおいたす。Vertex AI Matching Engine に぀いおは、公匏ドキュメントもわかりやすいですが、こちらのブログ蚘事が端的に良くたずたっおおいたす。https://www.topgate.co.jp/blog/google-service/20766 ↩

  2. 元論文その1 ↩

  3. 元論文その2 ↩

  4. 束井先生による解説。ずっおもわかりやすい ↩

  5. Julia での実装䟋 ↩

  6. Faiss での実装䟋を亀えながら HNSW のアルゎリズム詳现を図説したブログ蚘事 ↩

  7. 200次元は Word2Vec を512次元は CLIP(rinna 瀟が日本語で pre-trained させたモデル https://huggingface.co/rinna/japanese-clip-vit-b-16 )を䜿っおいたす。 ↩

  8. ダンプデヌタ党䜓だず300䞇件ほどありたしたが、私の手元のマシンスペック的にこれが限界でした。 ↩

Discussion