🤔

RWKVとsqlite-vssで高速なベクトル検索を作ってみる

2023/06/18に公開

はじめに

最近 langchain を使うようになってきて、OpenAIのAPIをちょこちょこ叩くのですが、いかんせん遅い
いや十分に早いのだけど、ドキュメントの量があると若干気になってくる速度です
そこで、 ローカルLLMとしてrinna を使ってみたりしたのですが、まだまだ遅いです

すでに先行して実装例を作ってくれていた RWKVでembedding vectorを計算 の記事と SQLiteでベクトル検索ができる拡張sqlite-vssを試す の記事を大いに参考にしながら
RWKVsqlite-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