diff --git a/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs index 7186ed21..51fa98fd 100644 --- a/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs +++ b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs @@ -8,11 +8,20 @@ public abstract class DatabaseClient(string name, string path) public virtual DatabaseClientStatus Status => DatabaseClientStatus.AVAILABLE; + /// + /// The version the running database reports about itself. + /// + /// + /// Empty when the client cannot tell. Callers which want to show a version in a headline read it + /// from here instead of picking it out of the label-value pairs the display info yields. + /// + public virtual string Version => string.Empty; + public bool IsAvailable => this.Status is DatabaseClientStatus.AVAILABLE; private string Path => path; - private ILogger? logger; + protected ILogger? Logger; public abstract IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo(); @@ -20,13 +29,13 @@ public abstract class DatabaseClient(string name, string path) { if (string.IsNullOrWhiteSpace(this.Path)) { - this.logger!.LogError($"Error: Database path '{this.Path}' cannot be null or empty."); + this.Logger!.LogError($"Error: Database path '{this.Path}' cannot be null or empty."); return "0 B"; } if (!Directory.Exists(this.Path)) { - this.logger!.LogError($"Error: Database path '{this.Path}' does not exist."); + this.Logger!.LogError($"Error: Database path '{this.Path}' does not exist."); return "0 B"; } var files = Directory.EnumerateFiles(this.Path, "*", SearchOption.AllDirectories) @@ -52,7 +61,7 @@ public abstract class DatabaseClient(string name, string path) public void SetLogger(ILogger logService) { - this.logger = logService; + this.Logger = logService; } public abstract void Dispose(); diff --git a/app/MindWork AI Studio/Tools/Databases/IndexStore/IndexStoreClient.cs b/app/MindWork AI Studio/Tools/Databases/IndexStore/IndexStoreClient.cs index 644529f2..daa58eea 100644 --- a/app/MindWork AI Studio/Tools/Databases/IndexStore/IndexStoreClient.cs +++ b/app/MindWork AI Studio/Tools/Databases/IndexStore/IndexStoreClient.cs @@ -40,4 +40,17 @@ public abstract class IndexStoreClient(string name, string path) : DatabaseClien public abstract Task> SearchChunksAsync(string dataSourceId, string query, int maxMatches, CancellationToken token); public abstract Task DeleteDataSourceAsync(string dataSourceId, CancellationToken token); + + /// + /// Counts the search chunks the index holds across all data sources. + /// + /// + /// One chunk is one vector: every chunk becomes exactly one point carrying the single named + /// vector "embedding". The vector store reports its vector count from here, because counting + /// the points in Qdrant Edge would have to load every shard first and would hold the global + /// database mutex against ongoing inserts and searches while doing so. + /// + /// The cancellation token. + /// The number of chunks, or null when the index cannot tell. Null and zero mean different things here. + public abstract Task GetTotalChunkCountAsync(CancellationToken token); } diff --git a/app/MindWork AI Studio/Tools/Databases/IndexStore/NoIndexStoreClient.cs b/app/MindWork AI Studio/Tools/Databases/IndexStore/NoIndexStoreClient.cs index b9373ea0..5ccf4206 100644 --- a/app/MindWork AI Studio/Tools/Databases/IndexStore/NoIndexStoreClient.cs +++ b/app/MindWork AI Studio/Tools/Databases/IndexStore/NoIndexStoreClient.cs @@ -55,6 +55,8 @@ public sealed class NoIndexStoreClient(string name, string? unavailableReason, D public override Task DeleteDataSourceAsync(string dataSourceId, CancellationToken token) => Task.CompletedTask; + public override Task GetTotalChunkCountAsync(CancellationToken token) => Task.FromResult(null); + public override void Dispose() { } diff --git a/app/MindWork AI Studio/Tools/Databases/IndexStore/SqliteIndexStoreClientImplementation.cs b/app/MindWork AI Studio/Tools/Databases/IndexStore/SqliteIndexStoreClientImplementation.cs index 739b0c4a..8612a079 100644 --- a/app/MindWork AI Studio/Tools/Databases/IndexStore/SqliteIndexStoreClientImplementation.cs +++ b/app/MindWork AI Studio/Tools/Databases/IndexStore/SqliteIndexStoreClientImplementation.cs @@ -25,6 +25,8 @@ public sealed class SqliteIndexStoreClientImplementation(string name, string dat public override string CacheKey => $"{this.Name}:{this.databasePath}:{version}"; + public override string Version => version; + public static async Task CreateAsync( ILogger logger, ILogger databaseClientLogger, @@ -65,7 +67,8 @@ public sealed class SqliteIndexStoreClientImplementation(string name, string dat yield return (TB("Storage size"), this.GetStorageSize()); yield return (TB("Indexed data sources"), (await context.DataSources.CountAsync(CancellationToken.None)).ToString(CultureInfo.InvariantCulture)); yield return (TB("Indexed files"), (await context.EmbeddedFiles.CountAsync(CancellationToken.None)).ToString(CultureInfo.InvariantCulture)); - yield return (TB("Search chunks"), (await context.EmbeddingChunks.CountAsync(CancellationToken.None)).ToString(CultureInfo.InvariantCulture)); + var searchChunks = await this.GetTotalChunkCountAsync(CancellationToken.None); + yield return (TB("Search chunks"), searchChunks?.ToString(CultureInfo.InvariantCulture) ?? TB("unknown")); yield return (TB("Permanently skipped files"), (await context.PermanentIndexingFailures.CountAsync(CancellationToken.None)).ToString(CultureInfo.InvariantCulture)); } @@ -348,6 +351,27 @@ public sealed class SqliteIndexStoreClientImplementation(string name, string dat await transaction.CommitAsync(token); } + public override async Task GetTotalChunkCountAsync(CancellationToken token) + { + try + { + await using var context = this.CreateContext(); + + // + // Sum the chunk counts the files carry instead of counting the rows of the chunk table: + // embedded_files holds one row per file, embedding_chunks one per chunk. On a large index + // that is a difference of two orders of magnitude, and the information page reads this on + // every visit. + // + return await context.EmbeddedFiles.SumAsync(file => (long)file.ChunkCount, token); + } + catch (Exception exception) + { + this.Logger?.LogWarning(exception, "Failed to count the search chunks of the local RAG index."); + return null; + } + } + public override void Dispose() { }