Net8-Qdrant.Client-1.19.0 增删改查完美封装
接着上一篇博客:
View Code
View Code
View Code
View Code
View Code
View Code
View Code
View Code
本篇的主要目标是查询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]; } } }
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; } } }
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 }
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('_'); } } }
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); } } }
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() ?? "" } }; } } }
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('_'); } } }
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; } = ""; // ❌ 无索引 } }

浙公网安备 33010602011771号