はじめに
ベクトルデータベース、触っていますか?
GPTを筆頭とするLLMの浸透とともに、データベースの世界で存在感を増しているのが、「ベクトルデータベース」。AIの根幹にある埋め込み(Embedding) によって生成したベクトル値を扱うのに特化したデータベースです。
Googleトレンドで"vector database"を見てみると、Chat GPTを追うようにスパイクしているのがわかります。
出典:Googleトレンド
私もOpenAIのEmbeddings APIを使ってデータをベクトル化して保存したり、LangChainのVectorstore Agentでお問い合わせbotを作ってみたり、自前で作ったChatGPT Retrieval Pluginと接続させてみたりと、Chat GPTをきっかけにベクトルデータベースで遊ぶようになりました。
これまでベクトルデータベースとしてPineconeを使っていましたが、そろそろ新しいベクトルデータベースにも触れてみようと思い立ち、Weaviateを試してみました。
本記事はその記録です。同じく新しいベクトルデータベースに触ってみようと思っている方の参考になればと、Weaviateの導入からデータ挿入、ハイブリット検索するまでを紹介します。
本記事の要旨
Weaviateの導入
- 試してみるなら、クラウドマネージド版Weaviateの無料プラン(sandbox)が簡単。
- 接続はURLとAPIキー。
データの取り込み
- Weaviateはデータ構造を定めたSchemaから作成するClassを持つ。
- データはJSON形式で用意。事前にベクトル化する必要はなく、Vectorizerという内部モジュールでベクトル化して取り込める。
ハイブリッド検索
- Weaviateにおけるハイブリッド検索とは、Sparse/Dense(粗密)、2つのベクトルを組み合わせて検索すること。キーワード検索とセマンティック検索(意味検索)の掛け合わせることができる。
- ハイブリッド検索を有効にするには、クエリにパラメータを加えるだけ。事前にベクトル化する必要はない。
- TypeScriptのClientライブラリではGraphQLクエリに
withHybrid
を追加する。
- TypeScriptのClientライブラリではGraphQLクエリに
- パラメータの
alpha
を調整することで、 Sparse/Denseの重みづけができる。
環境・必要なもの
- Node.js
- v18.16.1
- TypeScript
- 公式のクライアントライブラリを使用。
-
Weaviate Cloud Services(WCS)
- Weaviateのクラウドマネージド版
- 無料のsandboxesを使用
- データの保持期間は14日
- OpenAI API
- APIキーを使用します。
Weaviateについて
Weaviateを俯瞰する日本語の情報として、先行記事が二つあります。(Python、ローカル)本記事もこれらに負って(追って)いる点、最初におことわりしておきます。
Weaviateの概観は、屋上屋を架しても詮ないので、公式ドキュメントのWeaviate in a nutshellの意訳を上記記事から引用します。
- オープンソースです。
- データオブジェクトをベクターでインデックス化することで、そのセマンティックな特性に基づいて保存・取得することができます。
- スタンドアローン(純粋なベクトルデータベース)としても使えますし、コア機能を拡張する様々なモジュールと一緒に使うことでベクトル化を代替したりできます。
- GraphQL APIがあり、データに簡単にアクセスすることができます。
- 高速です(オープンソースのベンチマーク)
(引用元:ベクトルデータベースWeaviateの概念を整理する)
WeaviateはLangChainとの接続が用意されており、ChatGPT Retrieval Pluginでも、Pineconeの次に挙げられており、かなり主流のベクトルデータベースと言えそうです。
ChatGPT Retrieval PluginのREADMEでは次のように紹介されていました。
Weaviateは、膨大なデータオブジェクトをシームレスにスケールアップするために設計されたオープンソースのベクトル検索エンジンです。ハイブリッド検索を初めからサポートしており、効率的なキーワード検索が必要なユーザーに適しています。Weaviateはセルフホスティングもしくはマネージドのどちらでも可能で、デプロイメントにおける柔軟性を提供します。
(引用元:https://github.com/openai/chatgpt-retrieval-plugin/tree/main#choosing-a-vector-database )
ここで触れられているハイブリッド検索が我々の目標です。
Weaviate Cloud Services(WCS)
オープンソースのWeaviateは、Dockerなどでローカルに立てることもできますが、SaaSとしてWeaviate Cloud Servicesも提供されています。
本記事ではWeaviate Cloud Servicesの無料版を使用します。
クライアントライブラリ
ライブラリは執筆時点で以下の言語をサポートしています。
- Python
- TypeScript/JavaScript
- TypeScriptへの移行が推奨。
- Go
- Java
本記事ではTypeScriptライブラリを使用します。
ハイブリッド検索(Hybrid Search)について
「ハイブリッド検索」を文字通り取れば、「〇〇検索」と「××検索」の掛け合わせ検索ということになりますが、Weaviateでは「Sparse Vector」(粗なベクトル)と「Dense Vector」(密なベクトル)それぞれを用いた検索の掛け合わせを「ハイブリッド検索」と言っています。
結果として、キーワード検索とセマンティック検索の両方の特性を備えた検索ができます。
Sparse Vector
BM25、SPLADEといったアルゴリズムでベクトル化される。WeaviateはBM25/BM25Fを使っている。1
単語レベルで類似度(Score)を出す、つまりキーワード検索に適している。
Dense Vector
GloVe2、Transformersといった機械学習モデルでベクトル化される。(TransformersはGPTのTですね。)
文脈(コンテクスト)を踏まえたセマンティック検索に適している。
Weaviateは最初からハイブリッド検索をサポートしていたわけではなく、2022年12月のv1.17で導入されました。
ちなみにPineconeも、セマンティック検索と従来のキーワード検索の掛け合わせる「ハイブリッド検索」を持っています。
Pineconeでのハイブリッド検索の実装についてはこちら。
Weaviate Cloud Services(WCS)のセットアップ
概念はこのくらいにして、Weaviateをクラウド環境にセットアップします。
セットアップにあたっては、下記の公式ドキュメントも参考にしてください。
アカウント作成
セットアップは簡単で、こちらにアクセスし、「Start Free」を押下。
Weaviate ConsoleでDon't have an account? Register hereを選択し、アカウントを登録。
メールが飛びますので、メールのリンクを踏んでいき、Verificationを済ませればアカウント登録完了です。
データベース(Cluster)作成
Free sandboxにして、Createを押下して、作成完了です。作成完了まで30秒ほどがかかります。
認証情報を取得する
これでセットアップ完了です。
実装
Officialのtypescript-clientを使って、データ取り込みからハイブリッド検索まで、Quickstart Tutorialに従って実装していきます。
作曲家と曲名情報を持つJSON配列を取り込み、ハイブリッド検索を行うことを目標とします。
サンプルデータ
今回用いるサンプルデータの型定義はこちら。
interface Music {
"piece_id": string,
"composer": string,
"pieceName": string,
}
JSONオブジェクトの例
{
"piece_id": 206,
"composer": "ブラームス",
"pieceName": "ピアノ協奏曲 第1番",
}
以上の形式で、ショパンとブラームスの曲を250曲分を扱ってみます。
最終的には下記のようなディレクトリになります。
📦weaviate_hybrid_search
┣ 📂node_modules
┣ 📜config.ts
┣ 📜createClass.ts
┣ 📜formatJSON.ts
┣ 📜formatted_data.json
┣ 📜hybridSearch.ts
┣ 📜importData.ts
┣ 📜package-lock.json
┣ 📜package.json
┗ 📜sample.json
0. プロジェクトの作成
はじめにNode.jsプロジェクトを作成します。
mkdir weaviate_hybrid_search
cd weaviate_hybrid_search
npm init -y
続いてTypeScriptとクライアントライブラリ、コード実行用のts-nodeを導入します。
npm install --save-dev typescript ts-node
npm install weaviate-ts-client
バージョン確認のため、package.json
を掲載します。
{
"name": "weaviate_hybrid_search",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"weaviate-ts-client": "^1.4.0"
},
"devDependencies": {
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
}
}
1. Weaviateの接続情報をセットアップ
まず、config.ts
をプロジェクトルートに作成し、Weaviateの接続情報を記載します。
なお、データ取り込みの際のVevtorizerでOpenAI Embedding APIを用いるため、OpenAI-Api-Keyが必要です。(取得方法について本記事では触れません。)
import { ApiKey } from 'weaviate-ts-client';
const weaviateClientArg = {
scheme: 'https',
host: '<ClusterName>.weaviate.network', // 前項で取得したCluster URL
apiKey: new ApiKey('XXXXXXXXXXXXXXXXXXXX'), // 前項で取得したWeaviateのAPI KEY
headers: { 'X-OpenAI-Api-Key': 'sk-XXXXXXXXXXXXXXXXXXX' }, // OpenAIのAPI KEY
}
export { weaviateClientArg }
2. SchemaからClassを作成する
続いて、Classを作成していきます。createClass.ts
を下記のように追加します。
import weaviate, { WeaviateClient } from "weaviate-ts-client";
import { weaviateClientArg } from "./config";
async function createClass() {
const client: WeaviateClient = weaviate.client(weaviateClientArg);
const classObj = {
"class": "Music",
"vectorizer": "text2vec-openai",
"description": "Information about classical piano music.",
"properties": [
{
"dataType": ["text"],
"name": "composer",
"description": "The composer",
"moduleConfig": {
"text2vec-openai": {
"model": "ada",
"modelVersion": "002",
"type": "text",
"skip": false
}
},
},
{
"dataType": ["text"],
"name": "pieceName",
"description": "The name of piece",
"moduleConfig": {
"text2vec-openai": {
"model": "ada",
"modelVersion": "002",
"type": "text",
"skip": false
}
},
},
{
"dataType": ["int"],
"name": "piece_id",
"description": "The id of the piece",
"moduleConfig": {
"text2vec-openai": {
"model": "ada",
"modelVersion": "002",
"type": "text",
"skip": true
}
},
}
]
}
const res = await client.schema.classCreator().withClass(classObj).do();
console.log(res);
}
createClass()
特定のプロパティをベクトル化の対象外にしたい場合は、moduleConfig
内でskip
をtrueにします。
dataType
の一覧は下記にあります。
ts-nodeで実行します。
ts-node createClass.ts
Classを作成し直したい場合などのため、削除用の関数も貼っておきます。
Class名を指定して、ts-nodeで実行してください。
Classの削除
import weaviate, { WeaviateClient } from 'weaviate-ts-client';
import { weaviateClientArg } from './config';
/**
* Classの削除
* @param className
*/
async function deleteClass(className: string) {
const client: WeaviateClient = weaviate.client(weaviateClientArg);
client.schema
.classDeleter()
.withClassName(className)
.do()
.then((res: any) => {
console.log(res);
})
.catch((err: Error) => {
console.error(err)
});
}
deleteClass("Music")
3. JSONデータの用意
JSONデータにuuidを追加する
Weaviateは取り込み時にuuidを振ってくれますが、今回はuuid付きのJSONデータを事前に用意します。
npm install uuid
npm i --save-dev @types/uuid
データはsample.json
の名前でプロジェクトルートに置きます。
// 取り込むJSONの形式にしてください。
interface Music {
"piece_id": string,
"composer": string,
"pieceName": string,
}
import { v5 as uuidv5 } from 'uuid';
import fs from 'fs';
import path from 'path';
const NAMESPACE = 'fccc1a21-0397-4ecb-abfe-de06761b3616'; // 任意のuuid4を指定
function addUUID(data: Array<Music>): Array<Music & { uuid: string }> {
return data.map(item => ({
...item,
uuid: uuidv5(`${item.composer}${item.pieceName}`, NAMESPACE)
}));
}
async function formatData() {
const rawData = await fs.promises.readFile(path.join(__dirname, './sample.json'), 'utf-8');
const data = JSON.parse(rawData);
const formattedData = addUUID(data);
await fs.promises.writeFile(path.join(__dirname, './formatted_data.json'), JSON.stringify(formattedData, null, 2));
console.log('Data has been formatted and written to ./formatted_data.json');
}
formatData()
コードを実行します。
ts-node formatJSON.ts
これでuuid付きの取り込みデータが完成です。
[
{
"piece_id": 206,
"composer": "ブラームス",
"pieceName": "ピアノ協奏曲 第1番",
"uuid": "bce5c1ce-6e20-5226-a130-a9b991d62107"
},
{
"piece_id": 207,
"composer": "ブラームス",
"pieceName": "ピアノ協奏曲 第2番",
"uuid": "03f397b3-ce6b-5772-a177-2e2a8016c0c9"
},...
]
NAMESPACE
は下記のツールなどで作成した任意のuuid(v4)で置き換えてください。
https://www.uuidgenerator.net/version4
4. データを取り込む
前項で作成したformatted_data.json
をMusic
クラスに取り込みます。
下記のimportData.ts
を新たにプロジェクトルートに追加します。
import weaviate, { WeaviateClient, ObjectsBatcher, WeaviateObject } from 'weaviate-ts-client';
import { weaviateClientArg } from './config';
import fs from 'fs';
async function importData() {
const className = "Music" // 作成したClass名に置き換えてください。
const client: WeaviateClient = weaviate.client(weaviateClientArg);
const musicJSON = JSON.parse(await fs.promises.readFile('formatted_data.json', 'utf8'))
let batcher: ObjectsBatcher = client.batch.objectsBatcher();
let counter = 0;
let batchSize = 100;
for (const item of musicJSON) {
const obj: WeaviateObject = {
class: className,
id: item.uuid,
properties: {
piece_id: item.piece_id, // Classのオブジェクトのプロパティに置き変えてください。
composer: item.composer,
pieceName: item.pieceName,
},
}
batcher = batcher.withObject(obj);
if (counter++ == batchSize) {
const res = await batcher.do();
console.log(res);
counter = 0;
batcher = client.batch.objectsBatcher();
}
}
const res = await batcher.do();
console.log(res);
}
importData()
(参考:https://weaviate.io/developers/weaviate/quickstart#add-objects)
ts-node importData.ts
レスポンスは以下のようなJSONの配列になっており、result.status
がSUCCESS
となっていれば、取り込み成功です。
レスポンス(1項目分を抜粋)
{
class: 'Music',
creationTimeUnix: 1689176786417,
id: 'fecb6795-b1d5-5303-9fb6-6740f30fb0e0',
lastUpdateTimeUnix: 1689176786417,
properties: {
composer: 'ブラームス',
pieceName: 'ロベルト・シューマンの五重奏よりスケルツォ',
piece_id: 83446
},
vector: [
-0.01618669, -0.009350241, -0.02352856, -0.03952904,
-0.00037532306, 0.040167466, -0.018062059, -0.010101719,
-0.015840879, -0.0047582486, -0.014431027, -0.0021014768,
-0.032133974, 0.0004933649, -0.011770929, 0.006277829,
0.0049477806, 0.0019202576, 0.030591115, -0.016918218,
-0.000040291117, -0.012515756, -0.0019684718, 0.010540634,
-0.016785212, 0.0024988286, 0.017796049, -0.029553678,
0.009097532, -0.0026268458, 0.0493448, -0.0026750602,
-0.017210828, 0.0018753684, -0.005296918, -0.042614754,
-0.015947282, -0.011770929, -0.014298022, -0.0033134834,
-0.013047776, -0.00397352, 0.014869942, 0.0067499964,
-0.027851216, 0.0035213034, -0.005722534, -0.0040200716,
0.009303689, 0.010095068, 0.021533486, 0.017942354,
-0.041976333, 0.005675982, 0.019272404, -0.00014786399,
0.011850732, 0.0019235826, -0.00040836647, -0.0101083685,
0.00878497, 0.009090882, -0.025310824, 0.00040109275,
-0.011997038, -0.011644575, -0.02275713, 0.0068963016,
-0.017702946, -0.0083992565, -0.0009052643, 0.03697535,
-0.017796049, 0.0137394015, 0.024512794, -0.010906398,
-0.02848964, -0.015654672, 0.016732011, 0.006078322,
0.020549249, -0.012848269, -0.0011854058, 0.00021800326,
0.018487675, -0.007760833, -0.012302949, 0.0057391594,
-0.0191793, -0.02880885, 0.0024456268, -0.007780784,
-0.0016118526, 0.0006026782, -0.0048380517, 0.01123891,
0.006191376, 0.019418709, -0.028862054, -0.025443828,
... 1436 more items
],
deprecations: null,
result: { status: 'SUCCESS' }
}
5. ハイブリッド検索を行う
データが取り込めたので、いよいよ検索してみます。
TypeScriptのクライアントライブラリでは、withHybrid
を用いることでハイブリッド検索ができます。
パラメータのalpha
は0から1までの値を取り、0に近いほどキーワード検索、1に近いほどベクトル検索(意味検索) になります。
import weaviate, { WeaviateClient } from 'weaviate-ts-client';
import { weaviateClientArg } from './config';
async function hybridSearch({ query, alpha }: { query: string, alpha: number }) {
const client: WeaviateClient = weaviate.client(weaviateClientArg);
const className = "Music"
const res = await client.graphql
.get()
.withClassName(className)
.withFields('piece_id composer pieceName _additional { score explainScore }')
.withHybrid({
query,
alpha
})
.withLimit(5)
.do()
console.log(JSON.stringify(res, null, 2));
return res
}
const queryArg = {
query: '<質問内容>',
alpha: 0.75 // 重みづけ
}
hybridSearch(queryArg);
たとえば、作曲家の間違ったブラームス 幻想即興曲
というクエリで検証してみました。
この時、以下のような結果となりました。
Alpha = 0.1 (キーワード重視) | Alpha = 0.9 (ベクトル近似重視) | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
キーワード検索に比重を置くと、よりレアなキーワードである「幻想即興曲」の一致が優先されショパンの楽曲がヒットしました。
一方でalpha値をベクトル検索に寄せてみると、より全体の一致が優先され、ブラームスの「7つの幻想曲集」がヒットする結果となりました。
今回は作曲家、曲名という短い項目のみをデータとしましたが、文章検索の場合は、重みづけの差がより顕著に出るものと思われます。
おわりに
以上、Weaviateの導入から、TypeScriptでのデータ取り込み、ハイブリッド検索までを扱いました。
Weaviateは、その特徴であるモジュール、Vectorizerによって、JSONデータを事前のベクトル化なしに取り込めるのが、とっつきやすく魅力的です。
ハイブリッド検索に関しても、Sparse/Dense、二つのEmbeddingを作る必要なく、メソッドとパラメータを変えるだけでよく、ハイブリッド検索をお手軽に試してみるには、うってつけのベクトルデータベースです。
Weaviate、ぜひ触ってみてください。
-
BM25については以下の記事が非専門家にはわかりやすかったです。https://dev.classmethod.jp/articles/mrmo-ml-20200422/ ↩
-
Global Vectors for Word Representation ↩