簡單上手向量資料庫,打造 RAG 應用: Weaviate DB 101
當我們希望打造一個基於語意相似的文字搜尋時,一般的作法是將文字透過 AI model 轉成 embeddeding vector,透過計算 vector distance (cosine distancing) 找出距離最相近的文字 kNN (k nearest neighbors),例如OpenAI 的 demo code:Semantic_text_search_using_embeddings
當資料量小的時候,這樣做不會有什麼問題,但仔細看目前的比對方式是把輸入跟資料集的資料每一筆 vector 都做 distance 計算,所以複雜度會是 -O(N),N 是資料集數量
,如果資料量一大,這勢必會是造成瓶頸
Vector DB 就是要解決這樣的問題,主要透過
- 改用 ANN (approximate nearest neighbors) 取代 kNN,用相似度查詢換取執行速度
- 提供 database 功能,包含持久化保存、水平擴展 (sharding)、高可用性、API 封裝等功能
目前搜尋市面上有幾個常見的選擇
- Pinecone:閉源專案,就先略過不用
- Milvus:純 vector DB,看起來在基礎建設 (scaling、availability) 等做得比較完整
- Weaviate:有 module 可以支援 AI model 整合,也支援純 vector DB 使用
- Chroma:Fireship demo 用
- Elasticsearch: 8.0 後就有支援 vector search
這次 Demo 選擇用 Weaviate,主要是有支援 module 直接整合 AI model,這樣我就不用另外想 embedding vector 該如何產生
Weaviate DB 基本介紹
Weaviate DB 有以下幾個特色
- 便利性:
module 支援常見的 AI model,包含 transformer、openai 等,透過參數可以直接調整,但有些 AI model 部分需要自己架設 server,Weaviate DB 核心並不包含 AI model,只是會直接透過 interface 調用 - 搜尋與過濾:
除了 vector 搜尋外,在文字上會結合 inverted index 增加搜尋的精準度與效率,並提供filter 可以透過屬性篩選
- API 接口支援 REST 與 GraphQL
- Scalability:
會提供 Sharding 與 High Availability (後者還在開發中)
儲存概念
以 Class
為管理單位,可以理解成 table 或 collection 的概念,每個 class 有自己 vectorize 的機制、sharding 設定等,例如
{
"class": "string", // The name of the class in string format
"description": "string", // A description for your reference
"vectorIndexType": "hnsw", // Defaults to hnsw, can be omitted in schema definition since this is the only available type for now
"vectorIndexConfig": {
... // Vector index type specific settings, including distance metric
},
"vectorizer": "text2vec-contextionary", // Vectorizer to use for data objects added to this class
"moduleConfig": {
"text2vec-contextionary": {
"vectorizeClassName": true // Include the class name in vector calculation (default true)
}
},
"properties": [ // An array of the properties you are adding, same as a Property Object
{
"name": "string", // The name of the property
"description": "string", // A description for your reference
"dataType": [ // The data type of the object as described above. When creating cross-references, a property can have multiple data types, hence the array syntax.
"string"
],
"moduleConfig": { // Module-specific settings
"text2vec-contextionary": {
"skip": true, // If true, the whole property will NOT be included in vectorization. Default is false, meaning that the object will be NOT be skipped.
"vectorizePropertyName": true, // Whether the name of the property is used in the calculation for the vector position of data objects. Default false.
}
},
"indexInverted": true // Optional, default is true. By default each property is fully indexed both for full-text, as well as vector search. You can ignore properties in searches by explicitly setting index to false.
}
],
"invertedIndexConfig": {
...
},
"shardingConfig": {
... // Optional, controls behavior of class in a multi-node setting, see section below
}
}
每個 Class 內包含多個 Object 以 json 格式儲存,Object 內的屬性 (Property) 支援 多種格式,額外有提供地理位置、日期、電話號碼(有點不解?!),其中地理位置在查詢上還支援方圓內的篩選
Class schema 是靜態的,如果要增減欄位需要透過 API 修改,而不像某些 NoSQL 是動態寫入的
物理儲存以 Shard 為單位
一個 Class 在物理儲存上由多個 Shard 分散儲存,每個 Shard 建立時會同時包含 vector index / object store / inverted index 三個 index,其中 vecotr index 目前是用 HNSW,其餘兩個是用 LSMTree 儲存
Demo — 打造簡易的文字查詢
程式碼在 sj82516/weaviate-db-demo ,在本地端啟動 Weaviate DB 並做簡易的文字查詢
透過 docker-compose 啟動
這邊我們只做文字搜尋的部分,選用 transformer 當作 AI model,可以自行替換 不同的 modules
version: '3.4'
services:
weaviate:
image: semitechnologies/weaviate:1.18.3
ports:
- "8080:8080"
environment:
QUERY_DEFAULTS_LIMIT: 20
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true'
PERSISTENCE_DATA_PATH: "./data"
DEFAULT_VECTORIZER_MODULE: text2vec-transformers
ENABLE_MODULES: text2vec-transformers # 選擇的 text 向量化方式
TRANSFORMERS_INFERENCE_API: http://t2v-transformers:8080
CLUSTER_HOSTNAME: 'node1'
t2v-transformers:
image: semitechnologies/transformers-inference:sentence-transformers-multi-qa-MiniLM-L6-cos-v1
environment:
ENABLE_CUDA: 0
建立 Class 與匯入資料
Class schema 可以主動宣告,或是讓 Weaviate DB 自動幫忙建立 (有點像 Elasticsearch),這邊建立了一個 Class 並指定 module 用 text-transformer
client.Schema().ClassCreator().WithClass(&models.Class{
Class: className,
Description: "all books I have",
Vectorizer: "text2vec-transformers",
ModuleConfig: map[string]interface{}{
"text2vec-transformers": map[string]interface{}{},
},
Properties: []*models.Property{
{
Name: "title",
DataType: []string{"text"},
},
},
})
匯入資料可以批次匯入
objects := []*models.Object{
{
Class: className,
Properties: map[string]interface{}{
"title": "Hello World Blue",
"type": "program",
},
},
{
Class: className,
Properties: map[string]interface{}{
"title": "Hello World Red",
"type": "program",
},
}, {
Class: className,
Properties: map[string]interface{}{
"title": "Hello World Yellow",
"type": "science",
},
},
}
client.Batch().ObjectsBatcher().
WithObjects(objects...).
WithConsistencyLevel(replication.ConsistencyLevel.ALL).
Do(context.Background())
搜尋與篩選
完整查詢有三個部分 搜尋 + 篩選 + 欄位過濾
// 文字搜尋
nearText := client.GraphQL().NearTextArgBuilder().
// 搜尋的關鍵字
WithConcepts(concepts).
// 讓向量往某個方向靠近
WithMoveTo(&graphql.MoveParameters{
Force: 0.5,
Concepts: []string{
"Yellow",
},
})
// 篩選機制
where := filters.Where().
WithPath([]string{"type"}).
WithOperator(filters.Equal).
WithValueText("program")
// 選擇欄位回傳
fields := []graphql.Field{
{Name: "title"},
// 額外的欄位,算是 metadata
{Name: "_additional", Fields: []graphql.Field{
{Name: "id"},
{Name: "distance"},
}},
}
// GraphQL Request
result, err := client.GraphQL().Get().
WithClassName(className).
WithNearText(nearText).
WithWhere(where).
WithFields(fields...).
Do(context.Background())
if result.Errors != nil {
for _, err := range result.Errors {
fmt.Println(err.Message)
}
return
}
r := result.Data["Get"].(map[string]interface{})[className].([]interface{})
jsonbody, err := json.Marshal(r)
if err != nil {
// do error check
fmt.Println(err)
return
}
books := []Book{}
if err := json.Unmarshal(jsonbody, &books); err != nil {
// do error check
fmt.Println(err)
return
}
for _, book := range books {
fmt.Println(book)
}
在 searching 搜尋中,如果是 text Weaviate DB 會用 inverted index 與 vector 搜尋 WithNearText
,找出的結果會給出對應的 distance
nearText := client.GraphQL().NearTextArgBuilder().
WithConcepts(concepts).
WithMoveTo(&graphql.MoveParameters{
Force: 0.5,
Concepts: []string{
"Yellow",
},
})
- concepts 搜尋的文字陣列
- WithMoveTo 額外指定搜尋要特別接近哪些關鍵字
- 更多參數可參考 Vector search parameters
也可以針對屬性做篩選,例如我只要 object 中 type = program 的資料
where := filters.Where().
WithPath([]string{"type"}).
WithOperator(filters.Equal).
WithValueText("program")
搜尋的結果大概是 (Yellow 被篩選掉)
{Hello World Blue {b9f93a34–6cbd-45b3-afc3-f82e9d1d0da8 0.54240143}}
{Hello World Red {6575290e-53cc-4ab9–8d39–330e5a47b0d1 0.55338395}}
ANN 演算法:HNSW 介紹
主要參考自:https://www.pinecone.io/learn/series/faiss/hnsw/
HNSW (Hierarchical Navigable Small World) 是一種 ANN 演算法,主要是借鏡 probability skip list,透過分層查詢,先從最上層最稀疏的 layer 開始找最相近的鄰居,接著往下一層找該鄰居的最相近鄰居,一直到最底層 (layer 0)
查詢複雜度降至 log (N)
這部分有兩個參數可以注意
- efConstruction: 決定每次查詢回傳的鄰居數量,數量越多查詢越精準,但是效率越差
- maxConnections: 決定每個 point 可以連接的 edge 數量,同樣是數量越多越精準
如何決定 point 要插入哪一層
這部分便是透過機率所決定,越上層機率越低
結語
快速瞭解了一下 Vector DB,未來如果有需要做向量查詢使用上應該會蠻方便的,之後有機會在評估一下 Milvus,看起來基礎建設比 Weaviate DB 更完善、Github 上熱度也更高、也支援更多的 ANN 演算法