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)
這會建立一個 Vespa 向量儲存庫,並將該組文件饋送到 Vespa。向量儲存庫負責為每個文件呼叫嵌入函數,並將其插入資料庫。
我們現在可以查詢向量儲存庫。
query = "What did the president say about Ketanji Brown Jackson"
results = db.similarity_search(query)
print(results[0].page_content)
這將使用上面給出的嵌入函數來為查詢建立表示,並使用它來搜尋 Vespa。請注意,這將使用預設的排序函數,我們在上面的應用程式套件中設定了該函數。您可以使用 `similarity_search` 的 `ranking` 參數來指定要使用的排序函數。
請參閱 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"
這允許從向量儲存庫進行更通用、非結構化的檢索。
元數據
到目前為止的範例中,我們僅使用了文字和該文字的嵌入。文件通常包含其他資訊,在 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 上為混合搜尋建立一個新的排序設定檔。
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 索引,從而實現高效搜尋。設定完成後,我們可以透過將 approximate
參數設定為 True
來輕鬆使用 ANN 進行搜尋
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 向量儲存庫的大部分功能。