Net8-Qdrant.Client-1.19.0 增删改查完美封装

接着上一篇博客:
本篇的主要目标是查询Qdrant时,用拉姆达表达式的方式进行
本篇不写其他,只贴代码
1、阿里项目服务
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

2、自定义属性【用于反射,创建索引,向量,查询时使用】

using Qdrant.Client.Grpc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GroupChatAuto
{
    /// <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;
        }
    }


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



    /// <summary>
    /// 标记可查询的字段(用于 Filter 查询)
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public class QueryableAttribute : Attribute
    {
        public PayloadSchemaType SchemaType { get; set; }
        public string? FieldName { get; set; }

        public QueryableAttribute(PayloadSchemaType schemaType)
        {
            SchemaType = schemaType;
        }
    }
}
View Code

3、相关实体

using Qdrant.Client.Grpc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GroupChatAuto
{
    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<PayloadIndexParam_s> PayloadIndexes { get; set; } = new();
    }

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

     
    // ========================================================================
    // 插入配置
    // ========================================================================
    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; }
    }



    // ========================================================================
    // 数据项模型(业务层使用)
    // ========================================================================
    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();
    }


    #region 查询相关特性-实体 

    // ========================================================================
    // 2. 查询配置类
    // ========================================================================

    public class QueryConfig
    {
        /// <summary>
        /// 返回数量上限(默认 10)
        /// </summary>
        public int Limit { get; set; } = 10;

        /// <summary>
        /// 偏移量(分页用,默认 0)
        /// </summary>
        public int Offset { get; set; } = 0;

        /// <summary>
        /// 相似度阈值(0-1,默认 0.0,仅向量搜索有效)
        /// </summary>
        public float ScoreThreshold { get; set; } = 0.0f;

        /// <summary>
        /// 是否包含向量数据(默认 false)
        /// </summary>
        public bool WithVector { get; set; } = false;

        /// <summary>
        /// 需要返回的 Payload 字段(空则返回全部)
        /// </summary>
        public List<string>? IncludeFields { get; set; }

        /// <summary>
        /// 排序规则(Qdrant 不直接支持,需在内存中排序)
        /// </summary>
        public bool OrderByScore { get; set; } = true;
    }

    public class SearchConfig : QueryConfig
    {
        /// <summary>
        /// 向量数据(向量搜索必填)
        /// </summary>
        public float[]? Vector { get; set; }

        /// <summary>
        /// 待向量化的文本(如果 Vector 为空,则使用此文本生成向量)
        /// </summary>
        public string? SearchText { get; set; }
    }

    // ========================================================================
    // 3. 查询结果类
    // ========================================================================

    public class QueryResult<T>
    {
        /// <summary>
        /// 查询是否成功
        /// </summary>
        public bool Success { get; set; }

        /// <summary>
        /// 错误信息
        /// </summary>
        public string ErrorMessage { get; set; } = "";

        /// <summary>
        /// 命中数据列表
        /// </summary>
        public List<T> Items { get; set; } = new();

        /// <summary>
        /// 原始结果(带分数)
        /// </summary>
        public List<ScoredPoint> RawPoints { get; set; } = new();

        /// <summary>
        /// 总命中数(Qdrant 不直接支持,需估算)
        /// </summary>
        public int TotalCount { get; set; }

        /// <summary>
        /// 查询耗时
        /// </summary>
        public TimeSpan Elapsed { get; set; }

        /// <summary>
        /// 查询参数
        /// </summary>
        public QueryConfig Config { get; set; } = new();

        public ulong Page { get; set; } = 1;
        public ulong PageSize { get; set; } = 10;
    }

    public class ScoredItem<T>
    {
        public T Data { get; set; } = default!;
        public double Score { get; set; }
        public string PointId { get; set; } = "";
        public Dictionary<string, object?> Payload { get; set; } = new();
    }
    #endregion
}
View Code

4、扩展方式

using Google.Protobuf.Collections;
using Qdrant.Client;
using Qdrant.Client.Grpc;
using RagCurd;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace GroupChatAuto
{
    public static class CollectionConfigExtensions
    {
        /// <summary>
        /// 从 DTO 的 QdrantIndex 特性自动提取字段类型映射
        /// </summary>
        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<PayloadIndexParam_s> GetPayloadIndexesFromDto<T>()
        {
            var indexes = new List<PayloadIndexParam_s>();
            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 PayloadIndexParam_s
                {
                    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('_');
        }
    }


    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 static class QdrantQueryExtensions
    {
        /// <summary>
        /// 执行查询(带分页)
        /// </summary>
        public static async Task<List<ScoredPoint>> ExecuteAsync<T>(
            this QueryFilterBuilder<T> builder,
            QdrantClient client,
            string collectionName,
            ulong page = 1,
            ulong pageSize = 10)
        {
            var filter = builder.Build();
            var offset = (page - 1) * pageSize;

            var result = await client.QueryAsync(
                collectionName,
                filter: filter,
                limit: pageSize,
                offset: offset,
                payloadSelector: new WithPayloadSelector { Enable = true }
            );

            return result.ToList();  // ✅ 返回 List<ScoredPoint>
        }

        /// <summary>
        /// 执行查询(转换为 DTO)
        /// </summary>
        public static async Task<List<T>> ExecuteWithDtoAsync<T>(
            this QueryFilterBuilder<T> builder,
            QdrantClient client,
            string collectionName,
            ulong page = 1,
            ulong pageSize = 10)
        {
            var filter = builder.Build();
            var offset = (page - 1) * pageSize;

            var result = await client.QueryAsync(
                collectionName,
                filter: filter,
                limit: pageSize,
                offset: offset,
                payloadSelector: new WithPayloadSelector { Enable = true }
            );

            // ✅ 转换为 DTO
            return result.Select(point => ConvertToDto<T>(point.Payload)).ToList();
        }

        /// <summary>
        /// 执行查询(带完整结果)
        /// </summary>
        public static async Task<QueryResult<T>> ExecuteWithDetailsAsync<T>(
            this QueryFilterBuilder<T> builder,
            QdrantClient client,
            string collectionName,
            ulong page = 1,
            ulong pageSize = 10)
        {
            var filter = builder.Build();
            var offset = (page - 1) * pageSize;

            var result = await client.QueryAsync(
                collectionName,
                filter: filter,
                limit: pageSize,
                offset: offset,
                payloadSelector: new WithPayloadSelector { Enable = true }
            );

            return new QueryResult<T>
            {
                RawPoints = result.ToList(),  // ✅ ScoredPoint 列表
                Items = result.Select(p => ConvertToDto<T>(p.Payload)).ToList(),  // ✅ DTO 列表
                TotalCount = result.Count,
                Page = page,
                PageSize = pageSize
            };
        }

        /// <summary>
        /// Payload 转 DTO(修复 MapField 问题)
        /// </summary>
        private static T ConvertToDto<T>(MapField<string, Value> payload)  // ✅ 改 MapField
        {
            var dto = Activator.CreateInstance<T>();
            var properties = typeof(T).GetProperties();

            foreach (var prop in properties)
            {
                var fieldName = ToSnakeCase(prop.Name);
                if (payload.TryGetValue(fieldName, out var value))
                {
                    var convertedValue = ConvertValue(value, prop.PropertyType);
                    prop.SetValue(dto, convertedValue);
                }
            }

            return dto;
        }

        /// <summary>
        /// Payload 转 DTO(支持 Dictionary 和 MapField)
        /// </summary>
        private static T ConvertToDto<T>(IDictionary<string, Value> payload)  // ✅ 用接口
        {
            var dto = Activator.CreateInstance<T>();
            var properties = typeof(T).GetProperties();

            foreach (var prop in properties)
            {
                var fieldName = ToSnakeCase(prop.Name);
                if (payload.TryGetValue(fieldName, out var value))
                {
                    var convertedValue = ConvertValue(value, prop.PropertyType);
                    prop.SetValue(dto, convertedValue);
                }
            }

            return dto;
        }

        private static object? ConvertValue(Value value, Type targetType)
        {
            if (value == null) return null;

            var valueKind = value.KindCase;

            return valueKind switch
            {
                Value.KindOneofCase.StringValue => value.StringValue,
                Value.KindOneofCase.IntegerValue => Convert.ChangeType(value.IntegerValue, targetType),
                Value.KindOneofCase.DoubleValue => Convert.ChangeType(value.DoubleValue, targetType),
                Value.KindOneofCase.BoolValue => value.BoolValue,
                _ => null
            };
        }

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

5、创建集合帮助类

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

6、插入数据帮助类

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
{  
    // ========================================================================
    // 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

7、查询帮助类

using GroupChatAuto;
using Qdrant.Client.Grpc; 
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace RagCurd
{ 
    // ========================================================================
    // 4. Filter 构建器(核心:Lambda → Qdrant Filter)
    // ======================================================================= 

    public class QueryFilterBuilder<T>
    {
        private readonly List<FieldCondition> _fieldConditions = new();
        private readonly List<Condition> _specialConditions = new();
        private readonly Dictionary<string, PayloadSchemaType> _fieldTypes;

        public QueryFilterBuilder(Dictionary<string, PayloadSchemaType> fieldTypes)
        {
            _fieldTypes = fieldTypes;
        }
        /// <summary>
        /// 多值匹配(IN 查询 - Keyword)
        /// </summary>
        public QueryFilterBuilder<T> WhereIn<TValue>(Expression<Func<T, TValue>> selector, IEnumerable<TValue> values)
        {
            var fieldName = GetFieldName(selector);
            var match = new Match
            {
                Keywords = new RepeatedStrings()  // ✅ 先实例化
            };
            match.Keywords.Strings.AddRange(values.Select(v => v?.ToString() ?? ""));
            _fieldConditions.Add(new FieldCondition { Key = fieldName, Match = match });
            return this;
        }

        /// <summary>
        /// 多值整数匹配(IN 查询)
        /// </summary>
        public QueryFilterBuilder<T> WhereInInt(Expression<Func<T, int>> selector, IEnumerable<int> values)
        {
            var fieldName = GetFieldName(selector);
            var match = new Match
            {
                Integers = new RepeatedIntegers()  // ✅ 先实例化
            };
            match.Integers.Integers.AddRange(values.Select(v => (long)v));
            _fieldConditions.Add(new FieldCondition { Key = fieldName, Match = match });
            return this;
        }

        /// <summary>
        /// 排除关键字(NOT IN)
        /// </summary>
        public QueryFilterBuilder<T> WhereExceptKeywords(Expression<Func<T, string>> selector, IEnumerable<string> keywords)
        {
            var fieldName = GetFieldName(selector);
            var match = new Match
            {
                ExceptKeywords = new RepeatedStrings()  // ✅ 先实例化
            };
            match.ExceptKeywords.Strings.AddRange(keywords);
            _fieldConditions.Add(new FieldCondition { Key = fieldName, Match = match });
            return this;
        }

        /// <summary>
        /// 排除整数(NOT IN)
        /// </summary>
        public QueryFilterBuilder<T> WhereExceptIntegers(Expression<Func<T, int>> selector, IEnumerable<int> integers)
        {
            var fieldName = GetFieldName(selector);
            var match = new Match
            {
                ExceptIntegers = new RepeatedIntegers()  // ✅ 先实例化
            };
            match.ExceptIntegers.Integers.AddRange(integers.Select(v => (long)v));
            _fieldConditions.Add(new FieldCondition { Key = fieldName, Match = match });
            return this;
        } 

        /// <summary>
        /// 文本任意匹配(单个文本)
        /// </summary>
        public QueryFilterBuilder<T> WhereTextAny(Expression<Func<T, string>> selector, string text)
        {
            var fieldName = GetFieldName(selector);
            var match = new Match { TextAny = text };
            _fieldConditions.Add(new FieldCondition { Key = fieldName, Match = match });
            return this;
        }

        /// <summary>
        /// 多关键词匹配(多个文本任一匹配)
        /// </summary>
        public QueryFilterBuilder<T> WhereKeywordIn(Expression<Func<T, string>> selector, IEnumerable<string> keywords)
        {
            var fieldName = GetFieldName(selector);
            var match = new Match();
            match.Keywords.Strings.AddRange(keywords);
            _fieldConditions.Add(new FieldCondition { Key = fieldName, Match = match });
            return this;
        }
         

        /// <summary>
        /// 等于条件(Keyword 匹配)
        /// </summary>
        public QueryFilterBuilder<T> WhereEqual<TValue>(Expression<Func<T, TValue>> selector, TValue value)
        {
            var fieldName = GetFieldName(selector);
            _fieldConditions.Add(new FieldCondition
            {
                Key = fieldName,
                Match = new Match { Keyword = value?.ToString() ?? "" }
            });
            return this;
        }

        /// <summary>
        /// 等于条件(Integer 匹配)
        /// </summary>
        public QueryFilterBuilder<T> WhereEqualInt(Expression<Func<T, int>> selector, int value)
        {
            var fieldName = GetFieldName(selector);
            _fieldConditions.Add(new FieldCondition
            {
                Key = fieldName,
                Match = new Match { Integer = value }
            });
            return this;
        }

        /// <summary>
        /// 等于条件(Long 匹配)
        /// </summary>
        public QueryFilterBuilder<T> WhereEqualLong(Expression<Func<T, long>> selector, long value)
        {
            var fieldName = GetFieldName(selector);
            _fieldConditions.Add(new FieldCondition
            {
                Key = fieldName,
                Match = new Match { Integer = value }
            });
            return this;
        }

        /// <summary>
        /// 等于条件(Float 精确匹配 - 用 Range 近似)
        /// </summary>
        public QueryFilterBuilder<T> WhereEqualFloat(Expression<Func<T, float>> selector, float value)
        {
            var fieldName = GetFieldName(selector);
            _fieldConditions.Add(new FieldCondition
            {
                Key = fieldName,
                Range = new Qdrant.Client.Grpc.Range { Gte = value, Lte = value }
            });
            return this;
        }

        /// <summary>
        /// 布尔条件
        /// </summary>
        public QueryFilterBuilder<T> WhereBool(Expression<Func<T, bool>> selector, bool value)
        {
            var fieldName = GetFieldName(selector);
            _fieldConditions.Add(new FieldCondition
            {
                Key = fieldName,
                Match = new Match { Boolean = value }
            });
            return this;
        }
         

        /// <summary>
        /// 范围条件(大于)
        /// </summary>
        public QueryFilterBuilder<T> WhereGreater<TValue>(Expression<Func<T, TValue>> selector, TValue value)
            where TValue : IComparable
        {
            var fieldName = GetFieldName(selector);
            _fieldConditions.Add(new FieldCondition
            {
                Key = fieldName,
                Range = new Qdrant.Client.Grpc.Range { Gt = ConvertToDouble(value) }
            });
            return this;
        }

        /// <summary>
        /// 范围条件(大于等于)
        /// </summary>
        public QueryFilterBuilder<T> WhereGreaterEqual<TValue>(Expression<Func<T, TValue>> selector, TValue value)
            where TValue : IComparable
        {
            var fieldName = GetFieldName(selector);
            _fieldConditions.Add(new FieldCondition
            {
                Key = fieldName,
                Range = new Qdrant.Client.Grpc.Range { Gte = ConvertToDouble(value) }
            });
            return this;
        }

        /// <summary>
        /// 范围条件(小于)
        /// </summary>
        public QueryFilterBuilder<T> WhereLess<TValue>(Expression<Func<T, TValue>> selector, TValue value)
            where TValue : IComparable
        {
            var fieldName = GetFieldName(selector);
            _fieldConditions.Add(new FieldCondition
            {
                Key = fieldName,
                Range = new Qdrant.Client.Grpc.Range { Lt = ConvertToDouble(value) }
            });
            return this;
        }

        /// <summary>
        /// 范围条件(小于等于)
        /// </summary>
        public QueryFilterBuilder<T> WhereLessEqual<TValue>(Expression<Func<T, TValue>> selector, TValue value)
            where TValue : IComparable
        {
            var fieldName = GetFieldName(selector);
            _fieldConditions.Add(new FieldCondition
            {
                Key = fieldName,
                Range = new Qdrant.Client.Grpc.Range { Lte = ConvertToDouble(value) }
            });
            return this;
        }

        /// <summary>
        /// 范围条件(介于)
        /// </summary>
        public QueryFilterBuilder<T> WhereBetween<TValue>(Expression<Func<T, TValue>> selector, TValue min, TValue max)
            where TValue : IComparable
        {
            var fieldName = GetFieldName(selector);
            _fieldConditions.Add(new FieldCondition
            {
                Key = fieldName,
                Range = new Qdrant.Client.Grpc.Range
                {
                    Gte = ConvertToDouble(min),
                    Lte = ConvertToDouble(max)
                }
            });
            return this;
        }

        /// <summary>
        /// 文本包含(针对 Text 索引字段)
        /// </summary>
        public QueryFilterBuilder<T> WhereContains(Expression<Func<T, string>> selector, string text)
        {
            var fieldName = GetFieldName(selector);
            _fieldConditions.Add(new FieldCondition
            {
                Key = fieldName,
                Match = new Match { Text = text }
            });
            return this;
        }

        /// <summary>
        /// 文本前缀匹配
        /// </summary>
        public QueryFilterBuilder<T> WhereStartsWith(Expression<Func<T, string>> selector, string prefix)
        {
            var fieldName = GetFieldName(selector);
            _fieldConditions.Add(new FieldCondition
            {
                Key = fieldName,
                Match = new Match { Prefix = prefix }
            });
            return this;
        }
         

        /// <summary>
        /// 短语匹配(精确短语)
        /// </summary>
        public QueryFilterBuilder<T> WherePhrase(Expression<Func<T, string>> selector, string phrase)
        {
            var fieldName = GetFieldName(selector);
            _fieldConditions.Add(new FieldCondition
            {
                Key = fieldName,
                Match = new Match { Phrase = phrase }
            });
            return this;
        }

         
        /// <summary>
        /// 文本任意匹配(Text Any)✅ 最终修正版
        /// </summary>
        public QueryFilterBuilder<T> WhereTextAny(Expression<Func<T, string>> selector, IEnumerable<string> texts)
        {
            var fieldName = GetFieldName(selector);
            var match = new Match();
            match.Keywords.Strings.Add(texts);  // ✅ Keywords 才是 RepeatedStrings
            _fieldConditions.Add(new FieldCondition { Key = fieldName, Match = match });
            return this;
        }
         

        /// <summary>
        /// 字段为 NULL
        /// </summary>
        public QueryFilterBuilder<T> WhereIsNull(Expression<Func<T, object>> selector)
        {
            var fieldName = GetFieldName(selector);
            _specialConditions.Add(new Condition { IsNull = new IsNullCondition { Key = fieldName } });
            return this;
        }

        /// <summary>
        /// 字段为空(空字符串或空数组)
        /// </summary>
        public QueryFilterBuilder<T> WhereIsEmpty(Expression<Func<T, object>> selector)
        {
            var fieldName = GetFieldName(selector);
            _specialConditions.Add(new Condition { IsEmpty = new IsEmptyCondition { Key = fieldName } });
            return this;
        }

        /// <summary>
        /// 构建 Filter 对象(使用 Must 逻辑 - AND)
        /// </summary>
        public Filter Build()
        {
            var filter = new Filter();
            foreach (var fieldCond in _fieldConditions)
            {
                filter.Must.Add(new Condition { Field = fieldCond });
            }
            foreach (var specialCond in _specialConditions)
            {
                filter.Must.Add(specialCond);
            }
            return filter;
        }

        /// <summary>
        /// 构建 MUST NOT Filter(取反 - NOT)
        /// </summary>
        public Filter BuildMustNot()
        {
            var filter = new Filter();
            foreach (var fieldCond in _fieldConditions)
            {
                filter.MustNot.Add(new Condition { Field = fieldCond });
            }
            foreach (var specialCond in _specialConditions)
            {
                filter.MustNot.Add(specialCond);
            }
            return filter;
        }

        /// <summary>
        /// 构建 SHOULD Filter(或逻辑 - OR)
        /// </summary>
        public Filter BuildShould()
        {
            var filter = new Filter();
            foreach (var fieldCond in _fieldConditions)
            {
                filter.Should.Add(new Condition { Field = fieldCond });
            }
            foreach (var specialCond in _specialConditions)
            {
                filter.Should.Add(specialCond);
            }
            return filter;
        }

        // ====================================================================
        // 辅助方法
        // ====================================================================

        private string GetFieldName<TValue>(Expression<Func<T, TValue>> selector)
        {
            if (selector.Body is MemberExpression memberExpr)
            {
                var prop = memberExpr.Member as PropertyInfo;
                if (prop != null)
                {
                    var attr = prop.GetCustomAttribute<QueryableAttribute>();
                    if (attr?.FieldName != null) return attr.FieldName;

                    var qdrantIndexAttr = prop.GetCustomAttribute<QdrantIndexAttribute>();
                    if (qdrantIndexAttr?.FieldName != null) return qdrantIndexAttr.FieldName;

                    return ToSnakeCase(prop.Name);
                }
            }
            throw new ArgumentException("表达式必须是属性访问", nameof(selector));
        }

        private double ConvertToDouble<TValue>(TValue value)
        {
            return value switch
            {
                int i => i,
                long l => l,
                float f => f,
                double d => d,
                DateTime dt => new DateTimeOffset(dt).ToUnixTimeSeconds(),
                DateTimeOffset dto => dto.ToUnixTimeSeconds(),
                _ => Convert.ToDouble(value)
            };
        }

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

8、使用案例-main.cs

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

namespace GroupChatAuto
{
    internal class Program
    {
        static string CollectionName = "BookDb";
        static ulong Size = 1536;
        static List<bookDto> GenerateTestBooks()
        {
            var books = new List<bookDto>();
            var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();

            // ==================== 科幻小说 (5 本) ====================
            books.Add(new bookDto
            {
                BookName = "三体",
                Category = "科幻",
                Price = 59.9f,
                CreatedAt = now - 86400 * 30,
                Author = "刘慈欣",
                IsPublished = true,
                Description = "地球文明面临三体人入侵,人类在绝望中挣扎求生的宏大史诗",
                Conent = "中国科幻巅峰之作"
            });

            books.Add(new bookDto
            {
                BookName = "流浪地球",
                Category = "科幻",
                Price = 45.0f,
                CreatedAt = now - 86400 * 25,
                Author = "刘慈欣",
                IsPublished = true,
                Description = "太阳即将毁灭,人类启动流浪地球计划,带着地球逃离太阳系",
                Conent = "同名电影原著"
            });

            books.Add(new bookDto
            {
                BookName = "北京折叠",
                Category = "科幻",
                Price = 38.0f,
                CreatedAt = now - 86400 * 20,
                Author = "郝景芳",
                IsPublished = true,
                Description = "未来北京被折叠成三个空间,不同阶层的人生活在不同的时间里",
                Conent = "雨果奖获奖作品"
            });

            books.Add(new bookDto
            {
                BookName = "球状闪电",
                Category = "科幻",
                Price = 52.0f,
                CreatedAt = now - 86400 * 15,
                Author = "刘慈欣",
                IsPublished = true,
                Description = "主人公追寻球状闪电之谜,揭开量子物理的神秘面纱",
                Conent = "硬科幻代表作"
            });

            books.Add(new bookDto
            {
                BookName = "天年",
                Category = "科幻",
                Price = 48.0f,
                CreatedAt = now - 86400 * 10,
                Author = "韩松",
                IsPublished = true,
                Description = "人类寿命突破极限后的社会变革与伦理困境",
                Conent = "科幻现实主义"
            });

            // ==================== 武侠小说 (5 本) ====================
            books.Add(new bookDto
            {
                BookName = "天龙八部",
                Category = "武侠",
                Price = 68.0f,
                CreatedAt = now - 86400 * 100,
                Author = "金庸",
                IsPublished = true,
                Description = "乔峰、段誉、虚竹三位英雄的江湖传奇,爱恨情仇交织的武侠史诗",
                Conent = "金庸代表作"
            });

            books.Add(new bookDto
            {
                BookName = "射雕英雄传",
                Category = "武侠",
                Price = 65.0f,
                CreatedAt = now - 86400 * 90,
                Author = "金庸",
                IsPublished = true,
                Description = "郭靖从傻小子成长为一代大侠,与黄蓉携手守卫襄阳的传奇故事",
                Conent = "武侠经典"
            });

            books.Add(new bookDto
            {
                BookName = "笑傲江湖",
                Category = "武侠",
                Price = 62.0f,
                CreatedAt = now - 86400 * 80,
                Author = "金庸",
                IsPublished = true,
                Description = "令狐冲追求自由与真爱,在江湖纷争中保持本心的故事",
                Conent = "武侠巅峰之作"
            });

            books.Add(new bookDto
            {
                BookName = "多情剑客无情剑",
                Category = "武侠",
                Price = 55.0f,
                CreatedAt = now - 86400 * 70,
                Author = "古龙",
                IsPublished = true,
                Description = "李寻欢的小李飞刀例不虚发,却斩不断心中的情丝",
                Conent = "古龙代表作"
            });

            books.Add(new bookDto
            {
                BookName = "绝代双骄",
                Category = "武侠",
                Price = 58.0f,
                CreatedAt = now - 86400 * 60,
                Author = "古龙",
                IsPublished = true,
                Description = "小鱼儿与花无缺一对孪生兄弟,性格迥异却同样精彩的江湖路",
                Conent = "古龙经典"
            });

            // ==================== 言情小说 (4 本) ====================
            books.Add(new bookDto
            {
                BookName = "还珠格格",
                Category = "言情",
                Price = 42.0f,
                CreatedAt = now - 86400 * 150,
                Author = "琼瑶",
                IsPublished = true,
                Description = "小燕子与紫薇的宫廷奇遇,爱情与友情的动人故事",
                Conent = "琼瑶代表作"
            });

            books.Add(new bookDto
            {
                BookName = "何以笙箫默",
                Category = "言情",
                Price = 39.0f,
                CreatedAt = now - 86400 * 50,
                Author = "顾漫",
                IsPublished = true,
                Description = "七年等待,何以琛与赵默笙重逢后的甜蜜爱情故事",
                Conent = "都市言情经典"
            });

            books.Add(new bookDto
            {
                BookName = "微微一笑很倾城",
                Category = "言情",
                Price = 41.0f,
                CreatedAt = now - 86400 * 45,
                Author = "顾漫",
                IsPublished = true,
                Description = "网游高手与校花学霸的甜蜜邂逅,线上线下双重浪漫",
                Conent = "网游言情"
            });

            books.Add(new bookDto
            {
                BookName = "三生三世十里桃花",
                Category = "言情",
                Price = 45.0f,
                CreatedAt = now - 86400 * 40,
                Author = "唐七公子",
                IsPublished = true,
                Description = "白浅与夜华跨越三世的仙恋传奇,虐心又甜蜜",
                Conent = "仙侠言情"
            });

            // ==================== 悬疑小说 (4 本) ====================
            books.Add(new bookDto
            {
                BookName = "嫌疑人 X 的献身",
                Category = "悬疑",
                Price = 49.0f,
                CreatedAt = now - 86400 * 200,
                Author = "东野圭吾",
                IsPublished = true,
                Description = "数学天才为爱情设下完美诡计,与物理学家展开高智商对决",
                Conent = "东野圭吾巅峰作"
            });

            books.Add(new bookDto
            {
                BookName = "白夜行",
                Category = "悬疑",
                Price = 55.0f,
                CreatedAt = now - 86400 * 180,
                Author = "东野圭吾",
                IsPublished = true,
                Description = "一对男女在黑暗中相依前行,跨越十九年的悲剧宿命",
                Conent = "社会派推理经典"
            });

            books.Add(new bookDto
            {
                BookName = "无人生还",
                Category = "悬疑",
                Price = 46.0f,
                CreatedAt = now - 86400 * 160,
                Author = "阿加莎·克里斯蒂",
                IsPublished = true,
                Description = "十个人被困孤岛,按照童谣一个接一个死去,凶手究竟是谁",
                Conent = "推理女王代表作"
            });

            books.Add(new bookDto
            {
                BookName = "盗墓笔记",
                Category = "悬疑",
                Price = 52.0f,
                CreatedAt = now - 86400 * 30,
                Author = "南派三叔",
                IsPublished = true,
                Description = "吴邪跟随三叔探索古墓 secrets,揭开千年谜团与家族宿命",
                Conent = "盗墓题材开山作"
            });

            // ==================== 历史文学 (2 本) ====================
            books.Add(new bookDto
            {
                BookName = "明朝那些事儿",
                Category = "历史",
                Price = 58.0f,
                CreatedAt = now - 86400 * 300,
                Author = "当年明月",
                IsPublished = true,
                Description = "用幽默笔触讲述明朝三百年兴衰,历史也可以这样有趣",
                Conent = "现象级历史读物"
            });

            books.Add(new bookDto
            {
                BookName = "长安十二时辰",
                Category = "历史",
                Price = 54.0f,
                CreatedAt = now - 86400 * 25,
                Author = "马伯庸",
                IsPublished = true,
                Description = "张小敬在十二个时辰内拯救长安,紧张刺激的古代反恐行动",
                Conent = "历史悬疑佳作"
            });

            return books;
        }

        //static async Task Main(string[] args)
        //{
        //    var qdrantClient = new QdrantClient(ConstParm.QdrantHostIp, 6334, apiKey: ConstParm.QdrantApiKey);
        //    await qdrantClient.DeleteCollectionAsync(CollectionName);
        //    CollectionCreateHelper createHelper = new CollectionCreateHelper(qdrantClient);
        //    var vectorParams = new VectorParams() { Distance = Distance.Cosine, Size = Size };
        //    var payloadIndexes = CollectionConfigExtensions.GetPayloadIndexesFromDto<bookDto>();
        //    var createResult = await createHelper.CreateCollectionAsync(CollectionName, vectorParams, payloadIndexes);

        //    if (createResult.Success)
        //    {
        //        Console.WriteLine($"✅ 集合创建成功:{CollectionName}");
        //    }
        //    else
        //    {
        //        Console.WriteLine($"❌ 集合创建失败:{createResult.ErrorMessage}");
        //        return;
        //    }
        //    // 3. 生成 20 条测试数据
        //    var books = GenerateTestBooks();
        //    Console.WriteLine($"\n📚 生成 {books.Count} 条测试数据");

        //    // 4. 创建数据服务并插入
        //    Func<string, Task<float[]>> embeddingFunc = async (text) =>
        //    {
        //        return await AliyunEmbeddingService.GenerateEmbeddingAsync(text);
        //    };

        //    var dataService = new QdrantDataService(qdrantClient, embeddingFunc);

        //    var config = new InsertConfig
        //    {
        //        BatchSize = 5,
        //        AutoGenerateBusinessId = true,
        //        BusinessIdPrefix = "BOOK",
        //        ProgressCallback = async (processed, total) =>
        //        {
        //            Console.WriteLine($"  📊 进度:{processed}/{total} ({processed * 100.0 / total:F1}%)");
        //            await Task.CompletedTask;
        //        }
        //    };

        //    var items = books.ToDataItems("BOOK", startIndex: 1);
        //    var insertResult = await dataService.InsertBatchAsync(CollectionName, items, config);

        //    // 5. 输出结果
        //    if (insertResult.Success)
        //    {
        //        Console.WriteLine($@"
        //    ✅ 批量插入成功!
        //    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        //      总计:{insertResult.TotalCount} 条
        //      成功:{insertResult.SuccessCount} 条
        //      失败:{insertResult.FailedCount} 条
        //      耗时:{insertResult.Elapsed}
        //    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        //    ");
        //    }
        //    else
        //    {
        //        Console.WriteLine($"❌ 批量插入失败:{insertResult.ErrorMessage}");
        //        if (insertResult.FailedItems.Any())
        //        {
        //            Console.WriteLine("\n失败详情:");
        //            foreach (var (item, error) in insertResult.FailedItems)
        //            {
        //                Console.WriteLine($"  - {item.BusinessId}: {error}");
        //            }
        //        }
        //    }



        //}


        static async Task Main(string[] args)
        {
            ulong page = 1;          // 第几页
            ulong pageSize = 100;     // 每页多少条
            ulong offset = (page - 1) * pageSize;  // 计算偏移量

            var CheckBooks = GenerateTestBooks();
            var qdrantClient = new QdrantClient(ConstParm.QdrantHostIp, 6334, apiKey: ConstParm.QdrantApiKey);
            // 查询所有数据(不过滤)
            var allResult = await qdrantClient.QueryAsync(
                CollectionName,
                filter: null,
                limit: pageSize,    // ✅ 每页数量
                offset: offset,     // ✅ 偏移量
                payloadSelector: new WithPayloadSelector { Enable = true }
            );
            Console.WriteLine($"📊 集合总数据:{allResult.Count} 条");

            // 自动提取字段类型(不硬编码)
            var fieldTypes = CollectionConfigExtensions.GetFieldTypesFromDto<bookDto>();

            Console.WriteLine("\n🔍 开始查询测试...\n");

            // ========================================================================
            // 1️⃣ 精确匹配:分类=科幻
            // ========================================================================
            Console.WriteLine("【测试 1】查询科幻小说");
            var builder1 = new QueryFilterBuilder<bookDto>(fieldTypes);
            builder1.WhereEqual(x => x.Category, "科幻");
            var filter1 = builder1.Build();

            var result1 = await qdrantClient.QueryAsync(
                collectionName: CollectionName,
                filter: filter1, 
                limit: pageSize,    // ✅ 每页数量
                offset: offset,     // ✅ 偏移量
                 payloadSelector: new WithPayloadSelector { Enable = true }
            );
            Console.WriteLine($"✅ 找到 {result1.Count} 本科幻小说\n" + CheckBooks.Where(A => A.Category == "科幻").Count());

            // ========================================================================
            // 2️⃣ IN 查询:分类=科幻或武侠
            // ========================================================================
            Console.WriteLine("【测试 2】查询科幻或武侠小说");
            var builder2 = new QueryFilterBuilder<bookDto>(fieldTypes);
            builder2.WhereIn(x => x.Category, new[] { "科幻", "武侠" });
            var filter2 = builder2.Build();

            var result2 = await qdrantClient.QueryAsync(
                CollectionName,
                filter: filter2,
                limit: pageSize,    // ✅ 每页数量
                offset: offset,     // ✅ 偏移量
                payloadSelector: new WithPayloadSelector { Enable = true }
            );
            Console.WriteLine($"✅ 找到 {result2.Count} 本\n" + CheckBooks.Where(A => A.Category == "科幻" || A.Category == "武侠").Count());

            // ========================================================================
            // 3️⃣ NOT IN 查询:排除言情类
            // ========================================================================
            Console.WriteLine("【测试 3】查询非言情类书籍");
            var builder3 = new QueryFilterBuilder<bookDto>(fieldTypes);
            builder3.WhereExceptKeywords(x => x.Category, new[] { "言情" });
            var filter3 = builder3.Build();

            var result3 = await qdrantClient.QueryAsync(
                CollectionName,
                filter: filter3, 
                limit: pageSize,    // ✅ 每页数量
                offset: offset,     // ✅ 偏移量
                 payloadSelector: new WithPayloadSelector { Enable = true }
            );
            Console.WriteLine($"✅ 找到 {result3.Count} 本\n" + CheckBooks.Where(A => A.Category != "言情").Count());

            // ========================================================================
            // 4️⃣ 范围查询:价格在 40-60 元之间
            // ========================================================================
            Console.WriteLine("【测试 4】查询价格在 45-60 元之间的书");
            var builder4 = new QueryFilterBuilder<bookDto>(fieldTypes);
            builder4.WhereBetween(x => x.Price, 45f, 60f);
            var filter4 = builder4.Build();

            var result4 = await qdrantClient.QueryAsync(
                CollectionName,
                filter: filter4,
                limit: pageSize,    // ✅ 每页数量
                offset: offset,     // ✅ 偏移量
                payloadSelector: new WithPayloadSelector { Enable = true }
            );
            Console.WriteLine($"✅ 找到 {result4.Count} 本\n" + CheckBooks.Where(A => A.Price >= 45 && A.Price <= 60).Count());

            // ========================================================================
            // 5️⃣ 组合查询:科幻 AND 价格<60 AND 作者=刘慈欣
            // ========================================================================
            Console.WriteLine("【测试 5】查询刘慈欣的便宜科幻小说");
            var builder5 = new QueryFilterBuilder<bookDto>(fieldTypes);
            builder5
                .WhereEqual(x => x.Category, "科幻")
                .WhereLess(x => x.Price, 60f)
                .WhereEqual(x => x.Author, "刘慈欣");
            var filter5 = builder5.Build();

            var result5 = await qdrantClient.QueryAsync(
                CollectionName,
                filter: filter5,
                limit: pageSize,    // ✅ 每页数量
                offset: offset,     // ✅ 偏移量
                payloadSelector: new WithPayloadSelector { Enable = true }
            );
            Console.WriteLine($"✅ 找到 {result5.Count} 本" + CheckBooks.Where(A => A.Category == "科幻" && A.Price <= 60 && A.Author == "刘慈欣").Count());
            foreach (var point in result5)
            {
                var book = point.Payload;
                Console.WriteLine($"   📖 {book["book_name"]} | {book["author"]} | ¥{book["price"]}");
            }
            Console.WriteLine();

            // ========================================================================
            // 6️⃣ 文本搜索:书名包含"三体"
            // ========================================================================
            Console.WriteLine("【测试 6】查询书名包含'三体'的书");
            var builder6 = new QueryFilterBuilder<bookDto>(fieldTypes);
            builder6.WhereTextAny(x => x.BookName, "三体");
            var filter6 = builder6.Build();

            var result6 = await qdrantClient.QueryAsync(
                CollectionName,
                filter: filter6,
                limit: pageSize,    // ✅ 每页数量
                offset: offset,     // ✅ 偏移量
                 payloadSelector: new WithPayloadSelector { Enable = true }
            );
            Console.WriteLine($"✅ 找到 {result6.Count} 本\n" + CheckBooks.Where(A => A.BookName.Contains("三体")).Count());

            // ========================================================================
            // 7️⃣ 复杂组合:非言情 AND 价格>45 AND 最近 150 天内
            // ========================================================================
            Console.WriteLine("【测试 7】复杂组合查询");
            var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            var builder7 = new QueryFilterBuilder<bookDto>(fieldTypes);
            builder7
                .WhereExceptKeywords(x => x.Category, new[] { "言情" })
                .WhereGreater(x => x.Price, 45f)
                .WhereGreater(x => x.CreatedAt, now - 86400 * 150);
            var filter7 = builder7.Build();

            var result7 = await qdrantClient.QueryAsync(
                CollectionName,
                filter: filter7,
                limit: pageSize,    // ✅ 每页数量
                offset: offset,     // ✅ 偏移量

                 payloadSelector: new WithPayloadSelector { Enable = true }
            );
            Console.WriteLine($"✅ 找到 {result7.Count} 本" + CheckBooks.Where(A => A.Category != "言情" && A.Price > 45 && A.CreatedAt > (now - 86400 * 150)).Count());
            foreach (var point in result7)
            {
                var book = point.Payload;
                Console.WriteLine($"   📖 {book["book_name"]} | {book["category"]} | ¥{book["price"]}");
            }
            Console.WriteLine();

            // ========================================================================
            // 8️⃣ 向量搜索(Description 语义搜索)
            // ========================================================================
            Console.WriteLine("【测试 8】向量搜索:'江湖侠义'");
            var embeddingFunc = new Func<string, Task<float[]>>(async (text) =>
            {
                return await AliyunEmbeddingService.GenerateEmbeddingAsync(text);
            });

            var vector = await embeddingFunc("江湖侠义 武林高手");
            var searchResult = await qdrantClient.SearchAsync(
                CollectionName,
                vector: vector,
                filter: null,
                limit: pageSize,    // ✅ 每页数量
                offset: offset,     // ✅ 偏移量
                payloadSelector: new WithPayloadSelector { Enable = true }
            );

            Console.WriteLine($"✅ 找到 {searchResult.Count} 本");
            foreach (var point in searchResult)
            {
                var book = point.Payload;
                Console.WriteLine($"   📖 {book["book_name"]} | 相似度:{point.Score:F4}");
            }
            Console.WriteLine();

            Console.WriteLine("🎉 所有查询测试完成!");
            Console.ReadLine();
        }
    }



    class bookDto
    {
        [QdrantIndex(PayloadSchemaType.Text)]
        public string BookName { get; set; }
        [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; }


        [QdrantIndex(PayloadSchemaType.Keyword)]
        public string Author { get; set; }
        /// <summary>
        /// 是否发布 - Bool 索引
        /// </summary>
        [QdrantIndex(PayloadSchemaType.Bool)]
        public bool IsPublished { get; set; }
        [VectorizedText]
        public string Description { get; set; } = "";


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

项目源码地址:GroupChatAuto.zip [留言提供]

posted @ 2026-09-03 16:49  天才卧龙  阅读(12)  评论(0)    收藏  举报