语义搜索是基于自然语言处理(NLP)最新进展的新类别。传统搜索系统使用关键字查找数据。语义搜索对自然语言有了解,并确定具有相同含义的结果,不一定是相同的关键字。
虽然语义搜索增加了惊人的功能,但稀疏关键字索引仍然可以增加值。在某些情况下,找到确切匹配很重要,或者我们只希望快速索引快速进行数据集的初始扫描。
两种方法都有其优点。如果我们将它们结合在一起以构建统一的hybrid
搜索功能怎么办?我们可以两全其美吗?
本文将探讨混合搜索的好处。
安装依赖项
安装txtai
和所有依赖项。
# Install txtai
pip install txtai pytrec_eval rank-bm25 elasticsearch
引入语义,关键字和混合搜索
在潜入基准之前,让我们简要讨论语义和关键字搜索的工作原理。
语义搜索使用大型语言模型将输入量化为数字数组。类似的概念将具有相似的值。向量通常存储在矢量数据库中,该数据库是一个专门用于存储这些数值和查找匹配的系统。向量搜索将输入查询转换为向量,然后运行搜索以找到最佳的概念结果。
关键字搜索将文本标记为每个文档的令牌列表。这些令牌汇总到每个文档的令牌频率中,并以期限稀疏阵列存储。在搜索时间,查询被令牌化,并将查询的令牌与数据集中的令牌进行了比较。这是一个字面的过程。关键字搜索就像字符串匹配一样,它没有概念上的理解,它匹配字符和字节。
混合搜索结合了语义和关键字索引的分数。鉴于语义搜索分数通常为0-1,并且关键字搜索分数是无限的,因此需要一种方法来结合结果。
TXTAI支持的两种方法是:
- Convex Combination当稀疏分数归一化 时
- Reciprocal Rank Fusion (RRF)当稀疏分数未归一化 时
TXTAI中的默认方法是凸组合,我们将使用它。
评估性能
现在是时候对结果进行基准测试了。对于这些测试,我们将使用Beir数据集。我们还将使用TXTAI项目中的benchmarks script。此基准测试脚本具有与Beir DataSet合作的方法。
我们将为简洁选择Beir源的子集。对于每个来源,我们将基准为bm25
索引,embeddings
索引和hybrid
或组合索引。
import os
# Get benchmarks script
os.system("wget https://raw.githubusercontent.com/neuml/txtai/master/examples/benchmarks.py")
# Create output directory
os.makedirs("beir", exist_ok=True)
# Download subset of BEIR datasets
datasets = ["nfcorpus", "fiqa", "arguana", "scidocs", "scifact"]
for dataset in datasets:
url = f"https://public.ukp.informatik.tu-darmstadt.de/thakur/BEIR/datasets/{dataset}.zip"
os.system(f"wget {url}")
os.system(f"mv {dataset}.zip beir")
os.system(f"unzip -d beir beir/{dataset}.zip")
# Remove existing benchmark data
if os.path.exists("benchmarks.json"):
os.remove("benchmarks.json")
现在让我们运行基准。
# Remove existing benchmark data
if os.path.exists("benchmarks.json"):
os.remove("benchmarks.json")
# Runs benchmark evaluation
def evaluate(method):
for dataset in datasets:
command = f"python benchmarks.py beir {dataset} {method}"
print(command)
os.system(command)
# Calculate benchmarks
for method in ["bm25", "embed", "hybrid"]:
evaluate(method)
import json
import pandas as pd
def benchmarks():
# Read JSON lines data
with open("benchmarks.json") as f:
data = f.read()
df = pd.read_json(data, lines=True).sort_values(by=["source", "ndcg_cut_10"], ascending=[True, False])
return df[["source", "method", "ndcg_cut_10", "map_cut_10", "recall_10", "P_10", "index", "search", "memory"]].reset_index(drop=True)
# Load benchmarks dataframe
df = benchmarks()
df[df.source == "nfcorpus"].reset_index(drop=True)
来源 | 方法 | ndcg_cut_10 | map_cut_10 | recome_10 | p_10 | 索引 | 搜索 | 记忆 |
---|---|---|---|---|---|---|---|---|
nfcorpus | 混合动力 | 0.34531 | 0.13369 | 0.17437 | 0.25480 | 29.46 | 3.57 | 2900 |
nfcorpus | 嵌入 | 0.30917 | 0.10810 | 0.15327 | 0.23591 | 33.64 | 3.33 | 2876 |
nfcorpus | BM25 td> | 0.30639 | 0.11728 | 0.14891 | 0.21734 | 2.72 | 0.96 | 652 |
df[df.source == "fiqa"].reset_index(drop=True)
来源 | 方法 | ndcg_cut_10 | map_cut_10 | recome_10 | p_10 | 索引 | 搜索 | 记忆 |
---|---|---|---|---|---|---|---|---|
fiqa td> | 混合动力 | 0.36642 | 0.28846 | 0.43799 | 0.10340 | 233.90 | 68.42 | 3073 |
fiqa td> | 嵌入 | 0.36071 | 0.28450 | 0.43188 | 0.10216 | 212.30 | 58.83 | 2924 |
fiqa td> | BM25 td> | 0.23559 | 0.17619 | 0.29855 | 0.06559 | 19.78 | 12.84 | 76 |
df[df.source == "arguana"].reset_index(drop=True)
来源 | 方法 | ndcg_cut_10 | map_cut_10 | recome_10 | p_10 | 索引 | 搜索 | 记忆 |
---|---|---|---|---|---|---|---|---|
arguana | 混合动力 | 0.48467 | 0.40101 | 0.75320 | 0.07532 | 37.80 | 21.22 | 2924 |
arguana | 嵌入 | 0.47781 | 0.38781 | 0.76671 | 0.07667 | 34.11 | 10.21 | 2910 |
arguana | BM25 td> | 0.45713 | 0.37118 | 0.73471 | 0.07347 | 3.39 | 10.95 | 663 |
df[df.source == "scidocs"].reset_index(drop=True)
来源 | 方法 | ndcg_cut_10 | map_cut_10 | recome_10 | p_10 | 索引 | 搜索 | 记忆 |
---|---|---|---|---|---|---|---|---|
Scidocs | 嵌入 | 0.21718 | 0.12982 | 0.23217 | 0.1146 | 127.63 | 4.41 | 2929 |
Scidocs | 混合动力 | 0.21104 | 0.12450 | 0.22938 | 0.1134 | 138.00 | 6.43 | 2999 |
Scidocs | BM25 td> | 0.15063 | 0.08756 | 0.15637 | 0.0772 | 13.07 | 1.42 | 722 |
df[df.source == "scifact"].reset_index(drop=True)
来源 | 方法 | ndcg_cut_10 | map_cut_10 | recome_10 | p_10 | 索引 | 搜索 | 记忆 |
---|---|---|---|---|---|---|---|---|
scifact | 混合动力 | 0.71305 | 0.66773 | 0.83722 | 0.09367 | 39.51 | 2.35 | 2918 |
scifact | BM25 td> | 0.66324 | 0.61764 | 0.78761 | 0.08700 | 4.40 | 0.93 | 658 |
scifact | 嵌入 | 0.65149 | 0.60193 | 0.78972 | 0.08867 | 35.15 | 1.48 | 2889 |
上面的部分显示了每个源和方法的指标。
表标头列出了source (dataset)
,index method
,NDCG@10
/MAP@10
/RECALL@10
/RECALL@10
/P@10
精度指标,index time(s)
,search time(s)
和memory usage(MB)
。表由NDCG@10
降序排序。
查看结果,我们可以看到hybrid
搜索通常比单独的embeddings
或bm25
更好。在某些情况下,与SCIDOC一样,组合的性能较差。但是在总体上,得分更好。这对于整个Beir数据集都是正确的。对于某些来源,bm25
表现最好,有些embeddings
,但总体而言,hybrid
的总分表现最好。
混合搜索不是免费的,它较慢,因为它具有额外的逻辑来结合结果。对于单个查询,结果通常可以忽略不计。
包起来
本笔记本涵盖了使用混合方法提高搜索准确性的方法。我们评估了Beir数据集子集的性能,以显示在许多情况下混合搜索如何提高整体准确性。
也可以使用此方法作为specified in this link来评估自定义数据集。本笔记本和关联的基准测试脚本可以重复使用以评估哪种方法最适合您的数据。