上一篇内容中,我们了解了什么是 Faiss,以及如何将文本内容转换为向量数据。本篇文章中,我们来使用 Faiss 实现向量检索功能。
使用 Faiss 实现最简单的向量检索功能
接下来,我们将使用 Faiss 实现一个小功能,针对哈利波特小说全集内容,接触向量检索技术,完成相似内容搜索的功能。与我们使用 “CTRL+F” 或者把数据倒入 MySQL,使用 “%LIKE%” 去进行全文匹配不同,我们的工具性能,将会远远高于一般的检索方式。
为了能够得到“快到飞起”的执行效率,在使用 Faiss 查询大量数据之前,我们首先需要和其他追求效率的数据库软件一样,为数据建立索引,我们先来看看最简单的平面索引:IndexFlatL2
。
借助平面索引,完成基础的相似内容查询功能
Faiss 中最简单的索引,便是没有使用任何花哨技巧(压缩、分区等)的平面索引:IndexFlatL2
。当我们使用这种索引的时候,我们查询的数据会和索引中所有数据进行距离计算,获取它们之间的 L2 距离(欧几里得距离)。因为它会尽职尽责的和所有数据进行比对,所以它是所有索引类型中最慢的一种,但是也是最简单和最准确的索引类型,同时,因为类型简单,也是内存占用量最低的类型。而它采取的遍历式查找,也会被从业者打趣称之为“暴力搜索”。
在上文中,我们已经准备好了 768 维度的高维向量数据,接下来,我们就用这些数据来建立我们的“第一堆向量数据”的索引:
import faiss
dimension = sentence_embeddings.shape[1]
index = faiss.IndexFlatL2(dimension)
index.add(sentence_embeddings)
将我们数据的维度信息传递给 faiss.IndexFlatL2
函数,建立一个空的索引容器,然后使用 index.add(sentence_embeddings)
将我们在之前处理好的向量数据灌入这个索引容器中。
执行完毕上面的代码之后,我们执行 index.ntotal
来查看索引的数据是否正确:
# >>> index.ntotal
60028
确认所有数据都被索引之后,我们来写一段最简单的程序,来进行查询,为了演示“相似性检索”,而不是“关键词匹配”,我们来搜索一个离谱的原文肯定没有的内容“哈利波特猛然睡醒”:
topK = 5
search = model.encode(["哈利波特猛然睡醒"])
D, I = index.search(search, topK)
df['sentence'].iloc[I[0]]
执行程序之后,我们将能够看到比较符合预期的神奇结果:
# >>> topK = 5
# >>> search = model.encode(["哈利波特猛然睡醒"])
# >>> D, I = index.search(search, topK)
# >>> df['sentence'].iloc[I[0]]
38216 “我很好先生”哈利结巴的说擦着脸上的汗“我刚才只是睡着了做了个恶梦。
37890 “我很好先生”哈利结巴的说擦着脸上的汗“我刚才只是睡着了做了个恶梦。
8009 那天晚上哈利失眠了。
13996 最後哈利精疲力尽的爬上床猛拉他的四柱大床的蚊帐堵住射进来的一道月光翻身躺了进去而且几乎立即觉...
45306 罗恩立刻就进入了梦乡但是哈利在上床之前从行李箱里翻出了他的那本《高级魔药制备》。
Name: sentence, dtype: object
虽然没有完全匹配关键词,但是我们想要的内容还是被程序找到了。我们每天都在使用的搜索引擎背后的众多技术之一,也包括类似的向量检索(未来有机会的话,我们聊聊语义搜索)。
进一步了解向量检索的细节
我知道有一些同学,在惊叹上面这加起来不到 10 行的代码的效果之余,体验之后依旧对于“向量”的感知是零。为了让大家清清楚楚的“入坑”,我们先来展开聊聊上面的程序的含义。
topK = 5
search = model.encode(["哈利波特猛然睡醒"])
D, I = index.search(search, topK)
df['sentence'].iloc[I[0]]
第一行,topK
定义了我们要查找多少条最相似的数据,比如这里我就只想查询 5 条数据,避免有人说我水文章字数 :D
第二行,我们通过 model.encode
方法,来将要搜索的内容“哈利波特猛然睡醒”编码为向量(行内人称这个过程的黑话为“embedding”)
第三行,则是使用我们在前文中构建的 faiss 索引,来查找上面的“文本内容”,以及找到符合预期数量的条数后就停止查找。
如果我们将 “D
和 I
” 打印出来,可以看到类似下面的输出:
# >>> print (D)
[[206.22675 206.22675 212.70087 219.73259 221.30847]]
# >>> print (I)
[[38216 37890 8009 13996 45306]]
前者指的是“数据置信度/可信度”,而后者指的是我们之前数据准备时灌入的文本数据的具体行数。
最后一行,我们使用 df['sentence'].iloc[I[0]]
来利用 pandas 的 DataFrame.iloc
接口,基于查询结果的行数,找到对应的文本的原文。如果我们使用上面的 “[38216 37890 8009 13996 45306]
” 替换 iloc[I[0]]
中的数据,得到的结果也是一样的:
# >>> df['sentence'].iloc[[38216, 37890, 8009, 13996, 45306]]
38216 “我很好先生”哈利结巴的说擦着脸上的汗“我刚才只是睡着了做了个恶梦。
37890 “我很好先生”哈利结巴的说擦着脸上的汗“我刚才只是睡着了做了个恶梦。
8009 那天晚上哈利失眠了。
13996 最後哈利精疲力尽的爬上床猛拉他的四柱大床的蚊帐堵住射进来的一道月光翻身躺了进去而且几乎立即觉...
45306 罗恩立刻就进入了梦乡但是哈利在上床之前从行李箱里翻出了他的那本《高级魔药制备》。
Name: sentence, dtype: object
聊完程序的执行过程,我们来看看程序在“面对”的“向量”数据的真身是什么样的,以上文中结果中的第一条为例,我们将 38216
这条在 faiss 索引中存储的数据进行向量重建:
>>> index.reconstruct(38216)
array([ 5.07521369e-02, -3.93364072e-01, -1.19723105e+00, -3.36433440e-01,
1.06395984e+00, 3.83257926e-01, 1.24985963e-01, 2.79548287e-01,
-7.02445269e-01, 7.59876966e-01, -5.09731807e-02, -5.78854322e-01,
-2.41243094e-01, -6.83130026e-01, 2.50904560e-01, -3.06654796e-02,
1.09606862e+00, 1.76596511e-02, 4.99801673e-02, -1.00713462e-01,
...
...
7.15905130e-01, 2.10034728e-01, 2.63317943e-01, 7.68652320e-01],
dtype=float32)
>>> len(index.reconstruct(38216))
768
“array”中的一堆使用科学计数法表示的数据,就是我们的向量数据,通过 len
方法来获取数据长度,我们能够确认数据长度为 768,这个数据长度,就是被我们称呼为维度的神奇数字(可以发挥想象,一个 768 维的立体世界)。
好啦,对于目前的我们来说,了解到向量检索的过程和向量到这个程度就足够啦。如果你想对 “FLAT” 索引有更多了解,可以移步官方开源项目中的[facebookresearch/faiss/blob/main/benchs/bench\_index\_flat.py]() 文件。
最后
和传统数据库类似,当我们的数据量越来越大,用户规模越来越大之后,也会遇到性能问题,那么当相似度检索性能不够时,我们该怎么办呢?
下一篇内容中,我们将了解如何使用针对向量索引优化,来解决检索性能问题。
作者:苏洋
原文:《向量数据库入坑指南:聊聊来自元宇宙大厂 Meta 的相似度检索技术 Faiss》
链接:https://zhuanlan.zhihu.com/p/...
如果你觉得我们分享的内容还不错,请不要吝啬给我们一些鼓励:点赞、喜欢或者分享给你的小伙伴!
活动信息、技术分享和招聘速递请关注:https://zilliz.gitee.io/welcome/
如果你对我们的项目感兴趣请关注: