现代应用程序通常包含搜索解决方案,使用户能够按需快速访问现有内容。很难想象任何其他功能可以有效地检索此信息,从而使搜索成为大多数应用程序中的基本功能。
同时,即使查询和搜索的需求非常普遍,不同的应用程序也会采用截然不同的方法。
在大多数情况下,公司通过简单地直接查询 OLTP 数据库来保持非常基本的搜索级别。请求可能如下所示: SELECT id, title FROM, entities WHERE, description LIKE '%bow%
。
然而,更常见的是,它们以复杂的、多级的表连接结构表示,这些结构难以阅读、缓慢且原始。它们无法理解上下文,需要进行大量定制,并且很难正确实施。
虽然可以通过物化视图、查询缓存和其他技术来缩短查询执行时间,但增加的复杂性会导致主要更新和一致的搜索结果之间存在相当大的滞后。
原始的基于数据库的搜索解决方案的更有效替代方案可能构成开源搜索引擎,例如 Apache Lucene、Apache Solr、Elasticsearch、Sphinx、MeiliSearch、Typesense 等。
这些在处理复杂查询和使用过滤器方面往往相对更快并且更好。但是一旦将这些搜索引擎与谷歌搜索或 DuckDuckGo 等同类搜索引擎进行比较,就会发现开源解决方案无法构建适当的搜索上下文和查询模式——如果用户提供模糊的搜索请求,它们将无法理解查询。
想象一下,您根本不记得那种带有酸味的黄色柑橘类水果的名字!但是您想在该应用程序中搜索有关如何种植这种神秘水果的文章。你如何进行搜索?
您的问题可能是“如何在室内种植黄酸柑橘”。上述任何开源搜索引擎都可能很难返回此查询的相关结果,即使数据库确实包含有关种植“柠檬”的文章。
这是因为从查询中提取意义是一项自然语言任务,没有 AI 组件就不太可能解决。 GPT-3擅长这项任务。
OpenAI 提供
文档文本到矢量代表的转换可以在后台发生,而搜索查询的矢量化应该在运行时发生。 OpenAI 提供了几个 GPT-3 模型系列:
text-search-ada-doc-001: 1024 text-search-babbage-doc-001: 2048 text-search-curie-doc-001: 4096 text-search-davinci-doc-001: 12288
更高的向量维度会导致更多的嵌入信息,因此也会导致更高的成本和更慢的搜索。
文档通常很长,查询通常很短且不完整。因此,考虑到内容的密度和大小,任何文档的矢量化都与任何查询的矢量化有很大不同。 OpenAI 知道这一点,因此他们提供了两个配对模型, -doc
和-query
:
text-search-ada-query-001: 1024 text-search-babbage-query-001: 2048 text-search-curie-queryc-001: 4096 text-search-davinci-query-001: 12288
重要的是要注意查询和文档必须都使用相同的模型系列并且具有相同的输出向量长度。
通过示例可能最容易观察和理解此搜索解决方案的强大功能。对于这个例子,让我们借鉴
数据集包含大量列,但我们的矢量化过程将仅围绕标题和概述列构建。
Title: Harry Potter and the Half-Blood Prince Overview: As Harry begins his sixth year at Hogwarts, he discovers an old book marked as 'Property of the Half-Blood Prince', and begins to learn more about Lord Voldemort's dark past.
让我们将数据集映射到准备好索引的文本中:
datafile_path = "./tmdb_5000_movies.csv" df = pd.read_csv(datafile_path) def combined_info(row): columns = ['title', 'overview'] columns_to_join = [f"{column.capitalize()}: {row[column]}" for column in columns] return '\n'.join(columns_to_join) df['combined_info'] = df.apply(lambda row: combined_info(row), axis=1)
嵌入过程很简单:
def get_embedding(text, model="text-search-babbage-doc-001"): text = text.replace("\n", " ") return openai.Embedding.create(input = [text], model=model)['data'][0]['embedding'] get_embedding(df['combined_info'][0])
此代码块输出一个列表,其大小等于模型正在运行的参数,在text-search-babbage-doc-001
情况下为 2048。
类似的嵌入过程应该应用于我们想要搜索的所有文档:
df['combined_info_search'] = df['combined_info'].apply(lambda x: get_embedding(x, model='text-search-babbage-doc-001')) df.to_csv('./tmdb_5000_movies_search.csv', index=False)
列combined_info_search
将保存 combined_text 的矢量表示。
而且,令人惊讶的是,已经是这样了!最后,我们准备执行示例搜索查询:
from openai.embeddings_utils import get_embedding, cosine_similarity def search_movies(df, query, n=3, pprint=True): embedding = get_embedding( query, engine="text-search-babbage-query-001" ) df["similarities"] = df.combined_info_search.apply(lambda x: cosine_similarity(x, embedding)) res = ( df.sort_values("similarities", ascending=False) .head(n) .combined_info ) if pprint: for r in res: print(r[:200]) print() return res res = search_movies(df, "movie about the wizardry school", n=3)
Title: Harry Potter and the Philosopher's StoneOverview: Harry Potter has lived under the stairs at his aunt and uncle's house his whole life. But on his 11th birthday, he learns he's a powerful wizard — with a place waiting for him at the Hogwarts School of Witchcraft and Wizardry. As he learns to harness his newfound powers with the help of the school's kindly headmaster, Harry uncovers the truth about his parents' deaths — and about the villain who's to blame. Title: Harry Potter and the Goblet of FireOverview: Harry starts his fourth year at Hogwarts, competes in the treacherous Triwizard Tournament and faces the evil Lord Voldemort. Ron and Hermione help Harry manage the pressure — but Voldemort lurks, awaiting his chance to destroy Harry and all that he stands for. Title: Harry Potter and the Prisoner of AzkabanOverview: Harry, Ron and Hermione return to Hogwarts for another magic-filled year. Harry comes face to face with danger yet again, this time in the form of an escaped convict, Sirius Black — and turns to sympathetic Professor Lupin for help.
“ Harry Potter and the Philosopher's Stone ”的概述包含词“wizardry”和“school”,并且在搜索输出中排在第一位。第二个结果不再包含“学校”一词,但仍保留接近“巫师”、“三强争霸”的词。第三个结果只包含“巫术”的同义词——魔法。
当然,此数据库中还有许多其他电影以学校或巫师(或两者)为特色,但以上是唯一返回给我们的电影。这清楚地证明了搜索解决方案有效并且确实理解了我们查询的上下文。
我们使用了只有 2048 个参数的 Babbage 模型。 Davinci 有六倍多 (12,288) 个参数,因此在高度复杂的查询方面可以表现得更好。
搜索解决方案有时可能无法生成与某些查询相关的输出。例如,查询“关于学校巫师的电影”产生:
Title: Harry Potter and the Philosopher's StoneOverview: Harry Potter has lived under the stairs at his aunt and uncle's house his whole life. But on his 11th birthday, he learns he's a powerful wizard — with a place waiting for him at the Hogwarts School of Witchcraft and Wizardry. As he learns to harness his newfound powers with the help of the school's kindly headmaster, Harry uncovers the truth about his parents' deaths — and about the villain who's to blame. Title: Dumb and Dumberer: When Harry Met LloydOverview: This wacky prequel to the 1994 blockbuster goes back to the lame-brained Harry and Lloyd's days as classmates at a Rhode Island high school, where the unprincipled principal puts the pair in remedial courses as part of a scheme to fleece the school. Title: Harry Potter and the Prisoner of AzkabanOverview: Harry, Ron and Hermione return to Hogwarts for another magic-filled year. Harry comes face to face with danger yet again, this time in the form of an escaped convict, Sirius Black — and turns to sympathetic Professor Lupin for help.
您可能想知道“阿呆与笨蛋:当哈利遇见劳埃德”在这里做什么?值得庆幸的是,这个问题没有在参数更多的参数上重现。
搜索输出应包含按相关性降序排列的文档。为此,我们应该知道当前查询与每个文档之间的距离。长度越短,输出的相关性就越高。然后,在定义的最大范围之后,我们应该停止考虑剩余文档的相关性。
在上述示例中,我们使用了
距离计算算法倾向于用单个数字表示查询和文档之间的这种相似性(差异)。然而,我们不能依赖
如果愿意,您可以在此处查看生成的存储库:
或者,您可以在此处的Google Colab 中使用它。
我们使用蛮力方法对文档进行排序。让我们确定:
● n:训练数据集中的点数
● d:数据维度
蛮力解决方案的搜索时间复杂度为O(n * d * n * log(n)) 。参数d取决于模型(在 Babbage 的情况下,它等于 2048),而由于排序步骤,我们有O(nlog(n))块。
在这个阶段提醒我们自己,更小的模型更快、更便宜是很重要的。例如,在搜索案例相似度计算步骤中,Ada 模型快了两倍,而 Davinci 模型慢了六倍。
在我的 M1 Pro 上,查询和 2048 个维度的 4803 个文档之间的余弦相似度计算花费了 1260 毫秒。在当前的实现中,计算所需的时间将随着文档总数线性增长。同时,该方法支持计算并行化。
在搜索解决方案中,查询应该尽快完成。而这个代价通常是在训练和预缓存时间方面付出的。我们可以使用像 kd 树、r 树或 ball 树这样的数据结构。考虑来自的文章
Kd 树、ball 树和 r 树构成了用于存储和高效搜索 N 维空间中的点的数据结构,例如我们的意义向量。
Kd 和球树是基于树的数据结构,它们使用迭代的二进制分区方案将空间划分为区域,树中的每个节点代表一个子区域。 Kd 树在搜索特定范围内的点或找到给定点的最近邻居时特别有效。
类似地,r 树也用于在 N 维空间中存储点,但是,它们在特定区域内搜索点或查找给定点一定距离内的所有点时效率更高。重要的是,r-trees 使用与 kd 树和 ball 树不同的分区方案;他们将空间划分为“矩形”而不是二进制分区。
树的实现不在本文讨论范围之内,不同的实现将导致不同的搜索输出。
也许,当前搜索解决方案最显着的缺点是我们必须调用外部 OpenAI API 来检索查询嵌入向量。无论我们的算法能够多快地找到最近的邻居,都需要一个顺序阻塞步骤。
Text-search-babbage-query-001 Number of dimensions: 2048 Number of queries: 100 Average duration: 225ms Median duration: 207ms Max duration: 1301ms Min duration: 176ms
Text-search-ada-query-002 Number of dimensions: 1536 Number of queries: 100 Average duration: 264ms Median duration: 250ms Max duration: 859ms Min duration: 215ms
Text-search-davinci-query-001 Number of dimensions: 12288 Number of queries: 100 Average duration: 379ms Median duration: 364ms Max duration: 1161ms Min duration: 271ms
如果我们以中位数作为参考点,我们可以看到 ada-002 慢了 +28%,而 davinci-001 慢了 +76%。
参考
此外,OpenAI 的培训成本相对较高。
或者,您可以考虑尝试
Elasticsearch 8.0 支持高效的近似最近邻搜索 (ANN),可用于比任何线性 KNN 更快地解决我们的问题量级。 Elasticsearch 8.0 利用称为分层导航小世界图 (HNSW) 的 ANN 算法,根据相似性将向量组织成图。在包含 1000 万个嵌入向量的数据集上进行测试,我们在一台专注于计算的机器上使用 ANN 实现了每秒 200 次查询的惊人性能,而使用 KNN 每秒仅进行 2 次查询。两者均由 Elasticsearch 提供。
根据 ElasticSearch 的
正如您希望看到的那样,GPT-3 嵌入并不是解决任何和所有搜索问题的完美解决方案,因为它的索引复杂性、成本以及搜索操作(甚至是近似值)的高计算复杂性。尽管如此,GPT-3 Embeddings 仍然是任何为具有不频繁查询和适度索引要求的搜索解决方案寻找强大主干的人的绝佳选择。
此外,还值得补充的是,微软最近 __宣布__ Bing 搜索引擎现在使用 GPT 3.5 的新升级版本,称为“Prometheus”,最初是为搜索开发的。根据公告,新的 Prometheus 语言模型允许 Bing 增加相关性、更准确地注释片段、提供更新鲜的结果、理解地理定位并提高安全性。这可能会为将自回归语言模型用于搜索解决方案开辟新的可能性,我们一定会继续关注这一点。
参考:
也发布在这里。