We are having an issue with the current AI Management Qdrant integration implementation regarding authenticated Qdrant gRPC endpoints. The module current implementation seems to only be parsing a simple connection string (host:port or URL) and the implementation does not seem to be exposing authentication configuration.
We have been able to run the module using an authenticated Qdrant running locally, but we found no way to provide an api key (via header) which we need to access our Qdrant production endpoint.
We have checked documentation and module code. Is this something that is supported and we have not figured out how to do? Or is planned to be supported in the future?
7 Answer(s)
-
0
Hi,
You're right, the current
Volo.AIManagement.VectorStores.Qdrantmodule only parses host/port out of the connection string and callsnew QdrantClient(host, port), so there is no way to pass an API key or enable HTTPS through the built-in settings.The workaround is to replace the two built-in services —
QdrantVectorStoreFactoryandQdrantVectorStore— with your own. The factory readsQdrant:ApiKeyandQdrant:Httpsfromappsettings.json(keeping the secret out of the DB), builds aQdrantClientwith them, and wraps it in your customIVectorStore. The workspace'sVectorStoreSettingsfield stays in the existinghost:portformat.1.
MyQdrantVectorStoreFactory.csusing System; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Qdrant.Client; using Volo.Abp.DependencyInjection; using Volo.AIManagement.VectorStores; namespace MyCompanyName.MyProjectName; [ExposeServices(typeof(IVectorStoreFactory))] public class MyQdrantVectorStoreFactory : IVectorStoreFactory, ITransientDependency { public string Provider => "Qdrant"; private readonly ILoggerFactory _loggerFactory; private readonly IConfiguration _configuration; public MyQdrantVectorStoreFactory(ILoggerFactory loggerFactory, IConfiguration configuration) { _loggerFactory = loggerFactory; _configuration = configuration; } public Task<IVectorStore> CreateAsync(Guid workspaceId, string? connectionSettings = null) { var (host, port) = ParseHostPort(connectionSettings!); var apiKey = _configuration["Qdrant:ApiKey"]; var https = _configuration.GetValue<bool>("Qdrant:Https"); var client = new QdrantClient(host, port, https, apiKey, loggerFactory: _loggerFactory); return Task.FromResult<IVectorStore>( new MyQdrantVectorStore(client, workspaceId, _loggerFactory.CreateLogger<MyQdrantVectorStore>())); } private static (string Host, int Port) ParseHostPort(string raw) { if (raw.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || raw.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { var uri = new Uri(raw); return (uri.Host, uri.Port > 0 ? uri.Port : 6334); } var idx = raw.IndexOf(':'); return idx > 0 && int.TryParse(raw[(idx + 1)..], out var p) ? (raw[..idx], p) : (raw, 6334); } }2.
MyQdrantVectorStore.cs— same shape as the built-inQdrantVectorStore, but the constructor takes a pre-builtQdrantClient:using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Qdrant.Client; using Qdrant.Client.Grpc; using static Qdrant.Client.Grpc.Conditions; using Volo.AIManagement.VectorStores; namespace MyCompanyName.MyProjectName; public class MyQdrantVectorStore : IVectorStore, IDisposable { private readonly QdrantClient _client; private readonly ILogger<MyQdrantVectorStore> _logger; private readonly Guid _workspaceId; private int? _workspaceDimension; private readonly SemaphoreSlim _initLock = new(1, 1); public MyQdrantVectorStore(QdrantClient client, Guid workspaceId, ILogger<MyQdrantVectorStore> logger) { _client = client; _workspaceId = workspaceId; _logger = logger; } private string CollectionName => $"ai_vector_embeddings_{_workspaceId:N}"; public async Task InitializeAsync(CancellationToken cancellationToken = default) { await _client.ListCollectionsAsync(cancellationToken); } public async Task<string> StoreAsync(Guid workspaceId, string id, float[] embedding, Dictionary<string, object>? metadata = null, CancellationToken cancellationToken = default) { await EnsureCollectionAsync(embedding.Length, cancellationToken); var payload = new Dictionary<string, Value>(); if (metadata != null) { foreach (var kvp in metadata) payload[kvp.Key] = ToValue(kvp.Value); } payload["workspace_id"] = ToValue(_workspaceId.ToString()); payload["creation_time"] = ToValue(DateTime.UtcNow.ToString("O")); payload["original_id"] = ToValue(id); var point = new PointStruct { Id = ToPointId(id), Vectors = new Vectors { Vector = new Vector { Data = { embedding } } } }; foreach (var kvp in payload) point.Payload[kvp.Key] = kvp.Value; await _client.UpsertAsync(CollectionName, new[] { point }, cancellationToken: cancellationToken); return id; } public async Task<List<VectorSearchResult>> SearchAsync(Guid workspaceId, float[] queryEmbedding, int topK, CancellationToken cancellationToken = default) { if (!await CollectionExistsAsync(cancellationToken)) return new(); var results = await _client.SearchAsync(CollectionName, queryEmbedding, limit: (ulong)topK, searchParams: new SearchParams { HnswEf = 512 }, cancellationToken: cancellationToken); return results.Select(p => { var meta = FromPayload(p.Payload); var id = meta != null && meta.TryGetValue("original_id", out var oid) ? oid?.ToString() ?? p.Id.ToString() : p.Id.ToString(); return new VectorSearchResult { Id = id, Score = p.Score, Metadata = meta }; }).ToList(); } public async Task<bool> DeleteAsync(Guid workspaceId, string id, CancellationToken cancellationToken = default) { if (!await CollectionExistsAsync(cancellationToken)) return false; await _client.DeleteAsync(CollectionName, new[] { ToPointId(id) }, wait: true, cancellationToken: cancellationToken); return true; } public async Task<int> DeleteByWorkspaceIdAsync(Guid workspaceId, CancellationToken cancellationToken = default) { if (!await CollectionExistsAsync(cancellationToken)) return 0; await _client.DeleteCollectionAsync(CollectionName, cancellationToken: cancellationToken); _workspaceDimension = null; return 1; } public async Task<int> DeleteAllByDataSourceAsync(Guid workspaceId, Guid dataSourceId, CancellationToken cancellationToken = default) { if (!await CollectionExistsAsync(cancellationToken)) return 0; await _client.DeleteAsync(CollectionName, filter: MatchKeyword("data_source_id", dataSourceId.ToString()), wait: true, cancellationToken: cancellationToken); return 1; } public void Dispose() { _client.Dispose(); _initLock.Dispose(); } private async Task EnsureCollectionAsync(int dimension, CancellationToken cancellationToken) { await _initLock.WaitAsync(cancellationToken); try { if (_workspaceDimension == dimension) return; if (!await CollectionExistsAsync(cancellationToken)) { await _client.CreateCollectionAsync(CollectionName, vectorsConfig: new VectorParams { Size = (ulong)dimension, Distance = Distance.Cosine, HnswConfig = new HnswConfigDiff { M = 16, EfConstruct = 200 } }, cancellationToken: cancellationToken); } _workspaceDimension = dimension; } finally { _initLock.Release(); } } private async Task<bool> CollectionExistsAsync(CancellationToken cancellationToken) => (await _client.ListCollectionsAsync(cancellationToken)).Any(c => c == CollectionName); private static PointId ToPointId(string id) { if (Guid.TryParse(id, out var g)) return g; if (id.Contains('_') && Guid.TryParse(id.Split('_', 2)[0], out var gp)) return gp; return BitConverter.ToUInt64(System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(id)), 0); } private static Value ToValue(object? v) => v 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 }, null => new Value { NullValue = NullValue.NullValue }, _ => new Value { StringValue = v.ToString() ?? "" } }; private static Dictionary<string, object>? FromPayload(Google.Protobuf.Collections.MapField<string, Value> payload) { if (payload == null || payload.Count == 0) return null; var meta = new Dictionary<string, object>(); foreach (var kvp in payload) { if (kvp.Key is "workspace_id" or "creation_time") continue; meta[kvp.Key] = kvp.Value.KindCase switch { Value.KindOneofCase.StringValue => kvp.Value.StringValue, Value.KindOneofCase.IntegerValue => kvp.Value.IntegerValue, Value.KindOneofCase.DoubleValue => kvp.Value.DoubleValue, Value.KindOneofCase.BoolValue => kvp.Value.BoolValue, _ => kvp.Value.ToString()! }; } return meta; } }3. Remove the built-in factory registration in any of your existing modules that already depend on
AIManagementQdrantVectorStoreModule. It has to be done inPostConfigureServices(notPreConfigureServicesorConfigureServices), becauseAbpApplicationBaserunsServices.AddAssembly(...)betweenPreConfigureServicesandConfigureServices— if you callRemoveAlltoo early, the built-in factory hasn't been auto-registered yet and the call silently does nothing.public override void PostConfigureServices(ServiceConfigurationContext context) { context.Services.RemoveAll(d => d.ImplementationType == typeof(QdrantVectorStoreFactory)); }RemoveAllis needed (instead of[Dependency(ReplaceServices = true)]) becauseIVectorStoreFactoryis consumed asIEnumerable<IVectorStoreFactory>and resolved by_factories.FirstOrDefault(f => f.Provider == "Qdrant"). If both factories stay registered, the framework one is registered first (its module loads before yours) and wins — your factory is never called.4.
appsettings.json(the workspace'sVectorStoreSettingsfield stays asqdrant.example.com:6334):{ "Qdrant": { "ApiKey": "YOUR_PRODUCTION_API_KEY", "Https": true } }We'll add first-class
ApiKey/UseHttpssupport to the Qdrant module in the next release so this workaround is no longer needed.Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hello maliming,
Reopening the thread just to let you know that we had to make an adjustment to the workaround code, because we encountered another issue related to chunk storage in the Qdrant vector store implementation.
When uploading a document through AI Management Data Sources, the document is correctly chunked and embeddings are generated for all chunks. However, only the last chunk ends up being stored in Qdrant.
The issue comes from the ToPointId logic.
private static PointId ToPointId(string id) { if (Guid.TryParse(id, out var g)) return g; if (id.Contains('_') && Guid.TryParse(id.Split('_', 2)[0], out var gp)) return gp; return BitConverter.ToUInt64( SHA256.HashData(Encoding.UTF8.GetBytes(id)), 0); }Chunk IDs are generated in this format: guid_0 guid_1 guid_2 ... guid_130
But the implementation extracts only the GUID part before the underscore, which means all chunks resolve to the exact same PointId.
What we did was remove that underscore special handling, so the full chunk id is preserved.
Maybe this is something that is already solved for next version as well, but maybe this helps in case it is not.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
You're right, this is a real bug. The
Guid.TryParse(parts[0], ...)branch inQdrantVectorStore.BuildPointIdcollapses every chunk of the same data source onto the data source's GUID as the PointId, so each new chunk overwrites the previous one and only the last chunk survives in Qdrant. Verified end-to-end against a local Qdrant container: with the original code 3 stored chunks produce 1 point in the collection; with the underscore branch removed they produce 3 distinct points.Removing that branch (so every non-GUID id falls through to the SHA256 hash) is the correct fix. Your workaround is essentially what we'll ship.
The fix is scheduled for the next release. Existing Qdrant data already affected needs to be reindexed (
DeleteByWorkspaceIdAsyncon the workspace, then re-run indexing) since only one chunk per document was actually stored.Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)