From e8b703f42845744e7cbfc9f903025f58e65846d3 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Mon, 8 Apr 2024 15:22:21 -0500 Subject: [PATCH] [release/8.0] Initial work for Qdrant resource and component (#3131) (#3416) * Initial work for Qdrant resource and component (#3131) This PR adds a Resource and initial Component (plus playground and tests) Resource exposes two API access endpoints (REST + gRPC). The QdrantClient for .NET uses gRPC by default but Semantic Kernel does not use that client library so the other exposed endpoint is helpful. REST endpoint by default exposes a Web UI dashboard -- this is excluded in Publish (per documentation in source, confirmed with Qdrant contributors). Endpoints are secured non-optionally with an API Key (using ParameterResource -- will generate random if not provided). Component used QdrantClient .NET version (recommended from Qdrant). This component uses gRPC endpoint by default. Settings will read from standard env vars for ConnectionString + API key or from config for the component itself if provided. * Initial draft of QdrantServerResource - exposed gRPC endpoint as primary (c# client uses that) - exposes HTTP endpoint optionally (as dashboard is there) - defaults to secure the access via API key (mainly due to dashboard) * Fix-up after main rebase on removing InputAnnotation * Remove dashboard in Publish mode - change how dashboard is removed in Publish (per docs) - add 'qdrant' to spelling.dic - fix tests * Fixup code style violations * Change ApiKey reference - Add WithReference overload (setting cn string + key) - Added tests for named parameter on manifest - Changed playground app * Initial Qdrant component work - adds component (using Qdrant.QdrantClient) - Change playground to use DI component * Changed rest endpoint name - changed to 'rest' as named endpoint - added to reference as a ConnectionString_{name}_rest (for support for SK) - fixed tests * Addressing PR feedback - Changed parameters to component config - change playground app to keyed services - changed playground to use shared servicedefaults * Fixing volume mounts to correct target location * Add missing README to component * Adding logo usage comment to readme after permission from Andrey V from Qdrant * Changed playground sample - matching sample Qdrant/.NET workbook sample * PR Feedback: Name maps to client name used * PR feedback on connection sring - Moved to Endpoint/Key connection string - Renamed to Key - Modified schema to match/updated tests - Changed primary endpoint to use scheme instead of hardcode - removed env var for API key only - fixed renamed component proj location in sln * Updating readme/comments to match config * Endpoint property typo fix * PR Feedback - No need for endpoint null check - Fixed tests * Cleanup of ServiceDefaults Change to ReferenceExpression.Create * More PR Feedback - Rename component correctly - Add component client tests - Add component logging (and document) - Add property for 2nd endpoint on resource directly * PR feedback * Fix up the playground to run with latest changes. * Rename QdrantSettings to QdrantClientSettings to match the pattern in other components. Add class level summary docs. --------- Co-authored-by: Eric Erhardt * Addressing port/endpoint issues on Qdrant (#3422) * Addressing port/endpoint issues on Qdrant * PR feedback (param name) * Fix up tests * Respond to PR feedback. Rename ports and endpoint names to be consistent. * PR feedback - moved to using grpc/http endpoint name consistent with config and prior art - fixed wrong link in readme - fixed/validated resource hosting test --------- Co-authored-by: Eric Erhardt * Fix WithReference extension return type (#3449) * Fix Qdrant WithReference to allow any IResourceWithEnvironment. Also fix the same pattern in AWS. --------- Co-authored-by: Tim Heuer --- Aspire.sln | 38 +++ Directory.Packages.props | 1 + .../Qdrant/Qdrant.ApiService/Program.cs | 93 ++++++ .../Properties/launchSettings.json | 25 ++ .../Qdrant.ApiService.csproj | 15 + .../appsettings.Development.json | 8 + .../Qdrant/Qdrant.ApiService/appsettings.json | 10 + .../Qdrant.AppHost/Directory.Build.props | 8 + .../Qdrant.AppHost/Directory.Build.targets | 9 + playground/Qdrant/Qdrant.AppHost/Program.cs | 12 + .../Properties/launchSettings.json | 32 ++ .../Qdrant.AppHost/Qdrant.AppHost.csproj | 23 ++ .../appsettings.Development.json | 8 + .../Qdrant/Qdrant.AppHost/appsettings.json | 9 + spelling.dic | 1 + .../SDKResourceExtensions.cs | 5 +- .../Aspire.Hosting.Qdrant.csproj | 24 ++ .../QdrantBuilderExtensions.cs | 99 ++++++ .../QdrantContainerImageTags.cs | 10 + .../QdrantServerResource.cs | 56 ++++ src/Aspire.Hosting.Qdrant/README.md | 33 ++ .../Aspire.Qdrant.Client.csproj | 21 ++ .../AspireQdrantExtensions.cs | 96 ++++++ .../Aspire.Qdrant.Client/AssemblyInfo.cs | 9 + .../ConfigurationSchema.json | 39 +++ .../QdrantClientSettings.cs | 50 +++ src/Components/Aspire.Qdrant.Client/README.md | 107 +++++++ src/Components/Aspire_Components_Progress.md | 1 + src/Components/Telemetry.md | 8 + src/Shared/QdrantLogo_256x.png | Bin 0 -> 17190 bytes .../Aspire.Hosting.Tests.csproj | 1 + .../Qdrant/AddQdrantTests.cs | 300 ++++++++++++++++++ .../Aspire.Qdrant.Client.Tests.csproj | 15 + .../AspireQdrantHelpers.cs | 37 +++ .../ConfigurationTests.cs | 12 + .../ConformanceTests.cs | 71 +++++ tests/Aspire.Qdrant.Client.Tests/README.md | 13 + 37 files changed, 1297 insertions(+), 2 deletions(-) create mode 100644 playground/Qdrant/Qdrant.ApiService/Program.cs create mode 100644 playground/Qdrant/Qdrant.ApiService/Properties/launchSettings.json create mode 100644 playground/Qdrant/Qdrant.ApiService/Qdrant.ApiService.csproj create mode 100644 playground/Qdrant/Qdrant.ApiService/appsettings.Development.json create mode 100644 playground/Qdrant/Qdrant.ApiService/appsettings.json create mode 100644 playground/Qdrant/Qdrant.AppHost/Directory.Build.props create mode 100644 playground/Qdrant/Qdrant.AppHost/Directory.Build.targets create mode 100644 playground/Qdrant/Qdrant.AppHost/Program.cs create mode 100644 playground/Qdrant/Qdrant.AppHost/Properties/launchSettings.json create mode 100644 playground/Qdrant/Qdrant.AppHost/Qdrant.AppHost.csproj create mode 100644 playground/Qdrant/Qdrant.AppHost/appsettings.Development.json create mode 100644 playground/Qdrant/Qdrant.AppHost/appsettings.json create mode 100644 src/Aspire.Hosting.Qdrant/Aspire.Hosting.Qdrant.csproj create mode 100644 src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs create mode 100644 src/Aspire.Hosting.Qdrant/QdrantContainerImageTags.cs create mode 100644 src/Aspire.Hosting.Qdrant/QdrantServerResource.cs create mode 100644 src/Aspire.Hosting.Qdrant/README.md create mode 100644 src/Components/Aspire.Qdrant.Client/Aspire.Qdrant.Client.csproj create mode 100644 src/Components/Aspire.Qdrant.Client/AspireQdrantExtensions.cs create mode 100644 src/Components/Aspire.Qdrant.Client/AssemblyInfo.cs create mode 100644 src/Components/Aspire.Qdrant.Client/ConfigurationSchema.json create mode 100644 src/Components/Aspire.Qdrant.Client/QdrantClientSettings.cs create mode 100644 src/Components/Aspire.Qdrant.Client/README.md create mode 100644 src/Shared/QdrantLogo_256x.png create mode 100644 tests/Aspire.Hosting.Tests/Qdrant/AddQdrantTests.cs create mode 100644 tests/Aspire.Qdrant.Client.Tests/Aspire.Qdrant.Client.Tests.csproj create mode 100644 tests/Aspire.Qdrant.Client.Tests/AspireQdrantHelpers.cs create mode 100644 tests/Aspire.Qdrant.Client.Tests/ConfigurationTests.cs create mode 100644 tests/Aspire.Qdrant.Client.Tests/ConformanceTests.cs create mode 100644 tests/Aspire.Qdrant.Client.Tests/README.md diff --git a/Aspire.sln b/Aspire.sln index 8478c71c52..f92f463b3c 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -423,12 +423,24 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Azure.Sql", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Azure.Storage", "src\Aspire.Hosting.Azure.Storage\Aspire.Hosting.Azure.Storage.csproj", "{89E9F2B8-662C-4FFA-8F69-360680362653}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Qdrant", "src\Aspire.Hosting.Qdrant\Aspire.Hosting.Qdrant.csproj", "{D3CDBA75-7707-4884-908D-1BA22B8DF8E7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "qdrant", "qdrant", "{A4800EE3-F902-4B7B-AF53-01A85514E6B9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Qdrant.AppHost", "playground\Qdrant\Qdrant.AppHost\Qdrant.AppHost.csproj", "{F43586B8-FE36-490D-9EFA-82CFFB83A304}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Qdrant.ApiService", "playground\Qdrant\Qdrant.ApiService\Qdrant.ApiService.csproj", "{6B6D3953-E961-4720-B27E-9466A69BED1A}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Azure.EventHubs", "src\Aspire.Hosting.Azure.EventHubs\Aspire.Hosting.Azure.EventHubs.csproj", "{2580B014-E7FE-48D9-BE40-E90604365F0E}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Tests.SharedShim", "tests\Aspire.Hosting.Tests.SharedShim\Aspire.Hosting.Tests.SharedShim.csproj", "{74644A4D-8F61-4314-B6E8-5CE3802CD6C2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Azure.KeyVault", "src\Aspire.Hosting.Azure.KeyVault\Aspire.Hosting.Azure.KeyVault.csproj", "{427F4D7C-8969-4015-AD1A-5EFFE921A184}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Qdrant.Client", "src\Components\Aspire.Qdrant.Client\Aspire.Qdrant.Client.csproj", "{E0E1B557-D3CF-4446-B993-E5CE719234FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Qdrant.Client.Tests", "tests\Aspire.Qdrant.Client.Tests\Aspire.Qdrant.Client.Tests.csproj", "{A9CFA376-0C90-4231-9152-FBF14065195A}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Azure.Messaging.EventHubs.Tests", "tests\Aspire.Azure.Messaging.EventHubs.Tests\Aspire.Azure.Messaging.EventHubs.Tests.csproj", "{8191109E-130C-47F3-B84E-82070A6CD269}" EndProject Global @@ -437,6 +449,10 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A9CFA376-0C90-4231-9152-FBF14065195A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9CFA376-0C90-4231-9152-FBF14065195A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9CFA376-0C90-4231-9152-FBF14065195A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9CFA376-0C90-4231-9152-FBF14065195A}.Release|Any CPU.Build.0 = Release|Any CPU {B52DCF1A-465D-4E92-A68A-0EE1A9ED49DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B52DCF1A-465D-4E92-A68A-0EE1A9ED49DF}.Debug|Any CPU.Build.0 = Debug|Any CPU {B52DCF1A-465D-4E92-A68A-0EE1A9ED49DF}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1113,6 +1129,18 @@ Global {89E9F2B8-662C-4FFA-8F69-360680362653}.Debug|Any CPU.Build.0 = Debug|Any CPU {89E9F2B8-662C-4FFA-8F69-360680362653}.Release|Any CPU.ActiveCfg = Release|Any CPU {89E9F2B8-662C-4FFA-8F69-360680362653}.Release|Any CPU.Build.0 = Release|Any CPU + {D3CDBA75-7707-4884-908D-1BA22B8DF8E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3CDBA75-7707-4884-908D-1BA22B8DF8E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3CDBA75-7707-4884-908D-1BA22B8DF8E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3CDBA75-7707-4884-908D-1BA22B8DF8E7}.Release|Any CPU.Build.0 = Release|Any CPU + {F43586B8-FE36-490D-9EFA-82CFFB83A304}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F43586B8-FE36-490D-9EFA-82CFFB83A304}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F43586B8-FE36-490D-9EFA-82CFFB83A304}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F43586B8-FE36-490D-9EFA-82CFFB83A304}.Release|Any CPU.Build.0 = Release|Any CPU + {6B6D3953-E961-4720-B27E-9466A69BED1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B6D3953-E961-4720-B27E-9466A69BED1A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B6D3953-E961-4720-B27E-9466A69BED1A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B6D3953-E961-4720-B27E-9466A69BED1A}.Release|Any CPU.Build.0 = Release|Any CPU {2580B014-E7FE-48D9-BE40-E90604365F0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2580B014-E7FE-48D9-BE40-E90604365F0E}.Debug|Any CPU.Build.0 = Debug|Any CPU {2580B014-E7FE-48D9-BE40-E90604365F0E}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1125,6 +1153,10 @@ Global {427F4D7C-8969-4015-AD1A-5EFFE921A184}.Debug|Any CPU.Build.0 = Debug|Any CPU {427F4D7C-8969-4015-AD1A-5EFFE921A184}.Release|Any CPU.ActiveCfg = Release|Any CPU {427F4D7C-8969-4015-AD1A-5EFFE921A184}.Release|Any CPU.Build.0 = Release|Any CPU + {E0E1B557-D3CF-4446-B993-E5CE719234FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0E1B557-D3CF-4446-B993-E5CE719234FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0E1B557-D3CF-4446-B993-E5CE719234FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0E1B557-D3CF-4446-B993-E5CE719234FB}.Release|Any CPU.Build.0 = Release|Any CPU {8191109E-130C-47F3-B84E-82070A6CD269}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8191109E-130C-47F3-B84E-82070A6CD269}.Debug|Any CPU.Build.0 = Debug|Any CPU {8191109E-130C-47F3-B84E-82070A6CD269}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1134,6 +1166,7 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {A9CFA376-0C90-4231-9152-FBF14065195A} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} {B52DCF1A-465D-4E92-A68A-0EE1A9ED49DF} = {B80354C7-BE58-43F6-8928-9F3A74AB7F47} {E958BE04-81C2-434C-9E6C-CA145A2B8218} = {A68BA1A5-1604-433D-9778-DC0199831C2A} {C1D595AD-FFFD-4E52-AAF6-8DD8C4BD67F1} = {A68BA1A5-1604-433D-9778-DC0199831C2A} @@ -1330,9 +1363,14 @@ Global {CB7CAE39-F041-4B20-A0C4-D6F44920A647} = {77CFE74A-32EE-400C-8930-5025E8555256} {9FF853BD-FA56-4DA5-A50A-9867F2FAB1F0} = {77CFE74A-32EE-400C-8930-5025E8555256} {89E9F2B8-662C-4FFA-8F69-360680362653} = {77CFE74A-32EE-400C-8930-5025E8555256} + {D3CDBA75-7707-4884-908D-1BA22B8DF8E7} = {B80354C7-BE58-43F6-8928-9F3A74AB7F47} + {A4800EE3-F902-4B7B-AF53-01A85514E6B9} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} + {F43586B8-FE36-490D-9EFA-82CFFB83A304} = {A4800EE3-F902-4B7B-AF53-01A85514E6B9} + {6B6D3953-E961-4720-B27E-9466A69BED1A} = {A4800EE3-F902-4B7B-AF53-01A85514E6B9} {2580B014-E7FE-48D9-BE40-E90604365F0E} = {77CFE74A-32EE-400C-8930-5025E8555256} {74644A4D-8F61-4314-B6E8-5CE3802CD6C2} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} {427F4D7C-8969-4015-AD1A-5EFFE921A184} = {77CFE74A-32EE-400C-8930-5025E8555256} + {E0E1B557-D3CF-4446-B993-E5CE719234FB} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2} {8191109E-130C-47F3-B84E-82070A6CD269} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/Directory.Packages.props b/Directory.Packages.props index 27660f7e6b..383c504e5e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -97,6 +97,7 @@ + diff --git a/playground/Qdrant/Qdrant.ApiService/Program.cs b/playground/Qdrant/Qdrant.ApiService/Program.cs new file mode 100644 index 0000000000..589448e7a4 --- /dev/null +++ b/playground/Qdrant/Qdrant.ApiService/Program.cs @@ -0,0 +1,93 @@ +using Qdrant.Client; +using Qdrant.Client.Grpc; + +var builder = WebApplication.CreateBuilder(args); + +// Add service defaults & Aspire components. +builder.AddServiceDefaults(); + +// Add services to the container. +builder.Services.AddProblemDetails(); + +builder.AddQdrantClient("qdrant"); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +app.UseExceptionHandler(); + +app.MapGet("/create", async (QdrantClient client, ILogger logger) => +{ + var collections = await client.ListCollectionsAsync(); + if (collections.Any(x => x.Contains("movie_collection"))) + { + await client.DeleteCollectionAsync("movie_collection"); + } + + await client.CreateCollectionAsync("movie_collection", new VectorParams { Size = 2, Distance = Distance.Cosine }); + var collectionInfo = await client.GetCollectionInfoAsync("movie_collection"); + logger.LogInformation(collectionInfo.ToString()); + + // generate some vectors + var data = new[] + { + new PointStruct + { + Id = 1, + Vectors = new [] {0.10022575f, -0.23998135f}, + Payload = + { + ["title"] = "The Lion King" + } + }, + new PointStruct + { + Id = 2, + Vectors = new [] {0.10327095f, 0.2563685f}, + Payload = + { + ["title"] = "Inception" + } + }, + new PointStruct + { + Id = 3, + Vectors = new [] {0.095857024f, -0.201278f}, + Payload = + { + ["title"] = "Toy Story" + } + }, + new PointStruct + { + Id = 4, + Vectors = new [] {0.106827796f, 0.21676421f}, + Payload = + { + ["title"] = "Pulp Function" + } + }, + new PointStruct + { + Id = 5, + Vectors = new [] {0.09568083f, -0.21177962f}, + Payload = + { + ["title"] = "Shrek" + } + }, + }; + var updateResult = await client.UpsertAsync("movie_collection", data); + + return updateResult.Status; +}); + +app.MapGet("/search", async (QdrantClient client) => +{ + var results = await client.SearchAsync("movie_collection", new[] { 0.12217915f, -0.034832448f }, limit: 3); + return results.Select(titles => titles.Payload["title"].StringValue); +}); + +app.MapDefaultEndpoints(); + +app.Run(); diff --git a/playground/Qdrant/Qdrant.ApiService/Properties/launchSettings.json b/playground/Qdrant/Qdrant.ApiService/Properties/launchSettings.json new file mode 100644 index 0000000000..a728d35665 --- /dev/null +++ b/playground/Qdrant/Qdrant.ApiService/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "https://localhost:5450;http://localhost:5451", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "http://localhost:5451", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/playground/Qdrant/Qdrant.ApiService/Qdrant.ApiService.csproj b/playground/Qdrant/Qdrant.ApiService/Qdrant.ApiService.csproj new file mode 100644 index 0000000000..ffffb8fc49 --- /dev/null +++ b/playground/Qdrant/Qdrant.ApiService/Qdrant.ApiService.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + 1e49caab-af46-4c24-8011-953ec12b4069 + + + + + + + + diff --git a/playground/Qdrant/Qdrant.ApiService/appsettings.Development.json b/playground/Qdrant/Qdrant.ApiService/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/playground/Qdrant/Qdrant.ApiService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/playground/Qdrant/Qdrant.ApiService/appsettings.json b/playground/Qdrant/Qdrant.ApiService/appsettings.json new file mode 100644 index 0000000000..1e74af8c8a --- /dev/null +++ b/playground/Qdrant/Qdrant.ApiService/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Qdrant.Client": "Debug" + } + }, + "AllowedHosts": "*" +} diff --git a/playground/Qdrant/Qdrant.AppHost/Directory.Build.props b/playground/Qdrant/Qdrant.AppHost/Directory.Build.props new file mode 100644 index 0000000000..d9b2c324ac --- /dev/null +++ b/playground/Qdrant/Qdrant.AppHost/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/playground/Qdrant/Qdrant.AppHost/Directory.Build.targets b/playground/Qdrant/Qdrant.AppHost/Directory.Build.targets new file mode 100644 index 0000000000..29db89f209 --- /dev/null +++ b/playground/Qdrant/Qdrant.AppHost/Directory.Build.targets @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/playground/Qdrant/Qdrant.AppHost/Program.cs b/playground/Qdrant/Qdrant.AppHost/Program.cs new file mode 100644 index 0000000000..d7390fb4e2 --- /dev/null +++ b/playground/Qdrant/Qdrant.AppHost/Program.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +var builder = DistributedApplication.CreateBuilder(args); + +var qdrant = builder.AddQdrant("qdrant") + .WithDataVolume("qdrant_data"); + +builder.AddProject("apiservice") + .WithReference(qdrant); + +builder.Build().Run(); diff --git a/playground/Qdrant/Qdrant.AppHost/Properties/launchSettings.json b/playground/Qdrant/Qdrant.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000000..e75f3d87e9 --- /dev/null +++ b/playground/Qdrant/Qdrant.AppHost/Properties/launchSettings.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:15206;http://localhost:15207", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:16022", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:17038", + "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15207", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16022", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:17039", + "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true" + } + } + } +} diff --git a/playground/Qdrant/Qdrant.AppHost/Qdrant.AppHost.csproj b/playground/Qdrant/Qdrant.AppHost/Qdrant.AppHost.csproj new file mode 100644 index 0000000000..bf89e16e61 --- /dev/null +++ b/playground/Qdrant/Qdrant.AppHost/Qdrant.AppHost.csproj @@ -0,0 +1,23 @@ + + + + Exe + net8.0 + enable + enable + true + 10c36641-05e0-4bfb-ad9d-a588431430f0 + + + + + + + + + + + + + + diff --git a/playground/Qdrant/Qdrant.AppHost/appsettings.Development.json b/playground/Qdrant/Qdrant.AppHost/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/playground/Qdrant/Qdrant.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/playground/Qdrant/Qdrant.AppHost/appsettings.json b/playground/Qdrant/Qdrant.AppHost/appsettings.json new file mode 100644 index 0000000000..31c092aa45 --- /dev/null +++ b/playground/Qdrant/Qdrant.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/spelling.dic b/spelling.dic index fa2eff6f45..af6b38c5a5 100644 --- a/spelling.dic +++ b/spelling.dic @@ -45,6 +45,7 @@ pgadmin postgre postgres protoc +qdrant rabbitmq redis rediscommander diff --git a/src/Aspire.Hosting.AWS/SDKResourceExtensions.cs b/src/Aspire.Hosting.AWS/SDKResourceExtensions.cs index ae35c43265..4c756acb0f 100644 --- a/src/Aspire.Hosting.AWS/SDKResourceExtensions.cs +++ b/src/Aspire.Hosting.AWS/SDKResourceExtensions.cs @@ -48,12 +48,13 @@ public static IAWSSDKConfig WithRegion(this IAWSSDKConfig config, RegionEndpoint } /// - /// Add a reference to an AWS SDK configuration a project. + /// Add a reference to an AWS SDK configuration to the resource. /// /// An for /// The AWS SDK configuration /// - public static IResourceBuilder WithReference(this IResourceBuilder builder, IAWSSDKConfig awsSdkConfig) + public static IResourceBuilder WithReference(this IResourceBuilder builder, IAWSSDKConfig awsSdkConfig) + where TDestination : IResourceWithEnvironment { builder.WithEnvironment(context => { diff --git a/src/Aspire.Hosting.Qdrant/Aspire.Hosting.Qdrant.csproj b/src/Aspire.Hosting.Qdrant/Aspire.Hosting.Qdrant.csproj new file mode 100644 index 0000000000..4b29d0c93d --- /dev/null +++ b/src/Aspire.Hosting.Qdrant/Aspire.Hosting.Qdrant.csproj @@ -0,0 +1,24 @@ + + + + $(NetCurrent) + true + aspire hosting qdrant + Qdrant vector database support for .NET Aspire. + $(SharedDir)QdrantLogo_256x.png + + + + + + + + + + + + + + + + diff --git a/src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs b/src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs new file mode 100644 index 0000000000..735e33112d --- /dev/null +++ b/src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Qdrant; +using Aspire.Hosting.Utils; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding Qdrant resources to the application model. +/// +public static class QdrantBuilderExtensions +{ + private const int QdrantPortGrpc = 6334; + private const int QdrantPortHttp = 6333; + private const string ApiKeyEnvVarName = "QDRANT__SERVICE__API_KEY"; + private const string EnableStaticContentEnvVarName = "QDRANT__SERVICE__ENABLE_STATIC_CONTENT"; + + /// + /// Adds a Qdrant resource to the application. A container is used for local development. + /// + /// + /// This version the package defaults to the v1.8.3 tag of the qdrant/qdrant container image. + /// The .NET client library uses the gRPC port by default to communicate and this resource exposes that endpoint. + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency + /// The parameter used to provide the API Key for the Qdrant resource. If a random key will be generated as {name}-Key. + /// The host port of gRPC endpoint of Qdrant database. + /// The host port of HTTP endpoint of Qdrant database. + /// A reference to the . + public static IResourceBuilder AddQdrant(this IDistributedApplicationBuilder builder, + string name, + IResourceBuilder? apiKey = null, + int? grpcPort = null, + int? httpPort = null) + { + var apiKeyParameter = apiKey?.Resource ?? + ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-Key", special: false); + var qdrant = new QdrantServerResource(name, apiKeyParameter); + return builder.AddResource(qdrant) + .WithImage(QdrantContainerImageTags.Image, QdrantContainerImageTags.Tag) + .WithHttpEndpoint(port: grpcPort, targetPort: QdrantPortGrpc, name: QdrantServerResource.PrimaryEndpointName) + .WithHttpEndpoint(port: httpPort, targetPort: QdrantPortHttp, name: QdrantServerResource.HttpEndpointName) + .WithEnvironment(context => + { + context.EnvironmentVariables[ApiKeyEnvVarName] = qdrant.ApiKeyParameter; + + // If in Publish mode, disable static content, which disables the Dashboard Web UI + // https://github.com/qdrant/qdrant/blob/acb04d5f0d22b46a756b31c0fc507336a0451c15/src/settings.rs#L36-L40 + if (builder.ExecutionContext.IsPublishMode) + { + context.EnvironmentVariables[EnableStaticContentEnvVarName] = "0"; + } + }); + } + + /// + /// Adds a named volume for the data folder to a Qdrant container resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the resource name. + /// A flag that indicates if this is a read-only volume. + /// The . + public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null, bool isReadOnly = false) + => builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/qdrant/storage", isReadOnly); + + /// + /// Adds a bind mount for the data folder to a Qdrant container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// The . + public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source, bool isReadOnly = false) + => builder.WithBindMount(source, "/qdrant/storage", isReadOnly); + + /// + /// Add a reference to a Qdrant server to the resource. + /// + /// An for + /// The Qdrant server resource + /// + public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder qdrantResource) + where TDestination : IResourceWithEnvironment + { + builder.WithEnvironment(context => + { + // primary endpoint (gRPC) + context.EnvironmentVariables[$"ConnectionStrings__{qdrantResource.Resource.Name}"] = qdrantResource.Resource.ConnectionStringExpression; + + // HTTP endpoint + context.EnvironmentVariables[$"ConnectionStrings__{qdrantResource.Resource.Name}_{QdrantServerResource.HttpEndpointName}"] = qdrantResource.Resource.HttpConnectionStringExpression; + }); + + return builder; + } +} diff --git a/src/Aspire.Hosting.Qdrant/QdrantContainerImageTags.cs b/src/Aspire.Hosting.Qdrant/QdrantContainerImageTags.cs new file mode 100644 index 0000000000..9c598c5b7f --- /dev/null +++ b/src/Aspire.Hosting.Qdrant/QdrantContainerImageTags.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.Qdrant; + +internal static class QdrantContainerImageTags +{ + public const string Image = "qdrant/qdrant"; + public const string Tag = "v1.8.3"; +} diff --git a/src/Aspire.Hosting.Qdrant/QdrantServerResource.cs b/src/Aspire.Hosting.Qdrant/QdrantServerResource.cs new file mode 100644 index 0000000000..a198ef6ff9 --- /dev/null +++ b/src/Aspire.Hosting.Qdrant/QdrantServerResource.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a Qdrant database. +/// +public class QdrantServerResource : ContainerResource, IResourceWithConnectionString +{ + internal const string PrimaryEndpointName = "grpc"; + internal const string HttpEndpointName = "http"; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the resource. + /// A that contains the API Key + public QdrantServerResource(string name, ParameterResource apiKey) : base(name) + { + ArgumentNullException.ThrowIfNull(apiKey); + ApiKeyParameter = apiKey; + } + + private EndpointReference? _primaryEndpoint; + private EndpointReference? _httpEndpoint; + + /// + /// Gets the parameter that contains the Qdrant API key. + /// + public ParameterResource ApiKeyParameter { get; } + + /// + /// Gets the gRPC endpoint for the Qdrant database. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + + /// + /// Gets the HTTP endpoint for the Qdrant database. + /// + public EndpointReference HttpEndpoint => _httpEndpoint ??= new(this, HttpEndpointName); + + /// + /// Gets the connection string expression for the Qdrant gRPC endpoint. + /// + public ReferenceExpression ConnectionStringExpression => + ReferenceExpression.Create( + $"Endpoint={PrimaryEndpoint.Property(EndpointProperty.Scheme)}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)};Key={ApiKeyParameter}"); + + /// + /// Gets the connection string expression for the Qdrant HTTP endpoint. + /// + public ReferenceExpression HttpConnectionStringExpression => + ReferenceExpression.Create( + $"Endpoint={HttpEndpoint.Property(EndpointProperty.Scheme)}://{HttpEndpoint.Property(EndpointProperty.Host)}:{HttpEndpoint.Property(EndpointProperty.Port)};Key={ApiKeyParameter}"); +} diff --git a/src/Aspire.Hosting.Qdrant/README.md b/src/Aspire.Hosting.Qdrant/README.md new file mode 100644 index 0000000000..b5496872f4 --- /dev/null +++ b/src/Aspire.Hosting.Qdrant/README.md @@ -0,0 +1,33 @@ +# Aspire.Hosting.Qdrant library + +Provides extension methods and resource definitions for a .NET Aspire AppHost to configure a Qdrant vector database resource. + +## Getting started + +### Install the package + +In your AppHost project, install the .NET Aspire Qdrant Hosting library with [NuGet](https://www.nuget.org): + +```dotnetcli +dotnet add package Aspire.Hosting.Qdrant +``` + +## Usage example + +Then, in the _Program.cs_ file of `AppHost`, add a Qdrant resource and consume the connection using the following methods: + +```csharp +var qdrant = builder.AddQdrant("qdrant"); + +var myService = builder.AddProject() + .WithReference(qdrant); +``` + +## Additional documentation +* https://qdrant.tech/documentation + +## Feedback & contributing + +https://github.com/dotnet/aspire + +_Qdrant, and the Qdrant logo are trademarks or registered trademarks of Qdrant Solutions GmbH of Germany, and used with their permission._ diff --git a/src/Components/Aspire.Qdrant.Client/Aspire.Qdrant.Client.csproj b/src/Components/Aspire.Qdrant.Client/Aspire.Qdrant.Client.csproj new file mode 100644 index 0000000000..9edc30e8d3 --- /dev/null +++ b/src/Components/Aspire.Qdrant.Client/Aspire.Qdrant.Client.csproj @@ -0,0 +1,21 @@ + + + + $(NetCurrent) + true + $(ComponentCommonPackageTags) qdrant + A Qdrant client that integrates with Aspire, including logging. + $(SharedDir)QdrantLogo_256x.png + + + + + + + + + + + + + diff --git a/src/Components/Aspire.Qdrant.Client/AspireQdrantExtensions.cs b/src/Components/Aspire.Qdrant.Client/AspireQdrantExtensions.cs new file mode 100644 index 0000000000..e22929edaa --- /dev/null +++ b/src/Components/Aspire.Qdrant.Client/AspireQdrantExtensions.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Qdrant.Client; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Qdrant.Client; + +namespace Microsoft.Extensions.Hosting; + +/// +/// Provides extension methods for registering Qdrant-related services in an . +/// +public static class AspireQdrantExtensions +{ + private const string DefaultConfigSectionName = "Aspire:Qdrant:Client"; + + /// + /// Registers as a singleton in the services provided by the . + /// Configures logging for the . + /// + /// The to read config from and add services to. + /// The connection name to use to find a connection string. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// Reads the configuration from "Aspire:Qdrant:Client" section. + /// If required ConnectionString is not provided in configuration section + public static void AddQdrantClient( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null) + { + AddQdrant(builder, DefaultConfigSectionName, configureSettings, connectionName, serviceKey: null); + } + + /// + /// Registers as a keyed singleton for the given in the services provided by the . + /// Configures logging for the . + /// + /// The to read config from and add services to. + /// The connection name to use to find a connection string. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// Reads the configuration from "Aspire:Qdrant:Client" section. + /// If required ConnectionString is not provided in configuration section + public static void AddKeyedQdrantClient( + this IHostApplicationBuilder builder, + string name, + Action? configureSettings = null) + { + AddQdrant(builder, $"{DefaultConfigSectionName}:{name}", configureSettings, connectionName: name, serviceKey: name); + } + + private static void AddQdrant( + this IHostApplicationBuilder builder, + string configurationSectionName, + Action? configureSettings, + string connectionName, + string? serviceKey) + { + ArgumentNullException.ThrowIfNull(builder); + + var settings = new QdrantClientSettings(); + builder.Configuration.GetSection(configurationSectionName).Bind(settings); + + if (builder.Configuration.GetConnectionString(connectionName) is string connectionString) + { + settings.ParseConnectionString(connectionString); + } + + configureSettings?.Invoke(settings); + + if (serviceKey is null) + { + builder.Services.AddSingleton(ConfigureQdrant); + } + else + { + builder.Services.AddKeyedSingleton(serviceKey, (sp, key) => ConfigureQdrant(sp)); + } + + QdrantClient ConfigureQdrant(IServiceProvider serviceProvider) + { + if (settings.Endpoint is not null) + { + return new QdrantClient(settings.Endpoint, apiKey: settings.Key, loggerFactory: serviceProvider.GetRequiredService()); + } + else + { + throw new InvalidOperationException( + $"A QdrantClient could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}' or either " + + $"{nameof(settings.Endpoint)} must be provided " + + $"in the '{configurationSectionName}' configuration section."); + } + } + } +} diff --git a/src/Components/Aspire.Qdrant.Client/AssemblyInfo.cs b/src/Components/Aspire.Qdrant.Client/AssemblyInfo.cs new file mode 100644 index 0000000000..914347a4b9 --- /dev/null +++ b/src/Components/Aspire.Qdrant.Client/AssemblyInfo.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire; +using Aspire.Qdrant.Client; + +[assembly: ConfigurationSchema("Aspire:Qdrant:Client", typeof(QdrantClientSettings))] + +[assembly: LoggingCategories("Qdrant.Client")] diff --git a/src/Components/Aspire.Qdrant.Client/ConfigurationSchema.json b/src/Components/Aspire.Qdrant.Client/ConfigurationSchema.json new file mode 100644 index 0000000000..a0b7d5db3d --- /dev/null +++ b/src/Components/Aspire.Qdrant.Client/ConfigurationSchema.json @@ -0,0 +1,39 @@ +{ + "definitions": { + "logLevel": { + "properties": { + "Qdrant.Client": { + "$ref": "#/definitions/logLevelThreshold" + } + } + } + }, + "properties": { + "Aspire": { + "type": "object", + "properties": { + "Qdrant": { + "type": "object", + "properties": { + "Client": { + "type": "object", + "properties": { + "Endpoint": { + "type": "string", + "format": "uri", + "description": "The endpoint URI string of the Qdrant server to connect to." + }, + "Key": { + "type": "string", + "description": "The API Key of the Qdrant server to connect to." + } + }, + "description": "Provides the client configuration settings for connecting to a Qdrant server using QdrantClient." + } + } + } + } + } + }, + "type": "object" +} diff --git a/src/Components/Aspire.Qdrant.Client/QdrantClientSettings.cs b/src/Components/Aspire.Qdrant.Client/QdrantClientSettings.cs new file mode 100644 index 0000000000..5db43f6549 --- /dev/null +++ b/src/Components/Aspire.Qdrant.Client/QdrantClientSettings.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Data.Common; + +namespace Aspire.Qdrant.Client; + +/// +/// Provides the client configuration settings for connecting to a Qdrant server using QdrantClient. +/// +public sealed class QdrantClientSettings +{ + private const string ConnectionStringEndpoint = "Endpoint"; + private const string ConnectionStringKey = "Key"; + + /// + /// The endpoint URI string of the Qdrant server to connect to. + /// + public Uri? Endpoint { get; set; } + + /// + /// The API Key of the Qdrant server to connect to. + /// + public string? Key { get; set; } + + internal void ParseConnectionString(string? connectionString) + { + if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) + { + Endpoint = uri; + } + else + { + var connectionBuilder = new DbConnectionStringBuilder + { + ConnectionString = connectionString + }; + + if (connectionBuilder.ContainsKey(ConnectionStringEndpoint) && Uri.TryCreate(connectionBuilder[ConnectionStringEndpoint].ToString(), UriKind.Absolute, out var serviceUri)) + { + Endpoint = serviceUri; + } + + if (connectionBuilder.ContainsKey(ConnectionStringKey)) + { + Key = connectionBuilder[ConnectionStringKey].ToString(); + } + } + } +} diff --git a/src/Components/Aspire.Qdrant.Client/README.md b/src/Components/Aspire.Qdrant.Client/README.md new file mode 100644 index 0000000000..c956e2ec4b --- /dev/null +++ b/src/Components/Aspire.Qdrant.Client/README.md @@ -0,0 +1,107 @@ +# Aspire.Qdrant.Client library + +Registers a [QdrantClient](https://github.com/qdrant/qdrant-dotnet) in the DI container for connecting to a Qdrant server. + +## Getting started + +### Prerequisites + +- Qdrant server and connection string for accessing the server API endpoint. + +### Install the package + +Install the .NET Aspire Qdrant Client library with [NuGet](https://www.nuget.org): + +```dotnetcli +dotnet add package Aspire.Qdrant.Client +``` + +## Usage example + +In the _Program.cs_ file of your project, call the `AddQdrantClient` extension method to register a `QdrantClient` for use via the dependency injection container. The method takes a connection name parameter. + +```csharp +builder.AddQdrantClient("qdrant"); +``` + +## Configuration + +The .NET Aspire Qdrant Client component provides multiple options to configure the server connection based on the requirements and conventions of your project. + +### Use a connection string + +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddQdrantClient()`: + +```csharp +builder.AddQdrantClient("qdrant"); +``` + +And then the connection string will be retrieved from the `ConnectionStrings` configuration section: + +```json +{ + "ConnectionStrings": { + "qdrant": "Endpoint=http://localhost:6334;Key=123456!@#$%" + } +} +``` + +By default the `QdrantClient` uses the gRPC API endpoint. + +### Use configuration providers + +The .NET Aspire Qdrant Client component supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `QdrantSettings` from configuration by using the `Aspire:Qdrant:Client` key. Example `appsettings.json` that configures some of the options: + +```json +{ + "Aspire": { + "Qdrant": { + "Client": { + "Key": "123456!@#$%" + } + } + } +} +``` + +### Use inline delegates + +Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to set the API key from code: + +```csharp +builder.AddQdrantClient("qdrant", settings => settings.ApiKey = "12345!@#$%"); +``` + +## AppHost extensions + +In your AppHost project, install the `Aspire.Hosting.Qdrant` library with [NuGet](https://www.nuget.org): + +```dotnetcli +dotnet add package Aspire.Hosting.Qdrant +``` + +Then, in the _Program.cs_ file of `AppHost`, register a Qdrant server and consume the connection using the following methods: + +```csharp +var qdrant = builder.AddQdrant("qdrant"); + +var myService = builder.AddProject() + .WithReference(qdrant); +``` + +The `WithReference` method configures a connection in the `MyService` project named `qdrant`. In the _Program.cs_ file of `MyService`, the Qdrant connection can be consumed using: + +```csharp +builder.AddQdrantClient("qdrant"); +``` + +## Additional documentation + +* https://github.com/qdrant/qdrant-dotnet +* https://github.com/dotnet/aspire/tree/main/src/Components/README.md + +## Feedback & contributing + +https://github.com/dotnet/aspire + +_Qdrant, and the Qdrant logo are trademarks or registered trademarks of Qdrant Solutions GmbH of Germany, and used with their permission._ diff --git a/src/Components/Aspire_Components_Progress.md b/src/Components/Aspire_Components_Progress.md index 69ea259cef..0fe3966191 100644 --- a/src/Components/Aspire_Components_Progress.md +++ b/src/Components/Aspire_Components_Progress.md @@ -29,6 +29,7 @@ As part of the .NET Aspire November preview, we want to include a set of .NET As | Pomelo.EntityFrameworkCore.MySql | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | NATS.Net | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅ | | Seq | ✅ | ✅ | ✅ | ✅ | ✅ | N/A | N/A | ✅ | +| Qdrant.Client | ✅ | ✅ | ✅ | ✅ | ✅ | | | | Nomenclature used in the table above: diff --git a/src/Components/Telemetry.md b/src/Components/Telemetry.md index 3fea50c74f..ea137b68d6 100644 --- a/src/Components/Telemetry.md +++ b/src/Components/Telemetry.md @@ -274,6 +274,14 @@ Aspire.Pomelo.EntityFrameworkCore.MySql: - "db.client.connections.timeouts" - "db.client.connections.usage" +Aspire.Qdrant.Client: +- Log categories: + "Qdrant.Client" +- Activity source names: + - none (not currently supported by Qdrant.Client library) +- Metric names: + - none (currently not supported by Qdrant.Client library) + Aspire.RabbitMQ.Client: - Log categories: - "RabbitMQ.Client" diff --git a/src/Shared/QdrantLogo_256x.png b/src/Shared/QdrantLogo_256x.png new file mode 100644 index 0000000000000000000000000000000000000000..d49afd6f3b30559822a5716f1636f8514fd0141f GIT binary patch literal 17190 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 literal 0 HcmV?d00001 diff --git a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj index ef1d696493..69256ba50d 100644 --- a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj +++ b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj @@ -32,6 +32,7 @@ + diff --git a/tests/Aspire.Hosting.Tests/Qdrant/AddQdrantTests.cs b/tests/Aspire.Hosting.Tests/Qdrant/AddQdrantTests.cs new file mode 100644 index 0000000000..45d0172fd3 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/Qdrant/AddQdrantTests.cs @@ -0,0 +1,300 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Sockets; +using Aspire.Hosting.Qdrant; +using Aspire.Hosting.Tests.Utils; +using Aspire.Hosting.Utils; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Aspire.Hosting.Tests.Qdrant; + +public class AddQdrantTests +{ + private const int QdrantPortGrpc = 6334; + private const int QdrantPortHttp = 6333; + + [Fact] + public async Task AddQdrantWithDefaultsAddsAnnotationMetadata() + { + var appBuilder = DistributedApplication.CreateBuilder(); + appBuilder.AddQdrant("my-qdrant"); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var containerResource = Assert.Single(appModel.GetContainerResources()); + Assert.Equal("my-qdrant", containerResource.Name); + + var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); + Assert.Equal(QdrantContainerImageTags.Tag, containerAnnotation.Tag); + Assert.Equal(QdrantContainerImageTags.Image, containerAnnotation.Image); + Assert.Null(containerAnnotation.Registry); + + var endpoint = containerResource.Annotations.OfType() + .FirstOrDefault(e => e.Name == "grpc"); + Assert.NotNull(endpoint); + Assert.Equal(QdrantPortGrpc, endpoint.TargetPort); + Assert.False(endpoint.IsExternal); + Assert.Equal("grpc", endpoint.Name); + Assert.Null(endpoint.Port); + Assert.Equal(ProtocolType.Tcp, endpoint.Protocol); + Assert.Equal("http", endpoint.Transport); + Assert.Equal("http", endpoint.UriScheme); + + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerResource); + + Assert.Collection(config, + env => + { + Assert.Equal("QDRANT__SERVICE__API_KEY", env.Key); + Assert.False(string.IsNullOrEmpty(env.Value)); + }); + } + + [Fact] + public void AddQdrantWithDefaultsAndDashboardAddsAnnotationMetadata() + { + var appBuilder = DistributedApplication.CreateBuilder(); + appBuilder.AddQdrant("my-qdrant"); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var containerResource = Assert.Single(appModel.GetContainerResources()); + Assert.Equal("my-qdrant", containerResource.Name); + + var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); + Assert.Equal(QdrantContainerImageTags.Tag, containerAnnotation.Tag); + Assert.Equal(QdrantContainerImageTags.Image, containerAnnotation.Image); + Assert.Null(containerAnnotation.Registry); + + var endpoint = containerResource.Annotations.OfType() + .FirstOrDefault(e => e.Name == "http"); + + Assert.NotNull(endpoint); + Assert.Equal(QdrantPortHttp, endpoint.TargetPort); + Assert.False(endpoint.IsExternal); + Assert.Equal("http", endpoint.Name); + Assert.Null(endpoint.Port); + Assert.Equal(ProtocolType.Tcp, endpoint.Protocol); + Assert.Equal("http", endpoint.Transport); + Assert.Equal("http", endpoint.UriScheme); + } + + [Fact] + public async Task AddQdrantAddsAnnotationMetadata() + { + var appBuilder = DistributedApplication.CreateBuilder(); + appBuilder.Configuration["Parameters:pass"] = "pass"; + + var pass = appBuilder.AddParameter("pass"); + appBuilder.AddQdrant("my-qdrant", apiKey: pass); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var containerResource = Assert.Single(appModel.GetContainerResources()); + Assert.Equal("my-qdrant", containerResource.Name); + + var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); + Assert.Equal(QdrantContainerImageTags.Tag, containerAnnotation.Tag); + Assert.Equal(QdrantContainerImageTags.Image, containerAnnotation.Image); + Assert.Null(containerAnnotation.Registry); + + var endpoint = containerResource.Annotations.OfType() + .FirstOrDefault(e => e.Name == "grpc"); + Assert.NotNull(endpoint); + Assert.Equal(QdrantPortGrpc, endpoint.TargetPort); + Assert.False(endpoint.IsExternal); + Assert.Equal("grpc", endpoint.Name); + Assert.Null(endpoint.Port); + Assert.Equal(ProtocolType.Tcp, endpoint.Protocol); + Assert.Equal("http", endpoint.Transport); + Assert.Equal("http", endpoint.UriScheme); + + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerResource); + + Assert.Collection(config, + env => + { + Assert.Equal("QDRANT__SERVICE__API_KEY", env.Key); + Assert.Equal("pass", env.Value); + }); + } + + [Fact] + public async Task QdrantCreatesConnectionString() + { + var appBuilder = DistributedApplication.CreateBuilder(); + + appBuilder.Configuration["Parameters:pass"] = "pass"; + var pass = appBuilder.AddParameter("pass"); + + var qdrant = appBuilder.AddQdrant("my-qdrant", pass) + .WithEndpoint("grpc", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6334)); + + var connectionStringResource = qdrant.Resource as IResourceWithConnectionString; + + var connectionString = await connectionStringResource.GetConnectionStringAsync(); + Assert.Equal($"Endpoint=http://localhost:6334;Key=pass", connectionString); + } + + [Fact] + public async Task QdrantClientAppWithReferenceContainsConnectionStrings() + { + using var testProgram = CreateTestProgram(); + var appBuilder = DistributedApplication.CreateBuilder(); + + appBuilder.Configuration["Parameters:pass"] = "pass"; + var pass = appBuilder.AddParameter("pass"); + + var qdrant = appBuilder.AddQdrant("my-qdrant", pass) + .WithEndpoint("grpc", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6334)) + .WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6333)); + + var projectA = appBuilder.AddProject("projecta") + .WithReference(qdrant); + + // Call environment variable callbacks. + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectA.Resource); + + var servicesKeysCount = config.Keys.Count(k => k.StartsWith("ConnectionStrings__")); + Assert.Equal(2, servicesKeysCount); + + Assert.Contains(config, kvp => kvp.Key == "ConnectionStrings__my-qdrant" && kvp.Value == "Endpoint=http://localhost:6334;Key=pass"); + Assert.Contains(config, kvp => kvp.Key == "ConnectionStrings__my-qdrant_http" && kvp.Value == "Endpoint=http://localhost:6333;Key=pass"); + + var container1 = appBuilder.AddContainer("container1", "fake") + .WithReference(qdrant); + + // Call environment variable callbacks. + var containerConfig = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(container1.Resource); + + var containerServicesKeysCount = containerConfig.Keys.Count(k => k.StartsWith("ConnectionStrings__")); + Assert.Equal(2, containerServicesKeysCount); + + Assert.Contains(containerConfig, kvp => kvp.Key == "ConnectionStrings__my-qdrant" && kvp.Value == "Endpoint=http://localhost:6334;Key=pass"); + Assert.Contains(containerConfig, kvp => kvp.Key == "ConnectionStrings__my-qdrant_http" && kvp.Value == "Endpoint=http://localhost:6333;Key=pass"); + } + + [Fact] + public async Task VerifyManifest() + { + var appBuilder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions() { Args = new string[] { "--publisher", "manifest" } } ); + var qdrant = appBuilder.AddQdrant("qdrant"); + + var serverManifest = await ManifestUtils.GetManifest(qdrant.Resource); // using this method does not get any ExecutionContext.IsPublishMode changes + + var expectedManifest = $$""" + { + "type": "container.v0", + "connectionString": "Endpoint={qdrant.bindings.grpc.scheme}://{qdrant.bindings.grpc.host}:{qdrant.bindings.grpc.port};Key={qdrant-Key.value}", + "image": "{{QdrantContainerImageTags.Image}}:{{QdrantContainerImageTags.Tag}}", + "env": { + "QDRANT__SERVICE__API_KEY": "{qdrant-Key.value}", + "QDRANT__SERVICE__ENABLE_STATIC_CONTENT": "0" + }, + "bindings": { + "grpc": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "targetPort": 6334 + }, + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "targetPort": 6333 + } + } + } + """; + Assert.Equal(expectedManifest, serverManifest.ToString()); + } + + [Fact] + public async Task VerifyManifestWithParameters() + { + var appBuilder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions() { Args = new string[] { "--publisher", "manifest" } }); + + var apiKeyParameter = appBuilder.AddParameter("QdrantApiKey"); + var qdrant = appBuilder.AddQdrant("qdrant", apiKeyParameter); + + var serverManifest = await ManifestUtils.GetManifest(qdrant.Resource); // using this method does not get any ExecutionContext.IsPublishMode changes + + var expectedManifest = $$""" + { + "type": "container.v0", + "connectionString": "Endpoint={qdrant.bindings.grpc.scheme}://{qdrant.bindings.grpc.host}:{qdrant.bindings.grpc.port};Key={QdrantApiKey.value}", + "image": "{{QdrantContainerImageTags.Image}}:{{QdrantContainerImageTags.Tag}}", + "env": { + "QDRANT__SERVICE__API_KEY": "{QdrantApiKey.value}", + "QDRANT__SERVICE__ENABLE_STATIC_CONTENT": "0" + }, + "bindings": { + "grpc": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "targetPort": 6334 + }, + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "targetPort": 6333 + } + } + } + """; + Assert.Equal(expectedManifest, serverManifest.ToString()); + } + + [Fact] + public void AddQdrantWithSpecifyingPorts() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var qdrant = builder.AddQdrant("my-qdrant", grpcPort: 5503, httpPort: 5504); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var qdrantResource = Assert.Single(appModel.Resources.OfType()); + Assert.Equal("my-qdrant", qdrantResource.Name); + + Assert.Equal(2, qdrantResource.Annotations.OfType().Count()); + + var grpcEndpoint = qdrantResource.Annotations.OfType().Single(e => e.Name == "grpc"); + Assert.Equal(6334, grpcEndpoint.TargetPort); + Assert.False(grpcEndpoint.IsExternal); + Assert.Equal(5503, grpcEndpoint.Port); + Assert.Equal(ProtocolType.Tcp, grpcEndpoint.Protocol); + Assert.Equal("http", grpcEndpoint.Transport); + Assert.Equal("http", grpcEndpoint.UriScheme); + + var httpEndpoint = qdrantResource.Annotations.OfType().Single(e => e.Name == "http"); + Assert.Equal(6333, httpEndpoint.TargetPort); + Assert.False(httpEndpoint.IsExternal); + Assert.Equal(5504, httpEndpoint.Port); + Assert.Equal(ProtocolType.Tcp, httpEndpoint.Protocol); + Assert.Equal("http", httpEndpoint.Transport); + Assert.Equal("http", httpEndpoint.UriScheme); + } + + private static TestProgram CreateTestProgram(string[]? args = null) => TestProgram.Create(args); + + private sealed class ProjectA : IProjectMetadata + { + public string ProjectPath => "projectA"; + + public LaunchSettings LaunchSettings { get; } = new(); + } +} diff --git a/tests/Aspire.Qdrant.Client.Tests/Aspire.Qdrant.Client.Tests.csproj b/tests/Aspire.Qdrant.Client.Tests/Aspire.Qdrant.Client.Tests.csproj new file mode 100644 index 0000000000..43aeb02ea8 --- /dev/null +++ b/tests/Aspire.Qdrant.Client.Tests/Aspire.Qdrant.Client.Tests.csproj @@ -0,0 +1,15 @@ + + + + $(NetCurrent) + true + + + + + + + + + + diff --git a/tests/Aspire.Qdrant.Client.Tests/AspireQdrantHelpers.cs b/tests/Aspire.Qdrant.Client.Tests/AspireQdrantHelpers.cs new file mode 100644 index 0000000000..40c3e18400 --- /dev/null +++ b/tests/Aspire.Qdrant.Client.Tests/AspireQdrantHelpers.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.XUnitExtensions; +using Qdrant.Client; + +namespace Aspire.Qdrant.Client.Tests; +public static class AspireQdrantHelpers +{ + public const string TestingEndpoint = "http://localhost:6334"; + + private static readonly Lazy s_canConnectToServer = new(GetCanConnect); + public static bool CanConnectToServer => s_canConnectToServer.Value; + + public static void SkipIfCanNotConnectToServer() + { + if (!CanConnectToServer) + { + throw new SkipTestException("Unable to connect to the server."); + } + } + + private static bool GetCanConnect() + { + try + { + var client = new QdrantClient(new Uri(TestingEndpoint)); + client.ListCollectionsAsync().Wait(); + return true; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return false; + } + } +} diff --git a/tests/Aspire.Qdrant.Client.Tests/ConfigurationTests.cs b/tests/Aspire.Qdrant.Client.Tests/ConfigurationTests.cs new file mode 100644 index 0000000000..5761b2f0fb --- /dev/null +++ b/tests/Aspire.Qdrant.Client.Tests/ConfigurationTests.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Aspire.Qdrant.Client.Tests; +public class ConfigurationTests +{ + [Fact] + public void EndpointIsNullByDefault() + => Assert.Null(new QdrantClientSettings().Endpoint); +} diff --git a/tests/Aspire.Qdrant.Client.Tests/ConformanceTests.cs b/tests/Aspire.Qdrant.Client.Tests/ConformanceTests.cs new file mode 100644 index 0000000000..d75c8366c1 --- /dev/null +++ b/tests/Aspire.Qdrant.Client.Tests/ConformanceTests.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.ConformanceTests; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Qdrant.Client; + +namespace Aspire.Qdrant.Client.Tests; +public class ConformanceTests : ConformanceTests +{ + protected override bool SupportsKeyedRegistrations => true; + + protected override bool CanCreateClientWithoutConnectingToServer => false; + + protected override bool CanConnectToServer => AspireQdrantHelpers.CanConnectToServer; + + protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton; + + protected override string[] RequiredLogCategories => Array.Empty(); + + protected override string ActivitySourceName => ""; + + protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) + { + if (key is null) + { + builder.AddQdrantClient("qdrant", configure); + } + else + { + builder.AddKeyedQdrantClient(key, configure); + } + } + + protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) + => configuration.AddInMemoryCollection(new KeyValuePair[2] + { + new KeyValuePair(CreateConfigKey("Aspire:Qdrant:Client", key, "Endpoint"), "http://localhost:6334"), + new KeyValuePair($"ConnectionStrings:{key}","Endpoint=http://localhost:6334;Key=pass") + }); + + protected override void TriggerActivity(QdrantClient service) + { + } + + protected override void SetHealthCheck(QdrantClientSettings options, bool enabled) => throw new NotImplementedException(); + + protected override void SetTracing(QdrantClientSettings options, bool enabled) => throw new NotImplementedException(); + + protected override void SetMetrics(QdrantClientSettings options, bool enabled) => throw new NotImplementedException(); + + protected override string ValidJsonConfig => """ + { + "Aspire": { + "Qdrant": { + "Client": { + "Endpoint": "http://localhost:6334" + } + } + } + } + """; + + protected override (string json, string error)[] InvalidJsonToErrorMessage => new[] + { + ("""{"Aspire": { "Qdrant":{ "Client": { "Endpoint": 3 }}}}""", "Value is \"integer\" but should be \"string\""), + ("""{"Aspire": { "Qdrant":{ "Client": { "Endpoint": "hello" }}}}""", "Value does not match format \"uri\"") + }; +} diff --git a/tests/Aspire.Qdrant.Client.Tests/README.md b/tests/Aspire.Qdrant.Client.Tests/README.md new file mode 100644 index 0000000000..af9e6386a6 --- /dev/null +++ b/tests/Aspire.Qdrant.Client.Tests/README.md @@ -0,0 +1,13 @@ +# Aspire.Qdrant.Client.Tests + +This project contains tests for the Aspire.Qdrant.Client project. + +When running tests locally until TestContainers support is enabled, you will need to have a Qdrant instance running locally. + +Run: + +```bash +docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant:v1.8.0 +``` + +Then run the tests to enable the connected tests.