Net8-Qdrant.Client-1.19.0 反射实体动态创建集合及索引 及 插入数据封装

Net8-Qdrant.Client-1.19.0 反射实体动态创建集合及索引 及 插入数据封装

1、创建集合的封装

现有一个实体类,结构如下:

  public  class BookDto
 {
    /// <summary>
    /// 分类 - Keyword 索引
    /// </summary>
    [QdrantIndex(PayloadSchemaType.Keyword)]
    public string Category { get; set; } = "";

    /// <summary>
    /// 价格 - Float 索引
    /// </summary>
    [QdrantIndex(PayloadSchemaType.Float)]
    public float Price { get; set; }

    /// <summary>
    /// 创建时间 - Integer 索引
    /// </summary>
    [QdrantIndex(PayloadSchemaType.Integer)]
    public int CreatedAt { get; set; }

    /// <summary>
    /// 是否发布 - Bool 索引
    /// </summary>
    [QdrantIndex(PayloadSchemaType.Bool)]
    public bool IsPublished { get; set; }

    /// <summary>
    /// 内容 - Text 索引(全文搜索)
    /// </summary>
    [QdrantIndex(PayloadSchemaType.Text)]
    public string Description { get; set; } = "";

    /// <summary>
    /// 不需要索引的字段(不加特性即可)
    /// </summary>
    public string Author { get; set; } = "";  // ❌ 无索引
    public string Conent { get; set; } = "";  // ❌ 无索引
}

QdrantIndex 为自定义的属性

    /// <summary>
    /// 标记需要创建 Qdrant Payload 索引的字段
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public class QdrantIndexAttribute : Attribute
    {
        public PayloadSchemaType SchemaType { get; set; }
        public string? FieldName { get; set; }

        public QdrantIndexAttribute(PayloadSchemaType schemaType)
        {
            SchemaType = schemaType;
        }
    }

规则:实体类中带有 QdrantIndex  标识的,需要创建响应类型索引,譬如Category 需要创建 keyword索引,其他以此类推,无 QdrantIndex  标识的,则不创建索引!

好处是一个实体类既可以用于创建集合亦可以用于数据查询时映射实体

帮助类:

using Microsoft.Extensions.Logging;
using Qdrant.Client;
using Qdrant.Client.Grpc;
using System;
using System.ClientModel.Primitives;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks; 

namespace GroupChatAuto
{
     
    internal class CollectionCreateHelper
    { 

        private readonly QdrantClient _client;
        private readonly ILogger<CollectionCreateHelper>? _logger;

        public CollectionCreateHelper(QdrantClient client, ILogger<CollectionCreateHelper>? logger = null)
        {
            _client = client;
            _logger = logger;
        }

        // ========================================================================
        // 重载 1:简单参数(快速创建)
        // ========================================================================
        public async Task<(bool Success, string ErrorMessage)> CreateCollectionAsync(
            string collectionName,
            ulong size,
            Distance distance)
        {
            var config = new CollectionConfig
            {
                CollectionName = collectionName,
                VectorSize = size,
                Distance = distance,
                PayloadIndexes = new List<PayloadIndexParams>()
            };
            return await CreateCollectionAsync(config);
        }

        // ========================================================================
        // 重载 2:VectorParams(支持完整向量配置)
        // ========================================================================
        public async Task<(bool Success, string ErrorMessage)> CreateCollectionAsync(
            string collectionName,
            VectorParams vectorParams)
        {
            return await CreateCollectionAsync(collectionName, vectorParams, null);
        }

        // ========================================================================
        // 重载 3:VectorParams + Payload 索引(完整版)⭐
        // ========================================================================
        public async Task<(bool Success, string ErrorMessage)> CreateCollectionAsync(
            string collectionName,
            VectorParams vectorParams,
            IEnumerable<PayloadIndexParams>? payloadIndexes)
        {
            // ✅ 1. 参数验证
            if (string.IsNullOrWhiteSpace(collectionName))
            {
                return (false, "集合名称不能为空");
            }

            if (vectorParams == null)
            {
                return (false, "VectorParams 不能为空");
            }

            if (vectorParams.Size == 0 || vectorParams.Size > 65535)
            {
                return (false, $"向量维度无效:{vectorParams.Size}(有效范围:1-65535)");
            }

            // ✅ 2. 健康检查
            try
            {
                var health = await _client.HealthAsync(); 
            }
            catch (Exception ex)
            {
                _logger?.LogError(ex, "Qdrant 连接失败");
                return (false, $"Qdrant 连接失败:{ex.Message}");
            }

            // ✅ 3. 检查集合是否存在
            try
            {
                bool exists = await _client.CollectionExistsAsync(collectionName);
                if (exists)
                {
                    _logger?.LogWarning("集合已存在:{CollectionName}", collectionName);
                    return (false, $"集合 '{collectionName}' 已存在");
                }
            }
            catch (Exception ex)
            {
                _logger?.LogError(ex, "检查集合失败:{CollectionName}", collectionName);
                return (false, $"检查集合失败:{ex.Message}");
            }

            // ✅ 4. 创建集合(向量索引自动建立)
            try
            {
                await _client.CreateCollectionAsync(collectionName, vectorParams);
                _logger?.LogInformation(
                    "集合创建成功:{CollectionName} ({Size}维,{Distance}, HNSW_M={HnswM})",
                    collectionName,
                    vectorParams.Size,
                    vectorParams.Distance,
                    vectorParams.HnswConfig?.M ?? 0);
            }
            catch (Exception ex)
            {
                _logger?.LogError(ex, "创建集合失败:{CollectionName}", collectionName);
                return (false, $"创建集合失败:{ex.Message}");
            }

            // ✅ 5. 创建 Payload 索引(手动)
            if (payloadIndexes != null && payloadIndexes.Any())
            {
                foreach (var index in payloadIndexes)
                {
                    try
                    {
                        await _client.CreatePayloadIndexAsync(
                            collectionName,
                            index.FieldName,
                            index.SchemaType
                        );
                        _logger?.LogDebug(
                            "Payload 索引创建成功:{FieldName} ({SchemaType})",
                            index.FieldName,
                            index.SchemaType);
                    }
                    catch (Exception ex)
                    {
                        _logger?.LogWarning(
                            ex,
                            "Payload 索引创建失败:{FieldName} ({SchemaType})",
                            index.FieldName,
                            index.SchemaType);
                        // ⚠️ 索引失败不影响集合创建,继续下一个
                    }
                }
            }

            return (true, "");
        }

        // ========================================================================
        // 重载 4:配置对象(最清晰,推荐)
        // ========================================================================
        public async Task<(bool Success, string ErrorMessage)> CreateCollectionAsync(CollectionConfig config)
        {
            if (config == null)
            {
                return (false, "配置对象不能为空");
            }

            var vectorParams = new VectorParams
            {
                Size = config.VectorSize,
                Distance = config.Distance,
                HnswConfig = config.HnswConfig,
                OnDisk = config.OnDisk,
                QuantizationConfig = config.QuantizationConfig
            };

            return await CreateCollectionAsync(config.CollectionName, vectorParams, config.PayloadIndexes);
        }
    }

    // ============================================================================
    // 配置类
    // ============================================================================
    public class CollectionConfig
    {
        public string CollectionName { get; set; } = "";
        public ulong VectorSize { get; set; }
        public Distance Distance { get; set; } = Distance.Cosine;

        // 向量索引配置(可选)
        public HnswConfigDiff? HnswConfig { get; set; }
        public bool OnDisk { get; set; } = false;
        public QuantizationConfig? QuantizationConfig { get; set; }

        // Payload 索引列表
        public List<PayloadIndexParams> PayloadIndexes { get; set; } = new();
    }

    public class PayloadIndexParams
    {
        public string FieldName { get; set; } = "";
        public PayloadSchemaType SchemaType { get; set; }
    }


    /// <summary>
    /// 标记需要创建 Qdrant Payload 索引的字段
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public class QdrantIndexAttribute : Attribute
    {
        public PayloadSchemaType SchemaType { get; set; }
        public string? FieldName { get; set; }

        public QdrantIndexAttribute(PayloadSchemaType schemaType)
        {
            SchemaType = schemaType;
        }
    }

    public static class CollectionConfigExtensions
    {
        public static Dictionary<string, PayloadSchemaType> GetFieldTypesFromDto<T>()
        {
            var fieldTypes = new Dictionary<string, PayloadSchemaType>();
            var properties = typeof(T).GetProperties();

            foreach (var prop in properties)
            {
                var attr = prop.GetCustomAttribute<QdrantIndexAttribute>();
                if (attr != null)
                {
                    var fieldName = !string.IsNullOrEmpty(attr.FieldName)
                        ? attr.FieldName
                        : ToSnakeCase(prop.Name);

                    fieldTypes[fieldName] = attr.SchemaType;
                }
            }

            return fieldTypes;
        }

        public static List<PayloadIndexParams> GetPayloadIndexesFromDto<T>()
        {
            var indexes = new List<PayloadIndexParams>();
            var type = typeof(T);

            foreach (var prop in type.GetProperties())
            {
                var attr = prop.GetCustomAttribute<QdrantIndexAttribute>();
                if (attr == null)
                    continue;

                var fieldName = attr.FieldName ?? ToSnakeCase(prop.Name);
                indexes.Add(new PayloadIndexParams
                {
                    FieldName = fieldName,
                    SchemaType = attr.SchemaType
                });
            }

            return indexes;
        }

        private static string ToSnakeCase(string name)
        {
            return string.Concat(
                name.Select((c, i) =>
                    i > 0 && char.IsUpper(c) ? "_" + char.ToLower(c) : char.ToLower(c).ToString())
            ).TrimStart('_');
        }
    }

}
View Code

引用案例-main.cs

using GroupChatAuto.ConsoleApp1;
using GroupChatAuto.Services;
using Newtonsoft.Json;
using Qdrant.Client;
using Qdrant.Client.Grpc;
using System.ComponentModel;
using System.Reflection;

namespace GroupChatAuto
{
    internal class Program
    {
        static string CollectionName = "BookDb";
        static async Task Main(string[] args)
        {
            var qdrantClient = new QdrantClient(ConstParm.QdrantHostIp, 6334, apiKey: ConstParm.QdrantApiKey);

            var collectionInfo =await qdrantClient.GetCollectionInfoAsync(CollectionName);


            CollectionCreateHelper createHelper = new CollectionCreateHelper(qdrantClient);
            CollectionConfig config = new CollectionConfig()
            {
                CollectionName = CollectionName,
                Distance = Distance.Cosine,
                VectorSize = 1536,
                PayloadIndexes = CollectionConfigExtensions.GetPayloadIndexesFromDto<BookDto>()

            };
            var result = await createHelper.CreateCollectionAsync(config);
            if (result.Success)
            {
                Console.WriteLine($"{CollectionName}创建成功"); 
            }

            // 输出关键信息
            Console.WriteLine($"Payload数据量:{collectionInfo.PayloadSchema.Count}");
            Console.WriteLine($"状态:{collectionInfo.Status}");
            Console.WriteLine($"数据条数:{collectionInfo.PointsCount}");
            Console.WriteLine($"详细信息:");
            Console.WriteLine(JsonConvert.SerializeObject(collectionInfo));
            Console.WriteLine($"PayloadSchema-详细信息:" );
            Console.WriteLine(JsonConvert.SerializeObject(collectionInfo.PayloadSchema));
        }
    }

      public  class BookDto
     {
        /// <summary>
        /// 分类 - Keyword 索引
        /// </summary>
        [QdrantIndex(PayloadSchemaType.Keyword)]
        public string Category { get; set; } = "";

        /// <summary>
        /// 价格 - Float 索引
        /// </summary>
        [QdrantIndex(PayloadSchemaType.Float)]
        public float Price { get; set; }

        /// <summary>
        /// 创建时间 - Integer 索引
        /// </summary>
        [QdrantIndex(PayloadSchemaType.Integer)]
        public int CreatedAt { get; set; }

        /// <summary>
        /// 是否发布 - Bool 索引
        /// </summary>
        [QdrantIndex(PayloadSchemaType.Bool)]
        public bool IsPublished { get; set; }

        /// <summary>
        /// 内容 - Text 索引(全文搜索)
        /// </summary>
        [QdrantIndex(PayloadSchemaType.Text)]
        public string Description { get; set; } = "";

        /// <summary>
        /// 不需要索引的字段(不加特性即可)
        /// </summary>
        public string Author { get; set; } = "";  // ❌ 无索引
        public string Conent { get; set; } = "";  // ❌ 无索引
    }
}
 
View Code

 2、插入数据的封装

现在新增一个属性,用于标识实体类需要向量化的属性

    /// <summary>
    /// 标记用于生成向量的文本字段
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public class VectorizedTextAttribute : Attribute
    {
    }

实体类结构:

    public class BookDto
    {
        /// <summary>
        /// 分类 - Keyword 索引
        /// </summary>
        [QdrantIndex(PayloadSchemaType.Keyword)]
        public string Category { get; set; } = "";

        /// <summary>
        /// 价格 - Float 索引
        /// </summary>
        [QdrantIndex(PayloadSchemaType.Float)]
        public float Price { get; set; }

        /// <summary>
        /// 创建时间 - Integer 索引
        /// </summary>
        [QdrantIndex(PayloadSchemaType.Integer)]
        public long CreatedAt { get; set; }

        /// <summary>
        /// 是否发布 - Bool 索引
        /// </summary>
        [QdrantIndex(PayloadSchemaType.Bool)]
        public bool IsPublished { get; set; }

        /// <summary>
        /// 内容 - Text 索引(全文搜索)
        /// </summary>
        [QdrantIndex(PayloadSchemaType.Text)]
        public string Description { get; set; } = "";

        /// <summary>
        /// 不需要索引-需要向量化
        /// </summary> 
        [VectorizedText]
        public string Author { get; set; } = "";  //  向量化


        public string Conent { get; set; } = "";  // ❌ 无索引
    }

规则:实体类中带有 QdrantIndex  标识的,需要创建响应类型索引,譬如Category 需要创建 keyword索引,其他以此类推,无 QdrantIndex  标识的,则不创建索引!

实体类中带有 VectorizedText 标识的,需要向量化文本!

好处是一个实体类既可以用于创建集合亦可以用于数据查询时映射实体

帮助类

using Microsoft.Extensions.Logging;
using Qdrant.Client;
using Qdrant.Client.Grpc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace GroupChatAuto
{
    /// <summary>
    /// 标记用于生成向量的文本字段
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public class VectorizedTextAttribute : Attribute
    {
    }

    // ========================================================================
    // 数据项模型(业务层使用)
    // ========================================================================
    public class DataItem
    { 
        /// <summary>
        /// 业务 ID(可选,不传则自动生成)
        /// </summary>
        public string? BusinessId { get; set; }

        /// <summary>
        /// 待向量化的文本内容
        /// </summary>
        public string Text { get; set; } = "";

        /// <summary>
        /// 额外 Payload 数据(可选)
        /// </summary>
        public Dictionary<string, object?> ExtraPayload { get; set; } = new();
    }


    public static class DtoExtensions  // ✅ 必须是独立的静态类
    {
        /// <summary>
        /// 从 DTO 转换为 DataItem
        /// </summary>
        public static DataItem ToDataItem<T>(this T dto, string businessId)
        {
            var type = typeof(T);
            var extraPayload = new Dictionary<string, object?>();
            string text = "";

            foreach (var prop in type.GetProperties())
            {
                var value = prop.GetValue(dto);
                var fieldName = ToSnakeCase(prop.Name);

                // 检查是否有 [VectorizedText] 特性
                if (prop.GetCustomAttribute<VectorizedTextAttribute>() != null)
                {
                    text = value?.ToString() ?? "";
                }

                // 所有属性都加入 ExtraPayload
                extraPayload[fieldName] = value;
            }

            // 如果没有找到标记的字段,用所有字符串拼接
            if (string.IsNullOrEmpty(text))
            {
                text = string.Join(" ", extraPayload
                    .Where(kvp => kvp.Value is string)
                    .Select(kvp => kvp.Value!.ToString()));
            }

            return new DataItem
            {
                BusinessId = businessId,
                Text = text,
                ExtraPayload = extraPayload
            };
        }


        /// <summary>
        /// 批量转换为 DataItem(自动生成 BusinessId)⭐
        /// </summary>
        public static List<DataItem> ToDataItems<T>(
            this IEnumerable<T> dtos,
            string businessIdPrefix = "doc",
            int startIndex = 0)
        {
            var items = new List<DataItem>();
            var dtosList = dtos.ToList();

            for (int i = 0; i < dtosList.Count; i++)
            {
                var dto = dtosList[i];
                var businessId = $"{businessIdPrefix}_{(startIndex + i):D4}";
                items.Add(dto.ToDataItem(businessId));
            }

            return items;
        }

        /// <summary>
        /// 批量转换为 DataItem(使用自定义 BusinessId 生成器)⭐
        /// </summary>
        public static List<DataItem> ToDataItems<T>(
            this IEnumerable<T> dtos,
            Func<T, int, string> businessIdSelector)
        {
            var items = new List<DataItem>();
            var dtosList = dtos.ToList();

            for (int i = 0; i < dtosList.Count; i++)
            {
                var dto = dtosList[i];
                var businessId = businessIdSelector(dto, i);
                items.Add(dto.ToDataItem(businessId));
            }

            return items;
        }

        /// <summary>
        /// 批量转换为 DataItem(使用自定义 BusinessId 生成器,无索引)
        /// </summary>
        public static List<DataItem> ToDataItems<T>(
            this IEnumerable<T> dtos,
            Func<T, string> businessIdSelector)
        {
            return dtos.Select((dto, index) => dto.ToDataItem(businessIdSelector(dto)))
                       .ToList();
        }

        /// <summary>
        /// 驼峰转蛇形命名(C# Property → Qdrant Field)
        /// </summary>
        private static string ToSnakeCase(string name)  // ✅ 添加这个方法
        {
            if (string.IsNullOrEmpty(name))
                return name;

            return string.Concat(
                name.Select((c, i) =>
                    i > 0 && char.IsUpper(c)
                        ? "_" + char.ToLower(c)
                        : char.ToLower(c).ToString())
            ).TrimStart('_');
        }
    }

    // ========================================================================
    // 插入配置
    // ========================================================================
    public class InsertConfig
    {
        /// <summary>
        /// 批量大小(默认 10,阿里云 API 限制)
        /// </summary>
        public int BatchSize { get; set; } = 10;

        /// <summary>
        /// 是否自动生成业务 ID(默认 true)
        /// </summary>
        public bool AutoGenerateBusinessId { get; set; } = true;

        /// <summary>
        /// 业务 ID 前缀(默认 "doc")
        /// </summary>
        public string BusinessIdPrefix { get; set; } = "doc";

        /// <summary>
        /// 进度回调(可选)
        /// </summary>
        public Func<int, int, Task>? ProgressCallback { get; set; }
    }

    // ========================================================================
    // 插入结果
    // ========================================================================
    public class InsertResult
    {
        public bool Success { get; set; }
        public string ErrorMessage { get; set; } = "";
        public int TotalCount { get; set; }
        public int SuccessCount { get; set; }
        public int FailedCount { get; set; }
        public List<string> InsertedIds { get; set; } = new();
        public List<(DataItem Item, string Error)> FailedItems { get; set; } = new();
        public TimeSpan Elapsed { get; set; }
    }

    // ========================================================================
    // Qdrant 数据服务类 ⭐
    // ========================================================================
    // ========================================================================
    // Qdrant 数据服务类 ⭐ 修复版
    // ========================================================================
    public class QdrantDataService
    {
        private readonly QdrantClient _client;
        private readonly Func<string, Task<float[]>> _embeddingFunc;
        private readonly ILogger<QdrantDataService>? _logger;

        public QdrantDataService(
            QdrantClient client,
            Func<string, Task<float[]>> embeddingFunc,
            ILogger<QdrantDataService>? logger = null)
        {
            _client = client;
            _embeddingFunc = embeddingFunc;
            _logger = logger;
        }

        /// <summary>
        /// 插入单条数据
        /// </summary>
        public async Task<(bool Success, string QdrantId, string ErrorMessage)> InsertAsync(
            string collectionName,
            DataItem item,
            CancellationToken cancellationToken = default)
        {
            try
            {
                // 1. 生成向量
                float[] vector;
                try
                {
                    vector = await _embeddingFunc(item.Text);
                }
                catch (Exception ex)
                {
                    _logger?.LogError(ex, "向量生成失败:{Text}", item.Text);
                    return (false, "", $"向量生成失败:{ex.Message}");
                }

                // 2. 构建 Point
                var point = BuildPoint(item, vector);

                // 3. 插入 Qdrant
                var result = await _client.UpsertAsync(
                    collectionName: collectionName,
                    points: new[] { point },
                    cancellationToken: cancellationToken);

                _logger?.LogDebug("插入成功:{Id}", point.Id);
                return (true, point.Id.ToString(), "");
            }
            catch (Exception ex)
            {
                _logger?.LogError(ex, "插入失败:{BusinessId}", item.BusinessId);
                return (false, "", $"插入失败:{ex.Message}");
            }
        }

        /// <summary>
        /// 批量插入数据(推荐)⭐ 修复版
        /// </summary>
        public async Task<InsertResult> InsertBatchAsync(
            string collectionName,
            IEnumerable<DataItem> items,
            InsertConfig? config = null,
            CancellationToken cancellationToken = default)
        {
            var stopwatch = System.Diagnostics.Stopwatch.StartNew();
            config ??= new InsertConfig();

            var result = new InsertResult
            {
                TotalCount = items.Count(),
                InsertedIds = new List<string>(),
                FailedItems = new List<(DataItem, string)>()
            };

            if (result.TotalCount == 0)
            {
                result.Success = true;
                result.Elapsed = stopwatch.Elapsed;
                return result;
            }

            var batch = new List<PointStruct>();
            var dataItemList = items.ToList();
            int processedCount = 0;
            int batchNumber = 0;

            for (int i = 0; i < dataItemList.Count; i++)
            {
                cancellationToken.ThrowIfCancellationRequested();
                var item = dataItemList[i];

                // 1. 生成向量
                float[] vector;
                try
                {
                    vector = await _embeddingFunc(item.Text);
                }
                catch (Exception ex)
                {
                    _logger?.LogWarning(ex, "向量生成失败:{Text}", item.Text);
                    result.FailedItems.Add((item, $"向量生成失败:{ex.Message}"));
                    result.FailedCount++;
                    continue;
                }

                // 2. 构建 Point
                var point = BuildPoint(item, vector);
                batch.Add(point);

                // 3. 批量提交 ✅ 修复:达到 BatchSize 或最后一条时提交整个 batch
                if (batch.Count >= config.BatchSize || i == dataItemList.Count - 1)
                {
                    batchNumber++;
                    try
                    {
                        // ✅ 修复:传整个 batch,不是单个 point
                        var upsertResult = await _client.UpsertAsync(
                            collectionName: collectionName,
                            points: batch,  // ← 传整个 batch!
                            cancellationToken: cancellationToken);

                        // 记录成功插入的 ID
                        foreach (var p in batch)
                        {
                            result.InsertedIds.Add(p.Id.ToString());
                        }
                        result.SuccessCount += batch.Count;

                        _logger?.LogInformation(
                            "批量插入 #{BatchNumber}: {Count} 条,状态:{Status}",
                            batchNumber,
                            batch.Count,
                            upsertResult.Status);
                    }
                    catch (Exception ex)
                    {
                        _logger?.LogError(ex, "批量插入失败 #{BatchNumber}", batchNumber);
                        foreach (var p in batch)
                        {
                            var failedItem = dataItemList[i - batch.Count + batch.IndexOf(p)];
                            result.FailedItems.Add((failedItem, ex.Message));
                            result.FailedCount++;
                        }
                    }

                    processedCount += batch.Count;
                    batch.Clear();

                    // 进度回调
                    if (config.ProgressCallback != null)
                    {
                        await config.ProgressCallback(processedCount, result.TotalCount);
                    }
                }
            }

            result.Success = result.FailedCount == 0;
            result.Elapsed = stopwatch.Elapsed;

            _logger?.LogInformation(
                "插入完成:总计{Total},成功{Success},失败{Failed},耗时{Elapsed}",
                result.TotalCount,
                result.SuccessCount,
                result.FailedCount,
                result.Elapsed);

            return result;
        }

        /// <summary>
        /// 从对象构建 Point
        /// </summary>
        private PointStruct BuildPoint(DataItem item, float[] vector)
        {
            // 生成 Qdrant ID(Guid 格式)
            var qdrantId = Guid.NewGuid().ToString();

            // 生成业务 ID(如果未提供)
            if (string.IsNullOrEmpty(item.BusinessId))
            {
                item.BusinessId = qdrantId;
            }

            var point = new PointStruct
            {
                Id = new PointId { Uuid = qdrantId },
                Vectors = vector,
                Payload =
            {
                // 业务 ID(用于后续查询/更新)
                ["business_id"] = item.BusinessId,
                // 原始文本
                ["text"] = item.Text,
                // 创建时间
                ["created_at"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
            }
            };

            // 添加额外 Payload
            foreach (var kvp in item.ExtraPayload)
            {
                point.Payload[kvp.Key] = ConvertToValue(kvp.Value);
            }

            return point;
        }

        /// <summary>
        /// 对象转 Qdrant Value
        /// </summary>
        private static Value ConvertToValue(object? value)
        {
            if (value == null)
                return new Value(); // 空 Value

            return value switch
            {
                string s => new Value { StringValue = s },
                int i => new Value { IntegerValue = i },
                long l => new Value { IntegerValue = l },
                float f => new Value { DoubleValue = f },
                double d => new Value { DoubleValue = d },
                bool b => new Value { BoolValue = b },
                DateTime dt => new Value { IntegerValue = new DateTimeOffset(dt).ToUnixTimeSeconds() },
                DateTimeOffset dto => new Value { IntegerValue = dto.ToUnixTimeSeconds() },
                IEnumerable<object> list => new Value
                {
                    ListValue = new ListValue { Values = { list.Select(ConvertToValue) } }
                },
                _ => new Value { StringValue = value.ToString() ?? "" }
            };
        }
    }
}
View Code

引用实例-main.cs

using GroupChatAuto.ConsoleApp1;
using GroupChatAuto.Services;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Qdrant.Client;
using Qdrant.Client.Grpc;
using System.ComponentModel;
using System.Reflection;

namespace GroupChatAuto
{
    internal class Program
    {
        static string CollectionName = "BookDb";
        static async Task Main(string[] args)
        {
            var qdrantClient = new QdrantClient(ConstParm.QdrantHostIp, 6334, apiKey: ConstParm.QdrantApiKey);

            // 2. 定义向量生成函数(调用阿里云 API)
            Func<string, Task<float[]>> embeddingFunc = async (text) =>
            {
                // 调用你的 Embedding API
                return await AliyunEmbeddingService.GenerateEmbeddingAsync(text);
            };

            // 3. 创建数据服务
            var dataService = new QdrantDataService(
                qdrantClient,
                embeddingFunc
            );
            #region 单条插入
            //// 使用
            //var book = new BookDto
            //{
            //    Description = "这是一本武侠小说",
            //    Category = "科幻",
            //    Price = 59.9f,
            //    Author = "陈大六",
            //    Conent = "这是一本科幻小说:天龙八部",
            //    CreatedAt = 100,
            //    IsPublished = true
            //};

            //var item = book.ToDataItem("BOOK_001");  // ✅ 现在可以用了

            //var result = await dataService.InsertAsync(CollectionName, item);
            //if (result.Success)
            //{
            //    Console.WriteLine("插入数据成功");
            //}
            #endregion


            var books = new List<BookDto>();
            {
                books.Add(new BookDto
                {
                    Description = "这是一本科幻小说,讲述了一个关于未来世界的故事",
                    Category = "科幻",
                    Price = 59.9f,
                    Author = "刘慈欣",
                    Conent = "三体:地球往事",
                    CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
                    IsPublished = true
                });
                books.Add(new BookDto
                {
                    Description = "这是一本武侠小说,讲述了江湖恩怨和武林争霸",
                    Category = "武侠",
                    Price = 49.9f,
                    Author = "金庸",
                    Conent = "天龙八部:塞外牛羊空许约",
                    CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
                    IsPublished = true
                });
                books.Add(new BookDto
                {
                    Description = "这是一本言情小说,讲述了都市男女的爱情故事",
                    Category = "言情",
                    Price = 39.9f,
                    Author = "琼瑶",
                    Conent = "还珠格格:山无棱天地合",
                    CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
                    IsPublished = true
                });
                books.Add(new BookDto
                {
                    Description = "这是一本悬疑小说,讲述了一个离奇的谋杀案",
                    Category = "悬疑",
                    Price = 45.9f,
                    Author = "东野圭吾",
                    Conent = "嫌疑人 X 的献身",
                    CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
                    IsPublished = true
                });
            }

            // ✅ 配置批量参数
            var config = new InsertConfig
            {
                BatchSize = 10,  // 每批 10 条(阿里云 API 限制)
                AutoGenerateBusinessId = false,  // 使用 DTO 中手动指定的 BusinessId
                BusinessIdPrefix = "BOOK",  // 如果自动生成 ID,前缀为 "BOOK"
                ProgressCallback = async (processed, total) =>
                {
                    // 进度回调
                    Console.WriteLine($"📊 进度:{processed}/{total} ({processed * 100.0 / total:F1}%)");
                    await Task.CompletedTask;
                }
            };
            var items = books.ToDataItems("BOOK", startIndex: 100);
            // ✅ 批量插入(传入 config)
            var result = await dataService.InsertBatchAsync(CollectionName, items, config);

            // ✅ 输出结果
            if (result.Success)
            {
                Console.WriteLine($@"
✅ 批量插入成功!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  总计:{result.TotalCount} 条
  成功:{result.SuccessCount} 条
  失败:{result.FailedCount} 条
  耗时:{result.Elapsed}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
");

                Console.WriteLine("插入的 Qdrant ID:");
                foreach (var id in result.InsertedIds)
                {
                    Console.WriteLine($"  - {id}");
                }
            }
            else
            {
                Console.WriteLine($"❌ 批量插入失败:{result.ErrorMessage}");

                if (result.FailedItems.Any())
                {
                    Console.WriteLine("\n失败详情:");
                    foreach (var (item, error) in result.FailedItems)
                    {
                        Console.WriteLine($"  - {item.BusinessId}: {error}");
                    }
                }
            }
        }

      
    }

    public class BookDto
    {
        /// <summary>
        /// 分类 - Keyword 索引
        /// </summary>
        [QdrantIndex(PayloadSchemaType.Keyword)]
        public string Category { get; set; } = "";

        /// <summary>
        /// 价格 - Float 索引
        /// </summary>
        [QdrantIndex(PayloadSchemaType.Float)]
        public float Price { get; set; }

        /// <summary>
        /// 创建时间 - Integer 索引
        /// </summary>
        [QdrantIndex(PayloadSchemaType.Integer)]
        public long CreatedAt { get; set; }

        /// <summary>
        /// 是否发布 - Bool 索引
        /// </summary>
        [QdrantIndex(PayloadSchemaType.Bool)]
        public bool IsPublished { get; set; }

        /// <summary>
        /// 内容 - Text 索引(全文搜索)
        /// </summary>
        [QdrantIndex(PayloadSchemaType.Text)]
        public string Description { get; set; } = "";

        /// <summary>
        /// 不需要索引-需要向量化
        /// </summary> 
        [VectorizedText]
        public string Author { get; set; } = "";  //  向量化


        public string Conent { get; set; } = "";  // ❌ 无索引
    }
}
 
View Code

阿里云向量化-1536

using GroupChatAuto.ConsoleApp1;
using Microsoft.Extensions.Logging;
using OpenAI;
using OpenAI.Embeddings;
using System;
using System.ClientModel;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace GroupChatAuto.Services
{
    internal class AliyunEmbeddingService
    {
        private static readonly HttpClient _httpClient = new HttpClient
        {
            Timeout = TimeSpan.FromSeconds(30)
        };

        private const string _apiHost = $"https://{ConstParm.WorkspaceId}.cn-beijing.maas.aliyuncs.com/compatible-mode/v1";
        private const int _vectorDimensions = 1536;  // ✅ 1536 维度

        // ✅ 改用支持 1536 的模型
        private const string _embeddingModel = "qwen3.7-text-embedding";
        // 或 "text-embedding-v4" 或 "text-embedding-v2"

        public static async Task<List<float[]>> GenerateEmbeddingListAsync(List<string> texts)
        {
            if (texts.Count == 0) return new List<float[]>();

            var allResults = new List<float[]>();
            var batches = texts.Chunk(10);

            foreach (var batch in batches)
            {
                var batchResults = await BatchEmbedAsync(batch.ToList());
                allResults.AddRange(batchResults);
            }

            return allResults;
        }

        private static async Task<List<float[]>> BatchEmbedAsync(List<string> texts)
        {
            var requestBody = new
            {
                model = _embeddingModel,  // ✅ qwen3.7-text-embedding
                input = texts,
                parameters = new
                {
                    dimensions = _vectorDimensions  // ✅ 1536
                }
            };

            var json = JsonSerializer.Serialize(requestBody);
            var content = new StringContent(json, Encoding.UTF8, "application/json");

            using var request = new HttpRequestMessage(HttpMethod.Post, $"{_apiHost}/embeddings");
            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", ConstParm.apiKey);
            request.Content = content;

            var response = await _httpClient.SendAsync(request);
            var responseJson = await response.Content.ReadAsStringAsync();

            if (!response.IsSuccessStatusCode)
            {
                throw new Exception($"百炼 API 错误:{response.StatusCode}\n{responseJson}");
            }

            using var doc = JsonDocument.Parse(responseJson);
            var dataArray = doc.RootElement.GetProperty("data");

            var results = new List<float[]>();
            foreach (var item in dataArray.EnumerateArray())
            {
                var embedding = item.GetProperty("embedding");
                var vector = new float[embedding.GetArrayLength()];  // ✅ 应该是 1536
                int idx = 0;
                foreach (var v in embedding.EnumerateArray())
                {
                    vector[idx++] = v.GetSingle();
                }
                results.Add(vector);
            }

            return results;
        }

        public static async Task<float[]> GenerateEmbeddingAsync(string text)
        {
            var results = await GenerateEmbeddingListAsync(new List<string> { text });
            return results[0];
        }
    }
}
View Code

 

posted @ 2026-09-02 17:06  天才卧龙  阅读(12)  评论(0)    收藏  举报