面向 .NET 开发人员的 SQL Server 原生向量搜索
Posted: Sat Jan 25, 2025 10:11 am
Damir Dobric 博士是 DAENET GmbH 的首席执行官兼首席架构师,该公司是 Microsoft 高级专业合作伙伴,专门从事云计算、物联网和人工智能。他是 Microsoft 最有价值专家 (MVP)、区域总监和法兰克福应用科学大学教授。他拥有计算智能博士学位,是国际公认的技术领导者和影响者,专注于新兴技术和创新。您可以在 LinkedIn 上找到有关他的更多信息: 谢谢 Damir!
似乎大多数开发人员自然而然地认为软件开发这一领域属于数学和 Python 开发人员,他们最初开始构建第一个解决方案。然而,.NET 和 C# 为以高度专业的方式在任何平台上构建几乎任何类型的应用程序提供了良好的基础。
在这篇文章中,我们将逐步展示如何在 C#/.NET 中构建利用 SQL Server 本机向量搜索功能的 GenAI 解决方案。
介绍
在生成式人工智能领域,大型语言模型提供不同类型的功能,通常分为完成模型、聊天完成模型和嵌入模型。
完成模型以响应给定的提示而生成文本而闻名。它们旨在根据初始输入继续或完成一段文字,使其适用于各种文本生成任务。
聊天完成模型是为交互式对话量身定制的。这些模型擅长理解对话内容并在对话中做出响应,因此非常适合聊天机器人应用程序、客户服务和虚拟助手。
另一方面,嵌入模型通常不用于对话 所有者/合伙人/股东电子邮件列表 或文本生成。相反,它们会为任何给定的文本块(例如标记、单词、句子或段落)生成高维向量,称为嵌入。这些嵌入可以捕获文本的语义含义,可用于相似性匹配、聚类和检索等任务。在大型语言模型空间中,嵌入向量通常跨越 1536 个维度,每个元素代表 -1.0 到 1.0 范围内的标量值,例如:
这 1536 个值中的每一个都对文本的部分语义信息进行了编码,使得嵌入模型对于搜索、推荐系统、分类、聚类等各种应用至关重要。
为了说明其工作原理,假设您有一张发票文件。从中提取文本后,它可能看起来像这样:
此外,您可能拥有不同语言的相同文档。嵌入的强大之处在于它们以向量的形式进行语义表示。例如,您可以从文档中提取发货地址,并使用嵌入模型查找来自德国的所有发票。
当然,您可以遍历文档,使用 RegEx 提取城市,然后使用预定义列表将它们映射到各自的国家/地区。但如果城市名称拼写错误或使用不同的语言书写怎么办?您会同意这个问题可以解决,但这需要使用多种技术。
然而,嵌入向量提供了更直接的解决方案。为了演示语义的工作原理,可以创建和分析巴黎、里昂、法兰克福和汉堡等城市的嵌入。
如何创建嵌入向量?
例如,要在 C# 中生成嵌入向量,您可以使用 OpenAI 库和以下代码片段:
复制
EmbeddingClient client = new(“The name of the model”, “The OpenAI Key”);
var res = await client.GenerateEmbeddingsAsync(new List<string>() { “Frankfurt” });
ReadOnlyMemory<float> embeddingVector = res.Value.First().ToFloats();
接下来,计算每个城市嵌入与法国、德国(以及其他相关国家)等国家的嵌入向量之间的余弦距离。
计算后会得到这样的结果:汉堡和法兰克福与德国的语义相似度较高(距离较小),而巴黎和里昂与法国的相似度较高。
巴黎和德国:约 0.324
里昂和德国:约0.249
汉堡和德国:约 0.536
法兰克福和德国:约 0.545
由于嵌入的稳健性,即使城市名称拼写有小错误或格式不同,这种方法仍然有效。
现在想象一下,有许多实体需要计算此类关系。在这种情况下,显然可以使用某种向量数据库。否则,就需要重复计算大量文档的嵌入和相似度,这是一个非常缓慢的操作,而且成本可能非常高。请记住,模型是根据处理的标记数量收费的。
为了演示如何使用 SQL Server 的原生向量搜索,我们假设我们有大量不同语言的发票和装运文件(见上文示例)。目标是将所有文件分为两类:发票和装运。
让我们探索如何使用嵌入和 SQL Server 的原生向量搜索来实现这一点。此示例的目的是演示如何使用新的 SQL Server 向量类型。
为了实现这一点,将使用所有必需的本机向量搜索 T-SQL 操作。接下来将描述以下步骤:
本示例演示如何创建向量并将其插入到表中。注意:在运行此示例之前,请确保在架构 test 下创建一个名为 Vectors 的表。
该表将用于本文中描述的所有其他示例。VectorShort 列用于存储 3 维演示向量,而 Vector 列用于存储 1536 维的真实嵌入向量。
以下方法演示了如何将三维向量插入表中。Vector 类型是随原生向量搜索功能引入的新 T-SQL 类型。它在内部以二进制格式存储,但未记录在案。但是,可以使用以下 T-SQL 语句将向量 [1, 2, 3] 的浮点数组的 JSON 格式字符串转换为 Vector 类型:
设置 SQL 类型 Vector 的值的另一种类似方法是直接将向量作为 JSON 字符串提供:
到目前为止,我们已经了解了如何创建向量和嵌入以及如何将它们存储在表中。要从表中读取向量,可以使用以下 T-SQL 命令:
以下方法说明了如何在 C# 中使用 DataReader 执行此操作:
参数howMany指定应返回多少个最佳匹配(最近的向量)。第二个参数text是搜索中使用的文本块。该方法的任务是在Vectors表中查找与给定文本在语义上最相似的文本。
计算多维向量之间的距离是一项计算复杂的操作,会产生成本。比较大量多维向量对于 SQL Server 来说不是一件容易的事,表中的向量数量直接影响应用程序的性能。下图显示了返回给定嵌入向量的前 10 个最近向量(最相似)的执行时间。
图像 SQL 矢量搜索性能图
本实验中使用的 T-SQL 命令在 C# 中执行,返回 ID 和距离。实验在或多或少商品 SKU Azure SQL Server S1 20 DTU 上执行。
结果表明,随着嵌入表中行数的增加,查找时间几乎呈线性增加。查找 10,000 行需要 5 毫秒,查找 100,000 行需要近 2 秒。请不要将这些数字视为 100% 准确。它们取决于许多因素。但请记住,行数的增加会直接影响性能。计算表中 50 万行的距离将需要几分钟。此博客文章简要介绍了一种解决此问题的有趣方法:
为了展示语义匹配的强大功能,我们将对发票和交货单等文档进行分类,如前所述。假设两种类型的所有已知文档都已编入索引(与其相关嵌入一起存储在表中),我们将实施语义搜索来比较相似性。
例如,为了对发票进行分类,我们需要准备一个代表性的文本块,如“发票总项”,该文本块与存储在 SQL 服务器中的发票文档语料库的语义最匹配。或者,可以使用与存储的发票文档的语义上下文一致的任何其他文本。此文本也可以是任何其他语言。
接下来,应该调用方法 GetMatching:
结果,您将获得一个距离得分列表,您应该将其与所有文档类型进行比较。如果您比较给定文本(“发票总项”)的距离结果,您会发现所有发票的距离得分都大约小于 0.5。所有其他文档类型的距离都将大于 0.5。请注意,此阈值不必与您的用例完全匹配,但它应该可以让您了解分类的工作原理。
似乎大多数开发人员自然而然地认为软件开发这一领域属于数学和 Python 开发人员,他们最初开始构建第一个解决方案。然而,.NET 和 C# 为以高度专业的方式在任何平台上构建几乎任何类型的应用程序提供了良好的基础。
在这篇文章中,我们将逐步展示如何在 C#/.NET 中构建利用 SQL Server 本机向量搜索功能的 GenAI 解决方案。
介绍
在生成式人工智能领域,大型语言模型提供不同类型的功能,通常分为完成模型、聊天完成模型和嵌入模型。
完成模型以响应给定的提示而生成文本而闻名。它们旨在根据初始输入继续或完成一段文字,使其适用于各种文本生成任务。
聊天完成模型是为交互式对话量身定制的。这些模型擅长理解对话内容并在对话中做出响应,因此非常适合聊天机器人应用程序、客户服务和虚拟助手。
另一方面,嵌入模型通常不用于对话 所有者/合伙人/股东电子邮件列表 或文本生成。相反,它们会为任何给定的文本块(例如标记、单词、句子或段落)生成高维向量,称为嵌入。这些嵌入可以捕获文本的语义含义,可用于相似性匹配、聚类和检索等任务。在大型语言模型空间中,嵌入向量通常跨越 1536 个维度,每个元素代表 -1.0 到 1.0 范围内的标量值,例如:
这 1536 个值中的每一个都对文本的部分语义信息进行了编码,使得嵌入模型对于搜索、推荐系统、分类、聚类等各种应用至关重要。
为了说明其工作原理,假设您有一张发票文件。从中提取文本后,它可能看起来像这样:
此外,您可能拥有不同语言的相同文档。嵌入的强大之处在于它们以向量的形式进行语义表示。例如,您可以从文档中提取发货地址,并使用嵌入模型查找来自德国的所有发票。
当然,您可以遍历文档,使用 RegEx 提取城市,然后使用预定义列表将它们映射到各自的国家/地区。但如果城市名称拼写错误或使用不同的语言书写怎么办?您会同意这个问题可以解决,但这需要使用多种技术。
然而,嵌入向量提供了更直接的解决方案。为了演示语义的工作原理,可以创建和分析巴黎、里昂、法兰克福和汉堡等城市的嵌入。
如何创建嵌入向量?
例如,要在 C# 中生成嵌入向量,您可以使用 OpenAI 库和以下代码片段:
复制
EmbeddingClient client = new(“The name of the model”, “The OpenAI Key”);
var res = await client.GenerateEmbeddingsAsync(new List<string>() { “Frankfurt” });
ReadOnlyMemory<float> embeddingVector = res.Value.First().ToFloats();
接下来,计算每个城市嵌入与法国、德国(以及其他相关国家)等国家的嵌入向量之间的余弦距离。
计算后会得到这样的结果:汉堡和法兰克福与德国的语义相似度较高(距离较小),而巴黎和里昂与法国的相似度较高。
巴黎和德国:约 0.324
里昂和德国:约0.249
汉堡和德国:约 0.536
法兰克福和德国:约 0.545
由于嵌入的稳健性,即使城市名称拼写有小错误或格式不同,这种方法仍然有效。
现在想象一下,有许多实体需要计算此类关系。在这种情况下,显然可以使用某种向量数据库。否则,就需要重复计算大量文档的嵌入和相似度,这是一个非常缓慢的操作,而且成本可能非常高。请记住,模型是根据处理的标记数量收费的。
为了演示如何使用 SQL Server 的原生向量搜索,我们假设我们有大量不同语言的发票和装运文件(见上文示例)。目标是将所有文件分为两类:发票和装运。
让我们探索如何使用嵌入和 SQL Server 的原生向量搜索来实现这一点。此示例的目的是演示如何使用新的 SQL Server 向量类型。
为了实现这一点,将使用所有必需的本机向量搜索 T-SQL 操作。接下来将描述以下步骤:
本示例演示如何创建向量并将其插入到表中。注意:在运行此示例之前,请确保在架构 test 下创建一个名为 Vectors 的表。
该表将用于本文中描述的所有其他示例。VectorShort 列用于存储 3 维演示向量,而 Vector 列用于存储 1536 维的真实嵌入向量。
以下方法演示了如何将三维向量插入表中。Vector 类型是随原生向量搜索功能引入的新 T-SQL 类型。它在内部以二进制格式存储,但未记录在案。但是,可以使用以下 T-SQL 语句将向量 [1, 2, 3] 的浮点数组的 JSON 格式字符串转换为 Vector 类型:
设置 SQL 类型 Vector 的值的另一种类似方法是直接将向量作为 JSON 字符串提供:
到目前为止,我们已经了解了如何创建向量和嵌入以及如何将它们存储在表中。要从表中读取向量,可以使用以下 T-SQL 命令:
以下方法说明了如何在 C# 中使用 DataReader 执行此操作:
参数howMany指定应返回多少个最佳匹配(最近的向量)。第二个参数text是搜索中使用的文本块。该方法的任务是在Vectors表中查找与给定文本在语义上最相似的文本。
计算多维向量之间的距离是一项计算复杂的操作,会产生成本。比较大量多维向量对于 SQL Server 来说不是一件容易的事,表中的向量数量直接影响应用程序的性能。下图显示了返回给定嵌入向量的前 10 个最近向量(最相似)的执行时间。
图像 SQL 矢量搜索性能图
本实验中使用的 T-SQL 命令在 C# 中执行,返回 ID 和距离。实验在或多或少商品 SKU Azure SQL Server S1 20 DTU 上执行。
结果表明,随着嵌入表中行数的增加,查找时间几乎呈线性增加。查找 10,000 行需要 5 毫秒,查找 100,000 行需要近 2 秒。请不要将这些数字视为 100% 准确。它们取决于许多因素。但请记住,行数的增加会直接影响性能。计算表中 50 万行的距离将需要几分钟。此博客文章简要介绍了一种解决此问题的有趣方法:
为了展示语义匹配的强大功能,我们将对发票和交货单等文档进行分类,如前所述。假设两种类型的所有已知文档都已编入索引(与其相关嵌入一起存储在表中),我们将实施语义搜索来比较相似性。
例如,为了对发票进行分类,我们需要准备一个代表性的文本块,如“发票总项”,该文本块与存储在 SQL 服务器中的发票文档语料库的语义最匹配。或者,可以使用与存储的发票文档的语义上下文一致的任何其他文本。此文本也可以是任何其他语言。
接下来,应该调用方法 GetMatching:
结果,您将获得一个距离得分列表,您应该将其与所有文档类型进行比较。如果您比较给定文本(“发票总项”)的距离结果,您会发现所有发票的距离得分都大约小于 0.5。所有其他文档类型的距离都将大于 0.5。请注意,此阈值不必与您的用例完全匹配,但它应该可以让您了解分类的工作原理。