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。請注意,這將使用 default
排序函數,我們在上面的應用程式套件中進行了設定。您可以使用 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"
這允許從向量儲存庫進行更通用、非結構化的檢索。
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 向量儲存庫的大部分功能。