最近在做向量检索相关的功能技术栈是 .NET SqlSugar PostgreSQL pgvector。SqlSugar 官方对 pgvector 的支持比较有限网上能搜到的资料大多只解决了一半问题——要么只讲了插入、要么只讲了查询而且坑点散在好几个不同的地方单独看每一篇都跑不通。折腾了一天之后终于把整套方案打通了这里完整记录一下希望能帮后来人少走弯路。环境准备PostgreSQL 启用 pgvector 扩展CREATE EXTENSION IF NOT EXISTS vector;NuGet 安装两个包dotnet add package Pgvector dotnet add package Pgvector.Npgsql程序启动时注册 vector 类型映射必须否则 Npgsql 不认识 vector 类型NpgsqlConnection.GlobalTypeMapper.UseVector();核心难点SqlSugar pgvector 有两条独立的数据通路必须分别处理这是最容易踩坑的地方插入/更新路径走Insertable/UpdateableSqlSugar 会根据 .NET 类型自动推断参数DbType。Pgvector.Vector这个它不认识的类型会被推断成String导致 PostgreSQL 报22P02: invalid input syntax for type vector错误。查询表达式路径走 LINQ 的OrderBy/Select/Where里面调用相似度函数如-、、#需要通过SqlFuncExternal翻译成 SQL且参数查询向量必须以 pgvector 能识别的格式发送。把这两条路径分开理解整套方案就清晰了。第一步自定义 Converter解决插入/更新using System.Data; using SqlSugar; namespace xtop.core; public class PgVectorConverter : ISugarDataConverter { // Insert / Update 时触发 public SugarParameter ParameterConverterT(object columnValue, int columnIndex) { var name MyPgVector_ columnIndex; if (columnValue null) return new SugarParameter(name, null); if (columnValue is float[] floatArray) { var vectorValue new Pgvector.Vector(floatArray); return new SugarParameter(name, vectorValue) { // 【核心】必须显式指定为 Object // 否则 SqlSugar 会把 Pgvector.Vector 推断成 String // Npgsql 按字符串发送pgvector 列解析失败 // 设为 Object 后Npgsql 会走 GlobalTypeMapper.UseVector() 注册的原生映射 DbType DbType.Object }; } throw new Exception($不支持的向量参数类型: {columnValue.GetType().Name}); } // Select 时触发 public T QueryConverterT(IDataRecord dataRecord, int dataRecordIndex) { var columnValue dataRecord.GetValue(dataRecordIndex); if (columnValue null || columnValue DBNull.Value) return default; // 注册了 UseVector() 之后读出来直接就是 Pgvector.Vector 对象 if (columnValue is Pgvector.Vector vectorObj) { if (typeof(T) typeof(float[])) { return (T)(object)vectorObj.ToArray(); } } else if (columnValue is string str) // 兼容兜底 { if (string.IsNullOrWhiteSpace(str)) return default; var strArray str.Trim([, ]).Split(,); return (T)(object)strArray.Select(float.Parse).ToArray(); } throw new Exception($无法将向量转换至目标类型: {typeof(T).Name}); } }这里最关键的一行就是DbType DbType.Object。少了这一行所有插入操作都会失败。我在这上面卡了非常久。第二步实体定义[SugarTable(documents)] public class Document { [SugarColumn(IsPrimaryKey true, IsIdentity true)] public int Id { get; set; } public string Content { get; set; } [SugarColumn( ColumnDataType vector(1024), // 维度按你的 embedding 模型来 ColumnName contentvector, SqlParameterDbType typeof(PgVectorConverter))] public float[] ContentVector { get; set; } }注意ColumnDataType vector(1024)这一行是必须的。SqlSugar 的 CodeFirst 不认识 vector 类型需要原样指定。维度根据你用的 embedding 模型来选——OpenAI ada-002 是 1536BGE-large 是 1024等等。第三步定义占位扩展方法用于 LINQ 表达式namespace xtop.core; public static class PgVectorFunc { public static double L2Distance(float[] vectorColumn, float[] targetVector) throw new NotImplementedException(); public static double CosineDistance(float[] vectorColumn, float[] targetVector) throw new NotImplementedException(); public static double InnerProduct(float[] vectorColumn, float[] targetVector) throw new NotImplementedException(); }这些方法只在表达式树里使用运行时不会真的执行所以方法体直接抛异常就行。它们的作用是给 LINQ 提供强类型签名让 IDE 有提示、有类型检查。第四步注册 SqlFuncExternal解决查询这是整套方案里最巧妙的一步把上面三个占位方法翻译成 pgvector 的 SQL 操作符var SqlFuncList new ListSqlFuncExternal { new SqlFuncExternal { UniqueMethodName CosineDistance, MethodValue (expInfo, dbType, expContext) { // 1. 翻译列名 var colName expInfo.Args[0].MemberName?.ToString(); var col expContext.GetTranslationColumnName(colName); // 2. 取参数占位符名例如 MethodConst0 var valName expInfo.Args[1].MemberName?.ToString(); // 3. 【核心黑科技】拦截参数并改写值 // 从表达式上下文里找到这个参数如果它的值是 float[] // 就当场把它改成 pgvector 字面量字符串 var param expContext.Parameters.FirstOrDefault(p p.ParameterName valName); if (param ! null param.Value is float[] floatArr) { param.Value [ string.Join(,, floatArr) ]; } // 4. 用 ::vector 把字符串强转成 vector 类型 return $({col} {valName}::vector); } }, new SqlFuncExternal { UniqueMethodName L2Distance, MethodValue (expInfo, dbType, expContext) { var colName expInfo.Args[0].MemberName?.ToString(); var col expContext.GetTranslationColumnName(colName); var valName expInfo.Args[1].MemberName?.ToString(); var param expContext.Parameters.FirstOrDefault(p p.ParameterName valName); if (param ! null param.Value is float[] floatArr) { param.Value [ string.Join(,, floatArr) ]; } return $({col} - {valName}::vector); } }, new SqlFuncExternal { UniqueMethodName InnerProduct, MethodValue (expInfo, dbType, expContext) { var colName expInfo.Args[0].MemberName?.ToString(); var col expContext.GetTranslationColumnName(colName); var valName expInfo.Args[1].MemberName?.ToString(); var param expContext.Parameters.FirstOrDefault(p p.ParameterName valName); if (param ! null param.Value is float[] floatArr) { param.Value [ string.Join(,, floatArr) ]; } return $({col} # {valName}::vector); } } };为什么要伸手进参数集合改 ValueSqlSugar 在解析CosineDistance(it.ContentVector, vector)时会把vector一个float[]作为参数生成出来。但 SqlSugar 并不知道这个数组该按什么格式发给数据库默认推断会出问题。直接在调用方把数组转成字符串再传进去也能跑通但那样业务代码就脏了——每次查询都得手动转换一次。这个方案的精髓是在 SqlFunc 翻译的时候从expContext.Parameters里把已经生成好的参数找出来当场把 Value 改成字符串。配合 SQL 里的::vectorcast让 PostgreSQL 自己把字符串转回 vector 类型。这样一来业务代码可以保持完全的类型安全和直觉化所有的脏活都封装在了SqlFuncExternal内部。把这个 list 注册到 SqlSugarClientConfigureExternalServices new ConfigureExternalServices { SqlFuncServices SqlFuncList }第五步使用插入var doc new Document { Content hello vector, ContentVector embeddingFromModel // float[1024] }; await _rawRep.InsertAsync(doc);更新item.ContentVector vector; item.VectorStatus 2; item.VectorDate DateTime.Now; await _rawRep.AsUpdateable(item).ExecuteCommandAsync();相似度查询强类型 LINQvar list _rawRep.AsQueryable() // 可以混合其他业务条件 //.Where(it it.Id 0) // 强类型排序按余弦距离从近到远距离越小越相似 .OrderBy(it PgVectorFunc.CosineDistance(it.ContentVector, vector)) .Select(it new { it.Id, it.Content, // 在 Select 里直接把距离查出来方便前端展示匹配度 Distance PgVectorFunc.CosineDistance(it.ContentVector, vector) }) .Take(5) .ToList();可以看到业务代码非常干净和普通 LINQ 查询几乎没有区别。性能优化建索引数据量大了之后必须建索引否则全表扫描会非常慢-- HNSW 索引推荐查询快构建慢 CREATE INDEX ON documents USING hnsw (contentvector vector_cosine_ops); -- 或者 IVFFlat 索引构建快查询稍慢 CREATE INDEX ON documents USING ivfflat (contentvector vector_cosine_ops) WITH (lists 100);注意索引的操作符类必须和你查询用的距离函数匹配否则索引不会生效距离函数 SQL 操作符 索引操作符类CosineDistancevector_cosine_opsL2Distance-vector_l2_opsInnerProduct#vector_ip_ops踩坑总结按照我自己的踩坑顺序整理希望你不用再踩一遍DbType DbType.Object是 Converter 的灵魂。少这一行插入直接报22P02: invalid input syntax for type vector。NpgsqlConnection.GlobalTypeMapper.UseVector()必须在程序启动时调用。少了这步读取时会报Reading as System.Object is not supported for fields having DataTypeName public.vector。插入路径和查询路径是两条独立的管道要分别处理。Converter 解决插入/更新SqlFuncExternal 解决 LINQ 查询互不替代。SqlFuncExternal里改写param.Value是关键技巧。这样业务代码可以保持强类型 干净不用每次手动转字符串。::vectorcast 是必须的。因为参数被改成了字符串需要让 PostgreSQL 强转回 vector 类型。CosineDistance 越小越相似所以是OrderBy升序不是OrderByDescending。维度必须严格匹配。建表时声明的vector(N)和插入的float[]长度必须一致差一个都会报错。同一张表里所有向量维度也必须一致。embedding 模型一旦选定就和数据绑定了。换模型就必须重新生成所有向量没有捷径。所以选模型之前先想清楚。结语SqlSugar 没有官方的 pgvector 支持但通过ISugarDataConverter和SqlFuncExternal这两个扩展点完全可以做到强类型、干净的接入。希望这篇文章能帮到同样在折腾这个组合的朋友。如果有疑问或者更好的方案欢迎评论交流。