譯者 | 布加迪
審校 | 重樓
使用傳統(tǒng)的基于詞匯(或基于關(guān)鍵字)的搜索,我們可以找到含有我們搜索的確切單詞的文檔。關(guān)鍵詞搜索在準(zhǔn)確性方面表現(xiàn)出色,但在替代詞語(yǔ)或自然語(yǔ)言方面表現(xiàn)差強(qiáng)人意。
語(yǔ)義搜索通過(guò)捕獲文檔和用戶查詢背后的意圖來(lái)克服這些限制。這通常通過(guò)利用向量嵌入將文檔和查詢映射到高維空間,并計(jì)算向量相似性以檢索相關(guān)結(jié)果來(lái)實(shí)現(xiàn)。
針對(duì)幾種系統(tǒng),單一的搜索方法可能會(huì)失敗,導(dǎo)致向用戶顯示不完整的信息。結(jié)合上述兩種搜索方法的優(yōu)勢(shì)將使我們能夠提供出色的搜索體驗(yàn)。
Elasticsearch和Apache Solr等系統(tǒng)都很好地支持基于關(guān)鍵字的搜索。語(yǔ)義搜索通常需要使用向量數(shù)據(jù)庫(kù)進(jìn)行存儲(chǔ),市面上有多種解決方案。這篇文章解釋了我們?nèi)绾卧赑ostgres中使用單單一個(gè)熟悉的存儲(chǔ)系統(tǒng)來(lái)支持包括詞匯搜索和語(yǔ)義搜索的混合搜索。
假設(shè)我們有一個(gè)應(yīng)用程序使用下面的表,允許用戶通過(guò)關(guān)鍵字或自然語(yǔ)言搜索產(chǎn)品:
SQL
CREATE TABLE products (
id bigserial PRIMARY KEY,
description VARCHAR(255),
embedding vector(384)
);
description列包含產(chǎn)品的文本/自然語(yǔ)言描述。Postgres在該列上為全文搜索提供了默認(rèn)索引,但是我們也可以創(chuàng)建自定義索引以加速全文搜索,其作用類(lèi)似信息檢索的索引。
embedding列存儲(chǔ)產(chǎn)品描述的向量(浮點(diǎn))表示,捕獲語(yǔ)義含義而不是單詞。Postgres中的pgvector擴(kuò)展帶來(lái)了向量數(shù)據(jù)類(lèi)型和向量相似性度量指標(biāo):L2、余弦和點(diǎn)積距離。有幾種方法可以生成嵌入,比如使用詞級(jí)嵌入(如Word2Vec)、句子/文檔嵌入(如SBERT)或者來(lái)自基于Transformer的模型(如BERT模型)的嵌入。
為了演示,我們將在數(shù)據(jù)庫(kù)中插入以下數(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');
針對(duì)嵌入,我使用了SentenceTransformer模型(又名SBERT)以生成嵌入,然后將它們存儲(chǔ)在數(shù)據(jù)庫(kù)中。下面的Python代碼演示了這一點(diǎn):
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)鍵字搜索提供了廣泛的開(kāi)箱即用支持。我們可以為基于關(guān)鍵字的檢索編寫(xiě)如下查詢:
假設(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也會(huì)返回相同的結(jié)果。
當(dāng)然,上面只是一個(gè)簡(jiǎn)單的例子,Postgres的全文搜索功能允許我們進(jìn)行一番定制,比如跳過(guò)某些單詞、處理同義詞、使用復(fù)雜的解析等,通過(guò)覆蓋默認(rèn)的文本搜索配置來(lái)實(shí)現(xiàn)。
雖然這些查詢?cè)跊](méi)有索引的情況下也可以工作,但大多數(shù)應(yīng)用程序發(fā)現(xiàn)這種方法太慢了,可能除了偶爾的臨時(shí)搜索之外。文本搜索的實(shí)際應(yīng)用通常需要?jiǎng)?chuàng)建索引。下面的代碼演示了如何針對(duì)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);
語(yǔ)義搜索示例
現(xiàn)在不妨嘗試為我們的查詢意圖(“嬰兒睡眠用品”)執(zhí)行語(yǔ)義搜索請(qǐng)求。為此,我們計(jì)算嵌入(如上所述),并根據(jù)向量距離(在本例中為余弦距離)選擇最相似的產(chǎn)品。下面的代碼演示了這一點(diǎ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
除了每個(gè)結(jié)果外,我們還返回了相似性(就余弦相似性而言越低越好)。正如我們所見(jiàn),通過(guò)嵌入搜索,我們得到了更豐富的結(jié)果集,這很好地補(bǔ)充了基于關(guān)鍵字的搜索。
默認(rèn)情況下,pgvector執(zhí)行精確的最近鄰搜索,保證完美的召回。然而,隨著數(shù)據(jù)集大小增加,這種方法的成本相當(dāng)高。我們可以添加一個(gè)索引,以召回換取速度。一個(gè)例子是Postgres中的IVFFlat(倒置文件與平面壓縮)索引,其工作原理是,使用k-means聚類(lèi)將向量空間劃分為簇類(lèi)。在搜索期間,它識(shí)別最接近查詢向量的簇類(lèi),并在這些選定的簇類(lèi)中執(zhí)行線性掃描,計(jì)算查詢向量與這些簇類(lèi)中向量之間的精確距離。下面的代碼定義了如何創(chuàng)建這樣一個(gè)索引:
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é)果融合
上述兩種方法在不同的場(chǎng)景中表現(xiàn)出色,并相輔相成。將兩種方法的結(jié)果結(jié)合起來(lái)有望得到穩(wěn)健的搜索結(jié)果。倒數(shù)排序融合(RRF)是一種將多個(gè)具有不同相關(guān)指標(biāo)的結(jié)果集組合成單個(gè)結(jié)果集的方法。RRF不需要調(diào)優(yōu),不同的相關(guān)指標(biāo)也沒(méi)必要相互關(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. 通過(guò)在添加一個(gè)常數(shù)后取其排序的倒數(shù)來(lái)計(jì)算每個(gè)結(jié)果集中每個(gè)產(chǎn)品的排序。這個(gè)常數(shù)可以防止排名靠前的產(chǎn)品主導(dǎo)最終得分,并允許排名較低的產(chǎn)品做出有意義的貢獻(xiàn)。
2. 對(duì)來(lái)自所有結(jié)果集的排序倒數(shù)求和,以獲得產(chǎn)品的最終RRF分?jǐn)?shù)。
針對(duì)關(guān)鍵字搜索,Postgres提供了一個(gè)排序函數(shù)ts_rank(和一些變體),它可以用作結(jié)果集中產(chǎn)品的排序。針對(duì)語(yǔ)義搜索,我們可以使用嵌入距離來(lái)計(jì)算結(jié)果集中產(chǎn)品的排序。它可以用SQL來(lái)實(shí)現(xiàn),使用每種搜索方法的CTE,最后將它們組合起來(lái)。
此外,我們還可以在合并后使用機(jī)器學(xué)習(xí)模型對(duì)結(jié)果重新排序。由于計(jì)算成本高,在初始檢索后運(yùn)用基于機(jī)器學(xué)習(xí)模型的重新排序,將結(jié)果集縮減到一小部分有希望的候選對(duì)象。
結(jié)論
借助上述組件,我們構(gòu)建了一個(gè)智能搜索管道,它集成了以下部分:
- 全文搜索,面向精確的關(guān)鍵字匹配
- 向量搜索,面向語(yǔ)義匹配
- 結(jié)果融合,使用機(jī)器學(xué)習(xí)結(jié)合結(jié)果和重新排序
我們通過(guò)使用存儲(chǔ)所有數(shù)據(jù)的單一數(shù)據(jù)庫(kù)系統(tǒng)來(lái)做到這一點(diǎn)。由于避免了與單獨(dú)的搜索引擎或數(shù)據(jù)庫(kù)集成,我們就不需要擁有多個(gè)技術(shù)堆棧,并降低了系統(tǒng)的復(fù)雜性。
原文標(biāo)題:Hybrid Search Using Postgres DB,作者:Suraj Dharmapuram