AI 与向量

带权限控制的RAG

通过检索增强生成实现细粒度访问控制


由于 pgvector 构建在 Postgres 之上,您可以使用行级安全(RLS)对向量数据库实施细粒度访问控制。这意味着您可以在向量相似性搜索中限制只返回用户有权访问的文档。Supabase 还支持外部数据包装器(FDW),这意味着如果您的用户数据不在 Supabase 中,您可以使用外部数据库或数据源来确定这些权限。

本指南将介绍如何在执行检索增强生成(RAG)时限制对文档的访问。

示例

在一个典型的RAG(检索增强生成)设置中,您的文档会被分割成小的子段落,并在这些段落上执行相似性搜索:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 跟踪文档/页面/文件等create table documents ( id bigint primary key generated always as identity, name text not null, owner_id uuid not null references auth.users (id) default auth.uid(), created_at timestamp with time zone not null default now());-- 存储文档每个段落的内容和嵌入向量-- 并引用原始文档(一对多关系)create table document_sections ( id bigint primary key generated always as identity, document_id bigint not null references documents (id), content text not null, embedding vector (384));

注意我们如何在每个文档上记录owner_id。让我们创建一个RLS(行级安全)策略,根据用户是否拥有链接的文档来限制对document_sections的访问:

1
2
3
4
5
6
7
8
9
10
11
12
-- 启用行级安全alter table document_sections enable row level security;-- 为查询操作设置RLScreate policy "用户可以查询自己的文档段落"on document_sections for select to authenticated using ( document_id in ( select id from documents where (owner_id = (select auth.uid())) ));

现在,在document_sections上执行的每个select查询都会隐式地根据当前用户是否有访问权限来过滤返回的段落。

例如,执行:

1
select * from document_sections;

作为认证用户,将只返回他们拥有权限的行(由链接文档确定)。更重要的是,对这些段落的语义搜索(或任何额外的过滤条件)将继续遵守这些RLS策略:

1
2
3
4
5
-- 基于match_threshold执行内积相似度搜索select *from document_sectionswhere document_sections.embedding <#> embedding < -match_thresholdorder by document_sections.embedding <#> embedding;

上面的示例仅配置了用户的select访问权限。如果需要,您可以创建更多的RLS策略来为插入、更新和删除操作应用相同的权限逻辑。有关RLS策略的更深入指南,请参阅行级安全

替代场景

每个应用程序都有其独特的需求,可能与上述示例有所不同。以下是我们经常看到的一些替代场景以及它们在 Supabase 中的实现方式。

多人共有的文档

不同于 usersdocuments 之间的一对多关系,您可能需要多对多关系,以便多个人可以访问同一文档。让我们使用连接表重新实现这个功能:

1
2
3
4
5
create table document_owners ( id bigint primary key generated always as identity, owner_id uuid not null references auth.users (id) default auth.uid(), document_id bigint not null references documents (id));

然后您的行级安全策略(RLS)将修改为:

1
2
3
4
5
6
7
8
create policy "用户可查询自己拥有的文档章节"on document_sections for select to authenticated using ( document_id in ( select document_id from document_owners where (owner_id = (select auth.uid())) ));

现在我们不再直接查询 documents 表,而是通过查询连接表来实现。

用户和文档数据存储在 Supabase 外部

您可能已有系统将用户、文档及其权限存储在单独的数据库中。让我们探讨这种情况:这些数据存在于另一个Postgres数据库中。我们将使用外部数据包装器(FDW)从您的Supabase数据库连接到外部数据库:

假设您的外部数据库包含如下结构的usersdocuments表:

1
2
3
4
5
6
7
8
9
10
11
12
create table public.users ( id bigint primary key generated always as identity, email text not null, created_at timestamp with time zone not null default now());create table public.documents ( id bigint primary key generated always as identity, name text not null, owner_id bigint not null references public.users (id), created_at timestamp with time zone not null default now());

在您的Supabase数据库中,创建链接到上述表的外部表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
create schema external;create extension postgres_fdw with schema extensions;-- 设置外部服务器create server foreign_server foreign data wrapper postgres_fdw options (host '<db-host>', port '<db-port>', dbname '<db-name>');-- 将本地'authenticated'角色映射到外部'postgres'用户create user mapping for authenticated server foreign_server options (user 'postgres', password '<user-password>');-- 将外部'users'和'documents'表导入'external'模式import foreign schema public limit to (users, documents) from server foreign_server into external;

我们将document_sections及其嵌入向量存储在Supabase中,以便通过pgvector执行相似性搜索:

1
2
3
4
5
6
create table document_sections ( id bigint primary key generated always as identity, document_id bigint not null, content text not null, embedding vector (384));

我们通过document_id维护对外部文档的引用,但不设置外键约束,因为外键只能添加到本地表。请确保使用与外部文档表相同的ID数据类型。

由于我们在Supabase外部管理用户和认证,有两种选择:

  1. 直接建立到Supabase数据库的Postgres连接,并在每个请求中设置当前用户
  2. 从您的系统签发自定义JWT,并使用它通过REST API进行认证

直接连接Postgres

您可以使用项目数据库设置页面上的连接信息直接连接到Supabase Postgres数据库。要在此方法中使用RLS(行级安全),我们使用一个包含当前用户ID的自定义会话变量:

1
2
3
4
5
6
7
8
9
10
11
12
-- 启用行级安全alter table document_sections enable row level security;-- 为查询操作设置RLScreate policy "用户可以查询自己的文档片段"on document_sections for select to authenticated using ( document_id in ( select id from external.documents where owner_id = current_setting('app.current_user_id')::bigint ));

会话变量通过current_setting()函数访问。这里我们将变量命名为app.current_user_id,但您可以修改为任意名称。我们还将其转换为bigint类型,因为这是user.id列的数据类型。请根据您的ID数据类型调整此转换。

现在对于每个请求,我们在会话开始时设置用户ID:

1
set app.current_user_id = '<current-user-id>';

然后所有后续查询都将继承该用户的权限:

1
2
3
4
5
-- 只返回属于该用户的文档片段select *from document_sectionswhere document_sections.embedding <#> embedding < -match_thresholdorder by document_sections.embedding <#> embedding;

使用 REST API 的自定义 JWT

如果您希望使用自动生成的 REST API 通过外部身份验证提供商的 JWT 来查询 Supabase 数据库,可以让您的身份验证提供商为 Supabase 签发自定义 JWT。

参考 Clerk Supabase 文档 了解具体实现方式。根据您的身份验证提供商需求调整相关配置。

现在我们可以使用第一个示例中的相同 RLS 策略:

1
2
3
4
5
6
7
8
9
10
11
12
-- 启用行级安全alter table document_sections enable row level security;-- 为查询操作设置RLScreate policy "用户可查询自己的文档段落"on document_sections for select to authenticated using ( document_id in ( select id from documents where (owner_id = (select auth.uid())) ));

底层实现上,auth.uid() 引用了 current_setting('request.jwt.claim.sub'),这对应 JWT 的 sub (subject) 声明。该设置在每次 REST API 请求开始时自动设置。

所有后续查询都将继承该用户的权限:

1
2
3
4
5
-- 仅返回属于该用户的文档段落select *from document_sectionswhere document_sections.embedding <#> embedding < -match_thresholdorder by document_sections.embedding <#> embedding;

其他场景

根据系统复杂性的不同,这个问题有无数种解决方案。幸运的是,Postgres 提供了所有必要的原语,可以按照最适合您项目的方式实现访问控制。

如果上述示例不符合您的使用场景,或者您需要对它们进行微调以适应现有系统,请随时联系 技术支持,我们将竭诚为您提供帮助。