不牺牲搜索的加密向量数据库

  • hexens
  • 发布于 8小时前
  • 阅读 16

本文深入探讨了向量数据库中嵌入的隐私安全问题。

目录

现代 AI 产品不再搜索 单词,它们搜索的是 意义。它们不再通过关键词索引文档,而是将文本、图像、代码和用户活动嵌入到 $\mathbb{R}^d$ 空间中的向量中,然后检索与查询向量最近的邻居。这单一的基本操作,“查找最接近的 $k$ 个向量”,悄然支持着语义搜索、推荐系统和检索增强生成 (RAG) 管道,后者将相关上下文馈送给大型语言模型。

为了有用,向量数据库必须在服务器端计算相似性。而嵌入并非无害的“压缩表示”。在许多应用中,它们编码了高度敏感的信号,包括医疗记录、内部文档、用户消息和专有代码。嵌入空间的几何结构,意味着谁与谁接近以及簇如何形成,本身就可能泄露信息。即使原始文本从未被存储,允许一个不可信的服务观察嵌入和相似性结构,也可能导致成员推断、语义泄露,在某些情况下,甚至能部分重建原始输入。

因此,核心问题不仅仅是“我们能否加密嵌入?”而是:我们能否在尽可能少地泄露向量及其几何结构信息的情况下实现最近邻搜索,并且能否在不破坏使向量数据库具有吸引力的效率特性的前提下做到这一点?

要理解这个问题为何困难,首先了解向量数据库的工作原理、嵌入为何敏感以及“保距加密”究竟意味着什么会有所帮助。

什么是向量数据库

近年来的人工智能浪潮迫使人们重新思考曾经看似已解决的数据检索问题。大型语言模型和生成式 AI 系统需要快速获取正确的信息,通常是从庞大的语料库中,而且往往是实时获取。瓶颈不再仅仅是存储数据,而是高效地找到相关上下文,以便模型能够进行推理、回答和行动。

关键的推动因素是 向量嵌入,它将数据表示为 $\mathbb{R}^d$ 空间中的一个点,编码了语义信息。这些表示由包括 LLM 在内的 AI 模型生成,它们的许多维度捕获了模式、关系和潜在结构,这正是驱动相似性搜索的“意义”。但正是这种丰富性使得嵌入在传统数据库中显得格格不入。它们是高维的,无法干净地融入标量索引,并且“查询”它们意味着 最近邻搜索 而不是精确查找。

向量数据库 专为这种工作负载而构建。它们提供针对高维向量的优化相似性搜索,以及成熟数据库系统所期望的操作功能,包括元数据过滤器、更新、持久性和水平扩展。独立的向量索引可以进行快速的最近邻检索,但它们通常缺乏数据库的人体工程学特性。传统标量数据库拥有成熟的基础设施,但它们并非为高维相似性工作负载而设计。向量数据库弥合了这一差距。

相似性搜索在实践中如何运作

正式任务是 $k$ 近邻搜索 ($k$-NNS)。给定一个距离函数和一个查询,目标是找到数据集中与该查询距离最小的 $k$ 个元素。朴素的方法是计算查询与每个存储元素之间的距离,这与数据集大小呈线性关系,并很快变得不可行。由于“维度诅咒”,精确的 $k$-NNS 方法在高维空间中只能提供有限的加速,这促使了 近似最近邻搜索 ($k$-ANNS) 的发展,这些算法允许小而有界的误差,以换取显著更快的检索速度。

最广泛部署的方法之一是 分层可导航小世界 (HNSW),这是一种完全基于图的增量式 $k$-ANNS 结构。HNSW 将嵌入组织成一个多层层次结构,该结构由嵌套元素子集上的邻近图组成。每个元素都被分配一个从指数衰减概率分布中抽取的最大层,搜索通过从顶层向下进行贪婪路由来执行。从一个入口点开始,算法重复地移动到使与查询距离最小的邻居,并跟踪沿途发现的最佳候选者。这种分层尺度分离使得 HNSW 具有对数复杂度缩放和在各种工作负载下的强大性能。

嵌入的安全问题

嵌入通常被认为是安全的,因为它们将原始输入映射到抽象的浮点向量。这种假设是错误的。这些向量通过至少三类不同的攻击保留并可能泄露有关原始数据的敏感信息。

嵌入反演。 嵌入模型编码了关于确切单词而不仅仅是抽象语义的信息,这意味着攻击者可以尝试恢复原始输入。在白盒场景中,攻击者可以访问模型的架构和参数,通过解决优化问题来实现:找到其嵌入与目标向量距离最小的单词序列。在黑盒场景中,攻击者只有查询权限,他们可以在辅助数据集上训练多标签分类或多集预测模型,使用嵌入作为输入,原始单词集作为目标。这两种方法都可以重建原始文本的很大一部分。

敏感属性推断。 嵌入可能会无意中暴露与下游任务完全无关的输入属性,例如文本片段的特定作者或用户的个人特征。通过少量标记嵌入向量训练的简单推断分类器通常可以揭示这些属性。这种泄露因无监督训练框架(例如对比学习)而加剧,这些框架最大化出现在相同上下文中的数据的相似性,从而将共享潜在敏感类别(如相同作者)的数据在嵌入空间中更紧密地分组。

成员推断。 嵌入模型还会泄露关于特定数据是否用于训练它们的信息。由于模型被训练为最大化出现在相同上下文中的数据的语义相似性,攻击者可以使用相似性分数作为信号。对于词嵌入,测量滑动窗口中的单词相似性分数可以揭示该特定上下文是否出现在训练语料库中。对于句子嵌入,评估成对相似性分数,或聚合单个用户的一组句子之间的相似性,可以准确确定该数据是否是训练集的一部分。稀有或不常见的训练输入特别容易受到这种记忆化攻击。

为什么精确保距加密会失败

在经典的保距扰动设置中,数据所有者发布一个转换后的数据集

$$Y = M^\top X,$$

其中 $X \in \mathbb{R}^{n \times m}$ 是私有数据矩阵,列为记录,而 $M \in O(n)$ 是一个秘密的 正交 映射,因此所有欧几里得距离都精确保留。这对于实用性来说是理想的场景,任何只依赖于距离的东西,如 $k$NN 或聚类,在 $Y$ 上的行为与在 $X$ 上完全相同。

但从攻击者的角度来看,精确保距会因为保留了过多的刚性结构而使隐私崩溃。这篇论文通过给出两种现实的先验知识模型,精确地说明了这一点,在这两种模型下,攻击者可以以很小的误差(有时是完美地)恢复原始记录。

攻击 1:已知输入-输出

攻击者能力。 攻击者知道:

  • 完整发布的扰动数据库 $Y$
  • $k$ 个原始记录的子集 $X_k$ 及其对应的扰动版本 $Y_k$,这意味着她知道哪一列映射到哪一列。

设想一下,在某些情况下,少数用户公开了自己的向量,少数公开记录以两种形式存在,或者某些行在内部泄露。这正是实践中会出现的辅助信息类型。

当 $k=n$ 时完美恢复

如果攻击者知道 $n$ 个线性无关的原始记录,即 $X_k$ 张成 $\mathbb{R}^n$,那么正交映射就完全确定,其他所有记录都可以精确恢复。具体来说,攻击者可以求解变换并在所有剩余列上对其进行反演。换句话说,$n$ 个线性无关的输入-输出对足以解密整个数据库。

有趣的情况:$k<n$ 仍然会泄露很多信息

当 $k<n$ 时,有许多与已知对一致的正交矩阵:

$$\mathcal{M}(X_k, Y_k) = {\, M \in O(n) \ :\ MX_k = Y_k \,}。$$

攻击者不知道哪个是真正的 $M^\top$,但她仍然可以高概率地恢复一些未知记录。

论文中一个自然的攻击策略是:

  1. 从 $\mathcal{M}(X_k, Y_k)$ 中均匀采样 $\hat{M}$。
  2. 对于来自 $Y_{m-k}$ 的某个未知扰动列 $\hat{y}_i$,输出估计值

$$\hat{x} = \hat{M}^\top \hat{y}_i。$$

  1. 通过最大化 $\varepsilon$ 精确重建的概率来选择要攻击的目标记录。

论文将 $\varepsilon$ 泄露事件定义为

$|\hat{x} - x_i| \le \varepsilon |x_i|$

并推导出一个闭式泄露概率 $\rho(x_i, \varepsilon)$,它取决于一个单一的几何量,即 $x_i$ 到已知明文记录所张成的子空间的距离。

设 $\mathrm{Col}(X_k)$ 是由已知记录张成的 $k$ 维子空间,并定义

$$d(x, X_k) := \mathrm{dist}(x,\mathrm{Col}(X_k))。$$

那么泄露概率表现为

$$\rho(x,\varepsilon) \approx \frac{2}{\pi}\arcsin\left(\frac{\varepsilon|x|}{2d(x,X_k)}\right)$$

当 $\varepsilon|x| < 2d(x,X_k)$ 时,一旦 $\varepsilon|x| \ge 2d(x,X_k)$,它就变为 $1$。

解释。 随着受害者的记录越接近已知记录,泄露的概率就飙升。

攻击 2:已知样本

第二种攻击不假设攻击者知道任何特定的明文记录。相反,它假设她拥有与私有数据集来自相同分布的样本。

攻击者能力。 攻击者知道:

  • $Y = M^\top X$
  • 另一个数据集 $S$,由从生成 $X$ 列的同一基础随机向量 $V$ 中抽取的 $p$ 个独立样本组成。

设想一下,攻击者抓取了一个相似的群体数据,拥有历史日志,购买了第三方数据,或者仅仅利用了来自你的领域典型文本的嵌入具有相似统计特性这一事实。

为什么 PCA 会破坏精确保距

设 $V$ 是一个明文记录的随机向量,其协方差矩阵为 $\Sigma_V := \operatorname{Cov}(V)$。假设已发布的(扰动)向量是通过一个 正交 保距映射获得的

$$Y = M^\top V \qquad\text{其中 } M \in O(n)\text{ 且 } M^\top M = I。$$

那么转换后的协方差满足

$$\begin{aligned} \operatorname{Cov}(M^\top V) &= \mathbb{E}\left[\left(M^\top V-\mathbb{E}[M^\top V]\right)\left(M^\top V-\mathbb{E}[M^\top V]\right)^\top\right] \ &= \mathbb{E}\left[\left(M^\top (V-\mathbb{E}[V])\right)\left(M^\top (V-\mathbb{E}[V])\right)^\top\right] \ &= M^\top \mathbb{E}\left[(V-\mathbb{E}[V])(V-\mathbb{E}[V])^\top\right] M \ &= M^\top \operatorname{Cov}(V)\, M. \end{aligned}$$

因为 $M$ 是正交的,所以 $\Sigma_{M^\top V}$ 与 $\Sigma_V$ 正交相似。根据 谱定理,我们可以写出

$$\SigmaV = Z\Lambda Z^\top \quad\Longrightarrow\quad \Sigma{M^\top V} = (M^\top Z)\Lambda(M^\top Z)^\top,$$

所以 $\SigmaV$ 和 $\Sigma{M^\top V}$ 共享 相同的特征值,并且它们的特征向量通过与 $M^\top$ 相乘而关联。在 $\Sigma_V$ 具有 不同的特征值 的常见假设下,每个特征空间都是一维的,因此特征向量在符号上是唯一确定的。因此,PCA 可以在明文空间和密文空间中恢复主方向,并且隐藏的正交变换由一个对角符号翻转矩阵确定。

具体来说,攻击者按以下步骤进行:

  1. 从辅助的类明文样本 $S$ 中,估计 $\Sigma_V$ 并运行 PCA 以获得其特征向量矩阵 $Z$。
  2. 从发布的扰动数据集 $Y$ 中,估计 $\Sigma_{M^\top V}$ 并运行 PCA 以获得其特征向量矩阵 $W$。
  3. 根据协方差共轭恒等式,两个特征基通过隐藏的正交映射相关联,直至一个未知的对角符号矩阵 $D_0 \in {\pm1}^{n \times n}$,得到 $M^\top = W D_0 Z^\top$。

因此,破解方案就简化为识别正确的符号模式并进行反演:

$$\hat{X} = MY = (Z D_0 W^\top)Y。$$

该论文通过搜索候选 $D$ 并评估映射的辅助样本与观察到的 $Y$ 分布的匹配程度来解决符号模糊性,选择使匹配最大化的 $D$。随着样本数量的增加,PCA 估计值集中,重建变得高度准确。

嵌入的启示

精确的保距“加密”在任何密码学意义上都不是加密。它更接近于一种刚性的几何伪装。如果即使少量明文嵌入及其对应的密文泄露,攻击者也可以高概率地重建额外的嵌入,并使用几何邻近性针对最容易受攻击的受害者。即使没有直接的明文泄露,来自相同分布的辅助数据也能通过基于 PCA 的攻击有效地恢复隐藏的正交变换。精确保距从根本上与强大的隐私保证不兼容,因为它精确地保留了攻击者反向工程空间所需的全局结构。

近似保距比较加密

解决这些问题的方法是放松精确性。近似保距比较加密 ($\beta$-DCPE) 不再保证所有相对距离比较都得到保留,而是仅保证当距离差异超过指定的近似因子 $\beta$ 时,比较才得到保留。形式上,如果三个明文向量 $x, y$ 和 $z$ 满足 $|x - y| < |x - z| - \beta$,那么加密后,$x$ 的密文仍然比 $z$ 的密文更接近 $y$ 的密文。当给定噪声预算,距离过于相似而无法区分时,不作任何保证。

这种放松不仅仅是一个方便的让步,它是一个经过深思熟虑且有充分理由的设计选择。即使在明文数据上,近似最近邻搜索在高维设置中也是常态,因为随着维度的增加,精确距离的概念变得不那么有意义。$\beta$-DCPE 只是将相同的近似容差引入加密设置,并且该论文正式表明,在 $\beta$-DCP 加密向量上运行的任何最近邻算法都会返回一个点,其明文距离与查询的距离至多比真实最近邻的距离大 $\beta$。换句话说,上层搜索算法的近似因子得以保留。

这种放松也解锁了精确方案无法实现的东西:独立于底层数据的安全参数。精确保距比较加密需要针对消息空间中特定数据点定制参数,使得系统僵化。因为 $\beta$ 是限制允许扰动的固定常数,$\beta$-DCPE 没有这种依赖性。扰动界限一经设定便在整个数据集上统一适用,无论数据看起来如何。

缩放和扰动 (SAP) 方案

在实践中实现 $\beta$-DCPE 的核心算法是 缩放和扰动 (SAP) 方案。它的直觉直接源于保距比较函数的结构。该论文指出,任何 DCP 函数都必须是近似保距的,而欧几里得空间的等距变换(旋转、反射、平移和缩放)是构建此类函数的自然候选。缩放是这里的关键操作:将所有向量乘以一个常数因子会按相同量缩放所有成对距离,使相对距离比较完全不变。然后,向结果添加一个有界噪声向量可以掩盖精确的变换,而不会违反 $\beta$-DCP 保证,前提是噪声保持在严格的半径内。

参数和原语

加密前,方案定义:

  • 消息空间 $\mathcal{M}$:一个离散的 $d$ 维空间 $[-M, M]^d$
  • 近似因子 $\beta$ 且 $\beta < 2M\sqrt{d}$
  • 密钥空间 $\mathcal{S}$,从中抽取缩放因子
  • 伪随机函数 (PRF) $\text{PRF}: {0,1}^k \times {0,1}^l \rightarrow {0,1}^*$,用于去随机化扰动,以便在解密时可以重建相同的噪声

主密钥是一个元组 $(s, K)$,其中 $s$ 从 $\mathcal{S}$ 中均匀采样, $K$ 是从 ${0,1}^k$ 中采样的 PRF 密钥。

加密

要加密明文向量 $m$:

  1. 采样随机随机数 $n \in {0,1}^l$。
  2. 计算 $$\texttt{coins}_1 | \texttt{coins}_2 = \text{PRF}(K, n)。$$
  3. 从半径为 $\frac{s\beta}{4}$ 的球体中均匀采样噪声向量 $\lambda_m$。 步骤:

    • 采样 $u \sim \mathcal{N}(0,I_d)$
    • 采样 $x' \sim U(0,1)$
    • 计算 $x = \frac{s\beta}{4}(x')^{1/d}$
    • 设置 $\lambda_m = u \frac{x}{|u|}$ 这确保了球体体积内的均匀采样。
  4. 计算密文 $c = sm + \lambda_m$
  5. 输出 $(c,n)$。

解密

给定 $(c,n)$ 和密钥 $(s,K)$:

  1. 使用 PRF 重新计算相同的伪随机“Coin”。
  2. 重建 $\lambda_m$。
  3. 恢复 $$m = \frac{c-\lambda_m}{s}。$$

数学保证

由于扰动半径有界,为 $\frac{s\beta}{4}$,三角形不等式确保如果在明文空间中

$|x-y| < |y-z| - \beta$

那么加密后相同的比较仍然成立。

通过预处理加强 SAP

到目前为止,SAP 方案仅仅通过秘密密钥对单个向量进行即时加密。这已经足以提供核心的 $\beta$-DCP 保证,但它留下了两个残余的攻击面。第一个是身份泄露:即使不知道明文值,观察到一系列密文的攻击者仍可能根据位置、时间或其他上下文信号将它们与已知记录进行匹配。第二个是频率泄露:由于 SAP 是明文和密钥(不包括随机数)的确定性函数,观察到许多密文的攻击者可能通过构建密文统计直方图来推断哪些明文值频繁出现,这种攻击已经成功地破解了保序和揭序加密方案。

该论文通过在加密之前应用于数据集的两种预处理技术解决了这两个问题。关键是,这两种技术针对不同的威胁,并且被设计为可以干净地组合。

洗牌

在加密之前,整个数据集根据均匀采样的置换 $\pi$ 进行随机置换:

$$ (m_1, \dots, mn) \mapsto (m{\pi(1)}, \dots, m_{\pi(n)})。$$

然后,加密后的数据集将以这种置换后的顺序返回。由于置换是秘密的,攻击者收到的是一个无序的密文多集,位置和身份之间没有可靠的对应关系。即使攻击者事先知道一些明文值,也无法确定哪个密文编码了哪条记录。

这一步使得该论文的主要不可区分性结果成为可能。所使用的安全概念称为 真实或替换 (RoR):攻击者提交一个数据集,挑战者要么按原样加密并洗牌,要么首先用一个随机选择的、距离为 $\delta$ 内的邻近点替换其中一个点,然后洗牌并加密。攻击者无法区分这两种情况,这直接对应于对成员推断的抵抗,意味着它无法确定特定个人的数据是否存在于数据库中。该论文证明了 RoR 优势的具体界限,作为 $\beta$、$\delta$ 和数据集大小的函数,而洗牌步骤对于达到这些界限至关重要,没有它,密文仍然被位置隐式标记,安全论证就会失效。

规范化

第二个预处理步骤是将明文分布转换为近似多元正态分布,例如使用 BoxCox 变换。这不是 SAP 的正确性要求,加密算法对任何输入都能正确工作,但它是该论文针对频率查找攻击的积极安全结果的要求。

关于频率泄露的担忧在保性加密文献中已得到充分证实。能够估计密文经验分布的攻击者可以构建明文值的近似直方图,并由此部分或完全重建数据。该论文对此的防御是一种称为 属性窗口单向性 (AWOW) 的安全概念,它保证计算上难以猜测任何明文属性的近似频率。然而,AWOW 对 SAP 的证明假设明文向量是从多元正态分布中 i.i.d. 采样的。规范化就是使这一假设在实践中成立的步骤。

如果待加密的嵌入本身已经遵循接近多元正态的分布(这对于使用某些目标训练的高维嵌入模型来说并不少见),则可以跳过此步骤。否则,它充当了实际嵌入数据混乱的统计现实与正式安全证明所需的分布假设之间的桥梁。

实战演示:使用 IronCore Alloy 为 Pinecone 加密嵌入

在本节中,我们将通过一个端到端的集成示例,使用 Pinecone 作为向量存储,IronCore Alloy 作为客户端加密层。该工作流程模仿了你在生产环境中部署的方式:嵌入在客户端生成(或加载),在本地加密,然后才上传到 Pinecone。查询遵循相同的规则:在本地加密查询嵌入,对密文向量运行 ANN 搜索,并在客户端解密返回的元数据。

IronCore Alloy 提供两种原语:

  1. 向量加密(近似搜索仍然有效)
  2. 标准加密(用于元数据的 AES 风格加密)
import os
import time
import base64
import asyncio

import pinecone
from pinecone_datasets import load_dataset

import torch
from sentence_transformers import SentenceTransformer

import ironcore_alloy as alloy

## -----------------------------

## 1) Alloy SDK setup (Standalone)

## -----------------------------

def make_alloy_sdk(*, master_key_b64: str, approximation_factor: float) -> tuple[alloy.Standalone, alloy.AlloyMetadata]:
    """
    Standalone mode: you manage the master secret locally.
    - approximation_factor controls the accuracy/privacy tradeoff for vector search.
    """
    # Standalone 模式:你在本地管理主密钥。
    # approximation_factor 控制向量搜索的准确性/隐私权衡。
    key_bytes = base64.b64decode(master_key_b64)
    if len(key_bytes) != 32:
        raise ValueError("ALLOY_MASTER_KEY_B64 must decode to exactly 32 bytes")

    # Vector secrets are scoped by secret_path ("namespace") and derivation_path ("vector type").
    # 向量密钥通过 secret_path ("命名空间") 和 derivation_path ("向量类型") 进行范围限定。
    vector_secrets = {
        "quora": alloy.VectorSecret(
            approximation_factor,
            alloy.RotatableSecret(
                alloy.StandaloneSecret(1, alloy.Secret(key_bytes)),
                None,
            ),
        )
    }

    # Standard (non-vector) encryption secrets for metadata / documents.
    # 用于元数据/文档的标准(非向量)加密密钥。
    standard_secrets = alloy.StandardSecrets(
        1,
        [alloy.StandaloneSecret(1, alloy.Secret(key_bytes))],
    )

    deterministic_secrets = {}  # fill only if you need equality-searchable encrypted filters
                                # 仅在需要可相等搜索的加密过滤器时填充
    config = alloy.StandaloneConfiguration(standard_secrets, deterministic_secrets, vector_secrets)
    sdk = alloy.Standalone(config)

    # Tenant metadata: used for key scoping in multi-tenant apps.
    # If you have tenants/users/orgs, put an ID here (e.g., "org_123").
    # 租户元数据:用于多租户应用中的密钥范围限定。
    # 如果你有租户/用户/组织,请在此处放置一个 ID(例如,“org_123”)。
    tenant = alloy.AlloyMetadata.new_simple("")
    return sdk, tenant

## -----------------------------

## 2) Helper: encrypt one record

## -----------------------------

async def encrypt_record(
    sdk: alloy.Standalone,
    tenant: alloy.AlloyMetadata,
    *,
    vec: list[float],
    text: str,
    secret_path: str = "quora",
    derivation_path: str = "sentence",
):
    # Encrypt the embedding for similarity search.
    # 加密嵌入以进行相似性搜索。
    plaintext_vec = alloy.PlaintextVector(
        plaintext_vector=vec,
        secret_path=secret_path,
        derivation_path=derivation_path,
    )
    enc_vec = await sdk.vector().encrypt(plaintext_vec, tenant)

    # Encrypt the metadata (here, the original text). This is normal encryption (not searchable).
    # 加密元数据(此处为原始文本)。这是普通加密(不可搜索)。
    enc_doc = await sdk.standard().encrypt({"text": text.encode("utf-8")}, tenant)

    # Pinecone metadata must be JSON-serializable → store bytes as base64 strings.
    # Pinecone 元数据必须是 JSON 可序列化的 → 将字节存储为 base64 字符串。
    meta = {
        "text": base64.b64encode(enc_doc.document["text"]).decode("ascii"),
        "edek": base64.b64encode(enc_doc.edek).decode("ascii"),
    }
    return enc_vec.encrypted_vector, meta

## -----------------------------

## 3) Main: encrypt → upsert → encrypted query → decrypt metadata

## -----------------------------

async def main():
    # ----- Dataset -----
    dataset = load_dataset("quora_all-MiniLM-L6-bm25")
    docs = dataset.documents.copy()

    # the dataset stores original text in `blob`; rename to `metadata` to match your script style
    # 数据集将原始文本存储在 `blob` 中;重命名为 `metadata` 以符合你的脚本风格
    docs.drop(["metadata"], axis=1, inplace=True)
    docs.rename(columns={"blob": "metadata"}, inplace=True)

    # use a slice (80k rows)
    # 使用切片 (8 万行)
    docs = docs.iloc[240_000:320_000].reset_index(drop=True)

    dim = len(docs.iloc[0]["values"])

    # ----- Pinecone -----
    pinecone.init(
        api_key=os.environ["PINECONE_API_KEY"],
        environment=os.getenv("PINECONE_ENV", "gcp-starter"),
    )

    index_name = "semantic-search-fast-encrypted"

    if index_name not in pinecone.list_indexes():
        pinecone.create_index(name=index_name, dimension=dim, metric="cosine")
        time.sleep(2)

    index = pinecone.GRPCIndex(index_name)

    # ----- Alloy -----
    # IMPORTANT: set this env var to a secure random 32-byte key (base64-encoded)
    # e.g. python -c "import os,base64; print(base64.b64encode(os.urandom(32)).decode())"
    # 重要:将此环境变量设置为安全的随机 32 字节密钥(base64 编码)
    # 例如:python -c "import os,base64; print(base64.b64encode(os.urandom(32)).decode())"
    sdk, tenant = make_alloy_sdk(
        master_key_b64=os.environ["ALLOY_MASTER_KEY_B64"],
        approximation_factor=2.0,
    )

    # ----- Embedding model (only used for queries here; dataset already has vectors) -----
    # 嵌入模型(此处仅用于查询;数据集已有向量)
    device = "cuda" if torch.cuda.is_available() else "cpu"
    model = SentenceTransformer("all-MiniLM-L6-v2", device=device)

    # ----- Encrypt + upsert in batches -----
    # 批量加密 + 插入
    print("Encrypting and uploading encrypted vectors + encrypted metadata...")
    # 正在加密并上传加密向量 + 加密元数据...
    batch_size = 200
    to_upsert = []

    for i, row in enumerate(docs.itertuples(index=False), start=1):
        vec = row.values
        text = row.metadata["text"]

        enc_vec, meta = await encrypt_record(sdk, tenant, vec=vec, text=text)
        # Use dataset's existing id field (commonly `id`). If your dataset uses a different field, change here.
        # 使用数据集现有的 id 字段(通常为 `id`)。如果你的数据集使用不同的字段,请在此处更改。
        doc_id = str(row.id)

        to_upsert.append((doc_id, enc_vec, meta))

        if len(to_upsert) >= batch_size:
            index.upsert(vectors=to_upsert)
            to_upsert.clear()

        if i % 5000 == 0:
            print(f"  processed {i}/{len(docs)} records")
            #   已处理 {i}/{len(docs)} 条记录

    if to_upsert:
        index.upsert(vectors=to_upsert)

    # ----- Encrypted query + decrypt results -----
    # 加密查询 + 解密结果
    async def run_query(q: str, top_k: int = 5):
        print(f"\nQuery: {q}")
        # 查询:{q}
        q_vec = model.encode(q).tolist()

        # IMPORTANT: you do NOT call `encrypt` for queries.
        # You call `generate_query_vectors`, which may output 1+ encrypted query vectors depending on scheme.
        # 重要:你不对查询调用 `encrypt`。
        # 你调用 `generate_query_vectors`,它可能会根据方案输出 1 个或多个加密查询向量。
        plaintext_q = alloy.PlaintextVector(
            plaintext_vector=q_vec,
            secret_path="quora",
            derivation_path="sentence",
        )
        qvs = (await sdk.vector().generate_query_vectors({"q": plaintext_q}, tenant))["q"]
        enc_q = qvs[0].encrypted_vector

        res = index.query(vector=enc_q, top_k=top_k, include_metadata=True)

        for match in res["matches"]:
            # Rebuild EncryptedDocument from base64 fields
            # 从 base64 字段重建 EncryptedDocument
            enc_doc = alloy.EncryptedDocument(
                edek=base64.b64decode(match["metadata"]["edek"]),
                document={"text": base64.b64decode(match["metadata"]["text"])},
            )
            dec = await sdk.standard().decrypt(enc_doc, tenant)
            print(f"{match['score']:.3f}: {dec['text'].decode('utf-8')}")

    await run_query("which city has the highest population in the world?")
    await run_query("which metropolis has the highest number of people?")

    # Optional cleanup:
    # 可选清理:
    # pinecone.delete_index(index_name)

if __name__ == "__main__":
    asyncio.run(main())

输出看起来像这样

Encrypting embeddings and their associated text.
正在加密嵌入及其相关文本。

Query: which city has the highest population in the world?
查询:世界上人口最多的城市是哪个?
0.65:  What's the world's largest city?
0.6:  What is the biggest city?
0.56:  What are the world's most advanced cities?
0.54:  Where is the most beautiful city in the world?
0.54:  What is the greatest, most beautiful city in the world?

Query: which metropolis has the highest number of people?
查询:哪个大都市人口最多?
0.51:  What is the biggest city?
0.5:  What is the most dangerous city in USA?
0.49:  How many people to in the United States?
0.49:  What's the world's largest city?
0.48:  What are some of the most dangerous cities in America?

结论

向量数据库已成为现代 AI 系统的核心基础设施,但它们引入了新的隐私风险,这些风险很容易被低估。嵌入并非无害的抽象。它们可以被反演以恢复原始文本,可以被挖掘以获取敏感属性,并可用于推断训练成员资格。而精确的保距加密,作为对此问题最直观的反应,却失败了,因为它保留了导致这些攻击成为可能所需的全局几何结构。

近似保距比较加密提供了一种原则性的替代方案。通过容忍有界近似误差 $\beta$ 并向缩放后的明文添加均匀球面噪声,缩放和扰动方案打破了实现重建所需的刚性结构,同时保留了足够的关联信息以支持近似最近邻搜索。结合洗牌和规范化作为预处理步骤,它实现了可证明的抵抗成员推断和频率查找攻击,并且其安全参数与数据集无关,位安全性优于保序加密。结果是一种能够在不牺牲向量数据库实用性能的前提下,为 AI 系统实现隐私保护检索的方案。

  • 原文链接: hexens.io/blog/vector-da...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
hexens
hexens
江湖只有他的大名,没有他的介绍。