在AI应用中,一个经常出现的场景是进行检索。关于检索,一般有基于关键词的检索、基于语义的向量检索以及结合两种方式的混合检索。一般的数据库基本上都能够支持关键词检索(全文检索),而向量检索一般有相关的向量数据库做特定的优化。那么,如果我们的场景下想要实现这三种检索能力,并且我们的技术栈中已经使用了supabase,那么要怎么基于supabase来实现这些能力呢?是否需要引入额外的向量数据库呢?本文将描述如何在supabase中实现这些检索(关于supabase,可以参考我之前写过的文章)
Supabase中的语义检索
语义搜索主要解读的是用户查询背后的含义,而非确切的关键词。在某些场景下关键词描述可能不够准确,我们可能希望检索一些关键词背后的实际语义,这个时候就可以使用语义检索。从原理上来看,做语义检索时需要对已有的文档和查询进行嵌入,将它们转换成一个高维的向量,然后在向量空间中基于距离来衡量相似度,从而找到最相关的内容。在supabase中,为了实现语义检索,可以使用一个扩展:pgvector。接下来来看下官网的示例:
开启扩展
sql
create extension vector with schema extensions;
创建数据表
sql
create table documents (
id bigint primary key generated always as identity,
content text,
embedding vector(512)
);
如果已经有表了,可以通过以下方式增加一个列。
sql
alter table documents
add column embedding vector(512);
创建基于相似度度量的函数
最简单的进行语义检索的方法就是在数据库中创建一个函数:
sql
-- Match documents using cosine distance (<=>)
create or replace function match_documents (
query_embedding vector(512),
match_threshold float,
match_count int
)
returns setof documents
language sql
as $$
select *
from documents
where documents.embedding <=> query_embedding < 1 - match_threshold
order by documents.embedding <=> query_embedding asc
limit least(match_count, 200);
$$;
可以看到这里比较相似度时,用的是<=>,这表示相似度度量的距离是cosine distance。除了这种距离度量之外,还包括以下度量。
运算符 | 描述 | 使用场景 |
---|---|---|
<-> |
Euclidean distance描述向量空间中两点之间的直线距离 | 低维空间中高精度匹配 |
<#> |
negative inner product两个向量的内积的负值 | 向量已做归一化时处理最快 |
<=> |
cosine distance两个向量之间的夹角来计算的 | 未确认向量是否做归一化时安全的默认选择 |
执行检索
如果是在程序中调用,可以使用客户端的方式来进行调用。
csharp
const { data: documents } = await supabase.rpc('match_documents', {
query_embedding: embedding, // pass the query embedding
match_threshold: 0.78, // choose an appropriate threshold for your data
match_count: 10, // choose the number of matches
})
如果是在supabase的sql界面中,则也可以按照如下方式调用。
sql
select *
from match_documents(
'[...]'::vector(512), -- pass the query embedding
0.78, -- chose an appropriate threshold for your data
10 -- choose the number of matches
);
至此,就完成了在supabase中进行向量检索的能力。
Supabase中的关键词检索
在Supabase上,除了使用向量检索外,还可以使用关键词检索。关键词检索会利用关键词和文档内容精确匹配来找到对应内容,非常适合查找特定术语。关键词检索通常通过PostgreSQL的全文搜索功能实现。
创建数据表
在supabase上创建一个表,并添加一个tsvector
列以支持全文搜索。
sql
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT,
fts TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', content)) STORED
);
创建索引
为了高效的进行关键词检索,提高全文检索的性能,我们可以创建一个GIN索引。
scss
CREATE INDEX ON documents USING GIN(fts);
创建函数执行关键词搜索
sql
CREATE FUNCTION kw_match_documents(query_text TEXT, match_count INT)
RETURNS TABLE (id BIGINT, content TEXT, metadata JSONB, similarity REAL)
AS $$
BEGIN
RETURN QUERY EXECUTE
FORMAT('SELECT id, content, metadata, ts_rank(to_tsvector(content), plainto_tsquery($1)) AS similarity
FROM documents
WHERE to_tsvector(content) @@ plainto_tsquery($1)
ORDER BY similarity DESC
LIMIT $2')
USING query_text, match_count;
END;
$$ LANGUAGE PLPGSQL;
该函数使用plainto_tsquery
将查询文本转换为tsquery
,并结合ts_rank
计算关键词匹配的相关性得分。
执行检索
sql
SELECT * FROM kw_match_documents('your search query', 10);
Supabase中的混合检索
有时候单一的搜索方法可能不能完整的捕获我们的真实需求,这个时候可以考虑结合两种结合方式的优点,来实现混合检索。具体来说,首先,先分别执行每种搜索方法。关键词搜索是通过搜索内容中存在的特定单词或短语来进行的,它会产生自己的一组结果。语义搜索是通过理解搜索查询背后的上下文或含义,而不是所使用的特定单词来进行的,它会生成自己独特的结果。之后将它们合并成一个统一的列表。这是通过一个称为 "fusion" 的过程来实现的。fusion将两种搜索方法的结果收集起来,并根据特定的排名或评分系统将它们合并在一起。那么在supabase上应该如何实现呢?
创建数据表
sql
create table documents (
id bigint primary key generated always as identity,
content text,
fts tsvector generated always as (to_tsvector('english', content)) stored,
embedding vector(512)
);
创建索引
这一步中我们将为数据表创建用于全文搜索的gen索引和用于语义检索的HNSW索引。
sql
-- Create an index for the full-text search
create index on documents using gin(fts);
-- Create an index for the semantic vector search
create index on documents using hnsw (embedding vector_ip_ops);
创建混合检索方法
sql
create or replace function hybrid_search(
query_text text,
query_embedding vector(512),
match_count int,
full_text_weight float = 1,
semantic_weight float = 1,
rrf_k int = 50
)
returns setof documents
language sql
as $$
with full_text as (
select
id,
-- Note: ts_rank_cd is not indexable but will only rank matches of the where clause
-- which shouldn't be too big
row_number() over(order by ts_rank_cd(fts, websearch_to_tsquery(query_text)) desc) as rank_ix
from
documents
where
fts @@ websearch_to_tsquery(query_text)
order by rank_ix
limit least(match_count, 30) * 2
),
semantic as (
select
id,
row_number() over (order by embedding <#> query_embedding) as rank_ix
from
documents
order by rank_ix
limit least(match_count, 30) * 2
)
select
documents.*
from
full_text
full outer join semantic
on full_text.id = semantic.id
join documents
on coalesce(full_text.id, semantic.id) = documents.id
order by
coalesce(1.0 / (rrf_k + full_text.rank_ix), 0.0) * full_text_weight +
coalesce(1.0 / (rrf_k + semantic.rank_ix), 0.0) * semantic_weight
desc
limit
least(match_count, 30)
$$;
执行检索
sql
select
*
from
hybrid_search(
'Italian recipes with tomato sauce', -- user query
'[...]'::vector(512), -- embedding generated from user query
10
);
总结
至此,我们完成了在supabase中实现三种检索的方法的介绍。在这个过程中我们以具体代码的方式展示了应该如何去做,如果你的项目中也使用到了supabase并且也有这样的检索需求,那么本文应该会对你有所帮助。