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;
    }
}
View Code

 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 返回结果数量 51020
offset ulong 分页偏移量 01020
payloadSelector WithPayloadSelector? 是否返回 Payload 数据 new{Enable=true}
vectorsSelector WithVectorsSelector? 是否返回向量数据 new{Enable=false}
scoreThreshold float? 最低相似度阈值 null0.7f
vectorName string? 命名向量(多向量场景) null"title"
readConsistency ReadConsistency? 读一致性(分布式) null(单机不用)
shardKeySelector ShardKeySelector? 分片选择(分布式) null(单机不用)
sparseIndices ReadOnlyMemory<uint>? 稀疏向量索引 null(稠密向量不用)
timeout TimeSpan? 请求超时 nullTimeSpan.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 = "人工智能" } }
    }
};
View Code

📦 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 = "过时" } }

💡 使用技巧

  1. Must 是 AND - 所有条件都必须满足
  2. Should 是 OR - 满足任一即可(但通常配合 Must 使用)
  3. MustNot 是 NOT - 排除符合条件的
  4. 可以嵌套 - Filter 里面可以再套 Filter
  5. 空 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();
}
View Code

 删除数据案例

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();
    }
} 
View Code

更新数据案例

📝 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 } }
);
View Code

📊 对比表

方法是否需要向量影响范围推荐场景
SetPayloadAsync 只更新指定字段 ⭐ 最常用
UpsertAsync 完整覆盖 修改向量时用
OverwritePayloadAsync 清空后替换 重置数据时用
DeletePayloadAsync 删除指定字段 清理字段时用
posted @ 2026-07-31 18:03  天才卧龙  阅读(18)  评论(0)    收藏  举报