譯者 | 布加迪
審校 | 重樓
使用傳統(tǒng)的基于詞匯(或基于關(guān)鍵字)的搜索,我們可以找到含有我們搜索的確切單詞的文檔。關(guān)鍵詞搜索在準(zhǔn)確性方面表現(xiàn)出色,但在替代詞語或自然語言方面表現(xiàn)差強(qiáng)人意。
語義搜索通過捕獲文檔和用戶查詢背后的意圖來克服這些限制。這通常通過利用向量嵌入將文檔和查詢映射到高維空間,并計算向量相似性以檢索相關(guān)結(jié)果來實現(xiàn)。
針對幾種系統(tǒng),單一的搜索方法可能會失敗,導(dǎo)致向用戶顯示不完整的信息。結(jié)合上述兩種搜索方法的優(yōu)勢將使我們能夠提供出色的搜索體驗。
Elasticsearch和Apache Solr等系統(tǒng)都很好地支持基于關(guān)鍵字的搜索。語義搜索通常需要使用向量數(shù)據(jù)庫進(jìn)行存儲,市面上有多種解決方案。這篇文章解釋了我們?nèi)绾卧赑ostgres中使用單單一個熟悉的存儲系統(tǒng)來支持包括詞匯搜索和語義搜索的混合搜索。
假設(shè)我們有一個應(yīng)用程序使用下面的表,允許用戶通過關(guān)鍵字或自然語言搜索產(chǎn)品:
SQL
CREATE TABLE products (
id bigserial PRIMARY KEY,
description VARCHAR(255),
embedding vector(384)
);
description列包含產(chǎn)品的文本/自然語言描述。Postgres在該列上為全文搜索提供了默認(rèn)索引,但是我們也可以創(chuàng)建自定義索引以加速全文搜索,其作用類似信息檢索的索引。
embedding列存儲產(chǎn)品描述的向量(浮點)表示,捕獲語義含義而不是單詞。Postgres中的pgvector擴(kuò)展帶來了向量數(shù)據(jù)類型和向量相似性度量指標(biāo):L2、余弦和點積距離。有幾種方法可以生成嵌入,比如使用詞級嵌入(如Word2Vec)、句子/文檔嵌入(如SBERT)或者來自基于Transformer的模型(如BERT模型)的嵌入。
為了演示,我們將在數(shù)據(jù)庫中插入以下數(shù)據(jù):
SQL
INSERT INTO products (description) VALUES
('Organic Cotton Baby Onesie - Newborn Size, Blue'),
('Soft Crib Sheet for Newborn, Hypoallergenic'),
('Baby Monitor with Night Vision and Two-Way Audio'),
('Diaper Bag Backpack with Changing Pad - Unisex Design'),
('Stroller for Infants and Toddlers, Lightweight'),
('Car Seat for Newborn, Rear-Facing, Extra Safe'),
('Baby Food Maker, Steamer and Blender Combo'),
('Toddler Sippy Cup, Spill-Proof, BPA-Free'),
('Educational Toys for 6-Month-Old Baby, Colorful Blocks'),
('Baby Clothes Set - 3 Pack, Cotton, 0-3 Months'),
('High Chair for Baby, Adjustable Height, Easy to Clean'),
('Baby Carrier Wrap, Ergonomic Design for Newborns'),
('Nursing Pillow for Breastfeeding, Machine Washable Cover'),
('Baby Bath Tub, Non-Slip, for Newborn and Infant'),
('Baby Skincare Products - Lotion, Shampoo, Wash - Organic');
針對嵌入,我使用了SentenceTransformer模型(又名SBERT)以生成嵌入,然后將它們存儲在數(shù)據(jù)庫中。下面的Python代碼演示了這一點:
SQL
descriptions = [product[1] for product in products]
model = SentenceTransformer("all-MiniLM-L6-v2")
embeddings = model.encode(descriptions)
# Update the database with embeddings
for i, product in enumerate(products):
product_id = product[0]
embedding = embeddings[i] # Convert to Python list
# Construct the vector string representation
embedding_str = str(embedding.tolist())
cur.execute("UPDATE products SET embedding = %s WHERE id = %s", (embedding_str, product_id))
# Commit changes and close connection
conn.commit()
全文搜索
Postgres為關(guān)鍵字搜索提供了廣泛的開箱即用支持。我們可以為基于關(guān)鍵字的檢索編寫如下查詢:
假設(shè)我們想要搜索嬰兒睡眠用品。我們可以使用以下查詢進(jìn)行搜索:
SQL
SELECT id, description
FROM products
WHERE description @@ to_tsquery('english', 'crib | baby | bed');
這將返回以下產(chǎn)品:
SQL
"Soft Crib Sheet for Newborn, Hypoallergenic"
注意:ts_query搜索詞素/標(biāo)準(zhǔn)化關(guān)鍵字,因此用newborns或babies替換newborn也會返回相同的結(jié)果。
當(dāng)然,上面只是一個簡單的例子,Postgres的全文搜索功能允許我們進(jìn)行一番定制,比如跳過某些單詞、處理同義詞、使用復(fù)雜的解析等,通過覆蓋默認(rèn)的文本搜索配置來實現(xiàn)。
雖然這些查詢在沒有索引的情況下也可以工作,但大多數(shù)應(yīng)用程序發(fā)現(xiàn)這種方法太慢了,可能除了偶爾的臨時搜索之外。文本搜索的實際應(yīng)用通常需要創(chuàng)建索引。下面的代碼演示了如何針對description列創(chuàng)建GIN索引(廣義倒排索引),并使用它進(jìn)行高效搜索。
SQL
--Create a tsvector column (you can add this to your existing table)
ALTER TABLE products ADD COLUMN description_tsv tsvector;
--Update the tsvector column with indexed data from the description column
UPDATE products SET description_tsv = to_tsvector('english', description);
-- Create a GIN index on the tsvector column
CREATE INDEX idx_products_description_tsv ON products USING gin(description_tsv);
語義搜索示例
現(xiàn)在不妨嘗試為我們的查詢意圖(“嬰兒睡眠用品”)執(zhí)行語義搜索請求。為此,我們計算嵌入(如上所述),并根據(jù)向量距離(在本例中為余弦距離)選擇最相似的產(chǎn)品。下面的代碼演示了這一點:
Python
# The query string
query_string = 'baby sleeping accessories'
# Generate embedding for the query string
query_embedding = model.encode(query_string).tolist()
# Construct the SQL query using the cosine similarity operator (<->)
# Assuming you have an index that supports cosine similarity (e.g., ivfflat with vector_cosine_ops)
sql_query = """
SELECT id, description, (embedding <-> %s::vector) as similarity
FROM products
ORDER BY similarity
LIMIT 5;
"""
# Execute the query
cur.execute(sql_query, (query_embedding,))
# Fetch and print the results
results = cur.fetchall()
for result in results:
product_id, description, similarity = result
print(f"ID: {product_id}, Description: {description}, Similarity: {similarity}")
cur.close()
conn.close()
這給了我們以下結(jié)果:
Plain Text
ID: 12, Description: Baby Carrier Wrap, Ergonomic Design for Newborns, Similarity: 0.9956936200879117
ID: 2, Description: Soft Crib Sheet for Newborn, Hypoallergenic, Similarity: 1.0233573590998544
ID: 5, Description: Stroller for Infants and Toddlers, Lightweight, Similarity: 1.078171715208051
ID: 6, Description: Car Seat for Newborn, Rear-Facing, Extra Safe, Similarity: 1.08259154868697
ID: 3, Description: Baby Monitor with Night Vision and Two-Way Audio, Similarity: 1.0902734271784085
除了每個結(jié)果外,我們還返回了相似性(就余弦相似性而言越低越好)。正如我們所見,通過嵌入搜索,我們得到了更豐富的結(jié)果集,這很好地補(bǔ)充了基于關(guān)鍵字的搜索。
默認(rèn)情況下,pgvector執(zhí)行精確的最近鄰搜索,保證完美的召回。然而,隨著數(shù)據(jù)集大小增加,這種方法的成本相當(dāng)高。我們可以添加一個索引,以召回換取速度。一個例子是Postgres中的IVFFlat(倒置文件與平面壓縮)索引,其工作原理是,使用k-means聚類將向量空間劃分為簇類。在搜索期間,它識別最接近查詢向量的簇類,并在這些選定的簇類中執(zhí)行線性掃描,計算查詢向量與這些簇類中向量之間的精確距離。下面的代碼定義了如何創(chuàng)建這樣一個索引:
SQL
CREATE INDEX ON products USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
lists indicates the number of clusters to create.
vector_cosine_ops indicates the distance metric we are using (cosine, inner product, or Euclidean/L2)
結(jié)果融合
上述兩種方法在不同的場景中表現(xiàn)出色,并相輔相成。將兩種方法的結(jié)果結(jié)合起來有望得到穩(wěn)健的搜索結(jié)果。倒數(shù)排序融合(RRF)是一種將多個具有不同相關(guān)指標(biāo)的結(jié)果集組合成單個結(jié)果集的方法。RRF不需要調(diào)優(yōu),不同的相關(guān)指標(biāo)也沒必要相互關(guān)聯(lián)才能獲得高質(zhì)量的結(jié)果。RRF的核心體現(xiàn)在其公式中:
Mathematica
RRF(d) = (r R) 1 / k + r(d))
其中
- d 是文檔
- R 是排序器(檢索器)集
- k 是常數(shù)(通常是60)
- r(d) 是排序器(r)中的文檔(d)排序
在我們的例子中,我們將這樣做:
1. 通過在添加一個常數(shù)后取其排序的倒數(shù)來計算每個結(jié)果集中每個產(chǎn)品的排序。這個常數(shù)可以防止排名靠前的產(chǎn)品主導(dǎo)最終得分,并允許排名較低的產(chǎn)品做出有意義的貢獻(xiàn)。
2. 對來自所有結(jié)果集的排序倒數(shù)求和,以獲得產(chǎn)品的最終RRF分?jǐn)?shù)。
針對關(guān)鍵字搜索,Postgres提供了一個排序函數(shù)ts_rank(和一些變體),它可以用作結(jié)果集中產(chǎn)品的排序。針對語義搜索,我們可以使用嵌入距離來計算結(jié)果集中產(chǎn)品的排序。它可以用SQL來實現(xiàn),使用每種搜索方法的CTE,最后將它們組合起來。
此外,我們還可以在合并后使用機(jī)器學(xué)習(xí)模型對結(jié)果重新排序。由于計算成本高,在初始檢索后運(yùn)用基于機(jī)器學(xué)習(xí)模型的重新排序,將結(jié)果集縮減到一小部分有希望的候選對象。
結(jié)論
借助上述組件,我們構(gòu)建了一個智能搜索管道,它集成了以下部分:
- 全文搜索,面向精確的關(guān)鍵字匹配
- 向量搜索,面向語義匹配
- 結(jié)果融合,使用機(jī)器學(xué)習(xí)結(jié)合結(jié)果和重新排序
我們通過使用存儲所有數(shù)據(jù)的單一數(shù)據(jù)庫系統(tǒng)來做到這一點。由于避免了與單獨(dú)的搜索引擎或數(shù)據(jù)庫集成,我們就不需要擁有多個技術(shù)堆棧,并降低了系統(tǒng)的復(fù)雜性。
原文標(biāo)題:Hybrid Search Using Postgres DB,作者:Suraj Dharmapuram