Open Closed

AI Management Qdrant Authentication #10689


User avatar
0
tferrero created

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?

Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

7 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    You're right, the current Volo.AIManagement.VectorStores.Qdrant module only parses host/port out of the connection string and calls new 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 — QdrantVectorStoreFactory and QdrantVectorStore — with your own. The factory reads Qdrant:ApiKey and Qdrant:Https from appsettings.json (keeping the secret out of the DB), builds a QdrantClient with them, and wraps it in your custom IVectorStore. The workspace's VectorStoreSettings field stays in the existing host:port format.

    1. MyQdrantVectorStoreFactory.cs

    using 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-in QdrantVectorStore, but the constructor takes a pre-built QdrantClient:

    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 in PostConfigureServices (not PreConfigureServices or ConfigureServices), because AbpApplicationBase runs Services.AddAssembly(...) between PreConfigureServices and ConfigureServices — if you call RemoveAll too 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));
    }
    

    RemoveAll is needed (instead of [Dependency(ReplaceServices = true)]) because IVectorStoreFactory is consumed as IEnumerable<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's VectorStoreSettings field stays as qdrant.example.com:6334):

    {
      "Qdrant": {
        "ApiKey": "YOUR_PRODUCTION_API_KEY",
        "Https": true
      }
    }
    

    We'll add first-class ApiKey / UseHttps support 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)
  • User Avatar
    0
    tferrero created

    Hello maliming,

    Thanks for your reply and for the workaround suggestion. We will proceed with this workaround until ABP next version is released.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    : )

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    tferrero created

    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)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    You're right, this is a real bug. The Guid.TryParse(parts[0], ...) branch in QdrantVectorStore.BuildPointId collapses 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 (DeleteByWorkspaceIdAsync on 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)
  • User Avatar
    0
    tferrero created

    Ok, thanks again for the reply and help!

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    : )

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 28, 2026, 11:44
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.