跳至主要內容

Vespa

Vespa 是一個功能完整的搜尋引擎和向量資料庫。它在同一個查詢中支援向量搜尋 (ANN)、詞彙搜尋和結構化資料搜尋。

這個筆記本示範如何使用 Vespa.ai 作為 LangChain 向量儲存。

您需要使用 pip install -qU langchain-community 安裝 langchain-community 才能使用此整合。

為了建立向量儲存,我們使用 pyvespa 來建立與 Vespa 服務的連線。

%pip install --upgrade --quiet  pyvespa

使用 pyvespa 套件,您可以連線到 Vespa Cloud 實例 或本地 Docker 實例。在這裡,我們將建立一個新的 Vespa 應用程式並使用 Docker 部署它。

建立 Vespa 應用程式

首先,我們需要建立一個應用程式套件

from vespa.package import ApplicationPackage, Field, RankProfile

app_package = ApplicationPackage(name="testapp")
app_package.schema.add_fields(
Field(
name="text", type="string", indexing=["index", "summary"], index="enable-bm25"
),
Field(
name="embedding",
type="tensor<float>(x[384])",
indexing=["attribute", "summary"],
attribute=["distance-metric: angular"],
),
)
app_package.schema.add_rank_profile(
RankProfile(
name="default",
first_phase="closeness(field, embedding)",
inputs=[("query(query_embedding)", "tensor<float>(x[384])")],
)
)

這會為每個包含兩個欄位的文件設定一個 Vespa 應用程式架構:text 用於保存文件文字,而 embedding 用於保存嵌入向量。text 欄位設定為使用 BM25 索引,以實現高效的文字檢索,稍後我們將看到如何使用它和混合搜尋。

embedding 欄位設定為長度為 384 的向量,以保存文字的嵌入表示。有關 Vespa 中張量的更多資訊,請參閱 Vespa 的張量指南

最後,我們新增一個 排序設定檔,以指示 Vespa 如何排序文件。在這裡,我們使用 最近鄰搜尋 進行設定。

現在我們可以在本地部署這個應用程式

from vespa.deployment import VespaDocker

vespa_docker = VespaDocker()
vespa_app = vespa_docker.deploy(application_package=app_package)

這會部署並建立與 Vespa 服務的連線。如果您已經有一個 Vespa 應用程式在運行,例如在雲端中,請參閱 PyVespa 應用程式以了解如何連線。

建立 Vespa 向量儲存

現在,讓我們載入一些文件

from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import CharacterTextSplitter

loader = TextLoader("../../how_to/state_of_the_union.txt")
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
docs = text_splitter.split_documents(documents)

from langchain_community.embeddings.sentence_transformer import (
SentenceTransformerEmbeddings,
)

embedding_function = SentenceTransformerEmbeddings(model_name="all-MiniLM-L6-v2")

在這裡,我們還設定了本地句子嵌入器,以將文字轉換為嵌入向量。也可以使用 OpenAI 嵌入,但向量長度需要更新為 1536,以反映該嵌入的較大尺寸。

為了將這些饋送到 Vespa,我們需要配置向量儲存應如何對應到 Vespa 應用程式中的欄位。然後,我們直接從這組文件中建立向量儲存。

vespa_config = dict(
page_content_field="text",
embedding_field="embedding",
input_field="query_embedding",
)

from langchain_community.vectorstores import VespaStore

db = VespaStore.from_documents(docs, embedding_function, app=vespa_app, **vespa_config)
API 參考:VespaStore

這會建立一個 Vespa 向量儲存,並將該組文件饋送到 Vespa。向量儲存負責為每個文件呼叫嵌入函數,並將它們插入到資料庫中。

我們現在可以查詢向量儲存

query = "What did the president say about Ketanji Brown Jackson"
results = db.similarity_search(query)

print(results[0].page_content)

這將使用上面給出的嵌入函數來建立查詢的表示形式,並使用它來搜尋 Vespa。請注意,這將使用 default 排序函數,我們在上面的應用程式套件中進行了設定。您可以使用 similarity_searchranking 參數來指定要使用的排序函數。

請參閱 pyvespa 文件 以獲取更多資訊。

這涵蓋了 LangChain 中 Vespa 儲存的基本用法。現在您可以傳回結果,並繼續在 LangChain 中使用這些結果。

更新文件

除了呼叫 from_documents 之外,您可以直接建立向量儲存,並從中呼叫 add_texts。這也可以用於更新文件

query = "What did the president say about Ketanji Brown Jackson"
results = db.similarity_search(query)
result = results[0]

result.page_content = "UPDATED: " + result.page_content
db.add_texts([result.page_content], [result.metadata], result.metadata["id"])

results = db.similarity_search(query)
print(results[0].page_content)

但是,pyvespa 函式庫包含操作 Vespa 上內容的方法,您可以直接使用這些方法。

刪除文件

您可以使用 delete 函數刪除文件

result = db.similarity_search(query)
# docs[0].metadata["id"] == "id:testapp:testapp::32"

db.delete(["32"])
result = db.similarity_search(query)
# docs[0].metadata["id"] != "id:testapp:testapp::32"

同樣地,pyvespa 連線也包含刪除文件的方法。

回傳分數

similarity_search 方法只會依照相關性順序回傳文件。若要取得實際的分數

results = db.similarity_search_with_score(query)
result = results[0]
# result[1] ~= 0.463

這是使用 "all-MiniLM-L6-v2" 嵌入模型並使用餘弦距離函數(由應用程式函數中的 angular 參數指定)的結果。

不同的嵌入函數需要不同的距離函數,Vespa 需要知道使用哪個距離函數來排序文件。請參考關於距離函數的文件以獲取更多資訊。

作為檢索器

若要將此向量儲存庫用作 LangChain 檢索器,只需呼叫 as_retriever 函數,這是一個標準的向量儲存庫方法

db = VespaStore.from_documents(docs, embedding_function, app=vespa_app, **vespa_config)
retriever = db.as_retriever()
query = "What did the president say about Ketanji Brown Jackson"
results = retriever.invoke(query)

# results[0].metadata["id"] == "id:testapp:testapp::32"

這允許從向量儲存庫進行更通用、非結構化的檢索。

Metadata(元數據)

到目前為止的範例中,我們只使用了文字和該文字的嵌入。文件通常包含額外資訊,在 LangChain 中稱為元數據。

透過將欄位加入應用程式套件,Vespa 可以包含許多不同類型的欄位

app_package.schema.add_fields(
# ...
Field(name="date", type="string", indexing=["attribute", "summary"]),
Field(name="rating", type="int", indexing=["attribute", "summary"]),
Field(name="author", type="string", indexing=["attribute", "summary"]),
# ...
)
vespa_app = vespa_docker.deploy(application_package=app_package)

我們可以在文件中加入一些元數據欄位

# Add metadata
for i, doc in enumerate(docs):
doc.metadata["date"] = f"2023-{(i % 12)+1}-{(i % 28)+1}"
doc.metadata["rating"] = range(1, 6)[i % 5]
doc.metadata["author"] = ["Joe Biden", "Unknown"][min(i, 1)]

並讓 Vespa 向量儲存庫知道這些欄位

vespa_config.update(dict(metadata_fields=["date", "rating", "author"]))

現在,在搜尋這些文件時,這些欄位將會被回傳。此外,也可以根據這些欄位進行過濾

db = VespaStore.from_documents(docs, embedding_function, app=vespa_app, **vespa_config)
query = "What did the president say about Ketanji Brown Jackson"
results = db.similarity_search(query, filter="rating > 3")
# results[0].metadata["id"] == "id:testapp:testapp::34"
# results[0].metadata["author"] == "Unknown"

自訂查詢

如果相似度搜尋的預設行為不符合您的需求,您可以隨時提供自己的查詢。因此,您不需要將所有設定提供給向量儲存庫,而是自己編寫即可。

首先,讓我們在應用程式中加入 BM25 排序函數

from vespa.package import FieldSet

app_package.schema.add_field_set(FieldSet(name="default", fields=["text"]))
app_package.schema.add_rank_profile(RankProfile(name="bm25", first_phase="bm25(text)"))
vespa_app = vespa_docker.deploy(application_package=app_package)
db = VespaStore.from_documents(docs, embedding_function, app=vespa_app, **vespa_config)

然後,執行基於 BM25 的一般文字搜尋

query = "What did the president say about Ketanji Brown Jackson"
custom_query = {
"yql": "select * from sources * where userQuery()",
"query": query,
"type": "weakAnd",
"ranking": "bm25",
"hits": 4,
}
results = db.similarity_search_with_score(query, custom_query=custom_query)
# results[0][0].metadata["id"] == "id:testapp:testapp::32"
# results[0][1] ~= 14.384

透過使用自訂查詢,可以使用 Vespa 所有強大的搜尋和查詢功能。請參考 Vespa 文件中的 Query API 以獲取更多詳細資訊。

混合搜尋意味著同時使用經典的基於詞彙的搜尋(例如 BM25)和向量搜尋,並結合結果。我們需要在 Vespa 上為混合搜尋建立一個新的排序配置 (rank profile)

app_package.schema.add_rank_profile(
RankProfile(
name="hybrid",
first_phase="log(bm25(text)) + 0.5 * closeness(field, embedding)",
inputs=[("query(query_embedding)", "tensor<float>(x[384])")],
)
)
vespa_app = vespa_docker.deploy(application_package=app_package)
db = VespaStore.from_documents(docs, embedding_function, app=vespa_app, **vespa_config)

在這裡,我們將每個文件評分為其 BM25 分數和距離分數的組合。我們可以透過自訂查詢進行查詢

query = "What did the president say about Ketanji Brown Jackson"
query_embedding = embedding_function.embed_query(query)
nearest_neighbor_expression = (
"{targetHits: 4}nearestNeighbor(embedding, query_embedding)"
)
custom_query = {
"yql": f"select * from sources * where {nearest_neighbor_expression} and userQuery()",
"query": query,
"type": "weakAnd",
"input.query(query_embedding)": query_embedding,
"ranking": "hybrid",
"hits": 4,
}
results = db.similarity_search_with_score(query, custom_query=custom_query)
# results[0][0].metadata["id"], "id:testapp:testapp::32")
# results[0][1] ~= 2.897

Vespa 中的原生嵌入器

到目前為止,我們一直在 Python 中使用嵌入函數來為文本提供嵌入。Vespa 原生支援嵌入函數,因此您可以將此計算延遲到 Vespa 中進行。一個好處是,如果您有大量的集合,則可以在嵌入文件時使用 GPU。

請參考 Vespa 嵌入 以獲取更多資訊。

首先,我們需要修改我們的應用程式套件

from vespa.package import Component, Parameter

app_package.components = [
Component(
id="hf-embedder",
type="hugging-face-embedder",
parameters=[
Parameter("transformer-model", {"path": "..."}),
Parameter("tokenizer-model", {"url": "..."}),
],
)
]
Field(
name="hfembedding",
type="tensor<float>(x[384])",
is_document_field=False,
indexing=["input text", "embed hf-embedder", "attribute", "summary"],
attribute=["distance-metric: angular"],
)
app_package.schema.add_rank_profile(
RankProfile(
name="hf_similarity",
first_phase="closeness(field, hfembedding)",
inputs=[("query(query_embedding)", "tensor<float>(x[384])")],
)
)

請參考嵌入文件,了解如何將嵌入器模型和分詞器新增到應用程式。請注意,hfembedding 欄位包含使用 hf-embedder 進行嵌入的說明。

現在我們可以使用自訂查詢進行查詢

query = "What did the president say about Ketanji Brown Jackson"
nearest_neighbor_expression = (
"{targetHits: 4}nearestNeighbor(internalembedding, query_embedding)"
)
custom_query = {
"yql": f"select * from sources * where {nearest_neighbor_expression}",
"input.query(query_embedding)": f'embed(hf-embedder, "{query}")',
"ranking": "internal_similarity",
"hits": 4,
}
results = db.similarity_search_with_score(query, custom_query=custom_query)
# results[0][0].metadata["id"], "id:testapp:testapp::32")
# results[0][1] ~= 0.630

請注意,此處的查詢包含一個 embed 指令,用於使用與文件相同的模型嵌入查詢。

近似最近鄰

在上述所有範例中,我們都使用了精確最近鄰來尋找結果。但是,對於大量的文件集合,這並不可行,因為必須掃描所有文件才能找到最佳匹配。為了避免這種情況,我們可以使用近似最近鄰

首先,我們可以更改嵌入欄位以建立 HNSW 索引

from vespa.package import HNSW

app_package.schema.add_fields(
Field(
name="embedding",
type="tensor<float>(x[384])",
indexing=["attribute", "summary", "index"],
ann=HNSW(
distance_metric="angular",
max_links_per_node=16,
neighbors_to_explore_at_insert=200,
),
)
)

這會在嵌入資料上建立 HNSW 索引,從而可以進行高效的搜尋。設定完成後,我們可以輕鬆地使用 ANN 搜尋,方法是將 approximate 參數設定為 True

query = "What did the president say about Ketanji Brown Jackson"
results = db.similarity_search(query, approximate=True)
# results[0][0].metadata["id"], "id:testapp:testapp::32")

這涵蓋了 LangChain 中 Vespa 向量儲存庫的大部分功能。


此頁面是否有幫助?