Skip to content
Go back

pgvector + .NET 实战:用 PostgreSQL 做向量搜索

pgvector + .NET 实战:用 PostgreSQL 做向量搜索

不是每个 AI 功能都需要专门的向量数据库。

Pinecone、Qdrant、Weaviate 这些专用向量库固然强大,但如果你的数据本来就住在 PostgreSQL 里,引入一个新的基础设施组件意味着额外的部署、同步和运维成本。

pgvector 是一个 PostgreSQL 扩展,把向量存储和相似性搜索直接带进你的现有数据库。启用扩展,加一列 vector,就可以开始查询了。

这篇文章走一遍完整流程:

向量搜索是什么

传统数据库查询靠精确匹配。你搜索 "authentication",能找到包含这个词的行。但搜索 "login""sign-in""identity verification" 这些语义相近的词,LIKE 查询帮不上你。

向量搜索解决的是语义相似,而不是文本相似。

原理是这样的:把文本用机器学习模型转成一个浮点数数组,叫做 embedding(嵌入向量)。语义相近的文本会产生相近的向量。搜索时,把查询文本也转成向量,然后在数据库里找最近的向量。

常见用途:

关键判断:如果你已经在用 PostgreSQL,pgvector 能给你所有这些能力,不需要另一个数据库。

用 .NET Aspire 准备基础设施

.NET Aspire 在本地启动一个带 pgvector 的 PostgreSQL 容器,同时启动一个 Ollama 实例来生成 embedding:

var builder = DistributedApplication.CreateBuilder(args);

var ollama = builder.AddOllama("ollama")
    .WithLifetime(ContainerLifetime.Persistent)
    .WithDataVolume()
    .WithGPUSupport();

var embeddingModel = ollama.AddModel("qwen3-embedding:0.6b");

var postgres = builder.AddPostgres("postgres", port: 6432)
    .WithLifetime(ContainerLifetime.Persistent)
    .WithDataVolume()
    .WithImage("pgvector/pgvector", "pg17")
    .AddDatabase("articles");

builder.AddProject<Projects.PgVector_Articles>("pgvector-articles")
    .WithReference(embeddingModel)
    .WithReference(postgres)
    .WaitFor(embeddingModel)
    .WaitFor(postgres);

builder.Build().Run();

几个关键点:

不用 Aspire 也可以,直接用 docker composepgvector/pgvector:pg17,然后指定连接字符串即可。

配置 API 项目

需要三个 NuGet 包:

dotnet add package Aspire.Npgsql
dotnet add package Pgvector.Dapper
dotnet add package CommunityToolkit.Aspire.OllamaSharp

Pgvector.Dapper 提供 Dapper 的 Vector 类型处理器。如果你用 EF Core 或原生 Npgsql,也有对应的库可选。

Program.cs 里注册服务:

builder.AddOllamaApiClient("ollama-qwen3-embedding")
    .AddEmbeddingGenerator();

builder.AddNpgsqlDataSource("articles", configureDataSourceBuilder: b =>
{
    b.UseVector();
});

SqlMapper.AddTypeHandler(new VectorTypeHandler());

初始化数据库

在使用向量之前,需要启用扩展并建表:

app.MapPost("/init", async (NpgsqlDataSource dataSource) =>
{
    await using var conn = await dataSource.OpenConnectionAsync();

    await using var enableExt = new NpgsqlCommand(
        "CREATE EXTENSION IF NOT EXISTS vector", conn);
    await enableExt.ExecuteNonQueryAsync();

    conn.ReloadTypes();

    await conn.ExecuteAsync(
        """
        CREATE TABLE IF NOT EXISTS articles (
            id SERIAL PRIMARY KEY,
            url TEXT NOT NULL,
            title TEXT NOT NULL,
            embedding vector(1024) NOT NULL
        )
        """);

    await conn.ExecuteAsync(
        """
        CREATE INDEX IF NOT EXISTS articles_embedding_idx
        ON articles USING hnsw (embedding vector_cosine_ops)
        """);

    return Results.Ok("Database initialized.");
});

注意几点:

HNSW(Hierarchical Navigable Small World)是基于图的近似最近邻算法,通过多层结构加速相似性查找。数据量小时没有索引也能用,但随着数据增长,HNSW 能保持查询速度。

生成和存储 Embedding

给每条内容生成向量并存入数据库:

app.MapPost("/embeddings/generate", async (
    BlogService blogService,
    IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator,
    NpgsqlDataSource dataSource,
    ILogger<Program> logger) =>
{
    await using var conn = await dataSource.OpenConnectionAsync();
    conn.ReloadTypes();

    int count = 0;

    foreach (var articleUrl in File.ReadAllLines("sitemap_urls.txt"))
    {
        var (title, content) = await blogService.GetTitleAndContentAsync(articleUrl);

        var embedding = await embeddingGenerator.GenerateAsync(content);

        await conn.ExecuteAsync(
            "INSERT INTO articles (url, title, embedding) VALUES (@url, @title, @embedding)",
            new
            {
                url = articleUrl,
                title,
                embedding = new Vector(embedding.Vector.ToArray())
            });

        count++;
        logger.LogInformation("Processed ({Count}): {Url}", count, articleUrl);
    }

    return Results.Ok(new { processed = count });
});

embeddingGenerator.GenerateAsync(content) 把文本发给 Ollama 模型,返回一个向量。用 Pgvector.Vector 包装后,Dapper 自动处理剩下的事情。

IEmbeddingGenerator 是与提供商无关的抽象。如果以后想换成 OpenAI 或 Azure OpenAI,只改 Program.cs 里的注册,业务代码不用动。

相似性搜索

搜索时,把查询文本转成向量,然后找数据库中最近的向量:

app.MapGet("/search", async (
    string query,
    IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator,
    NpgsqlDataSource dataSource,
    int limit = 5) =>
{
    var searchEmbedding = await embeddingGenerator.GenerateAsync(query);

    await using var con = await dataSource.OpenConnectionAsync();
    con.ReloadTypes();

    var embedding = new Vector(searchEmbedding.Vector.ToArray());

    var results = await con.QueryAsync<SearchResult>(
        @"""
        SELECT title, url, embedding <=> @embedding as distance
        FROM articles
        ORDER BY embedding <=> @embedding
        LIMIT @limit
        """,
        new { embedding, limit });

    return Results.Ok(new { query, results });
});

record SearchResult(string Title, string Url, double Distance);

<=> 是 pgvector 的余弦距离操作符,值越小表示越相似。按距离升序排列,取前 N 条。

有一个关键约束:查询文本必须用和存储 embedding 时相同的模型来转换。不同模型产生的向量在不同的嵌入空间里,混用会得到没有意义的结果。

pgvector 支持三种距离操作符:

操作符含义索引类型
<->L2(欧氏)距离vector_l2_ops
<=>余弦距离vector_cosine_ops
<#>内积(负值)vector_ip_ops

文本 embedding 场景下通常选余弦距离。

小结

不需要专用向量数据库也能做语义搜索。如果你已经在用 PostgreSQL,pgvector 能让你直接在现有数据库里存储和查询向量,不用增加新的基础设施。

本文覆盖的要点:

向量数据就在关系数据旁边,联表、过滤、分页都可以直接用,不需要在两个数据库之间同步。


如果你关注 .NET 架构设计、软件工程实践和 AI 开发工具,可以关注 Aide Hub。这里会持续分享能落地的技术教程、工程经验和工具评测。

参考


Tags


Previous

.NET 6 到 .NET 10:LINQ 新增的 15 个方法,一文搞清楚

Next

别再用 DateTime.Now:用 TimeProvider 让 .NET 代码可测试