diff --git a/app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs b/app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs index dadd160f..88db7830 100644 --- a/app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs +++ b/app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs @@ -104,7 +104,7 @@ public sealed class DatabaseClientProvider(RustService rustService, ILoggerFacto private async Task CreateClientAsync(DatabaseRole databaseRole, CancellationToken cancellationToken) => databaseRole switch { - DatabaseRole.VECTOR_STORE => await QdrantEdgeClientImplementation.CreateAsync(rustService, this.logger, this.databaseClientLogger, cancellationToken), + DatabaseRole.VECTOR_STORE => await QdrantEdgeClientImplementation.CreateAsync(rustService, this.GetIndexStoreAsync, this.logger, this.databaseClientLogger, cancellationToken), DatabaseRole.INDEX_STORE => await SqliteIndexStoreClientImplementation.CreateAsync(this.logger, this.databaseClientLogger, cancellationToken), _ => new NoDatabaseClient(databaseRole.ToString(), "The requested database role is not supported.") }; diff --git a/app/MindWork AI Studio/Tools/Databases/VectorStore/QdrantEdgeClientImplementation.cs b/app/MindWork AI Studio/Tools/Databases/VectorStore/QdrantEdgeClientImplementation.cs index c00d8348..1666329b 100644 --- a/app/MindWork AI Studio/Tools/Databases/VectorStore/QdrantEdgeClientImplementation.cs +++ b/app/MindWork AI Studio/Tools/Databases/VectorStore/QdrantEdgeClientImplementation.cs @@ -1,15 +1,26 @@ +using System.Globalization; + +using AIStudio.Tools.Databases.IndexStore; using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Rust; using AIStudio.Tools.Services; namespace AIStudio.Tools.Databases.VectorStore; +/// +/// Resolves the index store, which is where the number of stored vectors comes from. Counting the +/// points in Qdrant Edge itself would have to load every shard first and would hold the global +/// database mutex against ongoing inserts and searches. The accessor is only called while building +/// the display info, never while this client is created: creating it already holds the vector store +/// lock, and the accessor takes the index store lock, so the two are never held at the same time. +/// public sealed class QdrantEdgeClientImplementation( string name, string path, string version, int storesCount, - RustService rustService) : VectorStoreClient(name, path) + RustService rustService, + Func> indexStoreAccessor) : VectorStoreClient(name, path) { private const string DATABASE_NAME = "Qdrant Edge"; private const string INFO_PATH = "/system/qdrant-edge/info"; @@ -28,6 +39,7 @@ public sealed class QdrantEdgeClientImplementation( public static async Task CreateAsync( RustService rustService, + Func> indexStoreAccessor, ILogger logger, ILogger databaseClientLogger, CancellationToken cancellationToken) @@ -60,7 +72,7 @@ public sealed class QdrantEdgeClientImplementation( return CreateNoVectorStoreClient(DATABASE_NAME, $"Failed to get the {DATABASE_NAME} path from Rust.", DatabaseClientStatus.UNAVAILABLE, databaseClientLogger); var name = string.IsNullOrWhiteSpace(qdrantEdgeInfo.Name) ? DATABASE_NAME : qdrantEdgeInfo.Name; - var client = new QdrantEdgeClientImplementation(name, qdrantEdgeInfo.Path, qdrantEdgeInfo.Version, qdrantEdgeInfo.StoresCount, rustService); + var client = new QdrantEdgeClientImplementation(name, qdrantEdgeInfo.Path, qdrantEdgeInfo.Version, qdrantEdgeInfo.StoresCount, rustService, indexStoreAccessor); client.SetLogger(databaseClientLogger); return client; } @@ -77,9 +89,35 @@ public sealed class QdrantEdgeClientImplementation( if (!currentInfo.IsAvailable) yield return (TB("Status"), currentInfo.UnavailableReason ?? TB("Qdrant Edge is not available.")); + var storedVectors = await this.GetStoredVectorCountAsync(); + yield return (TB("Reported version"), displayVersion); yield return (TB("Storage size"), $"{this.GetStorageSize()}"); - yield return (TB("Number of vector stores"), displayStoresCount.ToString()); + yield return (TB("Number of vector stores"), displayStoresCount.ToString("N0", I18N.I.Culture)); + yield return (TB("Stored vectors"), storedVectors?.ToString("N0", I18N.I.Culture) ?? TB("unknown")); + } + + /// + /// Reads how many vectors the vector stores hold in total. + /// + /// The number of vectors, or null when the index store cannot tell. + private async Task GetStoredVectorCountAsync() + { + try + { + // + // One chunk is one vector: every chunk becomes exactly one point carrying the single + // named vector "embedding". So the index store knows this number without Qdrant Edge + // having to load a single shard for it. + // + var indexStore = await indexStoreAccessor(CancellationToken.None); + return await indexStore.GetTotalChunkCountAsync(CancellationToken.None); + } + catch (Exception exception) + { + this.Logger?.LogWarning(exception, "Failed to read the number of stored vectors from the index store."); + return null; + } } public override async Task EnsureVectorStoreExists(string storeName, string dataSourceName, int vectorSize, CancellationToken token) =>