23 Commits

Author SHA1 Message Date
LD50
4c1f0305fc Merge pull request #89 from LD-Reborn/65-add-number-of-cached-queries-to-front-end
65 add number of cached queries to front end
2026-01-07 01:52:38 +01:00
e49a7c83ba Improved sql connection pool resiliency 2026-01-07 01:52:12 +01:00
e83ce61877 Added query cache entry count and capacity to front-end, Fixed ServerGetStatsResult field naming 2026-01-07 01:15:55 +01:00
LD50
c09514c657 Merge pull request #88 from LD-Reborn/66-add-query-cache-size-limit
66 add query cache size limit
2026-01-05 01:04:57 +01:00
3dfcaa19e6 Implemented query cache size limit in front-end and in logic, Reworked LRUCache for performance, Fixed updating entities from front-end not working 2026-01-05 01:04:26 +01:00
88d1b27394 Fixed LRUCache TryGetValue not updating the list 2026-01-03 18:22:30 +01:00
027a9244ad Added query cache size limiting, added custom enumerable LRUCache, renamed search to query in various places, fixed client GetEmbeddingsCacheSize endpoint 2026-01-03 17:57:18 +01:00
063c81e8dc Fixed front-end wrong endpoint name used 2026-01-03 14:39:20 +01:00
LD50
ad84efb611 Merge pull request #84 from LD-Reborn/83-warning-info-modals-text-and-close-button-must-be-dark
Fixed warning and info modal text light on dark mode
2026-01-02 23:20:33 +01:00
ecaa640ec0 Fixed warning and info modal text light on dark mode 2026-01-02 23:20:12 +01:00
LD50
37f1b285d8 Merge pull request #82 from LD-Reborn/81-add-dark-mode-support
Added dark mode, updated bootstrap
2026-01-02 23:11:27 +01:00
71b273f5d7 Added dark mode, updated bootstrap 2026-01-02 23:11:03 +01:00
LD50
1a823bb1e7 Merge pull request #80 from LD-Reborn/77-fix-long-loading-times-for-entity-count-and-query-cache-utilization
Replaced GetEmbeddingCacheSize with GetStats, fixed long loading time…
2026-01-02 02:05:24 +01:00
aa4fc03c3d Replaced GetEmbeddingCacheSize with GetStats, fixed long loading times for front-end stats retrieval 2026-01-02 02:04:19 +01:00
LD50
09832d1c0b Merge pull request #79 from LD-Reborn/74-fix-missing-front-end-localization
Fixed details button not visible
2026-01-01 20:46:54 +01:00
68630fdbef Fixed details button not visible 2026-01-01 19:43:54 +01:00
LD50
c9907da846 Merge pull request #76 from LD-Reborn/74-fix-missing-front-end-localization
74 fix missing front end localization
2026-01-01 19:31:33 +01:00
cddd305d26 Added logic to hint at the exit label in elmah 2026-01-01 19:29:51 +01:00
6f4ffbcaa6 Added more missing localization, added LocalizationChecker tool, moved CriticalCSSGenerator to tools folder 2026-01-01 19:03:57 +01:00
LD50
3e433c3cbe Merge pull request #75 from LD-Reborn/72-swagger-and-elmah-have-no-return-to-front-end-button
Added swagger and elmah return-to-front-end button
2026-01-01 17:39:08 +01:00
8cbc77eb1d Added swagger and elmah return-to-front-end button 2026-01-01 17:38:48 +01:00
LD50
977a8f1637 Merge pull request #73 from LD-Reborn/68-returnurl-does-not-work
Fixed ReturnUrl not working
2026-01-01 16:12:51 +01:00
65ed78462d Fixed ReturnUrl not working 2026-01-01 16:02:30 +01:00
34 changed files with 1159 additions and 193 deletions

4
.gitignore vendored
View File

@@ -18,5 +18,5 @@ src/Server/logs
src/Shared/bin
src/Shared/obj
src/Server/wwwroot/logs/*
src/Server/CriticalCSS/node_modules
src/Server/CriticalCSS/package*.json
src/Server/Tools/CriticalCSS/node_modules
src/Server/Tools/CriticalCSS/package*.json

View File

@@ -121,13 +121,13 @@ public class Client
}), new StringContent(settings, Encoding.UTF8, "application/json"));
}
public async Task<SearchdomainSearchesResults> SearchdomainGetQueriesAsync(string searchdomain)
public async Task<SearchdomainQueriesResults> SearchdomainGetQueriesAsync(string searchdomain)
{
Dictionary<string, string> parameters = new()
{
{"searchdomain", searchdomain}
};
return await FetchUrlAndProcessJson<SearchdomainSearchesResults>(HttpMethod.Get, GetUrl($"{baseUri}/Searchdomain", "Queries", parameters));
return await FetchUrlAndProcessJson<SearchdomainQueriesResults>(HttpMethod.Get, GetUrl($"{baseUri}/Searchdomain", "Queries", parameters));
}
public async Task<EntityQueryResults> SearchdomainQueryAsync(string query)
@@ -190,13 +190,13 @@ public class Client
return await FetchUrlAndProcessJson<SearchdomainUpdateResults>(HttpMethod.Put, GetUrl($"{baseUri}/Searchdomain", "Settings", parameters), content);
}
public async Task<SearchdomainSearchCacheSizeResults> SearchdomainGetQueryCacheSizeAsync(string searchdomain)
public async Task<SearchdomainQueryCacheSizeResults> SearchdomainGetQueryCacheSizeAsync(string searchdomain)
{
Dictionary<string, string> parameters = new()
{
{"searchdomain", searchdomain}
};
return await FetchUrlAndProcessJson<SearchdomainSearchCacheSizeResults>(HttpMethod.Get, GetUrl($"{baseUri}/Searchdomain/QueryCache", "Size", parameters));
return await FetchUrlAndProcessJson<SearchdomainQueryCacheSizeResults>(HttpMethod.Get, GetUrl($"{baseUri}/Searchdomain/QueryCache", "Size", parameters));
}
public async Task<SearchdomainInvalidateCacheResults> SearchdomainClearQueryCache(string searchdomain)
@@ -222,9 +222,9 @@ public class Client
return await FetchUrlAndProcessJson<ServerGetModelsResult>(HttpMethod.Get, GetUrl($"{baseUri}/Server", "Models", []));
}
public async Task<ServerGetEmbeddingCacheSizeResult> ServerGetEmbeddingCacheSizeAsync()
public async Task<ServerGetStatsResult> ServerGetStatsAsync()
{
return await FetchUrlAndProcessJson<ServerGetEmbeddingCacheSizeResult>(HttpMethod.Get, GetUrl($"{baseUri}/Server/EmbeddingCache", "Size", []));
return await FetchUrlAndProcessJson<ServerGetStatsResult>(HttpMethod.Get, GetUrl($"{baseUri}/Server/Stats", "Size", []));
}
private async Task<T> FetchUrlAndProcessJson<T>(HttpMethod httpMethod, string url, HttpContent? content = null)

View File

@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Server.Exceptions;
using Server.Helper;
using Shared;
using Shared.Models;
namespace Server.Controllers;
@@ -54,6 +55,10 @@ public class SearchdomainController : ControllerBase
{
try
{
if (settings.QueryCacheSize <= 0)
{
settings.QueryCacheSize = 1_000_000; // TODO get rid of this magic number
}
int id = _domainManager.CreateSearchdomain(searchdomain, settings);
return Ok(new SearchdomainCreateResults(){Id = id, Success = true});
} catch (Exception)
@@ -134,13 +139,13 @@ public class SearchdomainController : ControllerBase
/// </summary>
/// <param name="searchdomain">Name of the searchdomain</param>
[HttpGet("Queries")]
public ActionResult<SearchdomainSearchesResults> GetQueries([Required]string searchdomain)
public ActionResult<SearchdomainQueriesResults> GetQueries([Required]string searchdomain)
{
(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});
Dictionary<string, DateTimedSearchResult> searchCache = searchdomain_.searchCache;
Dictionary<string, DateTimedSearchResult> searchCache = searchdomain_.queryCache.AsDictionary();
return Ok(new SearchdomainSearchesResults() { Searches = searchCache, Success = true });
return Ok(new SearchdomainQueriesResults() { Searches = searchCache, Success = true });
}
/// <summary>
@@ -175,7 +180,7 @@ public class SearchdomainController : ControllerBase
{
(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});
Dictionary<string, DateTimedSearchResult> searchCache = searchdomain_.searchCache;
EnumerableLruCache<string, DateTimedSearchResult> searchCache = searchdomain_.queryCache;
bool containsKey = searchCache.ContainsKey(query);
if (containsKey)
{
@@ -196,7 +201,7 @@ public class SearchdomainController : ControllerBase
{
(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});
Dictionary<string, DateTimedSearchResult> searchCache = searchdomain_.searchCache;
EnumerableLruCache<string, DateTimedSearchResult> searchCache = searchdomain_.queryCache;
bool containsKey = searchCache.ContainsKey(query);
if (containsKey)
{
@@ -237,6 +242,7 @@ public class SearchdomainController : ControllerBase
};
searchdomain_.helper.ExecuteSQLNonQuery("UPDATE searchdomain set settings = @settings WHERE id = @id", parameters);
searchdomain_.settings = request;
searchdomain_.queryCache.Capacity = request.QueryCacheSize;
return Ok(new SearchdomainUpdateResults(){Success = true});
}
@@ -245,19 +251,17 @@ public class SearchdomainController : ControllerBase
/// </summary>
/// <param name="searchdomain">Name of the searchdomain</param>
[HttpGet("QueryCache/Size")]
public ActionResult<SearchdomainSearchCacheSizeResults> GetSearchCacheSize([Required]string searchdomain)
public ActionResult<SearchdomainQueryCacheSizeResults> GetQueryCacheSize([Required]string searchdomain)
{
if (!SearchdomainHelper.IsSearchdomainLoaded(_domainManager, searchdomain))
{
return Ok(new SearchdomainQueryCacheSizeResults() { SizeBytes = 0, ElementCount = 0, ElementMaxCount = 0, Success = true });
}
(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});
Dictionary<string, DateTimedSearchResult> searchCache = searchdomain_.searchCache;
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 });
int elementCount = searchdomain_.queryCache.Count;
int ElementMaxCount = searchdomain_.settings.QueryCacheSize;
return Ok(new SearchdomainQueryCacheSizeResults() { SizeBytes = searchdomain_.GetSearchCacheSize(), ElementCount = elementCount, ElementMaxCount = ElementMaxCount, Success = true });
}
/// <summary>
@@ -282,7 +286,7 @@ public class SearchdomainController : ControllerBase
{
(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});
long sizeInBytes = DatabaseHelper.GetSearchdomainDatabaseSize(searchdomain_.helper, searchdomain);
return Ok(new SearchdomainGetDatabaseSizeResult() { SearchdomainDatabaseSizeBytes = sizeInBytes, Success = true });
}
long EmbeddingCacheUtilization = DatabaseHelper.GetSearchdomainDatabaseSize(searchdomain_.helper, searchdomain);
return Ok(new SearchdomainGetDatabaseSizeResult() { SearchdomainDatabaseSizeBytes = EmbeddingCacheUtilization, Success = true });
}
}

View File

@@ -5,8 +5,10 @@ using System.Text.Json;
using AdaptiveExpressions;
using ElmahCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Server.Exceptions;
using Server.Helper;
using Server.Models;
using Shared.Models;
[ApiController]
@@ -17,13 +19,15 @@ public class ServerController : ControllerBase
private readonly IConfiguration _config;
private AIProvider _aIProvider;
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;
_config = config;
_aIProvider = aIProvider;
_searchdomainManager = searchdomainManager;
_options = options;
}
/// <summary>
@@ -47,31 +51,73 @@ public class ServerController : ControllerBase
}
/// <summary>
/// Gets the total memory size of the embedding cache
/// Gets numeric info regarding the searchdomains
/// </summary>
[HttpGet("EmbeddingCache/Size")]
public ActionResult<ServerGetEmbeddingCacheSizeResult> GetEmbeddingCacheSize()
[HttpGet("Stats")]
public async Task<ActionResult<ServerGetStatsResult>> Stats()
{
long size = 0;
long elementCount = 0;
long embeddingsCount = 0;
LRUCache<string, Dictionary<string, float[]>> embeddingCache = _searchdomainManager.embeddingCache;
var cacheListField = embeddingCache.GetType()
.GetField("_cacheList", BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new InvalidOperationException("_cacheList field not found"); // TODO Remove this unsafe reflection atrocity
LinkedList<string> cacheListOriginal = (LinkedList<string>)cacheListField.GetValue(embeddingCache)!;
LinkedList<string> cacheList = new(cacheListOriginal);
foreach (string key in cacheList)
try
{
if (!embeddingCache.TryGet(key, out var entry))
continue;
long size = 0;
long elementCount = 0;
long embeddingsCount = 0;
LRUCache<string, Dictionary<string, float[]>> embeddingCache = _searchdomainManager.embeddingCache;
var cacheListField = embeddingCache.GetType()
.GetField("_cacheList", BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new InvalidOperationException("_cacheList field not found"); // TODO Remove this unsafe reflection atrocity
LinkedList<string> cacheListOriginal = (LinkedList<string>)cacheListField.GetValue(embeddingCache)!;
LinkedList<string> cacheList = new(cacheListOriginal);
// estimate size
size += EstimateEntrySize(key, entry);
elementCount++;
embeddingsCount += entry.Keys.Count;
foreach (string key in cacheList)
{
if (!embeddingCache.TryGet(key, out var entry))
continue;
// estimate size
size += EstimateEntrySize(key, entry);
elementCount++;
embeddingsCount += entry.Keys.Count;
}
var sqlHelper = DatabaseHelper.GetSQLHelper(_options.Value);
Task<long> entityCountTask = DatabaseHelper.CountEntities(sqlHelper);
long queryCacheUtilization = 0;
long queryCacheElementCount = 0;
long queryCacheMaxElementCountAll = 0;
long queryCacheMaxElementCountLoadedSearchdomainsOnly = 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();
queryCacheElementCount += searchdomain_.queryCache.Count;
queryCacheMaxElementCountAll += searchdomain_.queryCache.Capacity;
queryCacheMaxElementCountLoadedSearchdomainsOnly += searchdomain_.queryCache.Capacity;
} else
{
var searchdomainSettings = DatabaseHelper.GetSearchdomainSettings(sqlHelper, searchdomain);
queryCacheMaxElementCountAll += searchdomainSettings.QueryCacheSize;
}
};
long entityCount = await entityCountTask;
return new ServerGetStatsResult() {
Success = true,
EntityCount = entityCount,
QueryCacheUtilization = queryCacheUtilization,
QueryCacheElementCount = queryCacheElementCount,
QueryCacheMaxElementCountAll = queryCacheMaxElementCountAll,
QueryCacheMaxElementCountLoadedSearchdomainsOnly = queryCacheMaxElementCountLoadedSearchdomainsOnly,
EmbeddingCacheUtilization = size,
EmbeddingCacheMaxElementCount = _searchdomainManager.EmbeddingCacheMaxCount,
EmbeddingCacheElementCount = elementCount,
EmbeddingsCount = embeddingsCount
};
} catch (Exception ex)
{
ElmahExtensions.RaiseError(ex);
return StatusCode(500, new ServerGetStatsResult(){Success = false, Message = ex.Message});
}
return new ServerGetEmbeddingCacheSizeResult() { Success = true, SizeInBytes = size, MaxElementCount = _searchdomainManager.EmbeddingCacheMaxCount, ElementCount = elementCount, EmbeddingsCount = embeddingsCount};
}
private static long EstimateEntrySize(string key, Dictionary<string, float[]> value)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,10 @@
using System.Configuration;
using System.Data.Common;
using System.Text;
using System.Text.Json;
using MySql.Data.MySqlClient;
using Server.Exceptions;
using Server.Models;
using Shared.Models;
namespace Server.Helper;
@@ -9,6 +13,14 @@ public class DatabaseHelper(ILogger<DatabaseHelper> 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)
{
Dictionary<string, object> parameters = [];
@@ -210,5 +222,45 @@ public class DatabaseHelper(ILogger<DatabaseHelper> logger)
attributeSumReader.Close();
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;
}
public static SearchdomainSettings GetSearchdomainSettings(SQLHelper helper, string searchdomain)
{
Dictionary<string, dynamic> parameters = new()
{
["name"] = searchdomain
};
DbDataReader reader = helper.ExecuteSQLCommand("SELECT settings from searchdomain WHERE name = @name", parameters);
try
{
reader.Read();
string settingsString = reader.GetString(0);
return JsonSerializer.Deserialize<SearchdomainSettings>(settingsString);
} finally
{
reader.Close();
}
}
}

View File

@@ -1,3 +1,4 @@
using System.Data;
using System.Data.Common;
using MySql.Data.MySqlClient;
@@ -6,6 +7,7 @@ namespace Server.Helper;
public class SQLHelper:IDisposable
{
public MySqlConnection connection;
public DbDataReader? dbDataReader;
public string connectionString;
public SQLHelper(MySqlConnection connection, string connectionString)
{
@@ -30,13 +32,15 @@ public class SQLHelper:IDisposable
lock (connection)
{
EnsureConnected();
EnsureDbReaderIsClosed();
using MySqlCommand command = connection.CreateCommand();
command.CommandText = query;
foreach (KeyValuePair<string, dynamic> parameter in parameters)
{
command.Parameters.AddWithValue($"@{parameter.Key}", parameter.Value);
}
return command.ExecuteReader();
dbDataReader = command.ExecuteReader();
return dbDataReader;
}
}
@@ -45,6 +49,7 @@ public class SQLHelper:IDisposable
lock (connection)
{
EnsureConnected();
EnsureDbReaderIsClosed();
using MySqlCommand command = connection.CreateCommand();
command.CommandText = query;
@@ -61,6 +66,7 @@ public class SQLHelper:IDisposable
lock (connection)
{
EnsureConnected();
EnsureDbReaderIsClosed();
using MySqlCommand command = connection.CreateCommand();
command.CommandText = query;
@@ -83,11 +89,29 @@ public class SQLHelper:IDisposable
connection.Close();
connection.Open();
}
catch (Exception)
catch (Exception ex)
{
throw; // TODO add logging here
ElmahCore.ElmahExtensions.RaiseError(ex);
throw;
}
}
return true;
}
public void EnsureDbReaderIsClosed()
{
int counter = 0;
int sleepTime = 10;
int timeout = 5000;
while (!(dbDataReader?.IsClosed ?? true))
{
if (counter > timeout / sleepTime)
{
TimeoutException ex = new("Unable to ensure dbDataReader is closed");
ElmahCore.ElmahExtensions.RaiseError(ex);
throw ex;
}
Thread.Sleep(sleepTime);
}
}
}

View File

@@ -88,7 +88,7 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
public Entity? EntityFromJSON(SearchdomainManager searchdomainManager, ILogger logger, JSONEntity jsonEntity) //string json)
{
SQLHelper helper = searchdomainManager.helper.DuplicateConnection();
using SQLHelper helper = searchdomainManager.helper.DuplicateConnection();
Searchdomain searchdomain = searchdomainManager.GetSearchdomain(jsonEntity.Searchdomain);
List<Entity> entityCache = searchdomain.entityCache;
AIProvider aIProvider = searchdomain.aIProvider;
@@ -299,4 +299,9 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
return (null, 404, $"Unable to update searchdomain {searchdomain}");
}
}
public static bool IsSearchdomainLoaded(SearchdomainManager searchdomainManager, string name)
{
return searchdomainManager.IsSearchdomainLoaded(name);
}
}

View File

@@ -91,4 +91,10 @@ public static class DatabaseMigrations
helper.ExecuteSQLNonQuery("ALTER TABLE datapoint ADD COLUMN similaritymethod VARCHAR(512) NULL DEFAULT 'Cosine' AFTER probmethod_embedding", []);
return 4;
}
public static int UpdateFrom4(SQLHelper helper)
{
helper.ExecuteSQLNonQuery("UPDATE searchdomain SET settings = JSON_SET(settings, '$.QueryCacheSize', 1000000) WHERE JSON_EXTRACT(settings, '$.QueryCacheSize') is NULL;", []); // Set QueryCacheSize to a default of 1000000
return 5;
}
}

View File

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

View File

@@ -14,6 +14,8 @@ using System.Configuration;
using Microsoft.OpenApi.Models;
using Shared.Models;
using Microsoft.AspNetCore.ResponseCompression;
using System.Net;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
@@ -140,6 +142,57 @@ var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// Configure Elmah
app.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/elmah"))
{
context.Response.OnStarting(() =>
{
context.Response.Headers.Append(
"Content-Security-Policy",
"default-src 'self' 'unsafe-inline' 'unsafe-eval'"
);
return Task.CompletedTask;
});
}
await next();
});
app.Use(async (context, next) =>
{
if (!context.Request.Path.StartsWithSegments("/elmah"))
{
await next();
return;
}
var originalBody = context.Response.Body;
using var memStream = new MemoryStream();
context.Response.Body = memStream;
await next();
memStream.Position = 0;
var html = await new StreamReader(memStream).ReadToEndAsync();
if (context.Response.ContentType?.Contains("text/html") == true)
{
html = html.Replace(
"</head>",
"""
<link rel="stylesheet" href="/elmah-ui/custom.css" />
<script src="/elmah-ui/custom.js"></script>
</head>
"""
);
}
var bytes = Encoding.UTF8.GetBytes(html);
context.Response.ContentLength = bytes.Length;
await originalBody.WriteAsync(bytes);
context.Response.Body = originalBody;
});
app.UseElmah();
app.MapHealthChecks("/healthz");
@@ -161,7 +214,7 @@ app.Use(async (context, next) =>
{
if (!context.User.Identity?.IsAuthenticated ?? true)
{
context.Response.Redirect("/Account/Login");
context.Response.Redirect($"/Account/Login?ReturnUrl={WebUtility.UrlEncode("/swagger")}");
return;
}
@@ -179,6 +232,8 @@ app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.EnablePersistAuthorization();
options.InjectStylesheet("/swagger-ui/custom.css");
options.InjectJavascript("/swagger-ui/custom.js");
});
//app.UseElmahExceptionPage(); // Messes with JSON response for API calls. Leaving this here so I don't accidentally put this in again later on.

View File

@@ -55,7 +55,7 @@
<value>Such-Cache</value>
</data>
<data name="Search cache utilization" xml:space="preserve">
<value>Such-Cache Speicherauslastung</value>
<value>Such-Cache-Speicherauslastung</value>
</data>
<data name="Clear" xml:space="preserve">
<value>Leeren</value>
@@ -121,7 +121,7 @@
<value>Searchdomain Name</value>
</data>
<data name="Enable cache reconciliation" xml:space="preserve">
<value>Cache Abgleich verwenden</value>
<value>Cache-Abgleich verwenden</value>
</data>
<data name="Create entity" xml:space="preserve">
<value>Entity erstellen</value>
@@ -175,10 +175,10 @@
<value>Searchdomain konnte nicht erstellt werden</value>
</data>
<data name="Searchdomain cache was cleared successfully" xml:space="preserve">
<value>Searchdomain Cache wurde erfolgreich geleert</value>
<value>Searchdomain-Cache wurde erfolgreich geleert</value>
</data>
<data name="Failed to clear searchdomain cache" xml:space="preserve">
<value>Searchdomain Cache konnte nicht geleert werden</value>
<value>Searchdomain-Cache konnte nicht geleert werden</value>
</data>
<data name="Entity was deleted successfully" xml:space="preserve">
<value>Entity wurde erfolgreich gelöscht</value>
@@ -229,7 +229,7 @@
<value>Searchdomain Einstellungen konnten nicht abgerufen werden</value>
</data>
<data name="Unable to fetch searchdomain cache utilization" xml:space="preserve">
<value>Searchdomain Cache-Auslastung konnte nicht abgerufen werden</value>
<value>Searchdomain-Cache-Auslastung konnte nicht abgerufen werden</value>
</data>
<data name="Details" xml:space="preserve">
<value>Details</value>
@@ -243,4 +243,76 @@
<data name="Close alert" xml:space="preserve">
<value>Benachrichtigung schließen</value>
</data>
<data name="Recent queries" xml:space="preserve">
<value>Letzte Queries</value>
</data>
<data name="Home" xml:space="preserve">
<value>Dashboard</value>
</data>
<data name="Searchdomains" xml:space="preserve">
<value>Searchdomains</value>
</data>
<data name="Swagger" xml:space="preserve">
<value>Swagger</value>
</data>
<data name="Elmah" xml:space="preserve">
<value>Elmah</value>
</data>
<data name="Hi!" xml:space="preserve">
<value>Hallo!</value>
</data>
<data name="Hi, {0}!" xml:space="preserve">
<value>Hallo {0}!</value>
</data>
<data name="Embedding Cache" xml:space="preserve">
<value>Embedding-Cache</value>
</data>
<data name="Size" xml:space="preserve">
<value>Größe</value>
</data>
<data name="Strings" xml:space="preserve">
<value>Zeichenketten</value>
</data>
<data name="stringsCountInfo" xml:space="preserve">
<value>Die Anzahl der Zeichenketten, für die Embeddings vorliegen. D.h. wenn zwei Modelle verwendet werden, ist die Zahl der Embeddings zweimal so hoch.</value>
</data>
<data name="Embeddings" xml:space="preserve">
<value>Embeddings</value>
</data>
<data name="Health Checks" xml:space="preserve">
<value>Health Checks</value>
</data>
<data name="Server" xml:space="preserve">
<value>Server</value>
</data>
<data name="AI Providers" xml:space="preserve">
<value>AI Providers</value>
</data>
<data name="Count" xml:space="preserve">
<value>Anzahl</value>
</data>
<data name="Total Entities" xml:space="preserve">
<value>Entities insgesamt</value>
</data>
<data name="Total query cache utilization" xml:space="preserve">
<value>Query-Cache-Verwendung insgesamt</value>
</data>
<data name="Unable to fetch searchdomain database utilization" xml:space="preserve">
<value>Searchdomain Datenbank-Auslastung konnte nicht abgerufen werden</value>
</data>
<data name="Query cache entry count" xml:space="preserve">
<value>Query-Cache Einträge</value>
</data>
<data name="Query cache capacity (all)" xml:space="preserve">
<value>Query-Cache Kapazität (alle)</value>
</data>
<data name="queryCacheEntryCountAllInfo" xml:space="preserve">
<value>Anzahl der Einträge, die insgesamt in den Query-Cache passen. Ungeladene Searchdomains werden berücksichtigt.</value>
</data>
<data name="Query cache capacity (loaded)" xml:space="preserve">
<value>Query-Cache Kapazität (geladen)</value>
</data>
<data name="queryCacheEntryCountLoadedInfo" xml:space="preserve">
<value>Anzahl der Einträge, die insgesamt in den Query-Cache der geladenen Searchdomains passen.</value>
</data>
</root>

View File

@@ -243,4 +243,76 @@
<data name="Close alert" xml:space="preserve">
<value>Close alert</value>
</data>
<data name="Recent queries" xml:space="preserve">
<value>Recent queries</value>
</data>
<data name="Home" xml:space="preserve">
<value>Dashboard</value>
</data>
<data name="Searchdomains" xml:space="preserve">
<value>Searchdomains</value>
</data>
<data name="Swagger" xml:space="preserve">
<value>Swagger</value>
</data>
<data name="Elmah" xml:space="preserve">
<value>Elmah</value>
</data>
<data name="Hi!" xml:space="preserve">
<value>Hi!</value>
</data>
<data name="Hi, {0}!" xml:space="preserve">
<value>Hi {0}!</value>
</data>
<data name="Embedding Cache" xml:space="preserve">
<value>Embedding Cache</value>
</data>
<data name="Size" xml:space="preserve">
<value>Size</value>
</data>
<data name="Strings" xml:space="preserve">
<value>Strings</value>
</data>
<data name="stringsCountInfo" xml:space="preserve">
<value>The number of strings for which there are embeddings. I.e. If you use two models, the amount of embeddings will be twice this number.</value>
</data>
<data name="Embeddings" xml:space="preserve">
<value>Embeddings</value>
</data>
<data name="Health Checks" xml:space="preserve">
<value>Health Checks</value>
</data>
<data name="Server" xml:space="preserve">
<value>Server</value>
</data>
<data name="AI Providers" xml:space="preserve">
<value>AI Providers</value>
</data>
<data name="Count" xml:space="preserve">
<value>Count</value>
</data>
<data name="Total Entities" xml:space="preserve">
<value>Total Entities</value>
</data>
<data name="Total query cache utilization" xml:space="preserve">
<value>Total query cache utilization</value>
</data>
<data name="Unable to fetch searchdomain database utilization" xml:space="preserve">
<value>Unable to fetch searchdomain database utilization</value>
</data>
<data name="Query cache entry count" xml:space="preserve">
<value>Query cache entry count</value>
</data>
<data name="Query cache capacity (all)" xml:space="preserve">
<value>Query cache capacity (all)</value>
</data>
<data name="queryCacheEntryCountAllInfo" xml:space="preserve">
<value>Number of query cache entries that can be stored in the query cache, including searchdomains that are currently not loaded.</value>
</data>
<data name="Query cache capacity (loaded)" xml:space="preserve">
<value>Query cache capacity (loaded)</value>
</data>
<data name="queryCacheEntryCountLoadedInfo" xml:space="preserve">
<value>Number of query cache entries that can be stored in the query cache of all loaded searchdomains.</value>
</data>
</root>

View File

@@ -4,6 +4,7 @@ using System.Text.Json;
using ElmahCore.Mvc.Logger;
using MySql.Data.MySqlClient;
using Server.Helper;
using Shared;
using Shared.Models;
using AdaptiveExpressions;
@@ -17,7 +18,7 @@ public class Searchdomain
public string searchdomain;
public int id;
public SearchdomainSettings settings;
public Dictionary<string, DateTimedSearchResult> searchCache; // Key: query, Value: Search results for that query (with timestamp)
public EnumerableLruCache<string, DateTimedSearchResult> queryCache; // Key: query, Value: Search results for that query (with timestamp)
public List<Entity> entityCache;
public List<string> modelsInUse;
public LRUCache<string, Dictionary<string, float[]>> embeddingCache;
@@ -33,12 +34,12 @@ public class Searchdomain
this.aIProvider = aIProvider;
this.embeddingCache = embeddingCache;
this._logger = logger;
searchCache = [];
entityCache = [];
connection = new MySqlConnection(connectionString);
connection.Open();
helper = new SQLHelper(connection, connectionString);
settings = GetSettings();
queryCache = new(settings.QueryCacheSize);
modelsInUse = []; // To make the compiler shut up - it is set in UpdateSearchDomain() don't worry // yeah, about that...
if (!runEmpty)
{
@@ -163,7 +164,7 @@ public class Searchdomain
public List<(float, string)> Search(string query, int? topN = null)
{
if (searchCache.TryGetValue(query, out DateTimedSearchResult cachedResult))
if (queryCache.TryGetValue(query, out DateTimedSearchResult cachedResult))
{
cachedResult.AccessDateTimes.Add(DateTime.Now);
return [.. cachedResult.Results.Select(r => (r.Score, r.Name))];
@@ -187,7 +188,7 @@ public class Searchdomain
[.. sortedResults.Select(r =>
new ResultItem(r.Item1, r.Item2 ))]
);
searchCache[query] = new DateTimedSearchResult(DateTime.Now, searchResult);
queryCache.Set(query, new DateTimedSearchResult(DateTime.Now, searchResult));
return results;
}
@@ -277,22 +278,14 @@ public class Searchdomain
public SearchdomainSettings GetSettings()
{
Dictionary<string, dynamic> parameters = new()
{
["name"] = searchdomain
};
DbDataReader reader = helper.ExecuteSQLCommand("SELECT settings from searchdomain WHERE name = @name", parameters);
reader.Read();
string settingsString = reader.GetString(0);
reader.Close();
return JsonSerializer.Deserialize<SearchdomainSettings>(settingsString);
return DatabaseHelper.GetSearchdomainSettings(helper, searchdomain);
}
public void ReconciliateOrInvalidateCacheForNewOrUpdatedEntity(Entity entity)
{
if (settings.CacheReconciliation)
{
foreach (KeyValuePair<string, DateTimedSearchResult> element in searchCache)
foreach (var element in queryCache)
{
string query = element.Key;
DateTimedSearchResult searchResult = element.Value;
@@ -322,7 +315,7 @@ public class Searchdomain
{
if (settings.CacheReconciliation)
{
foreach (KeyValuePair<string, DateTimedSearchResult> element in searchCache)
foreach (KeyValuePair<string, DateTimedSearchResult> element in queryCache)
{
string query = element.Key;
DateTimedSearchResult searchResult = element.Value;
@@ -337,6 +330,18 @@ public class Searchdomain
public void InvalidateSearchCache()
{
searchCache = [];
queryCache = new(settings.QueryCacheSize);
}
public long GetSearchCacheSize()
{
long EmbeddingCacheUtilization = 0;
foreach (var entry in queryCache)
{
EmbeddingCacheUtilization += sizeof(int); // string length prefix
EmbeddingCacheUtilization += entry.Key.Length * sizeof(char); // string characters
EmbeddingCacheUtilization += entry.Value.EstimateSize();
}
return EmbeddingCacheUtilization;
}
}

View File

@@ -6,6 +6,8 @@ using Server.Exceptions;
using AdaptiveExpressions;
using Shared.Models;
using System.Text.Json;
using Microsoft.Extensions.Options;
using Server.Models;
namespace Server;
@@ -13,24 +15,24 @@ public class SearchdomainManager
{
private Dictionary<string, Searchdomain> searchdomains = [];
private readonly ILogger<SearchdomainManager> _logger;
private readonly IConfiguration _config;
private readonly EmbeddingSearchOptions _options;
public readonly AIProvider aIProvider;
private readonly DatabaseHelper _databaseHelper;
private readonly string connectionString;
private MySqlConnection connection;
public SQLHelper helper;
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;
_config = config;
_options = options.Value;
this.aIProvider = aIProvider;
_databaseHelper = databaseHelper;
EmbeddingCacheMaxCount = config.GetValue<int?>("Embeddingsearch:EmbeddingCacheMaxCount") ?? 1000000;
embeddingCache = new(EmbeddingCacheMaxCount);
connectionString = _config.GetSection("Embeddingsearch").GetConnectionString("SQL") ?? "";
EmbeddingCacheMaxCount = _options.EmbeddingCacheMaxCount;
embeddingCache = new((int)EmbeddingCacheMaxCount);
connectionString = _options.ConnectionStrings.SQL;
connection = new MySqlConnection(connectionString);
connection.Open();
helper = new SQLHelper(connection, connectionString);
@@ -80,12 +82,18 @@ public class SearchdomainManager
{
DbDataReader reader = helper.ExecuteSQLCommand("SELECT name FROM searchdomain", []);
List<string> results = [];
while (reader.Read())
try
{
results.Add(reader.GetString(0));
while (reader.Read())
{
results.Add(reader.GetString(0));
}
return results;
}
finally
{
reader.Close();
}
reader.Close();
return results;
}
}
@@ -122,4 +130,9 @@ public class SearchdomainManager
searchdomains[name] = searchdomain;
return searchdomain;
}
public bool IsSearchdomainLoaded(string name)
{
return searchdomains.ContainsKey(name);
}
}

View File

@@ -19,7 +19,7 @@ const cookies = await page.cookies();
await browser.close();
async function generateCriticalCSSForViews() {
const viewsDir = '../Views';
const viewsDir = '../../Views';
// Helper function to get all .cshtml files recursively
function getAllCshtmlFiles(dir) {
@@ -29,8 +29,6 @@ async function generateCriticalCSSForViews() {
list.forEach(file => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
console.log("DEBUG@2");
console.log(filePath);
if (stat && stat.isDirectory()) {
// Recursively get files from subdirectories
results = results.concat(getAllCshtmlFiles(filePath));
@@ -78,11 +76,11 @@ async function generateCriticalCSSForViews() {
// Process each file
for (const file of cshtmlFiles) {
try {
const urlPath = filePathToUrlPath(file).replace("../", "").replace("/Views", "");
const urlPath = filePathToUrlPath(file).replace("../", "").replace("../", "").replace("/Views", "");
// Generate critical CSS
await generate({
src: `http://localhost:5146${urlPath}`,
src: `http://localhost:5146${urlPath}?noCriticalCSS`,
inline: false,
width: 1920,
height: 1080,
@@ -92,7 +90,7 @@ async function generateCriticalCSSForViews() {
},
forceExclude: ['.btn'], // Otherwise buttons end up colorless and .btn overrides other classes like .btn-warning, etc. - so it has to be force-excluded here and re-added later
forceInclude: [
'[data-bs-theme=dark]',
'[data-bs-theme="dark"]', '[data-bs-theme="dark"] body', '[data-bs-theme="dark"] .navbar', '[data-bs-theme="dark"] .card', '[data-bs-theme="dark"] .btn',
'.navbar',
'.col-md-4',
'.visually-hidden', // visually hidden headings
@@ -105,14 +103,14 @@ async function generateCriticalCSSForViews() {
'.d-flex', '.justify-content-between', '.mt-2', // card - content
'.progress', '.mt-3', // card - progress bar
'.list-group', '.list-group-flush', '.list-group-item', '.list-group-flush>.list-group-item', '.list-group-flush>.list-group-item:last-child', '.badge', '.bg-warning', '.bg-success', '.h-100', // card - health check list
'.btn', '.btn-sm', '.btn-primary', '.btn-warning', '.btn-danger', // Searchdomains buttons
'.btn-primary', '.btn-warning', '.btn-danger', '.btn-info', // Searchdomains buttons
'.col-md-8', '.sidebar',
'.mb-0', '.mb-2', '.align-items-center',
'h3', '.col-md-3', '.col-md-2', '.text-nowrap', '.overflow-auto'
]
},
target: {
css: path.join(criticalCssDir, urlPath.replace(/\//g, '.').replace(/^\./, '').replace("...", "") + '.css')
css: path.join(criticalCssDir, "../../CriticalCSS/" + urlPath.replace(/\//g, '.').replace(/^\./, '').replace("...", "") + '.css')
}
});

View File

@@ -7,4 +7,5 @@ npm install puppeteer
2. Run the css generator:
```bash
node CriticalCSSGenerator.js
```
```
3. Move the `.css` files from the current directory to the `CriticalCSS/` folder (overwrite existing files)

View File

@@ -0,0 +1,78 @@
import re
import sys
def extract_translations_from_View(view_path):
"""Extract all translation strings from file A"""
translations = {}
try:
with open(view_path, 'r', encoding='utf-8') as file_a:
for line_num, line in enumerate(file_a, 1):
# Match T["..."] patterns
matches = re.findall(r'T\["([^"]*)"\]', line)
for match in matches:
translations[match] = line_num
except FileNotFoundError:
print(f"Error: File {view_path} not found")
sys.exit(1)
except Exception as e:
print(f"Error reading file {view_path}: {e}")
sys.exit(1)
return translations
def extract_localizations_from_resource_file(file_b_path):
"""Extract all translation strings from file B"""
translations = set()
try:
with open(file_b_path, 'r', encoding='utf-8') as file_b:
for line in file_b:
# Match the pattern in file B
match = re.search(r'<data name="([^"]*)"', line)
if match:
translations.add(match.group(1))
except FileNotFoundError:
print(f"Error: File {file_b_path} not found")
sys.exit(1)
except Exception as e:
print(f"Error reading file {file_b_path}: {e}")
sys.exit(1)
return translations
def find_missing_translations(view, resource):
"""Find translations in file A that don't exist in file B"""
# Extract translations from both files
file_a_translations = extract_translations_from_View(view)
file_b_translations = extract_localizations_from_resource_file(resource)
# Find missing translations
missing_translations = []
for translation_text, line_number in file_a_translations.items():
if translation_text not in file_b_translations:
missing_translations.append((translation_text, line_number))
return missing_translations
def main():
views = ["Shared/_Layout.cshtml", "Home/Index.cshtml", "Home/Searchdomains.cshtml"]
resources = ["SharedResources.en.resx", "SharedResources.de.resx"]
print("Checking for missing translations...")
print("=" * 50)
for view in views:
for resource in resources:
missing = find_missing_translations("../../Views/" + view, "../../Resources/" + resource)
if missing:
print(f"Found {len(missing)} missing translations in {view}:")
print("-" * 50)
for translation_text, line_number in missing:
print(f"Line {line_number}: T[\"{translation_text}\"]")
else:
print(f"All localizations in {view} have a matching resource in {resource}!")
if __name__ == "__main__":
main()

View File

@@ -1,3 +1,4 @@
@using Microsoft.Extensions.Primitives
@using Server.Services
@inject LocalizationService T
@{
@@ -9,6 +10,10 @@
<h1>Login</h1>
<form asp-action="Login" method="post" class="mt-4" style="max-width: 400px; margin: auto;">
<div class="form-group mb-3">
@if (Context.Request.Query.TryGetValue("ReturnUrl", out StringValues returnUrl))
{
<input type="hidden" name="ReturnUrl" value="@(returnUrl)" />
}
<label for="username" class="form-label">@T["Username"]</label>
<input autofocus type="text" class="form-control" id="username" name="username" autocomplete="username" required>
</div>

View File

@@ -5,7 +5,6 @@
@using Server
@inject LocalizationService T
@inject AIProvider AIProvider
@model HomeIndexViewModel
@{
ViewData["Title"] = "Home Page";
@@ -41,7 +40,7 @@
@T["Strings"]
<i class="bi bi-info-circle-fill text-info"
data-bs-toggle="tooltip"
title="The number of strings for which there are embeddings. I.e. If you use two models, the amount of embeddings will be twice this number."></i>
title="@T["stringsCountInfo"]"></i>
</span>
<strong id="embeddingcacheElementCount"></strong>
</div>
@@ -105,6 +104,43 @@
<span>@T["Total query cache utilization"]</span>
<strong id="totalQuerycacheUtilization"></strong>
</div>
<!-- Query cache -->
<div class="d-flex justify-content-between mt-2">
<span>@T["Query cache entry count"]</span>
<strong id="querycacheCount"></strong>
</div>
<div class="d-flex justify-content-between mt-2">
<span>
@T["Query cache capacity (loaded)"]
<i class="bi bi-info-circle-fill text-info"
data-bs-toggle="tooltip"
title="@T["queryCacheEntryCountLoadedInfo"]"></i>
</span>
<strong id="querycacheLoadedMaxElementCount"></strong>
</div>
<div class="progress mt-3" style="height: 8px;">
<div id="querycacheLoadedMaxElementCountProgressBar" class="progress-bar"
style="width: 0.00%"></div>
</div>
<div class="d-flex justify-content-between mt-2">
<span>
@T["Query cache capacity (all)"]
<i class="bi bi-info-circle-fill text-info"
data-bs-toggle="tooltip"
title="@T["queryCacheEntryCountAllInfo"]"></i>
</span>
<strong id="querycacheMaxElementCount"></strong>
</div>
<div class="progress mt-3" style="height: 8px;">
<div id="querycacheMaxElementCountProgressBar" class="progress-bar"
style="width: 0.00%"></div>
</div>
</div>
</div>
</div>
@@ -136,6 +172,17 @@
let embeddingcacheEmbeddingCount = document.getElementById("embeddingcacheEmbeddingCount");
showThrobber(embeddingcacheEmbeddingCount);
let embeddingcacheElementCountProgressBar = document.getElementById("embeddingcacheElementCountProgressBar");
let querycacheCount = document.getElementById("querycacheCount");
showThrobber(querycacheCount);
let querycacheMaxElementCount = document.getElementById("querycacheMaxElementCount");
showThrobber(querycacheMaxElementCount);
let querycacheMaxElementCountProgressBar = document.getElementById("querycacheMaxElementCountProgressBar");
let querycacheLoadedMaxElementCount = document.getElementById("querycacheLoadedMaxElementCount");
showThrobber(querycacheLoadedMaxElementCount);
let querycacheLoadedElementCountProgressBar = document.getElementById("querycacheLoadedElementCountProgressBar");
let healthchecksServer = document.getElementById("healthchecksServer");
let healthchecksAiProvider = document.getElementById("healthchecksAiProvider");
@@ -144,46 +191,36 @@
searchdomains = result.Searchdomains;
hideThrobber(searchdomainCount);
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
};
});
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 maxElementCount = result.MaxElementCount;
let elementCount = result.ElementCount;
getServerStats().then(result => {
let utilization = result.EmbeddingCacheUtilization;
let embeddingCacheMaxElementCount = result.EmbeddingCacheMaxElementCount;
let embeddingCacheElementCount = result.ElementCount;
let embeddingCount = result.EmbeddingsCount;
let entityCount = result.EntityCount;
let queryCacheUtilization = result.QueryCacheUtilization;
let queryCacheElementCount = result.QueryCacheElementCount;
let queryCacheMaxElementCountAll = result.QueryCacheMaxElementCountAll;
let queryCacheMaxElementCountLoadedSearchdomainsOnly = result.QueryCacheMaxElementCountLoadedSearchdomainsOnly;
hideThrobber(embeddingcacheSize);
embeddingcacheSize.textContent = NumberOfBytesAsHumanReadable(utilization);
hideThrobber(embeddingcacheElementCount);
embeddingcacheElementCount.textContent = `${elementCount.toLocaleString()} / ${maxElementCount.toLocaleString()}`;
embeddingcacheElementCount.textContent = `${embeddingCacheElementCount.toLocaleString()} / ${embeddingCacheMaxElementCount.toLocaleString()}`;
hideThrobber(embeddingcacheEmbeddingCount);
embeddingcacheEmbeddingCount.textContent = embeddingCount;
embeddingcacheElementCountProgressBar.style.width = `${elementCount / maxElementCount * 100}%`;
embeddingcacheElementCountProgressBar.style.width = `${embeddingCacheElementCount / embeddingCacheMaxElementCount * 100}%`;
hideThrobber(searchdomainEntityCount);
searchdomainEntityCount.textContent = entityCount;
hideThrobber(totalQuerycacheUtilization);
totalQuerycacheUtilization.textContent = NumberOfBytesAsHumanReadable(queryCacheUtilization);
hideThrobber(querycacheMaxElementCount);
querycacheCount.textContent = queryCacheElementCount;
hideThrobber(querycacheCount);
querycacheMaxElementCount.textContent = queryCacheMaxElementCountAll.toLocaleString();
querycacheMaxElementCountProgressBar.style.width = `${queryCacheElementCount / queryCacheMaxElementCountAll * 100}%`;
hideThrobber(querycacheLoadedMaxElementCount);
querycacheLoadedMaxElementCount.textContent = queryCacheMaxElementCountLoadedSearchdomainsOnly.toLocaleString();
querycacheLoadedMaxElementCountProgressBar.style.width = `${queryCacheElementCount / queryCacheMaxElementCountLoadedSearchdomainsOnly * 100}%`;
});
getHealthCheckStatusAndApply(healthchecksServer, "/healthz/Database");
getHealthCheckStatusAndApply(healthchecksAiProvider, "/healthz/AIProvider");
@@ -206,8 +243,8 @@
.then(r => r.json());
}
async function getEmbeddingcacheUtilization() {
return await fetch(`/Server/EmbeddingCache/Size`)
async function getServerStats() {
return await fetch(`/Server/Stats`)
.then(r => r.json());
}

View File

@@ -62,11 +62,17 @@
<!-- Settings -->
<div class="row align-items-center mb-3">
<h3>@T["Settings"]</h3>
<div class="col-md-3">
<label class="form-check-label" for="searchdomainConfigQueryCacheSize">@T["Query cache size"]:</label>
<input type="number" class="form-control" id="searchdomainConfigQueryCacheSize" />
</div>
<div class="col-md-6">
<input type="checkbox" class="form-check-input" id="searchdomainConfigCacheReconciliation" />
<label class="form-check-label" for="searchdomainConfigCacheReconciliation">@T["Cache reconciliation"]</label>
</div>
<div class="col-md-2 mt-3 mt-md-0">
</div>
<div class="row align-items-center mb-3">
<div class="col-md-2 mt-md-0">
<button class="btn btn-warning w-100" id="searchdomainConfigUpdate">@T["Update"]</button>
</div>
</div>
@@ -92,7 +98,7 @@
<div class="card section-card mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h3>Recent queries</h3>
<h3>@T["Recent queries"]</h3>
<input
type="text"
class="form-control form-control-sm w-25"
@@ -103,8 +109,8 @@
<table id="queriesTable" class="table table-striped" style="max-height: 60vh; overflow-y: auto; display: block;">
<thead>
<tr>
<th class="visually-hidden">Name</th>
<th class="visually-hidden">Action</th>
<th class="visually-hidden">@T["Name"]</th>
<th class="visually-hidden">@T["Action"]</th>
</tr>
</thead>
<tbody>
@@ -129,8 +135,8 @@
<table id="entitiesTable" class="table table-striped" style="max-height: 60vh; overflow-y: auto; display: block;">
<thead>
<tr>
<th class="visually-hidden">Name</th>
<th class="visually-hidden">Action</th>
<th class="visually-hidden">@T["Name"]</th>
<th class="visually-hidden">@T["Action"]</th>
</tr>
</thead>
<tbody>
@@ -152,8 +158,8 @@
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header bg-info">
<h2 class="modal-title" id="entityDetailsTitle">@T["Entity Details"]</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<h2 class="modal-title text-dark" id="entityDetailsTitle">@T["Entity Details"]</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" style="filter: brightness(0);"></button>
</div>
<div class="modal-body">
@@ -200,8 +206,8 @@
<div class="modal-content">
<div class="modal-header bg-info">
<h2 class="modal-title" id="queryDetailsTitle">@T["Query Details"] - <span id="queryDetailsQueryName"></span></h2>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<h2 class="modal-title text-dark" id="queryDetailsTitle">@T["Query Details"] - <span id="queryDetailsQueryName"></span></h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" style="filter: brightness(0);"></button>
</div>
<div class="modal-body">
@@ -240,8 +246,8 @@
<div class="modal-content">
<div class="modal-header bg-warning">
<h2 class="modal-title" id="queryUpdateTitle">@T["Query Update"] - <span id="queryUpdateQueryName"></span></h2>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<h2 class="modal-title text-dark" id="queryUpdateTitle">@T["Query Update"] - <span id="queryUpdateQueryName"></span></h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" style="filter: brightness(0);"></button>
</div>
<div class="modal-body">
@@ -284,8 +290,8 @@
<div class="modal-content">
<div class="modal-header bg-warning">
<h2 class="modal-title" id="renameSearchdomainTitle">@T["Rename searchdomain"]</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<h2 class="modal-title text-dark" id="renameSearchdomainTitle">@T["Rename searchdomain"]</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" style="filter: brightness(0);"></button>
</div>
<div class="modal-body">
@@ -298,10 +304,10 @@
<div class="modal-footer">
<button type="button" class="btn btn-warning" onclick="renameSearchdomain(getSelectedDomainKey(), document.getElementById('renameSearchdomainNewName').value)" data-bs-dismiss="modal">
Rename
@T["Rename"]
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Close
@T["Close"]
</button>
</div>
</div>
@@ -347,10 +353,20 @@
</div>
<div class="modal-body">
<label for="createSearchdomainName" class="form-label">@T["Searchdomain name"]</label>
<input type="text" class="form-control mb-3" id="createSearchdomainName" placeholder="@T["Searchdomain name"]" />
<input type="checkbox" class="form-check-input" id="createSearchdomainWithCacheReconciliation" />
<label class="form-check-label" for="createSearchdomainWithCacheReconciliation">@T["Enable cache reconciliation"]</label>
<div class="row align-items-center mb-3">
<div class="col-md-12">
<label for="createSearchdomainName" class="form-label">@T["Searchdomain name"]</label>
<input type="text" class="form-control mb-3" id="createSearchdomainName" placeholder="@T["Searchdomain name"]" />
</div>
<div class="col-md-5">
<label class="form-check-label mb-2" for="createSearchdomainQueryCacheSize">@T["Query cache size"]:</label>
<input type="number" class="form-control" id="createSearchdomainQueryCacheSize" />
</div>
<div class="col-md-7">
<input type="checkbox" class="form-check-input" id="createSearchdomainWithCacheReconciliation" />
<label class="form-check-label" for="createSearchdomainWithCacheReconciliation">@T["Enable cache reconciliation"]</label>
</div>
</div>
</div>
<div class="modal-footer">
@@ -476,8 +492,8 @@
<div class="modal-content">
<div class="modal-header bg-warning text">
<h2 class="modal-title" id="updateEntityTitle">@T["Update entity"]</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<h2 class="modal-title text-dark" id="updateEntityTitle">@T["Update entity"]</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" style="filter: brightness(0);"></button>
</div>
<div class="modal-body">
@@ -694,7 +710,8 @@
.addEventListener('click', () => {
const domainKey = getSelectedDomainKey();
const cacheReconciliation = document.getElementById('searchdomainConfigCacheReconciliation').checked;
updateSearchdomainConfig(domainKey, { CacheReconciliation: cacheReconciliation});
const queryCacheSize = document.getElementById('searchdomainConfigQueryCacheSize').value;
updateSearchdomainConfig(domainKey, { CacheReconciliation: cacheReconciliation, QueryCacheSize: queryCacheSize});
});
document
@@ -745,7 +762,7 @@
"datapoints": datapoints
}];
showToast("@T["Creating entity"]", "primary");
fetch(`/Entity`, {
fetch(`/Entities`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
@@ -775,8 +792,9 @@
document.getElementById('createSearchdomainModal')
);
const name = document.getElementById('createSearchdomainName').value;
const queryCacheSize = document.getElementById('createSearchdomainQueryCacheSize').value;
const cacheReconciliation = document.getElementById('createSearchdomainWithCacheReconciliation').checked;
const settings = { CacheReconciliation: cacheReconciliation };
const settings = { CacheReconciliation: cacheReconciliation, QueryCacheSize: queryCacheSize };
// Implement create logic here
fetch(`/Searchdomain?searchdomain=${encodeURIComponent(name)}`, {
method: 'POST',
@@ -869,12 +887,12 @@
var data = [{
"name": name,
"probmethod": probMethod,
"searchdomain": encodeURIComponent(domains[getSelectedDomainKey()]),
"searchdomain": domains[getSelectedDomainKey()],
"attributes": attributes,
"datapoints": datapoints
}];
showToast("@T["Updating entity"]", "primary");
fetch(`/Entity`, {
fetch(`/Entities`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
@@ -1053,7 +1071,10 @@
let searchdomainConfigPromise = getSearchdomainConfig(getSelectedDomainKey());
let configElementCachereconciliation = document.getElementById('searchdomainConfigCacheReconciliation');
let configElementCacheSize = document.getElementById('searchdomainConfigQueryCacheSize');
showThrobber(document.querySelector('#searchdomainConfigQueryCacheSize'), true);
showThrobber(document.querySelector('#searchdomainConfigCacheReconciliation'), true);
let cacheUtilizationPromise = getSearchdomainCacheUtilization(getSelectedDomainKey());
let databaseUtilizationPromise = getSearchdomainDatabaseUtilization(getSelectedDomainKey());
@@ -1095,8 +1116,10 @@
});
searchdomainConfigPromise.then(searchdomainConfig => {
hideThrobber(document.querySelector('#searchdomainConfigCacheReconciliation'), true);
if (searchdomainConfig != null && searchdomainConfig.Settings != null)
{
configElementCacheSize.value = searchdomainConfig.Settings.QueryCacheSize;
configElementCachereconciliation.checked = searchdomainConfig.Settings.CacheReconciliation;
configElementCachereconciliation.disabled = false;
} else {
@@ -1106,10 +1129,11 @@
}
});
cacheUtilizationPromise.then(cacheUtilization => {
if (cacheUtilization != null && cacheUtilization.QueryCacheSizeBytes != null)
hideThrobber(document.querySelector('#searchdomainConfigQueryCacheSize'), true);
if (cacheUtilization != null && cacheUtilization.SizeBytes != null)
{
document.querySelector('#cacheUtilization').innerText =
`${NumberOfBytesAsHumanReadable(cacheUtilization.QueryCacheSizeBytes)}`;
`${NumberOfBytesAsHumanReadable(cacheUtilization.SizeBytes)}`;
} else {
showToast("@T["Unable to fetch searchdomain cache utilization"]", "danger");
console.error('Failed to fetch searchdomain cache utilization');
@@ -1289,14 +1313,30 @@
domainItem.classList.add('list-group-item-danger');
}
function showThrobber(element = null) {
function showThrobber(element = null, direct = false) {
if (element == null) element = document;
element.querySelector('.spinner').classList.remove('d-none');
if (direct) {
let spinner = document.createElement('div');
spinner.classList.add('spinner');
spinner.style.position = "absolute";
spinner.style.marginTop = "0.5rem";
spinner.style.marginLeft = "0.5rem";
element.style.opacity = "0.25";
element.parentElement.insertBefore(spinner, element);
} else {
element.querySelector('.spinner').classList.remove('d-none');
}
}
function hideThrobber(element = null) {
function hideThrobber(element = null, direct = false) {
if (element == null) element = document;
element.querySelector('.spinner').classList.add('d-none');
if (direct) {
element.previousElementSibling.remove()
element.style.opacity = "1";
} else {
element.querySelector('.spinner').classList.add('d-none');
}
}
function showEntityDetails(entity) {

View File

@@ -1,7 +1,10 @@
@using System.Globalization
@using Server.Services
@using System.Net
@inject LocalizationService T
@{
var currentUrl = WebUtility.HtmlEncode(Context.Request.Path);
}
<!DOCTYPE html>
<html lang="@CultureInfo.CurrentUICulture.TwoLetterISOLanguageName">
<head>
@@ -9,13 +12,18 @@
<meta name="description" content="Embeddingsearch server" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - embeddingsearch</title>
@if (!Context.Request.Query.ContainsKey("renderRaw"))
@if (!Context.Request.Query.ContainsKey("renderRaw") && !Context.Request.Query.ContainsKey("noCriticalCSS"))
{
<link rel="preload" href="~/lib/bootstrap/dist/css/bootstrap.min.css" as="style"/>
<link rel="stylesheet" fetchpriority="high"
href="~/lib/bootstrap/dist/css/bootstrap.min.css"
media="print"
onload="this.media='all'">
} else if (Context.Request.Query.ContainsKey("noCriticalCSS"))
{
<link rel="preload" href="~/lib/bootstrap/dist/css/bootstrap.min.css" as="style"/>
<link rel="stylesheet" fetchpriority="high"
href="~/lib/bootstrap/dist/css/bootstrap.min.css">
}
<style>
@Html.Raw(File.ReadAllText(System.IO.Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "css", "site.css")))
@@ -26,7 +34,6 @@
@if (Context.Request.Path.Value is not null)
{
string path = System.IO.Path.Combine("CriticalCSS", Context.Request.Path.Value.Trim('/').Replace("/", ".") + ".css");
Console.WriteLine(path);
if (File.Exists(path))
{
@Html.Raw(File.ReadAllText(path));
@@ -40,9 +47,9 @@
};
</script>
</head>
<body>
<body data-bs-theme="dark">
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">embeddingsearch</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
@@ -54,19 +61,31 @@
@if (User.Identity?.IsAuthenticated == true)
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">@T["Home"]</a>
<a class="nav-link text" asp-area="" asp-controller="Home" asp-action="Index">@T["Home"]</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Searchdomains">@T["Searchdomains"]</a>
<a class="nav-link text" asp-area="" asp-controller="Home" asp-action="Searchdomains">@T["Searchdomains"]</a>
</li>
@if (User.IsInRole("Admin") || User.IsInRole("Swagger"))
{
<li class="nav-item">
<a class="nav-link text" href="/swagger/index.html?ReturnUrl=@(currentUrl)">@T["Swagger"]</a>
</li>
}
@if (User.IsInRole("Admin"))
{
<li class="nav-item">
<a class="nav-link text" href="/elmah?ReturnUrl=@(currentUrl)">@T["Elmah"]</a>
</li>
}
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="Logout">@T["Logout"]</a>
<a class="nav-link text" asp-area="" asp-controller="Account" asp-action="Logout">@T["Logout"]</a>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="Login">@T["Login"]</a>
<a class="nav-link text" asp-area="" asp-controller="Account" asp-action="Login">@T["Login"]</a>
</li>
}
</ul>
@@ -91,3 +110,16 @@
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
<script>
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
function applyTheme(e) {
document.body.setAttribute(
'data-bs-theme',
e.matches ? 'dark' : 'light'
);
}
applyTheme(mediaQuery);
mediaQuery.addEventListener('change', applyTheme);
</script>

View File

@@ -75,3 +75,8 @@ url("/fonts/bootstrap-icons.woff") format("woff");
}
.bi-info-circle-fill::before { content: "\f430"; }
td.btn-group {
display: revert;
min-width: 15rem;
}

View File

@@ -0,0 +1,56 @@
.elmah-return-btn {
position: fixed;
top: 6px;
right: 24px;
z-index: 9999;
display: flex;
align-items: center;
height: 44px;
min-width: 44px;
padding: 0 14px;
background: #85ea2d;
color: black;
border-radius: 999px;
font-weight: 600;
text-decoration: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
overflow: hidden;
white-space: nowrap;
justify-content: center;
text-decoration: none !important;
transition:
top 0.25s ease,
background-color 0.2s ease;
}
/* hidden label */
.elmah-return-btn::before {
content: "Return to Front-end";
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 0;
opacity: 0;
transition:
max-width 0.3s ease,
opacity 0.2s ease;
}
/* expand on hover */
.elmah-return-btn.show-label::before,
.elmah-return-btn:hover::before {
max-width: 220px;
padding: 0.5rem;
opacity: 1;
}
/* hover colors */
.elmah-return-btn.show-label,
.elmah-return-btn:hover {
background: #0b5ed7;
color: white;
}

View File

@@ -0,0 +1,17 @@
document.addEventListener('DOMContentLoaded', async () => {
const url = new URL(window.location.href);
const btn = document.createElement("a");
btn.href = url.searchParams.get('ReturnUrl') ?? "/";
btn.innerText = "⎋";
btn.setAttribute("aria-label", "Return to Front-End");
btn.className = "elmah-return-btn";
document.body.appendChild(btn);
const showLabelBriefly = () => {
btn.classList.add("show-label");
setTimeout(() => btn.classList.remove("show-label"), 2000);
};
setTimeout(showLabelBriefly, 1000);
});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,58 @@
.swagger-return-btn {
position: fixed;
top: 6px;
left: 24px;
z-index: 9999;
display: flex;
align-items: center;
height: 44px;
min-width: 44px;
padding: 0 14px;
background: #85ea2d;
color: black;
border-radius: 999px;
font-weight: 600;
text-decoration: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
overflow: hidden;
white-space: nowrap;
justify-content: center;
transition:
top 0.25s ease,
background-color 0.2s ease;
}
/* hidden label */
.swagger-return-btn::after {
content: "Return to Front-end";
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 0;
opacity: 0;
transition:
max-width 0.3s ease,
opacity 0.2s ease;
}
/* expand on hover */
.swagger-return-btn:hover::after {
max-width: 220px;
padding: 0.5rem;
opacity: 1;
}
/* hover colors */
.swagger-return-btn:hover {
background: #0b5ed7;
color: white;
}
/* scrolled state */
.swagger-return-btn.scrolled {
top: 24px;
}

View File

@@ -0,0 +1,24 @@
document.addEventListener('DOMContentLoaded', async () => {
const url = new URL(window.location.href);
const btn = document.createElement("a");
btn.href = url.searchParams.get('ReturnUrl') ?? "/";
btn.innerText = "⎋";
btn.setAttribute("aria-label", "Return to Front-End");
btn.className = "swagger-return-btn";
document.body.appendChild(btn);
const togglePosition = () => {
if (window.scrollY > 0) {
btn.classList.add("scrolled");
} else {
btn.classList.remove("scrolled");
}
};
// Initial state
togglePosition();
// On scroll
window.addEventListener("scroll", togglePosition, { passive: true });
});

240
src/Shared/LRUCache.cs Normal file
View File

@@ -0,0 +1,240 @@
namespace Shared;
public sealed class EnumerableLruCache<TKey, TValue> where TKey : notnull
{
private sealed record CacheItem(TKey Key, TValue Value);
private readonly Dictionary<TKey, LinkedListNode<CacheItem>> _map;
private readonly LinkedList<CacheItem> _lruList;
private readonly ReaderWriterLockSlim _lock = new();
private int _capacity;
public EnumerableLruCache(int capacity)
{
if (capacity <= 0)
throw new ArgumentOutOfRangeException(nameof(capacity));
_capacity = capacity;
_map = new Dictionary<TKey, LinkedListNode<CacheItem>>(capacity);
_lruList = new LinkedList<CacheItem>();
}
public int Capacity
{
get
{
_lock.EnterReadLock();
try
{
return _capacity;
}
finally
{
_lock.ExitReadLock();
}
}
set
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);
_lock.EnterWriteLock();
try
{
_capacity = value;
TrimIfNeeded();
}
finally
{
_lock.ExitWriteLock();
}
}
}
public int Count
{
get
{
_lock.EnterReadLock();
try
{
return _map.Count;
}
finally
{
_lock.ExitReadLock();
}
}
}
public TValue this[TKey key]
{
get
{
if (!TryGetValue(key, out var value))
throw new KeyNotFoundException();
return value!;
}
set => Set(key, value);
}
public bool TryGetValue(TKey key, out TValue? value)
{
_lock.EnterUpgradeableReadLock();
try
{
if (!_map.TryGetValue(key, out var node))
{
value = default;
return false;
}
value = node.Value.Value;
// LRU aktualisieren
_lock.EnterWriteLock();
try
{
_lruList.Remove(node);
_lruList.AddFirst(node);
}
finally
{
_lock.ExitWriteLock();
}
return true;
}
finally
{
_lock.ExitUpgradeableReadLock();
}
}
public void Set(TKey key, TValue value)
{
_lock.EnterWriteLock();
try
{
if (_map.TryGetValue(key, out var existing))
{
// Update + nach vorne
existing.Value = existing.Value with { Value = value };
_lruList.Remove(existing);
_lruList.AddFirst(existing);
return;
}
var item = new CacheItem(key, value);
var node = new LinkedListNode<CacheItem>(item);
_lruList.AddFirst(node);
_map[key] = node;
TrimIfNeeded();
}
finally
{
_lock.ExitWriteLock();
}
}
public bool Remove(TKey key)
{
_lock.EnterWriteLock();
try
{
if (!_map.TryGetValue(key, out var node))
return false;
_lruList.Remove(node);
_map.Remove(key);
return true;
}
finally
{
_lock.ExitWriteLock();
}
}
public bool ContainsKey(TKey key)
{
_lock.EnterReadLock();
try
{
return _map.ContainsKey(key);
}
finally
{
_lock.ExitReadLock();
}
}
public Dictionary<TKey, TValue> AsDictionary()
{
_lock.EnterReadLock();
try
{
return _map.Values.ToDictionary(
n => n.Value.Key,
n => n.Value.Value
);
}
finally
{
_lock.ExitReadLock();
}
}
public IEnumerable<KeyValuePair<TKey, TValue>> Items()
{
_lock.EnterReadLock();
try
{
foreach (var item in _lruList)
{
yield return new KeyValuePair<TKey, TValue>(item.Key, item.Value);
}
}
finally
{
_lock.ExitReadLock();
}
}
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
List<KeyValuePair<TKey, TValue>> snapshot;
_lock.EnterReadLock();
try
{
snapshot = new List<KeyValuePair<TKey, TValue>>(_map.Count);
foreach (var item in _lruList)
{
snapshot.Add(new KeyValuePair<TKey, TValue>(
item.Key,
item.Value
));
}
}
finally
{
_lock.ExitReadLock();
}
return snapshot.GetEnumerator();
}
private void TrimIfNeeded()
{
while (_map.Count > _capacity)
{
var lruNode = _lruList.Last!;
_lruList.RemoveLast();
_map.Remove(lruNode.Value.Key);
}
}
}

View File

@@ -95,10 +95,12 @@ public struct DateTimedSearchResult(DateTime dateTime, List<ResultItem> results)
}
}
public struct SearchdomainSettings(bool cacheReconciliation = false)
public struct SearchdomainSettings(bool cacheReconciliation = false, int queryCacheSize = 1_000_000)
{
[JsonPropertyName("CacheReconciliation")]
public bool CacheReconciliation { get; set; } = cacheReconciliation;
[JsonPropertyName("QueryCacheSize")]
public int QueryCacheSize { get; set; } = queryCacheSize;
}
public static class MemorySizes

View File

@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using Shared;
namespace Shared.Models;
@@ -25,7 +26,7 @@ public class SearchdomainDeleteResults : SuccesMessageBaseModel
public required int DeletedEntities { get; set; }
}
public class SearchdomainSearchesResults : SuccesMessageBaseModel
public class SearchdomainQueriesResults : SuccesMessageBaseModel
{
[JsonPropertyName("Searches")]
public required Dictionary<string, DateTimedSearchResult> Searches { get; set; }
@@ -41,10 +42,14 @@ public class SearchdomainSettingsResults : SuccesMessageBaseModel
public required SearchdomainSettings? Settings { get; set; }
}
public class SearchdomainSearchCacheSizeResults : SuccesMessageBaseModel
public class SearchdomainQueryCacheSizeResults : SuccesMessageBaseModel
{
[JsonPropertyName("QueryCacheSizeBytes")]
public required long? QueryCacheSizeBytes { get; set; }
[JsonPropertyName("ElementCount")]
public required int? ElementCount { get; set; }
[JsonPropertyName("ElementMaxCount")]
public required int? ElementMaxCount { get; set; }
[JsonPropertyName("SizeBytes")]
public required long? SizeBytes { get; set; }
}
public class SearchdomainInvalidateCacheResults : SuccesMessageBaseModel {}

View File

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