RWKVとsqlite-vssで高速なベクトル検索を作ってみる
はじめに
最近 langchain を使うようになってきて、OpenAIのAPIをちょこちょこ叩くのですが、いかんせん遅い
いや十分に早いのだけど、ドキュメントの量があると若干気になってくる速度です
そこで、 ローカルLLMとしてrinna を使ってみたりしたのですが、まだまだ遅いです
すでに先行して実装例を作ってくれていた RWKVでembedding vectorを計算 の記事と SQLiteでベクトル検索ができる拡張sqlite-vssを試す の記事を大いに参考にしながら
RWKV と sqlite-vss を使って高速なベクトル検索を作ってみます
RWKVのモデル
今回は学習済みのRavenモデル RWKV-4-Raven-3B-v12-Eng98%-Other2% を使っていきます
OpenAI の text-embedding-ada-002 モデル では 1536次元と扱いやすいのですが、RWKVでは高次元のまま扱うことになるので次元圧縮をしないと扱うのに不便です(後述)
ちなみに、モデルによって次元数は変わるのですが
3Bモデルでは 409600次元 (160x2560)
7Bモデルでは 655360次元 (160x4096)
14Bモデル では 1024000次元 (160x6400)
と、とても大きいです
sqlite-vss
sqlite-vss は Faiss ベースで実装されている sqliteの拡張です
sqliteから利用できる API リファレンス をみてもらえればわかる通りですが、index検索まわりが抽象化されたような形となっています
使い方としては vss0
を使用した VIRTUAL TABLE
を作り、カラム名と次元数を指定して
CREATE VIRTUAL TABLE IF NOT EXISTS vss_wiki USING vss0(
embedding(409600) factory="L2norm,Flat,IDMap2"
)
のように定義します
factory=
から後ろは省略可能で、省略した場合のデフォルトは "Flat,IDMap2"
が使われるようです
何が指定可能かは Faissのfactory から確認できます
さて、VIRTUAL TABLEができたら、ここにvectorを入れていくのですが、L2norm
などはインデックス追加時に training を必要としないのですが、例えば IVF4096
を使う場合は training が必要となります
これは operation="training"
という特別なパラメータを指定して training しないと INSERT できません
事前に INSERT 前に
INSERT INTO vss_wiki(operation, embedding) VALUES("training", ?); -- 1回
INSERT INTO vss_wiki(operation, embedding) VALUES("training", ?); -- 2回
INSERT INTO vss_wiki(operation, embedding) VALUES("training", ?); -- ...
INSERT INTO vss_wiki(operation, embedding) VALUES("training", ?); --- N回
のようにします
ただ、実際には次のように コンテンツと共に embedding を持つようなテーブルを作成しておいて
CREATE TABLE IF NOT EXISTS wiki(
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
content_embedding BLOB NOT NULL
);
データの準備ができてから
INSERT INTO vss_wiki(operation, embedding)
SELECT "training", content_embedding FROM wiki ORDER BY RANDOM() LIMIT 100;
とするほうが楽かもしれません
trainingができたらインデックスを追加します
rowid
を指定して INSERT していくのですが、すでに上記のようなテーブルがある場合は
INSERT INTO vss_wiki(rowid, embedding)
SELECT id, content_embedding FROM wiki;
とするだけでインデックスに追加されます
RWKVのベクトル計算
sqlite の embedding に入れる vector なのですが、 こちらの記事 を参考に次のように RWKV の vector を計算します
import os
os.environ['RWKV_JIT_ON'] = "1"
os.environ["RWKV_CUDA_ON"] = "1"
from rwkv.model import RWKV
from rwkv.utils import PIPELINE
model = RWKV(
model="/path/to/rwkv-pile-3b-fp16",
strategy="cuda fp16",
)
pipeline = PIPELINE(model, "/path/to/20B_tokenizer.json")
from typing import List
import numpy as np
def calc_vec(text) -> List[float]:
invec = pipeline.encode(text)
out, state = model.forward(invec, None)
np_state = [i.detach().cpu().numpy() for i in state]
np_state = np.concatenate(np_state, axis=0)
return np_state
print(len(calc_vec("吾輩は猫である"))) // => 409600
また、sqlite には BLOB
で INSERT するため
def calc_vec_bytes(text) -> bytes:
vector = calc_vec(text)
return np.array(vector, dtype=np.float32).tobytes()
を用意しておきます
データの投入
さて、ベクトルも用意できたので sqlite にデータを入れていきます
実装は次のように用意しました
import sqlite3
import sqlite_vss
db = sqlite3.connect("wiki.db", timeout=30)
db.enable_load_extension(True)
sqlite_vss.load(db)
vss_version = db.execute("SELECT vss_version()").fetchone()
print("vss_version={0}".format(vss_version[0]))
db.execute("""
CREATE TABLE IF NOT EXISTS wiki(
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
content_embedding BLOB NOT NULL
)
""")
db.execute("""
CREATE VIRTUAL TABLE IF NOT EXISTS vss_wiki USING vss0(
embedding(409600) factory="L2norm,Flat,IDMap2"
)
""")
def insert_wiki(title: str, content: str):
vec_bytes = calc_vec_bytes(content)
with db:
db.execute("""
INSERT INTO wiki(title, content, content_embedding)
VALUES(?, ?, ?)
""", (title, content, vec_bytes))
def train_wiki():
with db:
db.execute("""
INSERT INTO vss_wiki(operation, embedding)
SELECT "training", content_embedding FROM wiki ORDER BY RANDOM() LIMIT 100
""")
def index_wiki():
with db:
db.execute("""
INSERT INTO vss_wiki(rowid, embedding)
SELECT id, content_embedding FROM wiki
""")
def search_similar(query, k=10, limit=3):
vec_bytes = calc_vec_bytes(query)
res = db.execute("""
WITH similar_matches AS (
SELECT rowid, distance
FROM vss_wiki
WHERE vss_search(embedding, vss_search_params(?, ?))
)
SELECT w.title, w.content, s.distance
FROM wiki AS w
JOIN similar_matches AS s ON (
w.id = s.rowid
)
ORDER BY s.distance ASC
LIMIT ?
""", (vec_bytes, k, limit))
return res.fetchall()
データは wikipedia の マンチカン 、プードル 、リャマ 、オットセイ 、ライオン 、データサイエンス 、 機械学習 あたりの概要の文字列を入れてみます
insert_wiki("マンチカン","""
マンチカン (Munchkin) は、北アメリカに起源を有する猫の一品種。マンチキンと呼ばれることもある...
""")
insert_wiki("プードル", """
プードル(英: poodle、仏: caniche、独: Pudel)は、水中回収犬、鳥獣猟犬や愛玩犬(ペット)として飼育される犬種...
""")
insert_wiki("リャマ", """
リャマ、ラマ、ジャマ(羊駝、駱馬、西: Llama、学名: Lama glama)は、鯨偶蹄目ラクダ科の動物である...
""")
insert_wiki("オットセイ", """
オットセイ(膃肭臍、海狗、英:Fur seal)は、鰭脚類アシカ科のうちキタオットセイ属(キタオットセイ)とミナミオットセイ属(ミナミオットセイ)の総称である...
""")
insert_wiki("ライオン", """
ライオン(獅、Panthera leo)は、哺乳綱食肉目ネコ科ヒョウ属に分類される食肉類...
""")
insert_wiki("データサイエンス", """
データサイエンス(英: data science、略称: DS)またはデータ科学とは、データを用いて新たな科学的...
""")
insert_wiki("機械学習", """
機械学習(きかいがくしゅう、英: machine learning)とは、経験からの学習により自動で改善するコンピューターアルゴリズムもしくはその研究領域で、人工知能の一種であるとみなされている...
""")
train_wiki()
index_wiki()
(... としているのは文字数が多いので省略してます)
データの投入とtrainingとindexができたら、検索してみます
import time
search_queries = [
"吾輩は猫である",
"I am cat",
"データサイエンス",
"機械音痴",
]
for query in search_queries:
s = time.perf_counter()
rows = search_similar(query)
elapsed = time.perf_counter() - s
print("similar_search='{0}' elapsed={1:0.3f}s".format(query, elapsed))
for row in rows:
print(" title={0} distance={1}".format(row[0], row[2]))
結果は次のようになっていて、非常に高速に (0.04s程度) に検索できそうです
similar_search='吾輩は猫である' elapsed=0.047s
title=マンチカン distance=0.5570117235183716
title=オットセイ distance=0.5861098766326904
title=データサイエンス distance=0.6253763437271118
similar_search='I am cat' elapsed=0.045s
title=マンチカン distance=0.905067503452301
title=オットセイ distance=0.9117523431777954
title=データサイエンス distance=0.9389098882675171
similar_search='データサイエンス' elapsed=0.041s
title=データサイエンス distance=0.6000469326972961
title=マンチカン distance=0.6089928150177002
title=オットセイ distance=0.6554186940193176
similar_search='機械音痴' elapsed=0.038s
title=マンチカン distance=0.6000436544418335
title=データサイエンス distance=0.6329144835472107
title=オットセイ distance=0.6481955051422119
一方で精度はというとデータサイエンスとマンチカンが近そうだったり、全体的にマンチカンが上位に出ていたりと厳しい感じです
ちなみに、 RWKV-4-Raven-7B-v12-Eng49%-Chn49%-Jpn1%-Other1%
の 655360 次元では
similar_search='吾輩は猫である' elapsed=0.079s
title=マンチカン distance=0.33136308193206787
title=データサイエンス distance=0.3533874750137329
title=オットセイ distance=0.35746341943740845
similar_search='I am cat' elapsed=0.073s
title=マンチカン distance=0.9278059005737305
title=データサイエンス distance=0.9483551383018494
title=オットセイ distance=0.9524267315864563
similar_search='データサイエンス' elapsed=0.074s
title=マンチカン distance=0.41380536556243896
title=データサイエンス distance=0.4183332324028015
title=オットセイ distance=0.45474180579185486
similar_search='機械音痴' elapsed=0.072s
title=マンチカン distance=0.4033798575401306
title=データサイエンス distance=0.420269638299942
title=オットセイ distance=0.4365586042404175
少し遅くなりましたがあまり変わりません(というより全体的にスコアが良くない)
PCAによる次元削減
やはり高次元すぎるとうまくうまく検索できていないようです
ここでは PCA を使って次元削減をやってみます
3Bモデルでは 409600次元 (160x2560)あるため、これを 5120次元(160x32)に削減します
FaissのPCAMatrixを用いて、calc_vecを変えてみます
import faiss
reducer = faiss.PCAMatrix(2560, 32)
# random training data
mt = np.random.rand(1000, 2560).astype('float16')
reducer.train(mt)
def calc_vec(text) -> List[float]:
assert reducer.is_trained
invec = pipeline.encode(text)
out, state = model.forward(invec, None)
np_state = [i.detach().cpu().numpy() for i in state]
np_state = reducer.apply(np.array(np_state, dtype=np.float16)) # 2560次元->32次元
np_state = np.concatenate(np_state, axis=0)
return np_state
VIRTUAL TABLE も作り直して
CREATE VIRTUAL TABLE IF NOT EXISTS vss_wiki USING vss0(
embedding(5120) factory="L2norm,Flat,IDMap2"
)
これの結果は
similar_search='吾輩は猫である' elapsed=0.031s
title=マンチカン distance=0.5895218253135681
title=オットセイ distance=0.6244730949401855
title=データサイエンス distance=0.6472294330596924
similar_search='I am cat' elapsed=0.030s
title=データサイエンス distance=0.953101634979248
title=マンチカン distance=0.9750327467918396
title=オットセイ distance=0.995472252368927
similar_search='データサイエンス' elapsed=0.029s
title=データサイエンス distance=0.6421447396278381
title=マンチカン distance=0.6550000905990601
title=オットセイ distance=0.7170669436454773
similar_search='機械音痴' elapsed=0.029s
title=データサイエンス distance=0.6380316019058228
title=マンチカン distance=0.6418352127075195
title=オットセイ distance=0.7222731709480286
データサイエンスや機械音痴のようなスコアが上昇してますね
ちなみに、7Bモデルでは 655360次元 (160x4096) を 10240次元(160x64)まで減らしてみた結果はこちらですが、あまり効果はなかった
similar_search='吾輩は猫である' elapsed=0.066s
title=マンチカン distance=0.32215720415115356
title=オットセイ distance=0.33024662733078003
title=データサイエンス distance=0.34118741750717163
similar_search='I am cat' elapsed=0.064s
title=マンチカン distance=0.9215264320373535
title=データサイエンス distance=0.9321926236152649
title=オットセイ distance=0.9323195219039917
similar_search='データサイエンス' elapsed=0.065s
title=マンチカン distance=0.4084858298301697
title=データサイエンス distance=0.41053399443626404
title=オットセイ distance=0.4283197820186615
similar_search='機械音痴' elapsed=0.065s
title=マンチカン distance=0.3993360996246338
title=データサイエンス distance=0.41531527042388916
title=オットセイ distance=0.41833579540252686
PCAMatrix.train をもう少しフツーのデータでやる
さきほどは PCAMatrix.train に np.random.rand(1000, 2560).astype('float16')
とランダムなデータを使ったのが良くなかった可能性もあるので、ことわざ100件ほどを使って train してみます
import faiss
reducer = faiss.PCAMatrix(2560, 32, -0.5)
data = [
"犬も歩けば棒に当たる",
"猫に小判",
"豚に真珠",
"猿も木から落ちる",
"馬の耳に念仏",
"カッパの川流れ",
"海老で鯛を釣る",
"亀の甲より年の劫",
"取らぬタヌキの皮算用",
"能ある鷹は爪を隠す",
"泣きっ面に蜂",
"釈迦に説法",
"仏の顔も三度まで",
"石橋を叩いて渡る",
"災い転じて福となす",
....,
]
for d in data:
invec = pipeline.encode(d)
out, state = model.forward(invec, None)
np_state = [i.detach().cpu().numpy() for i in state]
reducer.train(np.array(np_state, dtype=np.float16))
この時の結果(3B)は
similar_search='吾輩は猫である' elapsed=0.031s
title=マンチカン distance=0.2876427173614502
title=データサイエンス distance=0.291664183139801
title=オットセイ distance=0.3122714161872864
similar_search='I am cat' elapsed=0.029s
title=データサイエンス distance=0.6960464715957642
title=マンチカン distance=0.7110835313796997
title=オットセイ distance=0.730993926525116
similar_search='データサイエンス' elapsed=0.029s
title=データサイエンス distance=0.3671794533729553
title=マンチカン distance=0.3738189935684204
title=オットセイ distance=0.41920405626296997
similar_search='機械音痴' elapsed=0.029s
title=データサイエンス distance=0.3419957160949707
title=マンチカン distance=0.3452824354171753
title=オットセイ distance=0.390667200088501
あまり変わらないかもしれない...
AutoEncoderによる次元削減
AutoEncoder による次元削除もやってみます
ロジックは単純なものを用意しました
import torch
import torch.nn as nn
class Autoencoder(nn.Module):
def __init__(self, input_dim, encoding_dim):
super(Autoencoder, self).__init__()
self.encoder = nn.Sequential(
nn.Linear(input_dim, encoding_dim),
nn.ReLU()
)
self.decoder = nn.Sequential(
nn.Linear(encoding_dim, input_dim),
nn.ReLU()
)
def forward(self, x):
encoded = self.encoder(x)
decoded = self.decoder(encoded)
return decoded
これで、 2560次元を32次元まで減らします
先ほど作った、ことわざ100個を使って学習します
from rwkv.model import RWKV
from rwkv.utils import PIPELINE
model = RWKV(model="...", strategy="..")
pipeline = PIPELINE(model, "...")
enc = Autoencoder(2560, 32)
num_epochs = 10000
batch_size = 64
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(enc.parameters(), lr=0.001)
for text in data:
inp_vec = pipeline.encode(text)
out, state = model.forward(inp_vec, None)
np_state = [i.detach().cpu().numpy() for i in state]
a = torch.from_numpy(np.array(np_state, dtype=np.float16)).float()
for epoch in range(num_epochs):
for i in range(0, len(a), batch_size):
batch = a[i:i+batch_size]
output = enc(batch)
loss = criterion(output, batch)
optimizer.zero_grad()
loss.backward()
optimizer.step()
torch.save(enc.state_dict(), 'autoencoder.pth')
数十分で学習が終わります
次に vector 計算をここで作ったエンコーダを使って圧縮します、先ほど学習の時に作った autoencoder.pth
を読み込んで推論モードで動かします
enc = Autoencoder(2560, 32)
enc.load_state_dict(torch.load('autoencoder.pth'))
enc.eval()
def calc_vec(text) -> List[float]:
invec = pipeline.encode(text)
out, state = model.forward(invec, None)
np_state = [i.detach().cpu().numpy() for i in state]
data = torch.from_numpy(np.array(np_state, dtype=np.float16)).float()
return enc.encoder(data).detach().numpy()
これの結果(3B)は
similar_search='吾輩は猫である' elapsed=0.030s
title=マンチカン distance=0.2427930384874344
title=機械学習 distance=0.2530534863471985
title=データサイエンス distance=0.25930407643318176
similar_search='I am cat' elapsed=0.029s
title=データサイエンス distance=0.40085917711257935
title=マンチカン distance=0.42081785202026367
title=オットセイ distance=0.444001168012619
similar_search='データサイエンス' elapsed=0.030s
title=データサイエンス distance=0.26740995049476624
title=マンチカン distance=0.2830583155155182
title=オットセイ distance=0.3023841381072998
similar_search='機械音痴' elapsed=0.028s
title=データサイエンス distance=0.28901058435440063
title=マンチカン distance=0.30154144763946533
title=オットセイ distance=0.32397207617759705
今回のエンコーダでは PCA と似たような結果になりました
HNSW をためす
これまで L2norm を使っていましたが、HNSWにすることでグラフ検索になるため多少よくなるかもしれません(PCAで削減したものを使っています)
CREATE VIRTUAL TABLE IF NOT EXISTS vss_wiki USING vss0(
embedding(5120) factory="HNSW,Flat,IDMap2"
)
これの結果(3B)は
similar_search='吾輩は猫である' elapsed=0.075s
title=データサイエンス distance=19.633806228637695
title=マンチカン distance=26.319496154785156
title=オットセイ distance=27.723934173583984
similar_search='I am cat' elapsed=0.077s
title=データサイエンス distance=30.57122802734375
title=マンチカン distance=39.46844482421875
title=オットセイ distance=40.91743850708008
similar_search='データサイエンス' elapsed=0.092s
title=データサイエンス distance=22.767379760742188
title=マンチカン distance=30.439491271972656
title=オットセイ distance=32.70646286010742
similar_search='機械音痴' elapsed=0.063s
title=データサイエンス distance=22.039813995361328
title=マンチカン distance=29.496417999267578
title=オットセイ distance=31.843345642089844
微妙ですね...
langchain 連携
前置きが長くなりましたが、langchainとの連携も少し紹介します
RWKVは langchain からも使える のですが、 embedding は用意されていないため、基本的には今回使ったものを併用する形になります
また DDL は Documentクラス で扱いやすい形にしておくのと、TextSplitter も使うと思うので、分割に対応できるような形式だと良さそうです
まずRWKVと calc_vec
は次のようにしました
from typing import List
import numpy as np
import os
os.environ['RWKV_JIT_ON'] = "1"
os.environ["RWKV_CUDA_ON"] = "1"
from langchain.llms import RWKV
model = RWKV(
model="/path/to/rwkv-3b-fp16",
tokens_path="/path/to/20B_tokenizer.json",
strategy="cuda fp16",
temperature=0.25,
top_p=0,
penalty_alpha_frequency=0.85,
penalty_alpha_presence=0.85,
max_tokens_per_generation=512,
)
import faiss
reducer = faiss.PCAMatrix(2560, 32)
mt = np.random.rand(5000, 2560).astype('float16')
reducer.train(mt)
def calc_vec(text) -> List[float]:
assert reducer.is_trained
invec = model.pipeline.encode(text)
out, state = model.client.forward(invec, None)
np_state = [i.detach().cpu().numpy() for i in state]
np_state = reducer.apply(np.array(np_state, dtype=np.float16))
np_state = np.concatenate(np_state, axis=0)
return np_state
def calc_vec_bytes(text) -> bytes:
vector = calc_vec(text)
return np.array(vector, dtype=np.float32).tobytes()
また DDL ですが、次のようにしました
CREATE TABLE IF NOT EXISTS document(
id INTEGER PRIMARY KEY,
page_content TEXT NOT NULL,
meta_title TEXT NOT NULL,
meta_source TEXT NOT NULL
)
CREATE TABLE IF NOT EXISTS document_part(
id INTEGER PRIMARY KEY,
doc_id INTEGER NOT NULL,
part TEXT NOT NULL,
part_embedding BLOB NOT NULL
)
CREATE VIRTUAL TABLE IF NOT EXISTS vss_document_part USING vss0(
embedding(5120) factory="L2norm,Flat,IDMap2"
)
データ操作は次のように実装しました
from langchain.docstore.document import Document
def insert_document(doc: Document) -> int:
with db:
db.execute("""
INSERT INTO document(page_content, meta_title, meta_source)
VALUES(?, ?, ?)
""", (doc.page_content, doc.metadata["title"], doc.metadata["source"]))
last_insert_id = db.execute('SELECT last_insert_rowid()').fetchone()[0]
return last_insert_id
def insert_texts(doc_id: int, texts: List[str]):
with db:
for text in texts:
vec_bytes = calc_vec_bytes(text)
db.execute("""
INSERT INTO document_part(doc_id, part, part_embedding)
VALUES(?, ?, ?)
""", (doc_id, text, vec_bytes))
def train():
with db:
db.execute("""
INSERT INTO vss_document_part(operation, embedding)
SELECT "training", part_embedding FROM document_part ORDER BY RANDOM() LIMIT 100
""")
def index():
with db:
db.execute("""
INSERT INTO vss_document_part(rowid, embedding)
SELECT id, part_embedding FROM document_part
""")
from langchain.text_splitter import RecursiveCharacterTextSplitter
def add_document(doc: Document):
splitter = RecursiveCharacterTextSplitter(
chunk_size=100, chunk_overlap=10,
separators=["\n\n", "\n", " ", "", "。", " "],
)
doc_id = insert_document(doc)
insert_texts(doc_id, [d.page_content for d in splitter.split_documents([doc])])
データの入力は、おそらく langchain 使っている人なら既存の Loader があると思うのですが、
こんな感じで入れてあげます(変換してあげます)
add_document(Document(
page_content="""
マンチカン (Munchkin) は、北アメリカに起源を有する猫の一品種。マンチキンと呼ばれることもある。...
""", metadata={"title":"マンチカン", "source": "https://w.wiki/3Hi3"}))
add_document(Document(
page_content="""
プードル(英: poodle、仏: caniche、独: Pudel)は、水中回収犬、鳥獣猟犬や愛玩犬(ペット)として飼育される犬種。
....
""", metadata={"title":"プードル", "source":"https://w.wiki/6qfb"}))
...
train()
index()
最後に検索ですが
def search_similar(query, k=10, limit=3) -> List[Document]:
vec_bytes = calc_vec_bytes(query)
res = db.execute("""
WITH similar_matches AS (
SELECT rowid, distance
FROM vss_document_part
WHERE vss_search(embedding, vss_search_params(?, ?))
), similar_documents AS (
SELECT p.doc_id, p.part, s.distance
FROM document_part AS p
JOIN similar_matches AS s ON (
p.id = s.rowid
)
)
SELECT d.page_content, d.meta_title, d.meta_source, s.part, s.distance
FROM document AS d
LEFT JOIN similar_documents AS s ON (
d.id = s.doc_id
)
ORDER BY s.distance ASC
LIMIT ?
""", (vec_bytes, k, limit))
docs = []
rows = res.fetchall()
for row in rows:
docs.append(Document(
page_content=row[0],
metadata={
"title":row[1],
"source":row[2],
"distance":row[3],
},
))
return docs
のようにしています
あとは好みで load_qa_chain などに食わせてください
from langchain.chains.question_answering import load_qa_chain
from langchain.llms import OpenAI
llm = OpenAI(temperature=0)
docs = search_similar(query)
chain = load_qa_chain(llm, chain_type="map_reduce", return_map_steps=True)
ans = chain({"input_documents": docs, "question": query}, return_only_outputs=True)
print(ans["output_text"])
まとめ
精度は text-embedding-ada-002
と比べたら見劣りはするものの、チューニング次第では可能性がありそうです
検索速度がとても早いためもう一声なんとかできると使い物になりそうです
特にsqliteで実装しているため、embeddingができ dbファイルを配布して、 RWKV の軽量さを利用して CUDA を使わずに CPU のみで検索する、なんてことも出来るかもしれません
ただ次元削除はもう少し工夫の余地がありそうです
Faiss の encoding などを工夫すればもう少しよい結果が出せるかもしれません
Discussion