1.18.1版本 Qdrant-Client 增删改查 C# sdk
📊 概念对照表
| Qdrant | 关系型数据库 | 说明 |
|---|---|---|
| Point | Row(行) | 一条数据记录 |
| Collection | Table(表) | 数据集合 |
| Vector | 列(特殊类型) | 向量数据(浮点数组) |
| Payload | 其他列 | 元数据(文本、数字等) |
| Point ID | Primary Key | 主键(唯一标识) |
| Index | Index | 索引(加速查询) |
🔍 核心区别
| 特性 | 关系型数据库 | Qdrant |
|---|---|---|
| 主要查询方式 | WHERE 条件匹配 |
向量相似度搜索 |
| 索引类型 | B-Tree、Hash | HNSW(向量索引) |
| 擅长场景 | 精确查询、事务 | 模糊匹配、语义搜索 |
| 事务支持 | ✅ 完整 ACID | ❌ 无事务 |
| JOIN | ✅ 支持 | ❌ 不支持 |
📊 形象类比
| 场景 | 关系型数据库 | Qdrant |
|---|---|---|
| 找联系人 | 按名字精确查找"张三" | 给一张照片,找长得最像的人 |
| 找房子 | 筛选"3 室 + 价格<500 万" | 给一个理想房子描述,找最匹配的 |
| 找商品 | 筛选"品牌=Apple + 价格<1 万" | 给一张商品图,找相似款式 |
PointStruct 和关系型数据库对比
是的!你的理解完全正确!
| Qdrant | 关系型数据库 | 说明 |
|---|---|---|
PointStruct |
Row / 记录 |
一行数据 |
Collection |
Table / 表 |
数据表 |
Id |
Primary Key |
主键 |
Vectors |
特殊列 | 向量数据(Qdrant 独有) |
Payload |
其他列 | JSON 格式的字段集合 |
🔑 核心区别
| 特性 | 关系型数据库 | Qdrant |
|---|---|---|
| 主键查询 | WHERE id = ? |
RetrieveAsync |
| 条件查询 | WHERE category = ? |
Filter + SearchAsync |
| 相似度搜索 | ❌ 不支持 | ✅ 核心功能 |
| 数据结构 | 固定列 | Payload 是灵活 JSON |
1、创建集合并建立索引
1.1、接合千问 text-embedding,生成真实向量值
using AiTest; using Qdrant.Client; using Qdrant.Client.Grpc; using Sdcb.DashScope; using System.Net.Http; using System.Text; using System.Text.Json; class Program { private static readonly HttpClient _httpClient = new HttpClient(); static async Task Main(string[] args) { Console.WriteLine("🚀 Qdrant + 通义千问嵌入 演示开始!\n"); var qdrantClient = new QdrantClient(ConstParm.QdrantHostIp, 6334, apiKey: ConstParm.QdrantApiKey); try { var health = await qdrantClient.HealthAsync(); Console.WriteLine($"✅ Qdrant 版本:{health.Version}\n"); } catch (Exception ex) { Console.WriteLine($"❌ Qdrant 连接失败:{ex.Message}"); return; } var collectionName = "qwen-embeddings"; try { // 1. 创建集合 Console.WriteLine("1️⃣ 创建集合"); var exists = await qdrantClient.CollectionExistsAsync(collectionName); if (exists) await qdrantClient.DeleteCollectionAsync(collectionName); await qdrantClient.CreateCollectionAsync(collectionName, new VectorParams { Size = 1536, Distance = Distance.Cosine }); // Keyword 索引 await qdrantClient.CreatePayloadIndexAsync(collectionName, "category", PayloadSchemaType.Keyword); // Float 索引 await qdrantClient.CreatePayloadIndexAsync(collectionName, "score", PayloadSchemaType.Float); // Integer 索引 await qdrantClient.CreatePayloadIndexAsync(collectionName, "created_at", PayloadSchemaType.Integer); // Bool 索引 await qdrantClient.CreatePayloadIndexAsync(collectionName, "is_published", PayloadSchemaType.Bool); // Text 索引 - 全文搜索 await qdrantClient.CreatePayloadIndexAsync(collectionName, "content", PayloadSchemaType.Text); Console.WriteLine("✅ 集合创建成功\n"); // 2. 插入数据 Console.WriteLine("2️⃣ 插入 50 条数据"); var insertedIds = await InsertDataWithQwen(qdrantClient, collectionName, 50); Console.WriteLine("\n✅ 演示完成!"); } catch (Exception ex) { Console.WriteLine($"\n❌ 错误:{ex.Message}"); Console.WriteLine(ex.StackTrace); } finally { qdrantClient.Dispose(); } } // ✅ 插入数据(修正 ID 格式) static async Task<List<string>> InsertDataWithQwen(QdrantClient client, string collectionName, int count) { var insertedIds = new List<string>(); var categories = new[] { "科技", "新闻", "科学", "体育", "娱乐" }; var random = new Random(42); var points = new List<PointStruct>(); for (int i = 0; i < count; i++) { var pointId = $"doc_{i:D4}"; // 业务 ID(存在 Payload 里) var category = categories[random.Next(categories.Length)]; var content = $"这是第 {i + 1} 条测试文档,内容是关于{category}的介绍"; Console.WriteLine($" [{i + 1}/{count}] 生成向量..."); float[] vector = await GenerateEmbeddingAsync(content, ConstParm.apiKey); points.Add(new PointStruct { // ✅ 修正 1:用 Guid 作为 Qdrant ID Id = new PointId { Uuid = Guid.NewGuid().ToString() }, // ✅ 修正 2:Vectors 直接赋值数组 Vectors = vector, Payload = { ["content"] = content, ["category"] = category, ["score"] = (float)(random.NextDouble() * 100), ["created_at"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - random.Next(0, 86400 * 30), ["author"] = $"author_{random.Next(1, 11)}", ["is_published"] = (i / 2 == 0), ["doc_id"] = pointId // ✅ 修正 3:业务 ID 存在 Payload 里 } }); if (points.Count >= 10 || i == count - 1) { var result = await client.UpsertAsync(collectionName, points); insertedIds.AddRange(points.Select(p => p.Id.ToString())); Console.WriteLine($" ✅ 插入 {points.Count} 条,状态:{result.Status}"); points.Clear(); } } Console.WriteLine($"\n✅ 成功插入 {insertedIds.Count} 条数据\n"); return insertedIds; } // ✅ HTTP 调用通义千问嵌入 API static async Task<float[]> GenerateEmbeddingAsync(string text, string apiKey) { ///兼容OpenAI方式地址 var _embeddingEndpoint = "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings"; var requestBody = new { model = "text-embedding-v2", input = text }; var json = JsonSerializer.Serialize(requestBody); var content = new StringContent(json, Encoding.UTF8, "application/json"); _httpClient.DefaultRequestHeaders.Clear(); _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); Console.WriteLine($" 请求端点:{_embeddingEndpoint}"); var response = await _httpClient.PostAsync(_embeddingEndpoint, content); var responseJson = await response.Content.ReadAsStringAsync(); Console.WriteLine($" 响应状态:{response.StatusCode}"); if (!response.IsSuccessStatusCode) { throw new Exception($"百炼 API 错误:{response.StatusCode}\n{responseJson}"); } using var doc = JsonDocument.Parse(responseJson); var embedding = doc.RootElement .GetProperty("data")[0] .GetProperty("embedding"); var vector = new float[embedding.GetArrayLength()]; int idx = 0; foreach (var item in embedding.EnumerateArray()) { vector[idx++] = item.GetSingle(); } return vector; } }
1.2、查询
📚 SearchAsync 方法参数详解
public async Task<IReadOnlyList<ScoredPoint>> SearchAsync( string collectionName, // ① 集合名称 ReadOnlyMemory<float> vector, // ② 查询向量 Filter? filter = null, // ③ 过滤条件 SearchParams? searchParams = null, // ④ 搜索参数 ulong limit = 10, // ⑤ 返回数量 ulong offset = 0, // ⑥ 偏移量(分页) WithPayloadSelector? payloadSelector = null, // ⑦ 是否返回 Payload WithVectorsSelector? vectorsSelector = null, // ⑧ 是否返回向量 float? scoreThreshold = null, // ⑨ 分数阈值 string? vectorName = null, // ⑩ 向量名称(多向量) ReadConsistency? readConsistency = null, // ⑪ 读一致性 ShardKeySelector? shardKeySelector = null, // ⑫ 分片选择 ReadOnlyMemory<uint>? sparseIndices = null, // ⑬ 稀疏向量 TimeSpan? timeout = null, // ⑭ 超时 CancellationToken cancellationToken = default // ⑮ 取消令牌
📋 参数详细说明
| # | 参数 | 类型 | 说明 | 常用值 |
|---|---|---|---|---|
| ① | collectionName |
string |
要搜索的集合名称 | "qwen-embeddings" |
| ② | vector |
ReadOnlyMemory<float> |
查询向量(嵌入模型生成) | float[] 隐式转换 |
| ③ | filter |
Filter? |
过滤条件(类别/分数等) | null 或 new Filter{...} |
| ④ | searchParams |
SearchParams? |
搜索算法参数 | null(用默认) |
| ⑤ | limit |
ulong |
返回结果数量 | 5, 10, 20 |
| ⑥ | offset |
ulong |
分页偏移量 | 0, 10, 20 |
| ⑦ | payloadSelector |
WithPayloadSelector? |
是否返回 Payload 数据 | new{Enable=true} |
| ⑧ | vectorsSelector |
WithVectorsSelector? |
是否返回向量数据 | new{Enable=false} |
| ⑨ | scoreThreshold |
float? |
最低相似度阈值 | null, 0.7f |
| ⑩ | vectorName |
string? |
命名向量(多向量场景) | null, "title" |
| ⑪ | readConsistency |
ReadConsistency? |
读一致性(分布式) | null(单机不用) |
| ⑫ | shardKeySelector |
ShardKeySelector? |
分片选择(分布式) | null(单机不用) |
| ⑬ | sparseIndices |
ReadOnlyMemory<uint>? |
稀疏向量索引 | null(稠密向量不用) |
| ⑭ | timeout |
TimeSpan? |
请求超时 | null, TimeSpan.FromSeconds(30) |
| ⑮ | cancellationToken |
CancellationToken |
取消令牌 | default |
🎯 常用调用示例
1. 基础搜索 Copy var results = await client.SearchAsync( collectionName: "docs", vector: queryVector, limit: 10, payloadSelector: new WithPayloadSelector { Enable = true }, vectorsSelector: new WithVectorsSelector { Enable = false } ); 2. 带过滤搜索 Copy var filter = new Filter { Must = { new Condition { Match = new Match { Keyword = "科技" } } } }; var results = await client.SearchAsync( collectionName: "docs", vector: queryVector, filter: filter, limit: 10, payloadSelector: new WithPayloadSelector { Enable = true }, vectorsSelector: new WithVectorsSelector { Enable = false } ); 3. 分页搜索 Copy var page1 = await client.SearchAsync( collectionName: "docs", vector: queryVector, limit: 10, offset: 0, // 第一页 payloadSelector: new WithPayloadSelector { Enable = true }, vectorsSelector: new WithVectorsSelector { Enable = false } ); var page2 = await client.SearchAsync( collectionName: "docs", vector: queryVector, limit: 10, offset: 10, // 第二页 payloadSelector: new WithPayloadSelector { Enable = true }, vectorsSelector: new WithVectorsSelector { Enable = false } ); 4. 带分数阈值 Copy var results = await client.SearchAsync( collectionName: "docs", vector: queryVector, limit: 10, scoreThreshold: 0.7f, // 只返回相似度>0.7 的结果 payloadSelector: new WithPayloadSelector { Enable = true }, vectorsSelector: new WithVectorsSelector { Enable = false } ); 5. 多向量搜索 Copy var results = await client.SearchAsync( collectionName: "docs", vector: titleVector, vectorName: "title", // 指定用标题向量搜索 limit: 10, payloadSelector: new WithPayloadSelector { Enable = true }, vectorsSelector: new WithVectorsSelector { Enable = false } ); 📊 Filter 过滤条件详解 Copy // 1. 精确匹配(Keyword/Integer/Bool) var filter1 = new Filter { Must = { new Condition { Match = new Match { Keyword = "科技" } }, new Condition { Match = new Match { Boolean = true } } } }; // 2. 范围查询(Float/Integer) var filter2 = new Filter { Must = { new Condition { Field = new FieldCondition { Key = "score", Range = new Qdrant.Client.Grpc.Range { Gte = 50f, Lte = 100f } } } } }; // 3. 排除条件(MustNot) var filter3 = new Filter { Must = { new Condition { Match = new Match { Keyword = "科技" } } }, MustNot = { new Condition { Match = new Match { Keyword = "过时" } } } }; // 4. 或条件(Should) var filter4 = new Filter { Must = { new Condition { Match = new Match { Keyword = "科技" } } }, Should = { new Condition { Match = new Match { Keyword = "AI" } }, new Condition { Match = new Match { Keyword = "人工智能" } } } };
📦 SearchBatchAsync 方法详解
🎯 作用
批量搜索:一次请求执行多个不同的向量搜索,返回多组结果。
🔄 对比:SearchAsync vs SearchBatchAsync
| 方法 | 用途 | 请求次数 | 返回结果 |
|---|---|---|---|
SearchAsync |
单次搜索 | 1 次请求 | 1 组结果 |
SearchBatchAsync |
批量搜索 | 1 次请求 | N 组结果 |
📝 方法签名详解
public async Task<IReadOnlyList<BatchResult>> SearchBatchAsync( string collectionName, // 集合名称 IReadOnlyList<SearchPoints> searches, // ⭐ 多个搜索请求 ReadConsistency? readConsistency = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default )
关键参数:SearchPoints
public class SearchPoints { public string CollectionName { get; set; } // 集合名称 public RepeatedField<float> Vector { get; set; } // 查询向量 public Filter? Filter { get; set; } // 过滤条件 public ulong Limit { get; set; } // 返回数量 public WithPayloadSelector? WithPayload { get; set; } public WithVectorsSelector? WithVectors { get; set; } // ... 其他参数 }
📖 Qdrant Filter 类详解
这个 Filter 类是 Qdrant 的查询过滤条件,类似 SQL 的 WHERE 子句。
var filter = new Filter { Must = { // ① 必须满足的条件(AND) new Condition { // ② 一个条件 Field = new FieldCondition { // ③ 字段条件 Key = "category", // ④ 字段名 Match = new Match { // ⑤ 匹配规则 Keyword = category // ⑥ 精确匹配值 } } } } };
📊 层级结构图
Filter(过滤器) │ ├── Must(必须满足 - AND) │ └── Condition(条件 1) │ └── Field(字段条件) │ ├── Key = "category"(字段名) │ └── Match(匹配规则) │ └── Keyword = "科技"(匹配值) │ ├── MustNot(必须不满足 - NOT) │ └── Condition(条件 2) │ └── Field(字段条件) │ ├── Key = "score" │ └── Range(范围规则) │ └── Lt = 30f │ └── Should(应该满足 - OR) └── Condition(条件 3) └── Field(字段条件) ├── Key = "category" └── Match(匹配规则) └── Keyword = "AI"
📋 每个属性的详细说明
① Filter - 过滤器容器
public class Filter { public RepeatedField<Condition> Must { get; } // 必须满足(AND) public RepeatedField<Condition> MustNot { get; } // 必须不满足(NOT) public RepeatedField<Condition> Should { get; } // 应该满足(OR) }
| 属性 | 逻辑 | SQL 等价 | 说明 |
|---|---|---|---|
Must |
AND | WHERE A AND B |
所有条件都必须满足 |
MustNot |
NOT | WHERE NOT A |
所有条件都不能满足 |
Should |
OR | WHERE A OR B |
满足任一条件即可 |
② Condition - 单个条件
public class Condition { public FieldCondition? Field { get; set; } // 字段条件(最常用) public GeoCondition? Geo { get; set; } // 地理条件 public HasIdCondition? HasId { get; set; } // ID 存在检查 public IsEmptyCondition? IsEmpty { get; set; }// 字段为空检查 public Filter? Filter { get; set; } // 嵌套 Filter }
常用的是 Field(字段条件)。
③ FieldCondition - 字段条件
public class FieldCondition { public string Key { get; set; } // 字段名(必填) public Match? Match { get; set; } // 精确匹配 public Range? Range { get; set; } // 范围查询 public GeoBoundingBox? GeoBoundingBox { get; set; } public GeoRadius? GeoRadius { get; set; } public ValuesCount? ValuesCount { get; set; } public string? GeoPolygon { get; set; } }
| 属性 | 用途 | 示例 |
|---|---|---|
Key |
字段名 | "category", "score" |
Match |
精确匹配 | category = "科技" |
Range |
范围查询 | score BETWEEN 50 AND 100 |
GeoBoundingBox |
地理矩形 | 地理围栏 |
IsEmpty |
字段为空 | category IS NULL |
④ Key - 字段名
Key = "category" // 匹配 Payload 中的 ["category"] 字段 Key = "score" // 匹配 Payload 中的 ["score"] 字段 Key = "is_published" // 匹配 Payload 中的 ["is_published"] 字段
必须与插入数据时的 Payload 键名一致!
⑤ Match - 精确匹配规则
public class Match { public string? Keyword { get; set; } // 字符串匹配 public long? Integer { get; set; } // 整数匹配 public double? Double { get; set; } // 浮点数匹配 public bool? Boolean { get; set; } // 布尔匹配 public RepeatedField<string> Text { get; set; } // 文本匹配 public RepeatedField<long> Integers { get; set; } // 整数数组 }
| 属性 | 类型 | 示例 |
|---|---|---|
Keyword |
字符串 | Keyword = "科技" |
Integer |
整数 | Integer = 123 |
Double |
浮点数 | Double = 85.5 |
Boolean |
布尔 | Boolean = true |
Text |
文本数组 | Text = { "AI", "人工智能" } |
⑥ Keyword - 字符串匹配值
Match = new Match { Keyword = "科技" } // 等价于 SQL: WHERE category = '科技'
📝 完整示例对比
示例 1:单个条件(AND)
// SQL: WHERE category = '科技' var filter = new Filter { Must = { new Condition { Field = new FieldCondition { Key = "category", Match = new Match { Keyword = "科技" } } } } };
示例 2:多个条件(AND)
// SQL: WHERE category = '科技' AND score >= 80 var filter = new Filter { Must = { new Condition { Field = new FieldCondition { Key = "category", Match = new Match { Keyword = "科技" } } }, new Condition { Field = new FieldCondition { Key = "score", Range = new Range { Gte = 80f } } } } };
示例 3:排除条件(NOT)
// SQL: WHERE NOT (category = '过时') var filter = new Filter { MustNot = { new Condition { Field = new FieldCondition { Key = "category", Match = new Match { Keyword = "过时" } } } } };
示例 4:或条件(OR)
// SQL: WHERE category = 'AI' OR category = '人工智能' var filter = new Filter { Should = { new Condition { Field = new FieldCondition { Key = "category", Match = new Match { Keyword = "AI" } } }, new Condition { Field = new FieldCondition { Key = "category", Match = new Match { Keyword = "人工智能" } } } } };
示例 5:组合条件(复杂查询)
// SQL: WHERE (category = '科技' AND score >= 80) // AND NOT (is_published = false) // AND (author = '张三' OR author = '李四') var filter = new Filter { Must = { // category = '科技' new Condition { Field = new FieldCondition { Key = "category", Match = new Match { Keyword = "科技" } } }, // score >= 80 new Condition { Field = new FieldCondition { Key = "score", Range = new Range { Gte = 80f } } }, // NOT (is_published = false) new Condition { Field = new FieldCondition { Key = "is_published", Match = new Match { Boolean = true } } } }, Should = { // author = '张三' OR author = '李四' new Condition { Field = new FieldCondition { Key = "author", Match = new Match { Keyword = "张三" } } }, new Condition { Field = new FieldCondition { Key = "author", Match = new Match { Keyword = "李四" } } } } };
🎯 快速参考表
| 需求 | C# 代码 |
|---|---|
category = "科技" |
Match = new Match { Keyword = "科技" } |
score > 80 |
Range = new Range { Gt = 80f } |
score >= 80 |
Range = new Range { Gte = 80f } |
score < 100 |
Range = new Range { Lt = 100f } |
score <= 100 |
Range = new Range { Lte = 100f } |
is_published = true |
Match = new Match { Boolean = true } |
author_id = 123 |
Match = new Match { Integer = 123 } |
category IN ("科技", "科学") |
Must = { ..., Should = { ... } } |
category != "过时" |
MustNot = { Match = new Match { Keyword = "过时" } } |
💡 使用技巧
- Must 是 AND - 所有条件都必须满足
- Should 是 OR - 满足任一即可(但通常配合 Must 使用)
- MustNot 是 NOT - 排除符合条件的
- 可以嵌套 - Filter 里面可以再套 Filter
- 空 Filter -
new Filter()表示无过滤(返回所有)
新增/查询案例
using AiTest; using Qdrant.Client; using Qdrant.Client.Grpc; using Sdcb.DashScope; using System.Net.Http; using System.Text; using System.Text.Json; using Google.Protobuf.Collections; // ✅ 添加这个 using class Program { private static readonly HttpClient _httpClient = new HttpClient(); static async Task Main(string[] args) { Console.WriteLine("🚀 Qdrant + 通义千问嵌入 演示开始!\n"); var qdrantClient = new QdrantClient(ConstParm.QdrantHostIp, 6334, apiKey: ConstParm.QdrantApiKey); try { var health = await qdrantClient.HealthAsync(); Console.WriteLine($"✅ Qdrant 版本:{health.Version}\n"); } catch (Exception ex) { Console.WriteLine($"❌ Qdrant 连接失败:{ex.Message}"); return; } var collectionName = "qwen-embeddings"; try { // 1. 创建集合 Console.WriteLine("1️⃣ 创建集合"); var exists = await qdrantClient.CollectionExistsAsync(collectionName); if (exists) await qdrantClient.DeleteCollectionAsync(collectionName); await qdrantClient.CreateCollectionAsync(collectionName, new VectorParams { Size = 1536, Distance = Distance.Cosine }); // Keyword 索引 await qdrantClient.CreatePayloadIndexAsync(collectionName, "category", PayloadSchemaType.Keyword); // Float 索引 await qdrantClient.CreatePayloadIndexAsync(collectionName, "score", PayloadSchemaType.Float); // Integer 索引 await qdrantClient.CreatePayloadIndexAsync(collectionName, "created_at", PayloadSchemaType.Integer); // Bool 索引 await qdrantClient.CreatePayloadIndexAsync(collectionName, "is_published", PayloadSchemaType.Bool); // Text 索引 - 全文搜索 await qdrantClient.CreatePayloadIndexAsync(collectionName, "content", PayloadSchemaType.Text); Console.WriteLine("✅ 集合创建成功\n"); // 2. 插入数据 Console.WriteLine("2️⃣ 插入 50 条数据"); var insertedIds = await InsertDataWithQwen(qdrantClient, collectionName, 50); Console.WriteLine("\n✅ 演示完成!"); } catch (Exception ex) { Console.WriteLine($"\n❌ 错误:{ex.Message}"); Console.WriteLine(ex.StackTrace); } finally { qdrantClient.Dispose(); } } // ✅ 插入数据(修正 ID 格式) static async Task<List<string>> InsertDataWithQwen(QdrantClient client, string collectionName, int count) { var insertedIds = new List<string>(); var categories = new[] { "科技", "新闻", "科学", "体育", "娱乐" }; var random = new Random(42); var points = new List<PointStruct>(); for (int i = 0; i < count; i++) { var pointId = $"doc_{i:D4}"; // 业务 ID(存在 Payload 里) var category = categories[random.Next(categories.Length)]; var content = $"这是第 {i + 1} 条测试文档,内容是关于{category}的介绍"; Console.WriteLine($" [{i + 1}/{count}] 生成向量..."); float[] vector = await GenerateEmbeddingAsync(content, ConstParm.apiKey); points.Add(new PointStruct { // ✅ 修正 1:用 Guid 作为 Qdrant ID Id = new PointId { Uuid = Guid.NewGuid().ToString() }, // ✅ 修正 2:Vectors 直接赋值数组 Vectors = vector, Payload = { ["content"] = content, ["category"] = category, ["score"] = (float)(random.NextDouble() * 100), ["created_at"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - random.Next(0, 86400 * 30), ["author"] = $"author_{random.Next(1, 11)}", ["is_published"] = (i / 2 == 0), ["doc_id"] = pointId // ✅ 修正 3:业务 ID 存在 Payload 里 } }); if (points.Count >= 10 || i == count - 1) { var result = await client.UpsertAsync(collectionName, points); insertedIds.AddRange(points.Select(p => p.Id.ToString())); Console.WriteLine($" ✅ 插入 {points.Count} 条,状态:{result.Status}"); points.Clear(); } } Console.WriteLine($"\n✅ 成功插入 {insertedIds.Count} 条数据\n"); return insertedIds; } // ✅ HTTP 调用通义千问嵌入 API static async Task<float[]> GenerateEmbeddingAsync(string text, string apiKey) { ///兼容OpenAI方式地址 var _embeddingEndpoint = "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings"; var requestBody = new { model = "text-embedding-v2", input = text }; var json = JsonSerializer.Serialize(requestBody); var content = new StringContent(json, Encoding.UTF8, "application/json"); _httpClient.DefaultRequestHeaders.Clear(); _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); Console.WriteLine($" 请求端点:{_embeddingEndpoint}"); var response = await _httpClient.PostAsync(_embeddingEndpoint, content); var responseJson = await response.Content.ReadAsStringAsync(); Console.WriteLine($" 响应状态:{response.StatusCode}"); if (!response.IsSuccessStatusCode) { throw new Exception($"百炼 API 错误:{response.StatusCode}\n{responseJson}"); } using var doc = JsonDocument.Parse(responseJson); var embedding = doc.RootElement .GetProperty("data")[0] .GetProperty("embedding"); var vector = new float[embedding.GetArrayLength()]; int idx = 0; foreach (var item in embedding.EnumerateArray()) { vector[idx++] = item.GetSingle(); } return vector; } } public class QdrantQueryService { private readonly QdrantClient _client; private readonly string _collectionName; private readonly string _apiKey; public QdrantQueryService(QdrantClient client, string collectionName, string apiKey) { _client = client; _collectionName = collectionName; _apiKey = apiKey; } // ========== 1. 向量相似度搜索 ========== public async Task<List<SearchResult>> SearchAsync( string queryText, int limit = 5, Filter? filter = null) { Console.WriteLine($"\n🔍 向量搜索:\"{queryText}\""); var queryVector = await GenerateEmbeddingAsync(queryText); var results = await _client.SearchAsync( collectionName: _collectionName, vector: queryVector, filter: filter, limit: (ulong)limit, offset: 0, payloadSelector: new WithPayloadSelector { Enable = true }, // ✅ 正确参数名 vectorsSelector: new WithVectorsSelector { Enable = false } // ✅ 正确参数名 ); return results.Select(r => new SearchResult { Id = r.Id.ToString(), Score = r.Score, DocId = GetPayloadValue(r.Payload, "doc_id"), Content = GetPayloadValue(r.Payload, "content"), Category = GetPayloadValue(r.Payload, "category"), ScoreValue = GetPayloadNumber(r.Payload, "score"), IsPublished = GetPayloadBool(r.Payload, "is_published") }).ToList(); } // ========== 2. 按类别过滤搜索 ========== public async Task<List<SearchResult>> SearchByCategoryAsync( string queryText, string category, int limit = 5) { Console.WriteLine($"\n🔍 按类别搜索:\"{queryText}\" (category = {category})"); var filter = new Filter { Must = { new Condition { Field = new FieldCondition { Key = "category", // ⭐ 字段名 Match = new Match { // ⭐ Match 在 FieldCondition 里 Keyword = category } } } } }; return await SearchAsync(queryText, limit, filter); } // ========== 3. 按分数范围搜索 ========== public async Task<List<SearchResult>> SearchByScoreRangeAsync( string queryText, float minScore, float maxScore, int limit = 5) { Console.WriteLine($"\n🔍 按分数范围搜索:\"{queryText}\" ({minScore} <= score <= {maxScore})"); var filter = new Filter { Must = { new Condition { Field = new FieldCondition { Key = "score", Range = new Qdrant.Client.Grpc.Range { Gte = minScore, Lte = maxScore } } } } }; return await SearchAsync(queryText, limit, filter); } // ========== 4. 组合条件搜索 ========== public async Task<List<SearchResult>> SearchWithFiltersAsync( string queryText, string? category = null, float? minScore = null, float? maxScore = null, bool? isPublished = null, int limit = 5) { Console.WriteLine($"\n🔍 组合条件搜索:\"{queryText}\""); var conditions = new List<Condition>(); if (!string.IsNullOrEmpty(category)) { conditions.Add(new Condition { Field = new FieldCondition { Key = "category", Match = new Match { Keyword = category } } }); Console.WriteLine($" 条件:category = {category}"); } if (minScore.HasValue || maxScore.HasValue) { var range = new Qdrant.Client.Grpc.Range(); if (minScore.HasValue) range.Gte = minScore.Value; if (maxScore.HasValue) range.Lte = maxScore.Value; conditions.Add(new Condition { Field = new FieldCondition { Key = "score", Range = range } }); Console.WriteLine($" 条件:{minScore} <= score <= {maxScore}"); } if (isPublished.HasValue) { conditions.Add(new Condition { Field = new FieldCondition { Key = "is_published", Match = new Match { Boolean = true } } }); Console.WriteLine($" 条件:is_published = {isPublished.Value}"); } var filter = new Filter(); filter.Must.AddRange(conditions); return await SearchAsync(queryText, limit, filter); } // ========== 5. 按 ID 精确查询 ========== public async Task<SearchResult?> GetByIdAsync(string id) { Console.WriteLine($"\n🔍 按 ID 查询:{id}"); var points = await _client.RetrieveAsync( collectionName: _collectionName, ids: new[] { new PointId { Uuid = id } }, withPayload: true ); if (points.Count == 0) return null; var p = points[0]; return new SearchResult { Id = p.Id.ToString(), Score = 0, DocId = GetPayloadValue(p.Payload, "doc_id"), Content = GetPayloadValue(p.Payload, "content"), Category = GetPayloadValue(p.Payload, "category"), ScoreValue = GetPayloadNumber(p.Payload, "score"), IsPublished = GetPayloadBool(p.Payload, "is_published") }; } // ========== 6. 获取统计信息 ========== public async Task<CollectionStats> GetStatsAsync() { Console.WriteLine("\n📊 获取统计信息"); var info = await _client.GetCollectionInfoAsync(_collectionName); return new CollectionStats { TotalPoints = info.PointsCount, VectorDimension = info.Config.Params.VectorsConfig.Params.Size, Distance = info.Config.Params.VectorsConfig.Params.Distance.ToString(), Status = info.Status.ToString(), Indexes = info.PayloadSchema.ToDictionary(k => k.Key, v => v.Value.DataType.ToString()) }; } // ========== 工具方法:生成嵌入向量 ========== private async Task<float[]> GenerateEmbeddingAsync(string text) { var endpoint = "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings"; var requestBody = new { model = "text-embedding-v2", input = text }; var json = JsonSerializer.Serialize(requestBody); var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Clear(); httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}"); var response = await httpClient.PostAsync(endpoint, content); var responseJson = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new Exception($"API 错误:{response.StatusCode}\n{responseJson}"); } using var doc = JsonDocument.Parse(responseJson); var embedding = doc.RootElement.GetProperty("data")[0].GetProperty("embedding"); var vector = new float[embedding.GetArrayLength()]; int idx = 0; foreach (var item in embedding.EnumerateArray()) { vector[idx++] = item.GetSingle(); } return vector; } // ========== 工具方法:解析 Payload(✅ 修正版)========== private static string GetPayloadValue(MapField<string, Value> payload, string key) { if (!payload.ContainsKey(key)) return "N/A"; var value = payload[key]; // ✅ 根据 KindCase 判断类型 switch (value.KindCase) { case Value.KindOneofCase.StringValue: return value.StringValue; case Value.KindOneofCase.DoubleValue: // ✅ 是 DoubleValue 不是 NumberValue return value.DoubleValue.ToString(); case Value.KindOneofCase.BoolValue: return value.BoolValue.ToString(); default: return "N/A"; } } private static double GetPayloadNumber(MapField<string, Value> payload, string key) { if (!payload.ContainsKey(key)) return 0; var value = payload[key]; // ✅ 用 DoubleValue return value.KindCase == Value.KindOneofCase.DoubleValue ? value.DoubleValue : 0; } private static bool GetPayloadBool(MapField<string, Value> payload, string key) { if (!payload.ContainsKey(key)) return false; var value = payload[key]; return value.KindCase == Value.KindOneofCase.BoolValue ? value.BoolValue : false; } } public class SearchResult { public string Id { get; set; } = ""; public float Score { get; set; } public string DocId { get; set; } = ""; public string Content { get; set; } = ""; public string Category { get; set; } = ""; public double ScoreValue { get; set; } public bool IsPublished { get; set; } } public class CollectionStats { public ulong TotalPoints { get; set; } public ulong VectorDimension { get; set; } public string Distance { get; set; } = ""; public string Status { get; set; } = ""; public Dictionary<string, string> Indexes { get; set; } = new(); }
删除数据案例
static async Task Main(string[] args) { Console.WriteLine("🚀 Qdrant + 通义千问嵌入 演示开始!\n"); var qdrantClient = new QdrantClient(ConstParm.QdrantHostIp, 6334, apiKey: ConstParm.QdrantApiKey); try { var health = await qdrantClient.HealthAsync(); Console.WriteLine($"✅ Qdrant 版本:{health.Version}\n"); } catch (Exception ex) { Console.WriteLine($"❌ Qdrant 连接失败:{ex.Message}"); return; } var collectionName = "qwen-embeddings"; try { // 1. 创建集合 QdrantQueryService qdrant = new QdrantQueryService(qdrantClient, collectionName, ConstParm.apiKey); var result = await qdrant.SearchByCategoryAsync("16", "新闻"); if (result.Count > 0) { var pointId = result[0].Id; // Guid 格式的 ID //精确条件--单个删除/批量删除 await qdrantClient.DeleteAsync( collectionName: collectionName, ids: new[] { new PointId { Uuid = pointId } }); Console.WriteLine($"删除Id为" + pointId + "的数据。"); } var CollectionInfo = await qdrantClient.GetCollectionInfoAsync(collectionName); Console.WriteLine($"✅ 单个删除后集合还剩: {CollectionInfo.PointsCount}条数据"); var filter = new Filter { Must = { new Condition { Field = new FieldCondition { Key = "category", Match = new Match { Keyword = "体育" } } } } }; Console.WriteLine("✅ 删除所有体育类数据"); await qdrantClient.DeleteAsync( collectionName: collectionName, filter: filter ); CollectionInfo = await qdrantClient.GetCollectionInfoAsync(collectionName); Console.WriteLine($"✅ 删除所有体育类数据后还剩: {CollectionInfo.PointsCount}条数据"); Console.WriteLine("\n✅ 演示完成!"); } catch (Exception ex) { Console.WriteLine($"\n❌ 错误:{ex.Message}"); Console.WriteLine(ex.StackTrace); } finally { qdrantClient.Dispose(); } }
更新数据案例
📝 Qdrant 更新数据的 4 种方式 根据你的需求选择: 方式 1:SetPayloadAsync —— 只更新部分字段(⭐ 最推荐) 适用场景:只改几个字段,其他保持不变 Copy // 先查询拿到 Point ID var result = await qdrant.SearchByCategoryAsync("测试", "新闻"); if (result.Count > 0) { var pointId = result[0].Id; // ✅ 纯 UUID 字符串 // 直接更新 payload,不需要查出来再写回去 await qdrantClient.SetPayloadAsync( collectionName: collectionName, payload: new Dictionary<string, Value> { ["content"] = "更新后的内容", ["score"] = 99.5f, ["is_published"] = true }, ids: new[] { new PointId { Uuid = pointId } } ); Console.WriteLine($"✅ 更新成功:{pointId}"); } 方式 2:UpsertAsync —— 完整覆盖(需要向量) 适用场景:要修改向量,或完整替换整条数据 Copy var result = await qdrant.SearchByCategoryAsync("测试", "新闻"); if (result.Count > 0) { var pointId = result[0].Id; // 1. 先查出来(需要向量) var retrieved = await qdrantClient.RetrieveAsync( collectionName: collectionName, ids: new[] { new PointId { Uuid = pointId } }, withPayload: true, withVectors: true // ⚠️ 必须获取向量 ); if (retrieved.Count == 0) return; var r = retrieved[0]; // 2. 手动转换为 PointStruct var pointStruct = new PointStruct { Id = r.Id, Vectors = r.Vectors, // 保留原向量(或修改) Payload = { } }; // 3. 复制原 payload foreach (var kvp in r.Payload) { pointStruct.Payload[kvp.Key] = kvp.Value; } // 4. 修改要更新的字段 pointStruct.Payload["content"] = "更新后的内容"; pointStruct.Payload["score"] = 99.5f; // 5. Upsert 覆盖 await qdrantClient.UpsertAsync( collectionName: collectionName, points: new[] { pointStruct } ); Console.WriteLine($"✅ 更新成功:{pointId}"); } 方式 3:OverwritePayloadAsync —— 覆盖整个 Payload 适用场景:清空原有 payload,完全替换 Copy await qdrantClient.OverwritePayloadAsync( collectionName: collectionName, payload: new Dictionary<string, Value> { ["content"] = "全新内容", ["category"] = "科技", ["score"] = 88.0f // ⚠️ 原有其他字段会被清空! }, ids: new[] { new PointId { Uuid = pointId } } ); 方式 4:DeletePayloadAsync —— 删除某个字段 适用场景:只删除某个字段,其他保留 Copy await qdrantClient.DeletePayloadAsync( collectionName: collectionName, keys: new[] { "author", "tags" }, // 删除这些字段 ids: new[] { new PointId { Uuid = pointId } } );
📊 对比表
| 方法 | 是否需要向量 | 影响范围 | 推荐场景 |
|---|---|---|---|
SetPayloadAsync |
❌ | 只更新指定字段 | ⭐ 最常用 |
UpsertAsync |
✅ | 完整覆盖 | 修改向量时用 |
OverwritePayloadAsync |
❌ | 清空后替换 | 重置数据时用 |
DeletePayloadAsync |
❌ | 删除指定字段 | 清理字段时用 |

浙公网安备 33010602011771号