Skip to content
AI & Vectors

带权限的RAG

Fine-grained access control with Retrieval Augmented Generation.

由于 pgvector 是建立在 Postgres 之上的,你可以使用 行级安全(RLS) 在向量数据库上实现细粒度的访问控制。这意味着你可以限制在向量相似度搜索中,只有有权限的用户才能看到特定的文档。Supabase 还支持 外部数据封装器(FDW),这意味着如果你的用户数据不在 Supabase 中,你也可以用外部数据库或数据源来确定这些权限。

🌐 Since pgvector is built on top of Postgres, you can implement fine-grained access control on your vector database using Row Level Security (RLS). This means you can restrict which documents are returned during a vector similarity search to users that have access to them. Supabase also supports Foreign Data Wrappers (FDW) which means you can use an external database or data source to determine these permissions if your user data doesn't exist in Supabase.

使用本指南来学习在执行增强生成检索(RAG)时如何限制文档访问。

🌐 Use this guide to learn how to restrict access to documents when performing retrieval augmented generation (RAG).

示例 #

🌐 Example

在典型的 RAG 设置中,你的文档会被拆分成小块子部分,然后在这些部分上进行相似性匹配:

🌐 In a typical RAG setup, your documents are chunked into small subsections and similarity is performed over those sections:

1
-- Track documents/pages/files/etc
2
create table documents (
3
id bigint primary key generated always as identity,
4
name text not null,
5
owner_id uuid not null references auth.users (id) default auth.uid(),
6
created_at timestamp with time zone not null default now()
7
);
8
9
-- Store the content and embedding vector for each section in the document
10
-- with a reference to original document (one-to-many)
11
create table document_sections (
12
id bigint primary key generated always as identity,
13
document_id bigint not null references documents (id),
14
content text not null,
15
embedding extensions.vector (384)
16
);

注意每个文档上 owner_id 的记录。创建一个 RLS 策略,根据用户是否拥有相关文档来限制他们访问 document_sections

🌐 Notice the record of owner_id on each document. Create an RLS policy that restricts access to document_sections based on whether or not they own the linked document:

1
-- Grant the privileges the roles need
2
GRANT SELECT ON public.document_sections TO authenticated;
3
4
-- enable row level security
5
alter table document_sections enable row level security;
6
7
-- setup RLS for select operations
8
create policy "Users can query their own document sections"
9
on document_sections for select to authenticated using (
10
document_id in (
11
select id
12
from documents
13
where (owner_id = (select auth.uid()))
14
)
15
);

现在,每个在 document_sections 上执行的 select 查询都会根据当前用户是否有权限访问这些部分,自动过滤返回的结果。

🌐 Now every select query executed on document_sections will implicitly filter the returned sections based on whether or not the current user has access to them.

例如,执行:

🌐 For example, executing:

1
select * from document_sections;

作为已认证用户,将只返回他们拥有的行(由关联文档确定)。更重要的是,对这些部分的语义搜索(或任何其他额外的筛选)仍将继续遵守这些行级安全策略(RLS):

🌐 as an authenticated user will only return rows that they are the owner of (as determined by the linked document). More importantly, semantic search over these sections (or any additional filtering for that matter) will continue to respect these RLS policies:

1
-- Perform inner product similarity based on a match_threshold
2
select *
3
from document_sections
4
where document_sections.embedding <#> embedding < -match_threshold
5
order by document_sections.embedding <#> embedding;

上面的例子只配置了用户对 select 的访问权限。如果你愿意,你可以为插入、更新和删除操作创建更多的 RLS 策略,以便将相同的权限逻辑应用到这些操作上。有关 RLS 策略的更详细指南,请参见 行级安全

🌐 The above example only configures select access to users. If you wanted, you could create more RLS policies for inserts, updates, and deletes in order to apply the same permission logic for those other operations. See Row Level Security for a more in-depth guide on RLS policies.

备用方案 #

🌐 Alternative scenarios

每个应用都有自己独特的需求,可能会和上面的例子不同。这里是我们常见的一些替代场景,以及它们在 Supabase 中的实现方式。

🌐 Every app has its own unique requirements and may differ from the above example. Here are some alternative scenarios we often see and how they are implemented in Supabase.

多人拥有的文件 #

🌐 Documents owned by multiple people

与其在 usersdocuments 之间使用一对多的关系,你可能需要一个多对多的关系,这样多个人就可以访问同一个文档。用一个联接表重新实现它:

🌐 Instead of a one-to-many relationship between users and documents, you may require a many-to-many relationship so that multiple people can access the same document. Reimplement this using a join table:

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

那么你的 RLS 策略将会变成:

🌐 Then your RLS policy would change to:

1
create policy "Users can query their own document sections"
2
on document_sections for select to authenticated using (
3
document_id in (
4
select document_id
5
from document_owners
6
where (owner_id = (select auth.uid()))
7
)
8
);

我们没有直接查询 documents 表,而是查询了连接表。

🌐 Instead of directly querying the documents table, we query the join table.

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

🌐 User and document data live outside of Supabase

你可能有一个现有的系统,在单独的数据库中存储用户、文档及其权限。考虑一下这种情况:这些数据存在另一个 Postgres 数据库中。我们将使用外部数据封装器(FDW)从你的 Supabase 数据库连接到外部数据库:

🌐 You may have an existing system that stores users, documents, and their permissions in a separate database. Consider the scenario where this data exists in another Postgres database. We'll use a foreign data wrapper (FDW) to connect to the external DB from within your Supabase DB:

假设你的外部数据库包含像这样的 usersdocuments 表:

🌐 Assume your external DB contains a users and documents table like this:

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

在你的 Supabase 数据库中,创建与上述表关联的外部表:

🌐 In your Supabase DB, create foreign tables that link to the above tables:

1
create schema external;
2
create extension postgres_fdw with schema extensions;
3
4
-- Setup the foreign server
5
create server foreign_server
6
foreign data wrapper postgres_fdw
7
options (host '<db-host>', port '<db-port>', dbname '<db-name>');
8
9
-- Map local 'authenticated' role to external 'postgres' user
10
create user mapping for authenticated
11
server foreign_server
12
options (user 'postgres', password '<user-password>');
13
14
-- Import foreign 'users' and 'documents' tables into 'external' schema
15
import foreign schema public limit to (users, documents)
16
from server foreign_server into external;

我们会把 document_sections 及其嵌入存储在 Supabase 中,这样我们就可以通过 pgvector 对它们进行相似性搜索。

🌐 We'll store document_sections and their embeddings in Supabase so that we can perform similarity search over them via pgvector.

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

我们通过 document_id 保留对外部文档的引用,但不使用外键引用,因为外键只能添加到本地表中。记得使用和你的外部文档表相同的 ID 数据类型。

🌐 We maintain a reference to the foreign document via document_id, but without a foreign key reference since foreign keys can only be added to local tables. Be sure to use the same ID data type that you use on your external documents table.

既然我们在 Supabase 之外管理用户和身份验证,我们有两个选择:

🌐 Since we're managing users and authentication outside of Supabase, we have two options:

  1. 直接连接到 Supabase 数据库的 Postgres,并在每次请求时设置当前用户
  2. 从你的系统生成一个自定义 JWT,并用它来通过 REST API 进行认证

直接连接Postgres #

🌐 Direct Postgres connection

你可以直接使用项目页面上的连接信息连接到你的 Supabase Postgres 数据库。要用这种方法使用 RLS,我们会使用一个包含当前用户 ID 的自定义会话变量:

🌐 You can directly connect to your Supabase Postgres DB using the connection info on a project page. To use RLS with this method, we use a custom session variable that contains the current user's ID:

1
-- enable row level security
2
alter table document_sections enable row level security;
3
4
-- setup RLS for select operations
5
create policy "Users can query their own document sections"
6
on document_sections for select to authenticated using (
7
document_id in (
8
select id
9
from external.documents
10
where owner_id = current_setting('app.current_user_id')::bigint
11
)
12
);

会话变量是通过 current_setting() 函数访问的。我们这里将变量命名为 app.current_user_id,但你可以修改成任何你喜欢的名字。我们还将它转换为 bigint 类型,因为那是 user.id 列的数据类型。根据你使用的 ID 类型,将其改成相应的数据类型即可。

🌐 The session variable is accessed through the current_setting() function. We name the variable app.current_user_id here, but you can modify this to any name you like. We also cast it to a bigint since that was the data type of the user.id column. Change this to whatever data type you use for your ID.

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

🌐 Now for every request, we set the user's ID at the beginning of the session:

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

那么之后的所有查询都会继承该用户的权限:

🌐 Then all subsequent queries will inherit the permission of that user:

1
-- Only document sections owned by the user are returned
2
select *
3
from document_sections
4
where document_sections.embedding <#> embedding < -match_threshold
5
order by document_sections.embedding <#> embedding;

使用自定义 JWT 的 REST API #

🌐 Custom JWT with REST API

如果你想使用自动生成的 REST API,通过外部认证提供商的 JWT 来查询你的 Supabase 数据库,你可以让你的认证提供商为 Supabase 签发一个自定义的 JWT。

🌐 If you would like to use the auto-generated REST API to query your Supabase database using JWTs from an external auth provider, you can get your auth provider to issue a custom JWT for Supabase.

查看 Clerk Supabase 文档 了解如何操作的示例。根据需要修改这些说明以适应你自己的认证提供商。

🌐 See the Clerk Supabase docs for an example of how this can be done. Modify the instructions to work with your own auth provider as needed.

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

🌐 Now we can use the same RLS policy from our first example:

1
-- enable row level security
2
alter table document_sections enable row level security;
3
4
-- setup RLS for select operations
5
create policy "Users can query their own document sections"
6
on document_sections for select to authenticated using (
7
document_id in (
8
select id
9
from documents
10
where (owner_id = (select auth.uid()))
11
)
12
);

在底层,auth.uid() 引用 current_setting('request.jwt.claim.sub'),它对应于 JWT 的 sub(主题)声明。这个设置会在每次请求 REST API 开始时自动设置。

🌐 Under the hood, auth.uid() references current_setting('request.jwt.claim.sub') which corresponds to the JWT's sub (subject) claim. This setting is automatically set at the beginning of each request to the REST API.

之后的所有查询都会继承该用户的权限:

🌐 All subsequent queries will inherit the permission of that user:

1
-- Only document sections owned by the user are returned
2
select *
3
from document_sections
4
where document_sections.embedding <#> embedding < -match_threshold
5
order by document_sections.embedding <#> embedding;

其他情景 #

🌐 Other scenarios

针对这个问题有无数种方法,取决于每个系统的复杂性。幸运的是,Postgres 自带了所有提供访问控制所需的基础功能,你可以根据自己的项目需求来使用。

🌐 There are endless approaches to this problem based on the complexities of each system. Luckily Postgres comes with all the primitives needed to provide access control in the way that works best for your project.

如果上面的示例不适合你的使用情况,或者你需要稍微调整它们以更好地适应你现有的系统,随时可以联系 支持,我们很乐意为你提供帮助。

🌐 If the examples above didn't fit your use case or you need to adjust them slightly to better fit your existing system, feel free to reach out to support and we'll be happy to assist you.