Replaced GetEmbeddingCacheSize with GetStats, fixed long loading times for front-end stats retrieval

This commit is contained in:
2026-01-02 02:04:19 +01:00
parent 09832d1c0b
commit aa4fc03c3d
9 changed files with 140 additions and 76 deletions

View File

@@ -247,17 +247,13 @@ public class SearchdomainController : ControllerBase
[HttpGet("QueryCache/Size")] [HttpGet("QueryCache/Size")]
public ActionResult<SearchdomainSearchCacheSizeResults> GetSearchCacheSize([Required]string searchdomain) public ActionResult<SearchdomainSearchCacheSizeResults> GetSearchCacheSize([Required]string searchdomain)
{ {
if (!SearchdomainHelper.IsSearchdomainLoaded(_domainManager, searchdomain))
{
return Ok(new SearchdomainSearchCacheSizeResults() { QueryCacheSizeBytes = 0, Success = true });
}
(Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_domainManager, searchdomain, _logger); (Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_domainManager, searchdomain, _logger);
if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new SearchdomainUpdateResults(){Success = false, Message = message}); if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new SearchdomainUpdateResults(){Success = false, Message = message});
Dictionary<string, DateTimedSearchResult> searchCache = searchdomain_.searchCache; return Ok(new SearchdomainSearchCacheSizeResults() { QueryCacheSizeBytes = searchdomain_.GetSearchCacheSize(), Success = true });
long sizeInBytes = 0;
foreach (var entry in searchCache)
{
sizeInBytes += sizeof(int); // string length prefix
sizeInBytes += entry.Key.Length * sizeof(char); // string characters
sizeInBytes += entry.Value.EstimateSize();
}
return Ok(new SearchdomainSearchCacheSizeResults() { QueryCacheSizeBytes = sizeInBytes, Success = true });
} }
/// <summary> /// <summary>

View File

@@ -5,8 +5,10 @@ using System.Text.Json;
using AdaptiveExpressions; using AdaptiveExpressions;
using ElmahCore; using ElmahCore;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Server.Exceptions; using Server.Exceptions;
using Server.Helper; using Server.Helper;
using Server.Models;
using Shared.Models; using Shared.Models;
[ApiController] [ApiController]
@@ -17,13 +19,15 @@ public class ServerController : ControllerBase
private readonly IConfiguration _config; private readonly IConfiguration _config;
private AIProvider _aIProvider; private AIProvider _aIProvider;
private readonly SearchdomainManager _searchdomainManager; private readonly SearchdomainManager _searchdomainManager;
private readonly IOptions<EmbeddingSearchOptions> _options;
public ServerController(ILogger<ServerController> logger, IConfiguration config, AIProvider aIProvider, SearchdomainManager searchdomainManager) public ServerController(ILogger<ServerController> logger, IConfiguration config, AIProvider aIProvider, SearchdomainManager searchdomainManager, IOptions<EmbeddingSearchOptions> options)
{ {
_logger = logger; _logger = logger;
_config = config; _config = config;
_aIProvider = aIProvider; _aIProvider = aIProvider;
_searchdomainManager = searchdomainManager; _searchdomainManager = searchdomainManager;
_options = options;
} }
/// <summary> /// <summary>
@@ -47,10 +51,12 @@ public class ServerController : ControllerBase
} }
/// <summary> /// <summary>
/// Gets the total memory size of the embedding cache /// Gets numeric info regarding the searchdomains
/// </summary> /// </summary>
[HttpGet("EmbeddingCache/Size")] [HttpGet("Stats")]
public ActionResult<ServerGetEmbeddingCacheSizeResult> GetEmbeddingCacheSize() public async Task<ActionResult<ServerGetStatsResult>> Stats()
{
try
{ {
long size = 0; long size = 0;
long elementCount = 0; long elementCount = 0;
@@ -71,7 +77,25 @@ public class ServerController : ControllerBase
elementCount++; elementCount++;
embeddingsCount += entry.Keys.Count; embeddingsCount += entry.Keys.Count;
} }
return new ServerGetEmbeddingCacheSizeResult() { Success = true, SizeInBytes = size, MaxElementCount = _searchdomainManager.EmbeddingCacheMaxCount, ElementCount = elementCount, EmbeddingsCount = embeddingsCount}; var sqlHelper = DatabaseHelper.GetSQLHelper(_options.Value);
Task<long> entityCountTask = DatabaseHelper.CountEntities(sqlHelper);
long queryCacheUtilization = 0;
foreach (string searchdomain in _searchdomainManager.ListSearchdomains())
{
if (SearchdomainHelper.IsSearchdomainLoaded(_searchdomainManager, searchdomain))
{
(Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_searchdomainManager, searchdomain, _logger);
if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new ServerGetStatsResult(){Success = false, Message = message});
queryCacheUtilization += searchdomain_.GetSearchCacheSize();
}
};
long entityCount = await entityCountTask;
return new ServerGetStatsResult() { Success = true, EntityCount = entityCount, QueryCacheUtilization = queryCacheUtilization, SizeInBytes = size, MaxElementCount = _searchdomainManager.EmbeddingCacheMaxCount, ElementCount = elementCount, EmbeddingsCount = embeddingsCount};
} catch (Exception ex)
{
ElmahExtensions.RaiseError(ex);
return StatusCode(500, new ServerGetStatsResult(){Success = false, Message = ex.Message});
}
} }
private static long EstimateEntrySize(string key, Dictionary<string, float[]> value) private static long EstimateEntrySize(string key, Dictionary<string, float[]> value)

View File

@@ -1,6 +1,9 @@
using System.Configuration;
using System.Data.Common; using System.Data.Common;
using System.Text; using System.Text;
using MySql.Data.MySqlClient;
using Server.Exceptions; using Server.Exceptions;
using Server.Models;
using Shared.Models; using Shared.Models;
namespace Server.Helper; namespace Server.Helper;
@@ -9,6 +12,14 @@ public class DatabaseHelper(ILogger<DatabaseHelper> logger)
{ {
private readonly ILogger<DatabaseHelper> _logger = logger; private readonly ILogger<DatabaseHelper> _logger = logger;
public static SQLHelper GetSQLHelper(EmbeddingSearchOptions embeddingSearchOptions)
{
string connectionString = embeddingSearchOptions.ConnectionStrings.SQL;
MySqlConnection connection = new(connectionString);
connection.Open();
return new SQLHelper(connection, connectionString);
}
public static void DatabaseInsertEmbeddingBulk(SQLHelper helper, int id_datapoint, List<(string model, byte[] embedding)> data) public static void DatabaseInsertEmbeddingBulk(SQLHelper helper, int id_datapoint, List<(string model, byte[] embedding)> data)
{ {
Dictionary<string, object> parameters = []; Dictionary<string, object> parameters = [];
@@ -211,4 +222,26 @@ public class DatabaseHelper(ILogger<DatabaseHelper> logger)
return result; return result;
} }
public static async Task<long> CountEntities(SQLHelper helper)
{
DbDataReader searchdomainSumReader = helper.ExecuteSQLCommand("SELECT COUNT(*) FROM entity;", []);
bool success = searchdomainSumReader.Read();
long result = success && !searchdomainSumReader.IsDBNull(0) ? searchdomainSumReader.GetInt64(0) : 0;
searchdomainSumReader.Close();
return result;
}
public static long CountEntitiesForSearchdomain(SQLHelper helper, string searchdomain)
{
Dictionary<string, dynamic> parameters = new()
{
{ "searchdomain", searchdomain}
};
DbDataReader searchdomainSumReader = helper.ExecuteSQLCommand("SELECT COUNT(*) FROM entity e JOIN searchdomain s on e.id_searchdomain = s.id WHERE e.id_searchdomain = s.id AND s.name = @searchdomain;", parameters);
bool success = searchdomainSumReader.Read();
long result = success && !searchdomainSumReader.IsDBNull(0) ? searchdomainSumReader.GetInt64(0) : 0;
searchdomainSumReader.Close();
return result;
}
} }

View File

@@ -299,4 +299,9 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
return (null, 404, $"Unable to update searchdomain {searchdomain}"); return (null, 404, $"Unable to update searchdomain {searchdomain}");
} }
} }
public static bool IsSearchdomainLoaded(SearchdomainManager searchdomainManager, string name)
{
return searchdomainManager.IsSearchdomainLoaded(name);
}
} }

View File

@@ -6,7 +6,7 @@ namespace Server.Models;
public class EmbeddingSearchOptions : ApiKeyOptions public class EmbeddingSearchOptions : ApiKeyOptions
{ {
public required ConnectionStringsSection ConnectionStrings { get; set; } public required ConnectionStringsOptions ConnectionStrings { get; set; }
public ElmahOptions? Elmah { get; set; } public ElmahOptions? Elmah { get; set; }
public required long EmbeddingCacheMaxCount { get; set; } public required long EmbeddingCacheMaxCount { get; set; }
public required Dictionary<string, AiProvider> AiProviders { get; set; } public required Dictionary<string, AiProvider> AiProviders { get; set; }
@@ -34,3 +34,8 @@ public class SimpleUser
public string Password { get; set; } = ""; public string Password { get; set; } = "";
public string[] Roles { get; set; } = []; public string[] Roles { get; set; } = [];
} }
public class ConnectionStringsOptions
{
public required string SQL { get; set; }
}

View File

@@ -339,4 +339,16 @@ public class Searchdomain
{ {
searchCache = []; searchCache = [];
} }
public long GetSearchCacheSize()
{
long sizeInBytes = 0;
foreach (var entry in searchCache)
{
sizeInBytes += sizeof(int); // string length prefix
sizeInBytes += entry.Key.Length * sizeof(char); // string characters
sizeInBytes += entry.Value.EstimateSize();
}
return sizeInBytes;
}
} }

View File

@@ -6,6 +6,8 @@ using Server.Exceptions;
using AdaptiveExpressions; using AdaptiveExpressions;
using Shared.Models; using Shared.Models;
using System.Text.Json; using System.Text.Json;
using Microsoft.Extensions.Options;
using Server.Models;
namespace Server; namespace Server;
@@ -13,24 +15,24 @@ public class SearchdomainManager
{ {
private Dictionary<string, Searchdomain> searchdomains = []; private Dictionary<string, Searchdomain> searchdomains = [];
private readonly ILogger<SearchdomainManager> _logger; private readonly ILogger<SearchdomainManager> _logger;
private readonly IConfiguration _config; private readonly EmbeddingSearchOptions _options;
public readonly AIProvider aIProvider; public readonly AIProvider aIProvider;
private readonly DatabaseHelper _databaseHelper; private readonly DatabaseHelper _databaseHelper;
private readonly string connectionString; private readonly string connectionString;
private MySqlConnection connection; private MySqlConnection connection;
public SQLHelper helper; public SQLHelper helper;
public LRUCache<string, Dictionary<string, float[]>> embeddingCache; public LRUCache<string, Dictionary<string, float[]>> embeddingCache;
public int EmbeddingCacheMaxCount; public long EmbeddingCacheMaxCount;
public SearchdomainManager(ILogger<SearchdomainManager> logger, IConfiguration config, AIProvider aIProvider, DatabaseHelper databaseHelper) public SearchdomainManager(ILogger<SearchdomainManager> logger, IOptions<EmbeddingSearchOptions> options, AIProvider aIProvider, DatabaseHelper databaseHelper)
{ {
_logger = logger; _logger = logger;
_config = config; _options = options.Value;
this.aIProvider = aIProvider; this.aIProvider = aIProvider;
_databaseHelper = databaseHelper; _databaseHelper = databaseHelper;
EmbeddingCacheMaxCount = config.GetValue<int?>("Embeddingsearch:EmbeddingCacheMaxCount") ?? 1000000; EmbeddingCacheMaxCount = _options.EmbeddingCacheMaxCount;
embeddingCache = new(EmbeddingCacheMaxCount); embeddingCache = new((int)EmbeddingCacheMaxCount);
connectionString = _config.GetSection("Embeddingsearch").GetConnectionString("SQL") ?? ""; connectionString = _options.ConnectionStrings.SQL;
connection = new MySqlConnection(connectionString); connection = new MySqlConnection(connectionString);
connection.Open(); connection.Open();
helper = new SQLHelper(connection, connectionString); helper = new SQLHelper(connection, connectionString);
@@ -122,4 +124,9 @@ public class SearchdomainManager
searchdomains[name] = searchdomain; searchdomains[name] = searchdomain;
return searchdomain; return searchdomain;
} }
public bool IsSearchdomainLoaded(string name)
{
return searchdomains.ContainsKey(name);
}
} }

View File

@@ -5,7 +5,6 @@
@using Server @using Server
@inject LocalizationService T @inject LocalizationService T
@inject AIProvider AIProvider
@model HomeIndexViewModel @model HomeIndexViewModel
@{ @{
ViewData["Title"] = "Home Page"; ViewData["Title"] = "Home Page";
@@ -144,39 +143,14 @@
searchdomains = result.Searchdomains; searchdomains = result.Searchdomains;
hideThrobber(searchdomainCount); hideThrobber(searchdomainCount);
searchdomainCount.textContent = searchdomains.length; searchdomainCount.textContent = searchdomains.length;
const perDomainPromises = searchdomains.map(async domain => {
const [entityListResult, querycacheUtilizationResult] = await Promise.all([
listEntities(domain),
getQuerycacheUtilization(domain)
]);
return {
entityCount: entityListResult.Results.length,
utilization: querycacheUtilizationResult.QueryCacheSizeBytes
};
}); });
getServerStats().then(result => {
const results = await Promise.all(perDomainPromises);
let entityCount = 0;
let totalUtilization = 0;
for (const r of results) {
entityCount += r.entityCount;
totalUtilization += r.utilization;
}
hideThrobber(searchdomainEntityCount);
hideThrobber(totalQuerycacheUtilization);
searchdomainEntityCount.textContent = entityCount;
totalQuerycacheUtilization.textContent = NumberOfBytesAsHumanReadable(totalUtilization);
});
getEmbeddingcacheUtilization().then(result => {
let utilization = result.SizeInBytes; let utilization = result.SizeInBytes;
let maxElementCount = result.MaxElementCount; let maxElementCount = result.MaxElementCount;
let elementCount = result.ElementCount; let elementCount = result.ElementCount;
let embeddingCount = result.EmbeddingsCount; let embeddingCount = result.EmbeddingsCount;
let entityCount = result.EntityCount;
let queryCacheUtilization = result.QueryCacheUtilization;
hideThrobber(embeddingcacheSize); hideThrobber(embeddingcacheSize);
embeddingcacheSize.textContent = NumberOfBytesAsHumanReadable(utilization); embeddingcacheSize.textContent = NumberOfBytesAsHumanReadable(utilization);
hideThrobber(embeddingcacheElementCount); hideThrobber(embeddingcacheElementCount);
@@ -184,6 +158,10 @@
hideThrobber(embeddingcacheEmbeddingCount); hideThrobber(embeddingcacheEmbeddingCount);
embeddingcacheEmbeddingCount.textContent = embeddingCount; embeddingcacheEmbeddingCount.textContent = embeddingCount;
embeddingcacheElementCountProgressBar.style.width = `${elementCount / maxElementCount * 100}%`; embeddingcacheElementCountProgressBar.style.width = `${elementCount / maxElementCount * 100}%`;
hideThrobber(searchdomainEntityCount);
searchdomainEntityCount.textContent = entityCount;
hideThrobber(totalQuerycacheUtilization);
totalQuerycacheUtilization.textContent = NumberOfBytesAsHumanReadable(queryCacheUtilization);
}); });
getHealthCheckStatusAndApply(healthchecksServer, "/healthz/Database"); getHealthCheckStatusAndApply(healthchecksServer, "/healthz/Database");
getHealthCheckStatusAndApply(healthchecksAiProvider, "/healthz/AIProvider"); getHealthCheckStatusAndApply(healthchecksAiProvider, "/healthz/AIProvider");
@@ -206,8 +184,8 @@
.then(r => r.json()); .then(r => r.json());
} }
async function getEmbeddingcacheUtilization() { async function getServerStats() {
return await fetch(`/Server/EmbeddingCache/Size`) return await fetch(`/Server/Stats`)
.then(r => r.json()); .then(r => r.json());
} }

View File

@@ -8,14 +8,18 @@ public class ServerGetModelsResult : SuccesMessageBaseModel
public string[]? Models { get; set; } public string[]? Models { get; set; }
} }
public class ServerGetEmbeddingCacheSizeResult : SuccesMessageBaseModel public class ServerGetStatsResult : SuccesMessageBaseModel
{ {
[JsonPropertyName("SizeInBytes")] [JsonPropertyName("SizeInBytes")]
public required long? SizeInBytes { get; set; } public long? SizeInBytes { get; set; }
[JsonPropertyName("MaxElementCount")] [JsonPropertyName("MaxElementCount")]
public required long? MaxElementCount { get; set; } public long? MaxElementCount { get; set; }
[JsonPropertyName("ElementCount")] [JsonPropertyName("ElementCount")]
public required long? ElementCount { get; set; } public long? ElementCount { get; set; }
[JsonPropertyName("EmbeddingsCount")] [JsonPropertyName("EmbeddingsCount")]
public required long? EmbeddingsCount { get; set; } public long? EmbeddingsCount { get; set; }
[JsonPropertyName("EntityCount")]
public long? EntityCount { get; set; }
[JsonPropertyName("QueryCacheUtilization")]
public long? QueryCacheUtilization { get; set; }
} }