當(dāng)數(shù)據(jù)集膨脹到數(shù)百萬甚至數(shù)十億量級的向量時,怎么讓搜索在這種規(guī)模下依然又快又準(zhǔn)就成了一個實實在在的工程難題。這篇文章要聊的就是向量搜索系統(tǒng)的三個核心優(yōu)化方向——性能調(diào)優(yōu)、混合搜索和可擴(kuò)展架構(gòu)。
傳統(tǒng)搜索的問題
![]()
傳統(tǒng)搜索系統(tǒng)做的事情本質(zhì)上是詞法匹配:找文檔里有沒有出現(xiàn)查詢中的關(guān)鍵詞。至于查詢背后的意思?它不管。同義詞、上下文、用戶意圖、概念層面的相似性,統(tǒng)統(tǒng)不在考慮范圍內(nèi)。
比如說用戶查詢 cheap car,文檔里寫的是 affordable vehicle。意思完全一樣,但傳統(tǒng)搜索直接判定不匹配——因為沒有一個詞是相同的。
Elasticsearch 這類系統(tǒng)引入了 BM25 排序算法,根據(jù)詞頻和重要性給文檔打分。但再怎么優(yōu)化打分策略,底層邏輯還是關(guān)鍵詞重疊。換個說法表達(dá)同一個意思,排名可能就掉下去了。傳統(tǒng)詞法搜索在同義詞、改述、深層語義理解面前,始終力不從心。
用一段 Python 代碼直觀感受一下傳統(tǒng)關(guān)鍵詞匹配的局限:
documents = [
"Affordable vehicle for students",
"Best gaming laptop",
"Budget friendly smartphone"
]
def traditional_search(query, docs):
results = []
query_words = query.lower().split()
for doc in docs:
doc_lower = doc.lower()
if all(word in doc_lower for word in query_words):
results.append(doc)
return results
# Query
query = "cheap car"
print(traditional_search(query, documents))
輸出:
空的。"Affordable vehicle" 和 "cheap car" 語義上幾乎等價,但關(guān)鍵詞匹配毫無辦法。要解決這個問題就得換一種思路——向量搜索。
什么是向量搜索
不再比較字面上的詞而是比較含義。把文檔和查詢都轉(zhuǎn)換成數(shù)值向量(Embedding),這些向量編碼了語義信息。即使兩段文本用詞完全不同,只要意思相近,它們的向量在空間中就會彼此靠近。
![]()
整個流程拆開來看分四步。
第一步是文檔處理,屬于離線的索引階段。所有文檔經(jīng)過一個 Embedding 模型,轉(zhuǎn)換為向量表示,然后存入向量索引或向量數(shù)據(jù)庫。
第二步是查詢處理,在線階段。用戶提交查詢后,同一個 Embedding 模型把查詢也轉(zhuǎn)成向量,發(fā)送給向量索引。
第三步是相似性搜索。在向量索引內(nèi)部,系統(tǒng)拿查詢向量和所有文檔向量做比較,用余弦相似度之類的距離度量找出最接近的那些向量。
第四步是返回結(jié)果。距離最近的文檔就是語義上最相關(guān)的結(jié)果,哪怕它們和查詢沒有任何共同關(guān)鍵詞。
下面動手搭一個小型語義搜索引擎,看看向量搜索在實踐中是什么樣的。
步驟1:將文本轉(zhuǎn)換為 Embedding
用 Sentence Transformers 的 all-MiniLM-L6-v2 模型:
from sentence_transformers import SentenceTransformer
documents = [
"Affordable vehicle for students",
"Best gaming laptop",
"Budget friendly smartphone"
]
model = SentenceTransformer("all-MiniLM-L6-v2")
doc_embeddings = model.encode(documents)
print(doc_embeddings.shape)
輸出:
(3, 384)
每個句子變成了一個 384 維的向量。
步驟2:引入相似性概念
similarities = model.similarity(doc_embeddings, doc_embeddings)
print(similarities)
輸出:
tensor([[1.0000, 0.1679, 0.3233],
[0.1679, 1.0000, 0.2726],
[0.3233, 0.2726, 1.0000]])
這是一個成對余弦相似度矩陣。單元格 (i, j) 表示句子 i 和句子 j 的語義相似程度。對角線上都是 1.0000,因為每個句子和自身當(dāng)然完全相似。可以看到 "Affordable vehicle for students" 和 "Budget friendly smartphone" 之間的相似度(0.3233)比它和 "Best gaming laptop"(0.1679)更高,這符合直覺——前兩者都帶有"經(jīng)濟(jì)實惠"的語義。
步驟3:創(chuàng)建 FAISS 索引
import faiss
import numpy as np
doc_embeddings = np.array(doc_embeddings).astype("float32")
dimension = doc_embeddings.shape[1]
index = faiss.IndexFlatL2(dimension)
index.add(doc_embeddings)
FAISS 索引在高維空間里做 K 近鄰搜索,用 L2 距離作為度量,能快速找到和查詢向量最接近的 top-k 個 Embedding。
步驟4:搜索查詢
文檔 Embedding 已經(jīng)入庫,現(xiàn)在可以做語義搜索了。
query = "cheap car"
query_embedding = model.encode([query]).astype("float32")
distances, indices = index.search(query_embedding, k=1)
print(documents[indices[0][0]])
輸出:
Affordable vehicle for students
查詢被編碼為 384 維向量,F(xiàn)AISS 計算它和每個文檔向量的 L2 距離,返回距離最小的那個。"cheap car" 成功匹配到了 "Affordable vehicle for students",這里關(guān)鍵詞沒有任何重疊,但語義搜索抓住了含義。
這個小演示只處理三條文檔,跑起來毫無壓力。但現(xiàn)實場景動輒百萬、十億級別的 Embedding,IndexFlatL2 這種暴力全量比較的方式就扛不住了。搜索延遲飆升,內(nèi)存吃緊,擴(kuò)展性成問題。接下來要聊的就是怎么優(yōu)化。
性能調(diào)優(yōu)
IndexFlatL2 對每個查詢都遍歷全部向量做比較。數(shù)據(jù)量小的時候沒問題,數(shù)據(jù)量一上去就是災(zāi)難。性能調(diào)優(yōu)圍繞三件事展開:壓縮搜索時間、降低內(nèi)存消耗、同時盡量不犧牲檢索精度。
精確搜索 vs 近似最近鄰(ANN)
前面演示用的是:
faiss.IndexFlatL2(dimension)
這是精確最近鄰搜索,查詢和每一個向量都比一遍。精度有保證,但面對百萬級 Embedding 就跑不動了。
替代方案是近似最近鄰(ANN)。ANN 放棄一點點精度換來數(shù)量級的速度提升,F(xiàn)AISS 里常用的 ANN 索引比如 IndexIVFFlat 和 IndexHNSWFlat,都是通過減少實際參與比較的向量數(shù)量來加速檢索。
倒排文件索引(IVF)
IVF 的思路是把向量先聚類,查詢時只在最相關(guān)的幾個聚類里搜索,而不是掃描全部數(shù)據(jù)。對大數(shù)據(jù)集來說,延遲直接降一個量級。
quantizer = faiss.IndexFlatL2(dimension)
index = faiss.IndexIVFFlat(quantizer, dimension, nlist=100)
nlist 是聚類數(shù)量。聚類越多,精度越高,但計算開銷也越大。這里面有個平衡點需要根據(jù)實際場景調(diào)。
HNSW(基于圖的搜索)
HNSW(Hierarchical Navigable Small World)是另外一個方向:不做聚類而是構(gòu)建一個圖結(jié)構(gòu)。每個向量和它最近的鄰居相連,搜索時沿著圖的邊進(jìn)行導(dǎo)航。搜索速度快,召回率高,可擴(kuò)展性也不錯,是現(xiàn)代向量數(shù)據(jù)庫中非常主流的索引方式。
向量壓縮(Product Quantization)
數(shù)據(jù)量大了之后光是把向量存在內(nèi)存里就是個問題,Product Quantization 對向量做有損壓縮,減小內(nèi)存占用的同時還能加速搜索。百萬、十億級 Embedding 的場景下,這個技術(shù)基本是標(biāo)配。
硬件加速(GPU 支持)
FAISS 支持 GPU 加速,把向量計算并行化之后延遲大幅降低,適合對實時性要求高的場景,比如推薦系統(tǒng)和大型搜索平臺。
混合搜索
實際的搜索系統(tǒng)很少只用關(guān)鍵詞匹配或者只用向量相似性,更常見的做法是兩者結(jié)合。
![]()
用戶查詢進(jìn)來后,關(guān)鍵詞搜索和向量搜索并行執(zhí)行。關(guān)鍵詞搜索產(chǎn)出一個詞法相關(guān)性分?jǐn)?shù)(比如 BM25),向量搜索產(chǎn)出一個語義相似度分?jǐn)?shù)(比如余弦相似度),兩路分?jǐn)?shù)再融合成一個統(tǒng)一的排名分?jǐn)?shù)。
評分公式(分?jǐn)?shù)融合)
常見的加權(quán)融合公式長這樣:
FinalScore(d)=α?KeywordScore(d)+(1?α)?VectorScore(d)
# 其中,
d = document
∈ [0,1] 控制關(guān)鍵詞精確度的重要性
這個分?jǐn)?shù)不代表"正確性",只是用來給文檔排序。系統(tǒng)按 FinalScore 從高到低排列,排在前面的就是最終結(jié)果。
混合搜索偽代碼
# Step 1: Keyword Search
keyword_results = bm25.search(query)
# Step 2: Vector Search
query_embedding = embed(query)
vector_results = vector_db.search(query_embedding)
# Step 3: Merge + Weighted Ranking
def merge_and_rank(keyword_results, vector_results, alpha=0.6):
keyword_dict = {doc.id: doc.score for doc in keyword_results}
vector_dict = {doc.id: doc.score for doc in vector_results}
all_doc_ids = set(keyword_dict.keys()) | set(vector_dict.keys())
# Normalize scores (important in real systems)
def normalize(scores):
if not scores:
return {}
min_s = min(scores.values())
max_s = max(scores.values())
return {
doc: (score - min_s) / (max_s - min_s + 1e-9)
for doc, score in scores.items()
}
keyword_dict = normalize(keyword_dict)
vector_dict = normalize(vector_dict)
final_scores = {}
for doc_id in all_doc_ids:
kw_score = keyword_dict.get(doc_id, 0)
vec_score = vector_dict.get(doc_id, 0)
final_scores[doc_id] = (
alpha * kw_score +
(1 - alpha) * vec_score
)
ranked_docs = sorted(
final_scores.items(),
key=lambda x: x[1]S,
reverse=True
)
return ranked_docs
final_results = merge_and_rank(
keyword_results,
vector_results,
alpha=0.6
)
return final_results
這里有個細(xì)節(jié)值得注意:歸一化。BM25 的分?jǐn)?shù)是無界的,余弦相似度則在 -1 到 1 之間。不做歸一化直接加權(quán),某一路的分?jǐn)?shù)可能把另一路完全壓過去,排名就失真了。所以實際系統(tǒng)在融合之前一定要先歸一化。
擴(kuò)展向量搜索
小數(shù)據(jù)集上向量搜索跑得很漂亮。但從幾千個向量漲到幾百萬、幾十億的時候,單機(jī)就扛不住了。生產(chǎn)環(huán)境下的擴(kuò)展策略主要三個:Sharding、分布式搜索、緩存。
Sharding(水平擴(kuò)展)
Sharding 就是把向量分散存儲到多臺機(jī)器上。比如 3000 萬個向量,機(jī)器 1 存前 1000 萬,機(jī)器 2 存中間 1000 萬,機(jī)器 3 存最后 1000 萬。
查詢來了之后,系統(tǒng)先生成查詢 Embedding,然后把它發(fā)給所有分片。每個分片各自返回 top-K 結(jié)果,最后匯總在一起重新排名。想加容量?加機(jī)器就行,這就是水平擴(kuò)展的好處。
分布式搜索(并行查詢處理)
![]()
一臺服務(wù)器掃 1 億個向量太慢,那就拆成 10 臺,每臺掃 1000 萬,并行跑。工作量分?jǐn)偭耍阉魍竭M(jìn)行,結(jié)果最終在協(xié)調(diào)器節(jié)點匯合。這是分布式搜索引擎和現(xiàn)代向量數(shù)據(jù)庫的標(biāo)準(zhǔn)架構(gòu)。
緩存(速度優(yōu)化)
![]()
邏輯很簡單:If we have already answered this question before, don't compute again.
沒有緩存的情況下,每次查詢都要走完整流程——生成 Embedding、發(fā)請求到所有分片、算相似度、合并結(jié)果、返回 top-K。哪怕5秒前剛有人搜過一模一樣的東西,全套流程照跑一遍。這就是純粹的浪費。
有了緩存就不一樣了。熱門查詢再來的時候系統(tǒng)先查緩存:Have we seen this query before? 命中的話直接返回存好的結(jié)果,Embedding 生成和分布式搜索全部跳過。
想想 "iPhone 15 price" 或 "Weather today" 這種查詢,每天成千上萬人在搜。算一次就夠了,后面全部復(fù)用。緩存同時砍掉了延遲和基礎(chǔ)設(shè)施成本。
總結(jié)
向量搜索把信息檢索從字面匹配帶進(jìn)了語義理解的時代。但光有 Embedding 還不夠,真正讓系統(tǒng)在生產(chǎn)環(huán)境中跑起來的是背后的工程優(yōu)化——混合搜索把詞法和語義兩條路打通,ANN 和壓縮技術(shù)解決性能瓶頸,分布式架構(gòu)和緩存撐起大規(guī)模部署。
本文代碼:
https://avoid.overfit.cn/post/f8461443473745e6bd7ea21a5b43f44c
by Pawan
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務(wù)。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.