矢量数据库目前在科技界风靡一时,而不仅仅是炒作。由于利用矢量嵌入的人工智能进步,矢量搜索变得越来越重要。这些向量嵌入是单词嵌入、句子或文档的向量表示,只需查看向量之间的距离度量,即可为语义接近的输入提供语义相似性。
word2vec 的规范示例,其中单词“king”的嵌入非常接近单词“queen”、“man”和“woman”的向量的结果向量,当按以下公式排列时:
king - man + woman ≈ queen
事实上,这对我来说总是很神奇,但如果我们的嵌入空间具有足够高的维度,它甚至适用于相当大的文档。使用现代深度学习方法,您可以获得复杂文档的出色嵌入。
对于 TerminusDB,我们需要一种方法来利用这些类型的嵌入来完成用户要求的以下任务:
全文搜索
实体解析(查找可能与重复数据删除相同的其他文档)
相似性搜索(相关内容或推荐系统)
聚类
我们决定使用OpenAI的嵌入进行原型设计,但为了获得其余的功能,我们需要一个矢量数据库。
我们需要一些不寻常的功能,包括执行增量索引的能力,以及索引提交基础的能力,以便我们准确地知道索引适用于什么提交。这使我们能够将索引放入 CI 工作流中。
野外不存在版本化的开源矢量数据库。所以我们写了一个!
编写矢量数据库
向量数据库是向量的存储,能够使用某些指标比较任意两个向量。度量可以是很多不同的东西,例如欧几里得距离、余弦相似性、出租车几何,或者任何遵守定义度量空间所需的三角形不等式规则的东西。
为了快速做到这一点,您需要有某种索引结构来快速找到已经接近比较的候选人。否则,许多操作每次都需要与数据库中的每个内容进行比较。
有许多方法可以索引向量空间,但我们使用了HNSW(分层可导航小世界)图(参见Malkov和Yashunin)。HNSW易于理解,在低尺寸和高尺寸上都具有良好的性能,因此具有灵活性。最重要的是,我们发现了一个非常清晰的开源实现 - 用于 Rust 计算机视觉的 HNSW。
存储向量
向量存储在域中。这有助于分离不需要描述相同载体的不同载体存储。对于TerminusDB,我们有许多不同的提交,它们都与相同的向量有关,因此将它们全部放入同一域中非常重要。
矢量存储是基于页面的,其中每个缓冲区都设计为清晰地映射到操作系统页面,但适合我们紧密使用的矢量。我们为每个向量分配一个索引,然后我们可以从索引映射到适当的页面和偏移量。
在 HNSW 索引中,我们指的是 .这可确保页面位于当前加载的缓冲区中,以便我们可以对感兴趣的向量执行指标比较。LoadedVec
一旦最后一个缓冲区从缓冲区中删除,就可以将缓冲区添加回缓冲池中,以用于加载新页面。LoadedVec
创建版本化索引
我们为每个(域+提交)对构建一个HNSW结构。如果开始一个新索引,我们从一个空的 HNSW 开始。如果从上一次提交启动增量索引,则从上一次提交加载旧的 HNSW,然后开始索引操作。
新旧的内容都保存在TerminusDB中,它知道如何查找提交之间的更改,并可以将它们提交给矢量数据库索引器。索引器只需要知道要求它执行的操作(即、、)。InsertDeleteReplace
我们将索引本身维护在 LRU 池中,该池允许我们按需加载或在索引已在内存中使用缓存。由于我们只在提交时执行破坏性操作,因此此缓存始终是一致的。
当我们保存索引时,我们使用原始向量索引作为替代物序列化结构,这有助于保持索引较小。LoadedVec
将来,我们希望使用我们在 TerminusDB 中学到的一些技巧来保留索引层,这样就可以添加新层,而无需每个增量索引在序列化时添加副本。但是,与我们存储的向量相比,索引已经足够小,因此它并不重要。
注意:虽然我们目前进行增量索引,但我们尚未实现删除和替换操作(一周只有这么多小时!我读过关于HNSW的文献,似乎还没有很好的描述。
我们有一个删除和替换操作的设计,我们认为它可以很好地与HNSW配合使用,并希望在技术人员有想法的情况下进行解释:
如果我们在 HNSW 的上层,那么只需忽略删除 - 这应该无关紧要,因为大多数向量不在上层,而那些只是为了导航。
如果我们在零层但不在上层,请从索引中删除节点,同时尝试根据接近度替换已删除链接的所有邻居之间的链接。
如果我们在零层但也在上面,将节点标记为已删除,并将其用于导航,但不将此节点存储在候选池中。
查找嵌入
我们使用OpenAI来定义我们的嵌入,在向TerminusDB发出索引请求后,我们将每个文档提供给OpenAI,OpenAI以JSON形式返回浮点向量列表。
事实证明,嵌入对上下文非常敏感。我们最初尝试只提交TerminusDB JSON文档,结果并不好。
但是,我们发现,如果我们定义一个 GraphQL 查询 + Handlebars 模板,我们可以创建非常高质量的嵌入。因为在《星球大战》中,在我们的模式中定义的这对看起来像这样:People
{
"embedding": {
"query": "query($id: ID){ People(id : $id) { birth_year, created, desc, edited, eye_color, gender, hair_colors, height, homeworld { label }, label, mass, skin_colors, species { label }, url } }",
"template": "The person's name is {{label}}.{{#if desc}} They are described with the following synopsis: {{#each desc}} *{{this}} {{/each}}.{{/if}}{{#if gender}} Their gender is {{gender}}.{{/if}}{{#if hair_colors}} They have the following hair colours: {{hair_colors}}.{{/if}}{{#if mass}} They have a mass of {{mass}}.{{/if}}{{#if skin_colors}} Their skin colours are {{skin_colors}}.{{/if}}{{#if species}} Their species is {{species.label}}.{{/if}}{{#if homeworld}} Their homeworld is {{homeworld.label}}.{{/if}}"
}
}
对象中每个字段的含义都呈现为文本,这有助于OpenAI理解我们的意思,提供更好的语义。People
最终,如果我们能从模式文档和模式结构的组合中猜出这些句子,那就太好了,这可能也可以使用 AI 聊天!但就目前而言,这非常有效,不需要太多的技术复杂性。
为星球大战编制索引
那么当我们实际运行这个东西时会发生什么?好吧,我们在《星球大战》数据产品上进行了尝试,看看会发生什么。
首先,我们发出一个索引请求,我们的索引器从 TerminusDB 获取信息:
curl 'localhost:8080/index?commit=o2uq7k1mrun1vp4urktmw55962vlpto&domain=admin/star_wars'
这将返回一个任务 ID,我们可以使用它来轮询端点以完成。
域和提交的索引文件和向量文件显示为:和 。admin/star_warso2uq7k1mrun1vp4urktmw55962vlptoadmin%2Fstar_wars@o2uq7k1mrun1vp4urktmw55962vlpto.hnswadmin%2Fstar_wars.vecs
现在,我们可以在指定的提交时向语义索引服务器询问我们的文档。
curl 'localhost:8080/search?commit=o2uq7k1mrun1vp4urktmw55962vlpto&domain=admin/star_wars' -d "Who are the squid people"
我们以 JSON 的形式返回许多结果,如下所示:
[{"id":"terminusdb:///star-wars/Species/8","distance":0.09396297}, ...]
但是我们用来产生这个结果的嵌入字符串是什么?以下是 id 的文本呈现方式:Species/8
"The species name is Mon Calamari. They have the following hair colours:
none. Their skin colours are red, blue, brown, magenta. They speak the
Mon Calamarian language."
了不起!请注意,它从不在任何地方说鱿鱼!我们的嵌入在这里做了一些非常惊人的工作。
让我们再试一次:
curl 'localhost:8080/search?commit=o2uq7k1mrun1vp4urktmw55962vlpto&domain=admin/star_wars' -d "Wise old man"
"The person's name is Yoda. They are described with the following synopsis:
Lucas, first appearing in the 1980 film The Empire Strikes Back. In the
original films, he trains Luke Skywalker to fight against the Galactic
Empire. In the prequel films, he serves as the Grand Master of the Jedi
Order and as a high-ranking general of Clone Troopers in the Clone Wars.
Following his death in Return of the Jedi at the age of 900, Yoda was the
oldest living character in the Star Wars franchise in canon, until the
introduction of Maz Kanata in Star Wars: The Force Awakens. Their gender
is male. They have the following hair colours: white. They have a mass of
17. Their skin colours are green."
不可思议!虽然我们在文本中确实说“最古老”,但我们没有说“聪明”或“人”!
我希望您能看到这对您获得高质量的数据语义索引有何帮助!
结论
我们还添加了端点来查找相邻文档并查找搜索整个语料库的重复项。后者被用于一些基准测试,表现令人钦佩。我们希望很快在这里展示这些实验的结果。
虽然在野外确实有很棒的矢量数据库,例如Pinecone,但我们希望有一个与TerminusDB很好地集成的sidecar,它可以用于主要关心内容并且不会启动自己的矢量数据库的技术水平较低的用户。
审核编辑:郭婷
评论
查看更多