如何使用OpenAI和Weaviate在Python中使用向量数据库进行语义搜索、问题回答和生成式搜索

如果您看到了这篇文章,我假设您已经在使用大型语言模型(LLM)构建应用程序,并遇到了向量数据库这个术语。
使用LLM构建应用程序的工具生态系统正在迅速发展,其中诸如LangChain或LlamaIndex等工具越来越受欢迎。
在最近的一篇文章中,我描述了如何使用LangChain入门,在本文中,我想继续探索LLM工具生态系统,并尝试使用Weaviate。
什么是Weaviate?
Weaviate是一个开源的向量数据库。它使您能够存储数据对象和向量嵌入,并基于相似度度量对它们进行查询。
GitHub – weaviate/weaviate: Weaviate是一个开源的向量数据库,可以同时存储对象和向量
Weaviate是一个开源的向量数据库,可以同时存储对象和向量,允许组合向量搜索
github.com
自从媒体关注LLM以来,向量数据库一直受到广泛关注。在LLM的背景下,向量数据库最受欢迎的用例可能是“为LLM提供长期记忆”。
如果您需要了解向量数据库的概念,请参阅我的上一篇文章:
以3个难度级别解释向量数据库
从新手到专家:揭秘不同背景下的向量数据库
towardsdatascience.com
在本教程中,我们将演示如何使用您的数据集的嵌入来填充Weaviate向量数据库。然后,我们将介绍三种不同的从中检索信息的方法:
- 向量搜索
- 问题回答
- 生成式搜索
先决条件
要参与本教程,您需要具备以下条件:
- Python 3环境
- OpenAI API密钥(或者,Hugging Face、Cohere或PaLM的API密钥)
关于API密钥的说明:在本教程中,我们将通过推理服务(在本例中为OpenAI)从文本生成嵌入。根据您使用的推理服务,确保查看提供商的定价页面,以避免意外费用。例如,使用的Ada模型(版本2)在撰写本教程时每1,000个标记的成本为0.0001美元,并且在本教程的推理费用中不到1美分。
设置
您可以在自己的实例上运行Weaviate(使用Docker、Kubernetes或嵌入式Weaviate),也可以使用Weaviate Cloud Services(WCS)作为托管服务运行Weaviate。对于本教程,我们将使用WCS运行Weaviate实例,因为这是推荐且最简单的方式。
如何使用Weaviate Cloud Services(WCS)创建集群
要使用该服务,您首先需要在WCS上注册。
注册后,您可以通过点击“创建集群”按钮来创建一个新的Weaviate集群。

在本教程中,我们将使用免费试用计划,为您提供一个为期14天的沙箱环境。(您不需要添加任何付款信息。相反,在试用期结束后,沙箱将自动过期。但您可以随时创建一个新的免费试用沙箱。)
在“免费沙箱”选项卡下,进行以下设置:
- 输入群集名称
- 启用身份验证(设置为“是”)

最后,点击“创建”来创建您的沙箱实例。
如何在Python中安装Weaviate
最后一步,使用pip
将weaviate-client
添加到您的Python环境中
$ pip install weaviate-client
并导入库:
import weaviate
如何通过客户端访问Weaviate群集
在下一步中,您将需要以下两个信息来访问您的群集:
- 群集URL
- Weaviate API密钥(在“启用 – 身份验证”下)

现在,您可以实例化一个Weaviate客户端来访问您的Weaviate群集,如下所示。
auth_config = weaviate.AuthApiKey(api_key="YOUR-WEAVIATE-API-KEY") # 替换为您的Weaviate实例API密钥# 实例化客户端client = weaviate.Client( url="https://<your-sandbox-name>.weaviate.network", # 替换为您的Weaviate群集URL auth_client_secret=auth_config, additional_headers={ "X-OpenAI-Api-Key": "YOUR-OPENAI-API-KEY", # 替换为您的OpenAI密钥 })
如您所见,我们将在additional_headers
下使用OpenAI API密钥来访问嵌入模型。如果您使用的是除OpenAI之外的其他提供商,请将密钥参数更改为以下适用之一:X-Cohere-Api-Key
、X-HuggingFace-Api-Key
或X-Palm-Api-Key
。
为了检查是否设置正确,请运行:
client.is_ready()
如果返回True
,则已准备好进行下一步。
如何创建和填充Weaviate向量数据库
现在,我们准备在Weaviate中创建一个向量数据库,并填充一些数据。
在本教程中,我们将使用Kaggle上的200,000+ Jeopardy问题数据集[1]的前100行。
import pandas as pddf = pd.read_csv("your_file_path.csv", nrows = 100)
![开始使用Weaviate:向量数据库搜索入门指南 四海 第5张-四海吧 来自Kaggle的200,000+ Jeopardy问题数据集[1]的前几行。](https://miro.medium.com/v2/resize:fit:640/format:webp/1*PFx77R8i42o_1Zs3PC_HiQ.png)
关于令牌数量和相关成本的说明:在下面的示例中,我们将嵌入前100行的“category”、“question”和“answer”列。根据使用tiktoken库的计算,这将导致大约3000个令牌需要嵌入,这在2023年7月时以OpenAI的Ada模型(版本2)进行推理的成本大约为0.0003美元。
步骤 1:创建模式
首先,我们需要定义底层的数据结构和一些配置:
class
: 这个向量空间中的对象集合将被称为什么?properties
: 对象的属性,包括属性名称和数据类型。在Pandas Dataframe中的类比,这些将成为DataFrame中的列。vectorizer
: 生成嵌入的模型。对于文本对象,您通常会选择text2vec
模块之一(text2vec-cohere
、text2vec-huggingface
、text2vec-openai
或text2vec-palm
),具体根据您使用的提供者来决定。moduleConfig
: 在这里,您可以定义所使用模块的详细信息。例如,嵌入器是一个模块,您可以定义要使用的模型和版本。
class_obj = { # 类定义 "class": "JeopardyQuestion", # 属性定义 "properties": [ { "name": "category", "dataType": ["text"], }, { "name": "question", "dataType": ["text"], }, { "name": "answer", "dataType": ["text"], }, ], # 指定一个嵌入器 "vectorizer": "text2vec-openai", # 模块设置 "moduleConfig": { "text2vec-openai": { "vectorizeClassName": False, "model": "ada", "modelVersion": "002", "type": "text" }, },}
在上面的模式中,您可以看到我们将创建一个名为"JeopardyQuestion"
的类,其中包含三个文本属性"category"
、"question"
和"answer"
。我们使用的嵌入器是OpenAI的Ada模型(版本2)。所有属性都将被嵌入,但类名不会被嵌入("vectorizeClassName" : False
)。如果您有不希望嵌入的属性,您可以指定这一点(请参阅文档)。
一旦您定义了模式,您可以使用create_class()
方法创建类。
client.schema.create_class(class_obj)
要检查类是否已成功创建,您可以按以下方式查看其模式:
client.schema.get("JeopardyQuestion")
创建的模式如下所示:
{ "class": "JeopardyQuestion", "invertedIndexConfig": { "bm25": { "b": 0.75, "k1": 1.2 }, "cleanupIntervalSeconds": 60, "stopwords": { "additions": null, "preset": "en", "removals": null } }, "moduleConfig": { "text2vec-openai": { "model": "ada", "modelVersion": "002", "type": "text", "vectorizeClassName": false } }, "properties": [ { "dataType": [ "text" ], "indexFilterable": true, "indexSearchable": true, "moduleConfig": { "text2vec-openai": { "skip": false, "vectorizePropertyName": false } }, "name": "category", "tokenization": "word" }, { "dataType": [ "text" ], "indexFilterable": true, "indexSearchable": true, "moduleConfig": { "text2vec-openai": { "skip": false, "vectorizePropertyName": false } }, "name": "question", "tokenization": "word" }, { "dataType": [ "text" ], "indexFilterable": true, "indexSearchable": true, "moduleConfig": { "text2vec-openai": { "skip": false, "vectorizePropertyName": false } }, "name": "answer", "tokenization": "word" } ], "replicationConfig": { "factor": 1 }, "shardingConfig": { "virtualPerPhysical": 128, "desiredCount": 1, "actualCount": 1, "desiredVirtualCount": 128, "actualVirtualCount": 128, "key": "_id", "strategy": "hash", "function": "murmur3" }, "vectorIndexConfig": { "skip": false, "cleanupIntervalSeconds": 300, "maxConnections": 64, "efConstruction": 128, "ef": -1, "dynamicEfMin": 100, "dynamicEfMax": 500, "dynamicEfFactor": 8, "vectorCacheMaxObjects": 1000000000000, "flatSearchCutoff": 40000, "distance": "cosine", "pq": { "enabled": false, "bitCompression": false, "segments": 0, "centroids": 256, "encoder": { "type": "kmeans", "distribution": "log-normal" } } }, "vectorIndexType": "hnsw", "vectorizer": "text2vec-openai"}
步骤2:将数据导入Weaviate
在这个阶段,向量数据库具有模式但还是空的。所以,让我们用我们的数据集填充它。这个过程也被称为“upserting”。
我们将以200的批次进行upsert。如果你注意到了,你就会知道这在这里是不必要的,因为我们只有100行数据。但是一旦你准备好upsert更大量的数据,你就会希望以批次进行。这就是为什么我会在这里留下批处理代码的原因:
from weaviate.util import generate_uuid
with client.batch(
batch_size=200, # 指定批次大小
num_workers=2, # 并行化处理
) as batch:
for _, row in df.iterrows():
question_object = {
"category": row.category,
"question": row.question,
"answer": row.answer,
}
batch.add_data_object(
question_object,
class_name="JeopardyQuestion",
uuid=generate_uuid5(question_object)
)
虽然Weaviate会自动生成一个全局唯一标识符(uuid
),但我们会使用generate_uuid5()
函数从question_object
手动生成uuid
,以避免导入重复项。
为了进行合理性检查,你可以使用以下代码片段查看导入的对象数量:
client.query.aggregate("JeopardyQuestion").with_meta_count().do()
{'data': {'Aggregate': {'JeopardyQuestion': [{'meta': {'count': 100}}]}}}
如何查询Weaviate向量数据库
你在向量数据库中最常见的操作是检索对象。要检索对象,你可以使用get()
函数查询Weaviate向量数据库:
client.query.get(
<类名>,
[<属性>]
).<参数>.do()
类名
:指定要检索的对象类的名称。这里是:"JeopardyQuestion"
属性
:指定要检索的对象的属性。这里是一个或多个"category"
,"question"
和"answer"
。参数
:指定用于检索对象的搜索条件,如限制或聚合。我们将在以下示例中介绍其中一些。
让我们使用get()
函数从JeopardyQuestion
类中检索一些条目,看看它们是什么样子。在Pandas类比中,你可以将以下内容视为df.head(2)
。由于get()
函数的响应是JSON格式,所以我们将导入相关的库以以视觉上令人愉悦的格式显示结果。
import json
res = client.query.get("JeopardyQuestion",
["question", "answer", "category"])
.with_additional(["id", "vector"])
.with_limit(2)
.do()
print(json.dumps(res, indent=4))
{ "data": { "Get": { "JeopardyQuestion": [ { "_additional": { "id": "064fee53-f8fd-4513-9294-432170cc9f77", "vector": [ -0.02465364, ...] # 向量被截断以便更好读取 }, "answer": "(Lou) Gehrig", "category": "ESPN's TOP 10 ALL-TIME ATHLETES", "question": "No. 10: FB/LB for Columbia U. in the 1920s; MVP for the Yankees in '27 & '36; \"Gibraltar in Cleats\"" }, { "_additional": { "id": "1041117a-34af-40a4-ad05-3dae840ad6b9", "vector": [ -0.031970825, ...] # 向量被截断以便更好读取 }, "answer": "Jim Thorpe", "category": "ESPN's TOP 10 ALL-TIME ATHLETES", "question": "No. 2: 1912 Olympian; football star at Carlisle Indian School; 6 MLB seasons with the Reds, Giants & Braves" }, ] } }}
在上面的代码片段中,您可以看到我们正在从”JeopardyQuestion”类中检索对象。我们指定了要检索的属性”category”,”question”和”answer”。
我们指定了两个额外的参数:首先,我们使用”.with_additional()”参数指定检索对象的id和向量嵌入的附加信息。然后,我们使用”.with_limit(2)”参数指定只检索两个对象。这个限制很重要,在后面的示例中您会再次看到它。这是因为从向量数据库中检索对象不返回精确匹配,而是根据相似性返回对象,这必须通过阈值进行限制。
向量搜索
现在,我们准备进行一些向量搜索!从向量数据库中检索信息的很酷之处在于,您可以告诉它检索与动物相关的”concepts”的Jeopardy问题。
为此,我们可以使用”.with_near_text()”参数,并将我们感兴趣的”concepts”作为参数传递,如下所示:
res = client.query.get( "JeopardyQuestion", ["question", "answer", "category"])\ .with_near_text({"concepts": "animals"})\ .with_limit(2)\ .do()
指定的向量化器然后将输入文本(“animals”)转换为向量嵌入并检索两个最接近的结果:
{ "data": { "Get": { "JeopardyQuestion": [ { "answer": "an octopus", "category": "SEE & SAY", "question": "Say the name of <a href=\"http://www.j-archive.com/media/2010-07-06_DJ_26.jpg\" target=\"_blank\">this</a> type of mollusk you see" }, { "answer": "the ant", "category": "3-LETTER WORDS", "question": "In the title of an Aesop fable, this insect shared billing with a grasshopper" } ] } }}
您已经可以看到这是多么酷:我们可以看到向量搜索返回了两个答案都是动物的问题,而这两个问题属于完全不同的类别。使用传统的关键字搜索,您首先需要定义一个动物列表,然后检索包含其中一个定义的动物的所有问题。
问答
问答是将LLMs与向量数据库结合的最受欢迎的示例之一。
要启用问答功能,您需要在模块配置下指定一个向量化器(您应该已经有了)和一个问答模块,如下例所示:
# 模块设置 "moduleConfig": { "text2vec-openai": { ... }, "qna-openai": { "model": "text-davinci-002" } },
对于问答,您需要添加”with_ask()”参数,并且还需要检索”_additional”属性。
ask = { "question": "Which animal was mentioned in the title of the Aesop fable?", "properties": ["answer"]}res = ( client.query .get("JeopardyQuestion", [ "question", "_additional {answer {hasAnswer property result} }" ]) .with_ask(ask) .with_limit(1) .do())
上面的代码片段查找可能包含答案”Which animal was mentioned in the title of the Aesop fable?”的所有问题,并返回答案”The ant”。
{ "JeopardyQuestion": [ { "_additional": { "answer": { "hasAnswer": true, "property": "", "result": " The ant" } }, "question": "In the title of an Aesop fable, this insect shared billing with a grasshopper" } ]}
生成式搜索
通过集成LLMs,您还可以在返回搜索结果之前对数据进行转换。这个概念被称为生成式搜索。
要启用生成式搜索,您需要在模块配置下指定一个生成式模块,如下例所示:
# 模块设置 "moduleConfig": { "text2vec-openai": { ... }, "generative-openai": { "model": "gpt-3.5-turbo" } },
对于生成式搜索,您只需要在之前的向量搜索代码中添加with_generate()
参数,如下所示:
res = client.query.get( "JeopardyQuestion", ["question", "answer"])\ .with_near_text({"concepts": ["animals"]})\ .with_limit(1)\ .with_generate(single_prompt= "生成一个问题,其答案为{answer}")\ .do()
上面的代码段执行以下操作:
- 搜索与概念
"animals"
最接近的问题 - 返回问题
"Say the name of this type of mollusk you see"
和答案"an octopus"
- 生成一个针对提示
"生成一个问题,其答案为an octopus"
的补全,最终结果如下:
{ "generate": { "error": null, "singleResult": "What sea creature has eight arms and is known for its intelligence and camouflage abilities?" }}
总结
LLM空间的流行不仅带来了许多有趣的新开发者工具,如LangChain或LLaMaIndex,而且还向我们展示了如何利用已有的工具,如向量数据库,来增强LLM驱动应用的潜力。
在本文中,我们开始使用Weaviate来不仅使用向量数据库进行向量搜索,还结合LLMs进行问答和生成式搜索。
如果您对更深入的介绍感兴趣,我建议查看这个关于向量数据库和Weaviate的全面四部分课程:
1. 从零开始到MVP | Weaviate-向量数据库
课程概述
weaviate.io
免责声明:我是Weaviate的开发者倡导者,在撰写本文时。
喜欢这个故事吗?
免费订阅以在我发布新故事时收到通知。
想要阅读超过3个免费故事吗?-成为VoAGI会员,每月5美元。您可以通过在注册时使用我的推荐链接来支持我。我将不会额外收取费用。
使用我的推荐链接加入VoAGI-Leonie Monigatti
作为VoAGI会员,您会员费的一部分将给到您阅读的作家,并且您将获得对每个故事的完全访问权限…
VoAGI.com
在LinkedIn、Twitter和Kaggle上找到我!
参考文献
数据集
[1] Ulrik Thyge Pedersen (2023). 200.000+ Jeopardy Questions in Kaggle Datasets.
许可证:署名 4.0 国际(CC BY 4.0)
图片来源
除非另有说明,所有图片均由作者创建。
网络与文献
[2] Weaviate (2023). Weaviate documentation (accessed 14. July 2023)