4 Commits

8 changed files with 344 additions and 42 deletions

View File

@@ -121,13 +121,13 @@ public class Client
}), new StringContent(settings, Encoding.UTF8, "application/json")); }), 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() Dictionary<string, string> parameters = new()
{ {
{"searchdomain", searchdomain} {"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) 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); 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() Dictionary<string, string> parameters = new()
{ {
{"searchdomain", searchdomain} {"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) public async Task<SearchdomainInvalidateCacheResults> SearchdomainClearQueryCache(string searchdomain)
@@ -222,9 +222,9 @@ public class Client
return await FetchUrlAndProcessJson<ServerGetModelsResult>(HttpMethod.Get, GetUrl($"{baseUri}/Server", "Models", [])); 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) 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 Microsoft.AspNetCore.Mvc;
using Server.Exceptions; using Server.Exceptions;
using Server.Helper; using Server.Helper;
using Shared;
using Shared.Models; using Shared.Models;
namespace Server.Controllers; namespace Server.Controllers;
@@ -54,6 +55,10 @@ public class SearchdomainController : ControllerBase
{ {
try try
{ {
if (settings.QueryCacheSize <= 0)
{
settings.QueryCacheSize = 1_000_000; // TODO get rid of this magic number
}
int id = _domainManager.CreateSearchdomain(searchdomain, settings); int id = _domainManager.CreateSearchdomain(searchdomain, settings);
return Ok(new SearchdomainCreateResults(){Id = id, Success = true}); return Ok(new SearchdomainCreateResults(){Id = id, Success = true});
} catch (Exception) } catch (Exception)
@@ -134,13 +139,13 @@ public class SearchdomainController : ControllerBase
/// </summary> /// </summary>
/// <param name="searchdomain">Name of the searchdomain</param> /// <param name="searchdomain">Name of the searchdomain</param>
[HttpGet("Queries")] [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); (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; Dictionary<string, DateTimedSearchResult> searchCache = searchdomain_.queryCache.AsDictionary();
return Ok(new SearchdomainSearchesResults() { Searches = searchCache, Success = true }); return Ok(new SearchdomainQueriesResults() { Searches = searchCache, Success = true });
} }
/// <summary> /// <summary>
@@ -175,7 +180,7 @@ public class SearchdomainController : ControllerBase
{ {
(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; EnumerableLruCache<string, DateTimedSearchResult> searchCache = searchdomain_.queryCache;
bool containsKey = searchCache.ContainsKey(query); bool containsKey = searchCache.ContainsKey(query);
if (containsKey) if (containsKey)
{ {
@@ -196,7 +201,7 @@ public class SearchdomainController : ControllerBase
{ {
(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; EnumerableLruCache<string, DateTimedSearchResult> searchCache = searchdomain_.queryCache;
bool containsKey = searchCache.ContainsKey(query); bool containsKey = searchCache.ContainsKey(query);
if (containsKey) if (containsKey)
{ {
@@ -237,6 +242,7 @@ public class SearchdomainController : ControllerBase
}; };
searchdomain_.helper.ExecuteSQLNonQuery("UPDATE searchdomain set settings = @settings WHERE id = @id", parameters); searchdomain_.helper.ExecuteSQLNonQuery("UPDATE searchdomain set settings = @settings WHERE id = @id", parameters);
searchdomain_.settings = request; searchdomain_.settings = request;
searchdomain_.queryCache.Capacity = request.QueryCacheSize;
return Ok(new SearchdomainUpdateResults(){Success = true}); return Ok(new SearchdomainUpdateResults(){Success = true});
} }
@@ -245,15 +251,17 @@ public class SearchdomainController : ControllerBase
/// </summary> /// </summary>
/// <param name="searchdomain">Name of the searchdomain</param> /// <param name="searchdomain">Name of the searchdomain</param>
[HttpGet("QueryCache/Size")] [HttpGet("QueryCache/Size")]
public ActionResult<SearchdomainSearchCacheSizeResults> GetSearchCacheSize([Required]string searchdomain) public ActionResult<SearchdomainQueryCacheSizeResults> GetQueryCacheSize([Required]string searchdomain)
{ {
if (!SearchdomainHelper.IsSearchdomainLoaded(_domainManager, searchdomain)) if (!SearchdomainHelper.IsSearchdomainLoaded(_domainManager, searchdomain))
{ {
return Ok(new SearchdomainSearchCacheSizeResults() { QueryCacheSizeBytes = 0, Success = true }); return Ok(new SearchdomainQueryCacheSizeResults() { SizeBytes = 0, ElementCount = 0, ElementMaxCount = 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});
return Ok(new SearchdomainSearchCacheSizeResults() { QueryCacheSizeBytes = searchdomain_.GetSearchCacheSize(), 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> /// <summary>

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", []); helper.ExecuteSQLNonQuery("ALTER TABLE datapoint ADD COLUMN similaritymethod VARCHAR(512) NULL DEFAULT 'Cosine' AFTER probmethod_embedding", []);
return 4; 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

@@ -4,6 +4,7 @@ using System.Text.Json;
using ElmahCore.Mvc.Logger; using ElmahCore.Mvc.Logger;
using MySql.Data.MySqlClient; using MySql.Data.MySqlClient;
using Server.Helper; using Server.Helper;
using Shared;
using Shared.Models; using Shared.Models;
using AdaptiveExpressions; using AdaptiveExpressions;
@@ -17,7 +18,7 @@ public class Searchdomain
public string searchdomain; public string searchdomain;
public int id; public int id;
public SearchdomainSettings settings; 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<Entity> entityCache;
public List<string> modelsInUse; public List<string> modelsInUse;
public LRUCache<string, Dictionary<string, float[]>> embeddingCache; public LRUCache<string, Dictionary<string, float[]>> embeddingCache;
@@ -33,12 +34,12 @@ public class Searchdomain
this.aIProvider = aIProvider; this.aIProvider = aIProvider;
this.embeddingCache = embeddingCache; this.embeddingCache = embeddingCache;
this._logger = logger; this._logger = logger;
searchCache = [];
entityCache = []; entityCache = [];
connection = new MySqlConnection(connectionString); connection = new MySqlConnection(connectionString);
connection.Open(); connection.Open();
helper = new SQLHelper(connection, connectionString); helper = new SQLHelper(connection, connectionString);
settings = GetSettings(); settings = GetSettings();
queryCache = new(settings.QueryCacheSize);
modelsInUse = []; // To make the compiler shut up - it is set in UpdateSearchDomain() don't worry // yeah, about that... modelsInUse = []; // To make the compiler shut up - it is set in UpdateSearchDomain() don't worry // yeah, about that...
if (!runEmpty) if (!runEmpty)
{ {
@@ -163,7 +164,7 @@ public class Searchdomain
public List<(float, string)> Search(string query, int? topN = null) 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); cachedResult.AccessDateTimes.Add(DateTime.Now);
return [.. cachedResult.Results.Select(r => (r.Score, r.Name))]; return [.. cachedResult.Results.Select(r => (r.Score, r.Name))];
@@ -187,7 +188,7 @@ public class Searchdomain
[.. sortedResults.Select(r => [.. sortedResults.Select(r =>
new ResultItem(r.Item1, r.Item2 ))] new ResultItem(r.Item1, r.Item2 ))]
); );
searchCache[query] = new DateTimedSearchResult(DateTime.Now, searchResult); queryCache.Set(query, new DateTimedSearchResult(DateTime.Now, searchResult));
return results; return results;
} }
@@ -292,7 +293,7 @@ public class Searchdomain
{ {
if (settings.CacheReconciliation) if (settings.CacheReconciliation)
{ {
foreach (KeyValuePair<string, DateTimedSearchResult> element in searchCache) foreach (var element in queryCache)
{ {
string query = element.Key; string query = element.Key;
DateTimedSearchResult searchResult = element.Value; DateTimedSearchResult searchResult = element.Value;
@@ -322,7 +323,7 @@ public class Searchdomain
{ {
if (settings.CacheReconciliation) if (settings.CacheReconciliation)
{ {
foreach (KeyValuePair<string, DateTimedSearchResult> element in searchCache) foreach (KeyValuePair<string, DateTimedSearchResult> element in queryCache)
{ {
string query = element.Key; string query = element.Key;
DateTimedSearchResult searchResult = element.Value; DateTimedSearchResult searchResult = element.Value;
@@ -337,13 +338,13 @@ public class Searchdomain
public void InvalidateSearchCache() public void InvalidateSearchCache()
{ {
searchCache = []; queryCache = new(settings.QueryCacheSize);
} }
public long GetSearchCacheSize() public long GetSearchCacheSize()
{ {
long sizeInBytes = 0; long sizeInBytes = 0;
foreach (var entry in searchCache) foreach (var entry in queryCache)
{ {
sizeInBytes += sizeof(int); // string length prefix sizeInBytes += sizeof(int); // string length prefix
sizeInBytes += entry.Key.Length * sizeof(char); // string characters sizeInBytes += entry.Key.Length * sizeof(char); // string characters

View File

@@ -62,11 +62,17 @@
<!-- Settings --> <!-- Settings -->
<div class="row align-items-center mb-3"> <div class="row align-items-center mb-3">
<h3>@T["Settings"]</h3> <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"> <div class="col-md-6">
<input type="checkbox" class="form-check-input" id="searchdomainConfigCacheReconciliation" /> <input type="checkbox" class="form-check-input" id="searchdomainConfigCacheReconciliation" />
<label class="form-check-label" for="searchdomainConfigCacheReconciliation">@T["Cache reconciliation"]</label> <label class="form-check-label" for="searchdomainConfigCacheReconciliation">@T["Cache reconciliation"]</label>
</div> </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> <button class="btn btn-warning w-100" id="searchdomainConfigUpdate">@T["Update"]</button>
</div> </div>
</div> </div>
@@ -347,11 +353,21 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="row align-items-center mb-3">
<div class="col-md-12">
<label for="createSearchdomainName" class="form-label">@T["Searchdomain name"]</label> <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="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" /> <input type="checkbox" class="form-check-input" id="createSearchdomainWithCacheReconciliation" />
<label class="form-check-label" for="createSearchdomainWithCacheReconciliation">@T["Enable cache reconciliation"]</label> <label class="form-check-label" for="createSearchdomainWithCacheReconciliation">@T["Enable cache reconciliation"]</label>
</div> </div>
</div>
</div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" id="searchdomainConfirmCreate" class="btn btn-primary" data-bs-dismiss="modal"> <button type="button" id="searchdomainConfirmCreate" class="btn btn-primary" data-bs-dismiss="modal">
@@ -694,7 +710,8 @@
.addEventListener('click', () => { .addEventListener('click', () => {
const domainKey = getSelectedDomainKey(); const domainKey = getSelectedDomainKey();
const cacheReconciliation = document.getElementById('searchdomainConfigCacheReconciliation').checked; const cacheReconciliation = document.getElementById('searchdomainConfigCacheReconciliation').checked;
updateSearchdomainConfig(domainKey, { CacheReconciliation: cacheReconciliation}); const queryCacheSize = document.getElementById('searchdomainConfigQueryCacheSize').value;
updateSearchdomainConfig(domainKey, { CacheReconciliation: cacheReconciliation, QueryCacheSize: queryCacheSize});
}); });
document document
@@ -775,8 +792,9 @@
document.getElementById('createSearchdomainModal') document.getElementById('createSearchdomainModal')
); );
const name = document.getElementById('createSearchdomainName').value; const name = document.getElementById('createSearchdomainName').value;
const queryCacheSize = document.getElementById('createSearchdomainQueryCacheSize').value;
const cacheReconciliation = document.getElementById('createSearchdomainWithCacheReconciliation').checked; const cacheReconciliation = document.getElementById('createSearchdomainWithCacheReconciliation').checked;
const settings = { CacheReconciliation: cacheReconciliation }; const settings = { CacheReconciliation: cacheReconciliation, QueryCacheSize: queryCacheSize };
// Implement create logic here // Implement create logic here
fetch(`/Searchdomain?searchdomain=${encodeURIComponent(name)}`, { fetch(`/Searchdomain?searchdomain=${encodeURIComponent(name)}`, {
method: 'POST', method: 'POST',
@@ -869,12 +887,12 @@
var data = [{ var data = [{
"name": name, "name": name,
"probmethod": probMethod, "probmethod": probMethod,
"searchdomain": encodeURIComponent(domains[getSelectedDomainKey()]), "searchdomain": domains[getSelectedDomainKey()],
"attributes": attributes, "attributes": attributes,
"datapoints": datapoints "datapoints": datapoints
}]; }];
showToast("@T["Updating entity"]", "primary"); showToast("@T["Updating entity"]", "primary");
fetch(`/Entity`, { fetch(`/Entities`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -1053,7 +1071,10 @@
let searchdomainConfigPromise = getSearchdomainConfig(getSelectedDomainKey()); let searchdomainConfigPromise = getSearchdomainConfig(getSelectedDomainKey());
let configElementCachereconciliation = document.getElementById('searchdomainConfigCacheReconciliation'); 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 cacheUtilizationPromise = getSearchdomainCacheUtilization(getSelectedDomainKey());
let databaseUtilizationPromise = getSearchdomainDatabaseUtilization(getSelectedDomainKey()); let databaseUtilizationPromise = getSearchdomainDatabaseUtilization(getSelectedDomainKey());
@@ -1095,8 +1116,10 @@
}); });
searchdomainConfigPromise.then(searchdomainConfig => { searchdomainConfigPromise.then(searchdomainConfig => {
hideThrobber(document.querySelector('#searchdomainConfigCacheReconciliation'), true);
if (searchdomainConfig != null && searchdomainConfig.Settings != null) if (searchdomainConfig != null && searchdomainConfig.Settings != null)
{ {
configElementCacheSize.value = searchdomainConfig.Settings.QueryCacheSize;
configElementCachereconciliation.checked = searchdomainConfig.Settings.CacheReconciliation; configElementCachereconciliation.checked = searchdomainConfig.Settings.CacheReconciliation;
configElementCachereconciliation.disabled = false; configElementCachereconciliation.disabled = false;
} else { } else {
@@ -1106,10 +1129,11 @@
} }
}); });
cacheUtilizationPromise.then(cacheUtilization => { cacheUtilizationPromise.then(cacheUtilization => {
if (cacheUtilization != null && cacheUtilization.QueryCacheSizeBytes != null) hideThrobber(document.querySelector('#searchdomainConfigQueryCacheSize'), true);
if (cacheUtilization != null && cacheUtilization.SizeBytes != null)
{ {
document.querySelector('#cacheUtilization').innerText = document.querySelector('#cacheUtilization').innerText =
`${NumberOfBytesAsHumanReadable(cacheUtilization.QueryCacheSizeBytes)}`; `${NumberOfBytesAsHumanReadable(cacheUtilization.SizeBytes)}`;
} else { } else {
showToast("@T["Unable to fetch searchdomain cache utilization"]", "danger"); showToast("@T["Unable to fetch searchdomain cache utilization"]", "danger");
console.error('Failed to fetch searchdomain cache utilization'); console.error('Failed to fetch searchdomain cache utilization');
@@ -1289,15 +1313,31 @@
domainItem.classList.add('list-group-item-danger'); domainItem.classList.add('list-group-item-danger');
} }
function showThrobber(element = null) { function showThrobber(element = null, direct = false) {
if (element == null) element = document; if (element == null) element = document;
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'); element.querySelector('.spinner').classList.remove('d-none');
} }
function hideThrobber(element = null) { }
function hideThrobber(element = null, direct = false) {
if (element == null) element = document; if (element == null) element = document;
if (direct) {
element.previousElementSibling.remove()
element.style.opacity = "1";
} else {
element.querySelector('.spinner').classList.add('d-none'); element.querySelector('.spinner').classList.add('d-none');
} }
}
function showEntityDetails(entity) { function showEntityDetails(entity) {
// Title // Title

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")] [JsonPropertyName("CacheReconciliation")]
public bool CacheReconciliation { get; set; } = cacheReconciliation; public bool CacheReconciliation { get; set; } = cacheReconciliation;
[JsonPropertyName("QueryCacheSize")]
public int QueryCacheSize { get; set; } = queryCacheSize;
} }
public static class MemorySizes public static class MemorySizes

View File

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