Compare commits
46 Commits
68-returnu
...
6d39540e8d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d39540e8d | ||
| 328615be97 | |||
|
|
20cbbfd06c | ||
| cfeefa385a | |||
| 49ecb06fb0 | |||
|
|
a15548ea77 | ||
| e2cfe56b49 | |||
|
|
9c306a0917 | ||
| 5f05aac909 | |||
|
|
76c9913485 | ||
| 4f257a745b | |||
|
|
59945cb523 | ||
| c13214c4e9 | |||
|
|
6e9e795a16 | ||
| 337782661e | |||
|
|
b6692770c1 | ||
| 141a567927 | |||
|
|
ba41c1cd82 | ||
| b6b812f458 | |||
|
|
9d5f53c5f4 | ||
| a9a5ee4cb6 | |||
|
|
17cc8f41d5 | ||
| a01985d1b8 | |||
|
|
4c1f0305fc | ||
| e49a7c83ba | |||
| e83ce61877 | |||
|
|
c09514c657 | ||
| 3dfcaa19e6 | |||
| 88d1b27394 | |||
| 027a9244ad | |||
| 063c81e8dc | |||
|
|
ad84efb611 | ||
| ecaa640ec0 | |||
|
|
37f1b285d8 | ||
| 71b273f5d7 | |||
|
|
1a823bb1e7 | ||
| aa4fc03c3d | |||
|
|
09832d1c0b | ||
| 68630fdbef | |||
|
|
c9907da846 | ||
| cddd305d26 | |||
| 6f4ffbcaa6 | |||
|
|
3e433c3cbe | ||
| 8cbc77eb1d | |||
|
|
977a8f1637 | ||
| 65ed78462d |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version / Commit ID [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -18,5 +18,6 @@ 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
|
||||
*.db*
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ElmahCore" Version="2.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.14" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.14.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.2" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.3" />
|
||||
<PackageReference Include="Python" Version="3.13.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.0" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.2" />
|
||||
<PackageReference Include="Python" Version="3.14.2" />
|
||||
<PackageReference Include="Pythonnet" Version="3.0.5" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -31,7 +31,12 @@ public class AIProvider
|
||||
}
|
||||
}
|
||||
|
||||
public float[] GenerateEmbeddings(string modelUri, string[] input)
|
||||
public float[] GenerateEmbeddings(string modelUri, string input)
|
||||
{
|
||||
return [.. GenerateEmbeddings(modelUri, [input]).First()];
|
||||
}
|
||||
|
||||
public IEnumerable<float[]> GenerateEmbeddings(string modelUri, string[] input)
|
||||
{
|
||||
Uri uri = new(modelUri);
|
||||
string provider = uri.Scheme;
|
||||
@@ -103,13 +108,13 @@ public class AIProvider
|
||||
try
|
||||
{
|
||||
JObject responseContentJson = JObject.Parse(responseContent);
|
||||
JToken? responseContentTokens = responseContentJson.SelectToken(embeddingsJsonPath);
|
||||
List<JToken>? responseContentTokens = [.. responseContentJson.SelectTokens(embeddingsJsonPath)];
|
||||
if (responseContentTokens is null)
|
||||
{
|
||||
_logger.LogError("Unable to select tokens using JSONPath {embeddingsJsonPath} for string: {responseContent}.", [embeddingsJsonPath, responseContent]);
|
||||
throw new JSONPathSelectionException(embeddingsJsonPath, responseContent);
|
||||
}
|
||||
return [.. responseContentTokens.Values<float>()];
|
||||
return [.. responseContentTokens.Select(token => token.ToObject<float[]>() ?? throw new Exception("Unable to cast embeddings response to float[]"))];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
namespace Server.Controllers;
|
||||
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using AdaptiveExpressions;
|
||||
using ElmahCore;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Server.Exceptions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Server.Helper;
|
||||
using Server.Models;
|
||||
using Shared;
|
||||
using Shared.Models;
|
||||
|
||||
[ApiController]
|
||||
@@ -17,13 +16,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 +48,74 @@ 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;
|
||||
EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache = _searchdomainManager.embeddingCache;
|
||||
|
||||
// estimate size
|
||||
size += EstimateEntrySize(key, entry);
|
||||
elementCount++;
|
||||
embeddingsCount += entry.Keys.Count;
|
||||
foreach (KeyValuePair<string, Dictionary<string, float[]>> kv in embeddingCache)
|
||||
{
|
||||
string key = kv.Key;
|
||||
Dictionary<string, float[]> entry = kv.Value;
|
||||
size += EstimateEntrySize(key, entry);
|
||||
elementCount++;
|
||||
embeddingsCount += entry.Keys.Count;
|
||||
}
|
||||
var sqlHelper = DatabaseHelper.GetSQLHelper(_options.Value);
|
||||
var databaseTotalSize = DatabaseHelper.GetTotalDatabaseSize(sqlHelper);
|
||||
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;
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
long ramTotalSize = GC.GetTotalMemory(false);
|
||||
|
||||
return new ServerGetStatsResult() {
|
||||
Success = true,
|
||||
EntityCount = entityCount,
|
||||
QueryCacheUtilization = queryCacheUtilization,
|
||||
QueryCacheElementCount = queryCacheElementCount,
|
||||
QueryCacheMaxElementCountAll = queryCacheMaxElementCountAll,
|
||||
QueryCacheMaxElementCountLoadedSearchdomainsOnly = queryCacheMaxElementCountLoadedSearchdomainsOnly,
|
||||
EmbeddingCacheUtilization = size,
|
||||
EmbeddingCacheMaxElementCount = _searchdomainManager.EmbeddingCacheMaxCount,
|
||||
EmbeddingCacheElementCount = elementCount,
|
||||
EmbeddingsCount = embeddingsCount,
|
||||
DatabaseTotalSize = databaseTotalSize,
|
||||
RamTotalSize = ramTotalSize
|
||||
};
|
||||
} 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
@@ -1,6 +1,7 @@
|
||||
using AdaptiveExpressions;
|
||||
using OllamaSharp;
|
||||
using OllamaSharp.Models;
|
||||
using Shared;
|
||||
|
||||
namespace Server;
|
||||
|
||||
@@ -26,36 +27,99 @@ public class Datapoint
|
||||
return probMethod.method(probabilities);
|
||||
}
|
||||
|
||||
public static Dictionary<string, float[]> GenerateEmbeddings(string content, List<string> models, AIProvider aIProvider)
|
||||
public static Dictionary<string, float[]> GetEmbeddings(string content, List<string> models, AIProvider aIProvider, EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache)
|
||||
{
|
||||
return GenerateEmbeddings(content, models, aIProvider, new());
|
||||
Dictionary<string, float[]> embeddings = [];
|
||||
bool embeddingCacheHasContent = embeddingCache.TryGetValue(content, out var embeddingCacheForContent);
|
||||
if (!embeddingCacheHasContent || embeddingCacheForContent is null)
|
||||
{
|
||||
models.ForEach(model =>
|
||||
embeddings[model] = GenerateEmbeddings(content, model, aIProvider, embeddingCache)
|
||||
);
|
||||
return embeddings;
|
||||
}
|
||||
models.ForEach(model =>
|
||||
{
|
||||
bool embeddingCacheHasModel = embeddingCacheForContent.TryGetValue(model, out float[]? embeddingCacheForModel);
|
||||
if (embeddingCacheHasModel && embeddingCacheForModel is not null)
|
||||
{
|
||||
embeddings[model] = embeddingCacheForModel;
|
||||
} else
|
||||
{
|
||||
embeddings[model] = GenerateEmbeddings(content, model, aIProvider, embeddingCache);
|
||||
}
|
||||
});
|
||||
return embeddings;
|
||||
}
|
||||
|
||||
public static Dictionary<string, float[]> GenerateEmbeddings(string content, List<string> models, AIProvider aIProvider, LRUCache<string, Dictionary<string, float[]>> embeddingCache)
|
||||
public static Dictionary<string, Dictionary<string, float[]>> GetEmbeddings(string[] content, List<string> models, AIProvider aIProvider, EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache)
|
||||
{
|
||||
Dictionary<string, float[]> retVal = [];
|
||||
Dictionary<string, Dictionary<string, float[]>> embeddings = [];
|
||||
foreach (string model in models)
|
||||
{
|
||||
bool embeddingCacheHasModel = embeddingCache.TryGet(model, out var embeddingCacheForModel);
|
||||
if (embeddingCacheHasModel && embeddingCacheForModel.ContainsKey(content))
|
||||
List<string> toBeGenerated = [];
|
||||
embeddings[model] = [];
|
||||
foreach (string value in content)
|
||||
{
|
||||
retVal[model] = embeddingCacheForModel[content];
|
||||
continue;
|
||||
bool generateThisEntry = true;
|
||||
bool embeddingCacheHasContent = embeddingCache.TryGetValue(value, out var embeddingCacheForContent);
|
||||
if (embeddingCacheHasContent && embeddingCacheForContent is not null)
|
||||
{
|
||||
bool embeddingCacheHasModel = embeddingCacheForContent.TryGetValue(model, out float[]? embedding);
|
||||
if (embeddingCacheHasModel && embedding is not null)
|
||||
{
|
||||
embeddings[model][value] = embedding;
|
||||
generateThisEntry = false;
|
||||
}
|
||||
}
|
||||
if (generateThisEntry)
|
||||
{
|
||||
if (!toBeGenerated.Contains(value))
|
||||
{
|
||||
toBeGenerated.Add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
var response = aIProvider.GenerateEmbeddings(model, [content]);
|
||||
if (response is not null)
|
||||
IEnumerable<float[]> generatedEmbeddings = GenerateEmbeddings([.. toBeGenerated], model, aIProvider, embeddingCache);
|
||||
if (generatedEmbeddings.Count() != toBeGenerated.Count)
|
||||
{
|
||||
retVal[model] = response;
|
||||
if (!embeddingCacheHasModel)
|
||||
{
|
||||
embeddingCacheForModel = [];
|
||||
}
|
||||
if (!embeddingCacheForModel.ContainsKey(content))
|
||||
{
|
||||
embeddingCacheForModel[content] = response;
|
||||
}
|
||||
throw new Exception("Requested embeddings count and generated embeddings count mismatched!");
|
||||
}
|
||||
for (int i = 0; i < toBeGenerated.Count; i++)
|
||||
{
|
||||
embeddings[model][toBeGenerated.ElementAt(i)] = generatedEmbeddings.ElementAt(i);
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
return embeddings;
|
||||
}
|
||||
|
||||
public static IEnumerable<float[]> GenerateEmbeddings(string[] content, string model, AIProvider aIProvider, EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache)
|
||||
{
|
||||
IEnumerable<float[]> embeddings = aIProvider.GenerateEmbeddings(model, content);
|
||||
if (embeddings.Count() != content.Length)
|
||||
{
|
||||
throw new Exception("Resulting embeddings count does not match up with request count");
|
||||
}
|
||||
for (int i = 0; i < content.Length; i++)
|
||||
{
|
||||
if (!embeddingCache.ContainsKey(content[i]))
|
||||
{
|
||||
embeddingCache[content[i]] = [];
|
||||
}
|
||||
embeddingCache[content[i]][model] = embeddings.ElementAt(i);
|
||||
}
|
||||
return embeddings;
|
||||
}
|
||||
|
||||
|
||||
public static float[] GenerateEmbeddings(string content, string model, AIProvider aIProvider, EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache)
|
||||
{
|
||||
float[] embeddings = aIProvider.GenerateEmbeddings(model, content);
|
||||
if (!embeddingCache.ContainsKey(content))
|
||||
{
|
||||
embeddingCache[content] = [];
|
||||
}
|
||||
embeddingCache[content][model] = embeddings;
|
||||
return embeddings;
|
||||
}
|
||||
}
|
||||
242
src/Server/Helper/CacheHelper.cs
Normal file
242
src/Server/Helper/CacheHelper.cs
Normal file
@@ -0,0 +1,242 @@
|
||||
using System.Configuration;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OllamaSharp.Models;
|
||||
using Server.Models;
|
||||
using Shared;
|
||||
|
||||
namespace Server.Helper;
|
||||
|
||||
public static class CacheHelper
|
||||
{
|
||||
public static EnumerableLruCache<string, Dictionary<string, float[]>> GetEmbeddingStore(EmbeddingSearchOptions options)
|
||||
{
|
||||
SQLiteHelper helper = new(options);
|
||||
EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache = new((int)(options.Cache.StoreTopN ?? options.Cache.CacheTopN));
|
||||
helper.ExecuteQuery(
|
||||
"SELECT cache_key, model_key, embedding, idx FROM embedding_cache ORDER BY idx ASC", [], r =>
|
||||
{
|
||||
int embeddingOrdinal = r.GetOrdinal("embedding");
|
||||
int length = (int)r.GetBytes(embeddingOrdinal, 0, null, 0, 0);
|
||||
byte[] buffer = new byte[length];
|
||||
r.GetBytes(embeddingOrdinal, 0, buffer, 0, length);
|
||||
var cache_key = r.GetString(r.GetOrdinal("cache_key"));
|
||||
var model_key = r.GetString(r.GetOrdinal("model_key"));
|
||||
var embedding = SearchdomainHelper.FloatArrayFromBytes(buffer);
|
||||
var index = r.GetInt32(r.GetOrdinal("idx"));
|
||||
if (cache_key is null || model_key is null || embedding is null)
|
||||
{
|
||||
throw new Exception("Unable to get the embedding store due to a returned element being null");
|
||||
}
|
||||
if (!embeddingCache.TryGetValue(cache_key, out Dictionary<string, float[]>? keyElement) || keyElement is null)
|
||||
{
|
||||
keyElement = [];
|
||||
embeddingCache[cache_key] = keyElement;
|
||||
}
|
||||
keyElement[model_key] = embedding;
|
||||
return 0;
|
||||
}
|
||||
);
|
||||
embeddingCache.Capacity = (int)options.Cache.CacheTopN;
|
||||
return embeddingCache;
|
||||
}
|
||||
|
||||
public static async Task UpdateEmbeddingStore(EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache, EmbeddingSearchOptions options)
|
||||
{
|
||||
if (options.Cache.StoreTopN is not null)
|
||||
{
|
||||
embeddingCache.Capacity = (int)options.Cache.StoreTopN;
|
||||
}
|
||||
SQLiteHelper helper = new(options);
|
||||
EnumerableLruCache<string, Dictionary<string, float[]>> embeddingStore = GetEmbeddingStore(options);
|
||||
|
||||
|
||||
var embeddingCacheMappings = GetCacheMappings(embeddingCache);
|
||||
var embeddingCacheIndexMap = embeddingCacheMappings.positionToEntry;
|
||||
var embeddingCacheObjectMap = embeddingCacheMappings.entryToPosition;
|
||||
|
||||
var embeddingStoreMappings = GetCacheMappings(embeddingStore);
|
||||
var embeddingStoreIndexMap = embeddingStoreMappings.positionToEntry;
|
||||
var embeddingStoreObjectMap = embeddingStoreMappings.entryToPosition;
|
||||
|
||||
List<int> deletedEntries = [];
|
||||
|
||||
foreach (KeyValuePair<int, KeyValuePair<string, Dictionary<string, float[]>>> kv in embeddingStoreIndexMap)
|
||||
{
|
||||
int storeEntryIndex = kv.Key;
|
||||
string storeEntryString = kv.Value.Key;
|
||||
bool cacheEntryExists = embeddingCacheObjectMap.TryGetValue(storeEntryString, out int cacheEntryIndex);
|
||||
|
||||
if (!cacheEntryExists) // Deleted
|
||||
{
|
||||
deletedEntries.Add(storeEntryIndex);
|
||||
}
|
||||
}
|
||||
Task removeEntriesFromStoreTask = RemoveEntriesFromStore(helper, deletedEntries);
|
||||
|
||||
|
||||
List<(int Index, KeyValuePair<string, Dictionary<string, float[]>> Entry)> createdEntries = [];
|
||||
List<(int Index, int NewIndex)> changedEntries = [];
|
||||
List<(int Index, string Model, string Key, float[] Embedding)> AddedModels = [];
|
||||
List<(int Index, string Model)> RemovedModels = [];
|
||||
foreach (KeyValuePair<int, KeyValuePair<string, Dictionary<string, float[]>>> kv in embeddingCacheIndexMap)
|
||||
{
|
||||
int cacheEntryIndex = kv.Key;
|
||||
string cacheEntryString = kv.Value.Key;
|
||||
|
||||
bool storeEntryExists = embeddingStoreObjectMap.TryGetValue(cacheEntryString, out int storeEntryIndex);
|
||||
|
||||
if (!storeEntryExists) // Created
|
||||
{
|
||||
createdEntries.Add((
|
||||
Index: cacheEntryIndex,
|
||||
Entry: kv.Value
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if (cacheEntryIndex != storeEntryIndex) // Changed
|
||||
{
|
||||
changedEntries.Add((
|
||||
Index: cacheEntryIndex,
|
||||
NewIndex: storeEntryIndex
|
||||
));
|
||||
}
|
||||
|
||||
// Check for new/removed models
|
||||
var storeModels = embeddingStoreIndexMap[storeEntryIndex].Value;
|
||||
var cacheModels = kv.Value.Value;
|
||||
// New models
|
||||
foreach (var model in storeModels.Keys.Except(cacheModels.Keys))
|
||||
{
|
||||
RemovedModels.Add((
|
||||
Index: cacheEntryIndex,
|
||||
Model: model
|
||||
));
|
||||
}
|
||||
// Removed models
|
||||
foreach (var model in cacheModels.Keys.Except(storeModels.Keys))
|
||||
{
|
||||
AddedModels.Add((
|
||||
Index: cacheEntryIndex,
|
||||
Model: model,
|
||||
Key: cacheEntryString,
|
||||
Embedding: cacheModels[model]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
var taskSet = new List<Task>
|
||||
{
|
||||
removeEntriesFromStoreTask,
|
||||
CreateEntriesInStore(helper, createdEntries),
|
||||
UpdateEntryIndicesInStore(helper, changedEntries),
|
||||
AddModelsToIndices(helper, AddedModels),
|
||||
RemoveModelsFromIndices(helper, RemovedModels)
|
||||
};
|
||||
|
||||
await Task.WhenAll(taskSet);
|
||||
}
|
||||
|
||||
private static async Task CreateEntriesInStore(
|
||||
SQLiteHelper helper,
|
||||
List<(int Index, KeyValuePair<string, Dictionary<string, float[]>> Entry)> createdEntries)
|
||||
{
|
||||
helper.BulkExecuteNonQuery(
|
||||
"INSERT INTO embedding_cache (cache_key, model_key, embedding, idx) VALUES (@cache_key, @model_key, @embedding, @index)",
|
||||
createdEntries.SelectMany(element => {
|
||||
return element.Entry.Value.Select(model => new object[]
|
||||
{
|
||||
new SqliteParameter("@cache_key", element.Entry.Key),
|
||||
new SqliteParameter("@model_key", model.Key),
|
||||
new SqliteParameter("@embedding", SearchdomainHelper.BytesFromFloatArray(model.Value)),
|
||||
new SqliteParameter("@index", element.Index)
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private static async Task UpdateEntryIndicesInStore(
|
||||
SQLiteHelper helper,
|
||||
List<(int Index, int NewIndex)> changedEntries)
|
||||
{
|
||||
helper.BulkExecuteNonQuery(
|
||||
"UPDATE embedding_cache SET idx = @newIndex WHERE idx = @index",
|
||||
changedEntries.Select(element => new object[]
|
||||
{
|
||||
new SqliteParameter("@index", element.Index),
|
||||
new SqliteParameter("@newIndex", -element.NewIndex) // The "-" prevents in-place update collisions
|
||||
})
|
||||
);
|
||||
helper.BulkExecuteNonQuery(
|
||||
"UPDATE embedding_cache SET idx = @newIndex WHERE idx = @index",
|
||||
changedEntries.Select(element => new object[]
|
||||
{
|
||||
new SqliteParameter("@index", -element.NewIndex),
|
||||
new SqliteParameter("@newIndex", element.NewIndex) // Flip the negative prefix
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private static async Task RemoveEntriesFromStore(
|
||||
SQLiteHelper helper,
|
||||
List<int> deletedEntries)
|
||||
{
|
||||
helper.BulkExecuteNonQuery(
|
||||
"DELETE FROM embedding_cache WHERE idx = @index",
|
||||
deletedEntries.Select(index => new object[]
|
||||
{
|
||||
new SqliteParameter("@index", index)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private static async Task AddModelsToIndices(
|
||||
SQLiteHelper helper,
|
||||
List<(int Index, string Model, string Key, float[] Embedding)> addedModels)
|
||||
{
|
||||
helper.BulkExecuteNonQuery(
|
||||
"INSERT INTO embedding_cache (cache_key, model_key, embedding, idx) VALUES (@cache_key, @model_key, @embedding, @index)",
|
||||
addedModels.Select(element => new object[]
|
||||
{
|
||||
new SqliteParameter("@cache_key", element.Key),
|
||||
new SqliteParameter("@model_key", element.Model),
|
||||
new SqliteParameter("@embedding", SearchdomainHelper.BytesFromFloatArray(element.Embedding)),
|
||||
new SqliteParameter("@index", element.Index)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private static async Task RemoveModelsFromIndices(
|
||||
SQLiteHelper helper,
|
||||
List<(int Index, string Model)> removedModels)
|
||||
{
|
||||
helper.BulkExecuteNonQuery(
|
||||
"DELETE FROM embedding_cache WHERE idx = @index AND model_key = @model",
|
||||
removedModels.Select(element => new object[]
|
||||
{
|
||||
new SqliteParameter("@index", element.Index),
|
||||
new SqliteParameter("@model", element.Model)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static (Dictionary<int, KeyValuePair<string, Dictionary<string, float[]>>> positionToEntry,
|
||||
Dictionary<string, int> entryToPosition)
|
||||
GetCacheMappings(EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache)
|
||||
{
|
||||
var positionToEntry = new Dictionary<int, KeyValuePair<string, Dictionary<string, float[]>>>();
|
||||
var entryToPosition = new Dictionary<string, int>();
|
||||
|
||||
int position = 0;
|
||||
|
||||
foreach (var entry in embeddingCache)
|
||||
{
|
||||
positionToEntry[position] = entry;
|
||||
entryToPosition[entry.Key] = position;
|
||||
position++;
|
||||
}
|
||||
|
||||
return (positionToEntry, entryToPosition);
|
||||
}
|
||||
}
|
||||
@@ -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,60 @@ public class DatabaseHelper(ILogger<DatabaseHelper> logger)
|
||||
attributeSumReader.Close();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static long GetTotalDatabaseSize(SQLHelper helper)
|
||||
{
|
||||
Dictionary<string, dynamic> parameters = [];
|
||||
DbDataReader searchdomainSumReader = helper.ExecuteSQLCommand("SELECT SUM(Data_length) FROM information_schema.tables", parameters);
|
||||
try
|
||||
{
|
||||
bool success = searchdomainSumReader.Read();
|
||||
long result = success && !searchdomainSumReader.IsDBNull(0) ? searchdomainSumReader.GetInt64(0) : 0;
|
||||
return result;
|
||||
} finally
|
||||
{
|
||||
searchdomainSumReader.Close();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/Server/Helper/SQLiteHelper.cs
Normal file
76
src/Server/Helper/SQLiteHelper.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Server.Models;
|
||||
using MySql.Data.MySqlClient;
|
||||
using System.Configuration;
|
||||
|
||||
namespace Server.Helper;
|
||||
|
||||
public class SQLiteHelper : SqlHelper, IDisposable
|
||||
{
|
||||
public SQLiteHelper(DbConnection connection, string connectionString) : base(connection, connectionString)
|
||||
{
|
||||
Connection = connection;
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
|
||||
public SQLiteHelper(EmbeddingSearchOptions options) : base(new SqliteConnection(options.ConnectionStrings.Cache), options.ConnectionStrings.Cache ?? "")
|
||||
{
|
||||
if (options.ConnectionStrings.Cache is null)
|
||||
{
|
||||
throw new ConfigurationErrorsException("Cache options must not be null when instantiating SQLiteHelper");
|
||||
}
|
||||
ConnectionString = options.ConnectionStrings.Cache;
|
||||
Connection = new SqliteConnection(ConnectionString);
|
||||
}
|
||||
|
||||
public override SQLiteHelper DuplicateConnection()
|
||||
{
|
||||
SqliteConnection newConnection = new(ConnectionString);
|
||||
return new SQLiteHelper(newConnection, ConnectionString);
|
||||
}
|
||||
|
||||
public override int ExecuteSQLCommandGetInsertedID(string query, object[] parameters)
|
||||
{
|
||||
lock (Connection)
|
||||
{
|
||||
EnsureConnected();
|
||||
EnsureDbReaderIsClosed();
|
||||
using DbCommand command = Connection.CreateCommand();
|
||||
|
||||
command.CommandText = query;
|
||||
command.Parameters.AddRange(parameters);
|
||||
command.ExecuteNonQuery();
|
||||
command.CommandText = "SELECT last_insert_rowid();";
|
||||
return Convert.ToInt32(command.ExecuteScalar());
|
||||
}
|
||||
}
|
||||
|
||||
public int BulkExecuteNonQuery(string sql, IEnumerable<object[]> parameterSets)
|
||||
{
|
||||
lock (Connection)
|
||||
{
|
||||
EnsureConnected();
|
||||
EnsureDbReaderIsClosed();
|
||||
|
||||
using var transaction = Connection.BeginTransaction();
|
||||
using var command = Connection.CreateCommand();
|
||||
|
||||
command.CommandText = sql;
|
||||
command.Transaction = transaction;
|
||||
|
||||
int affectedRows = 0;
|
||||
|
||||
foreach (var parameters in parameterSets)
|
||||
{
|
||||
command.Parameters.Clear();
|
||||
command.Parameters.AddRange(parameters);
|
||||
affectedRows += command.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
return affectedRows;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using AdaptiveExpressions;
|
||||
using Server.Exceptions;
|
||||
using Shared;
|
||||
using Shared.Models;
|
||||
|
||||
namespace Server.Helper;
|
||||
@@ -15,7 +16,7 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
|
||||
|
||||
public static byte[] BytesFromFloatArray(float[] floats)
|
||||
{
|
||||
var byteArray = new byte[floats.Length * 4];
|
||||
var byteArray = new byte[floats.Length * sizeof(float)];
|
||||
var floatArray = floats.ToArray();
|
||||
Buffer.BlockCopy(floatArray, 0, byteArray, 0, byteArray.Length);
|
||||
return byteArray;
|
||||
@@ -23,7 +24,7 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
|
||||
|
||||
public static float[] FloatArrayFromBytes(byte[] bytes)
|
||||
{
|
||||
var floatArray = new float[bytes.Length / 4];
|
||||
var floatArray = new float[bytes.Length / sizeof(float)];
|
||||
Buffer.BlockCopy(bytes, 0, floatArray, 0, bytes.Length);
|
||||
return floatArray;
|
||||
}
|
||||
@@ -47,7 +48,7 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
|
||||
|
||||
public List<Entity>? EntitiesFromJSON(SearchdomainManager searchdomainManager, ILogger logger, string json)
|
||||
{
|
||||
LRUCache<string, Dictionary<string, float[]>> embeddingCache = searchdomainManager.embeddingCache;
|
||||
EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache = searchdomainManager.embeddingCache;
|
||||
AIProvider aIProvider = searchdomainManager.aIProvider;
|
||||
SQLHelper helper = searchdomainManager.helper;
|
||||
|
||||
@@ -57,22 +58,42 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
|
||||
return null;
|
||||
}
|
||||
|
||||
// toBeCached: model -> [datapoint.text * n]
|
||||
// Prefetch embeddings
|
||||
Dictionary<string, List<string>> toBeCached = [];
|
||||
Dictionary<string, List<string>> toBeCachedParallel = [];
|
||||
foreach (JSONEntity jSONEntity in jsonEntities)
|
||||
{
|
||||
Dictionary<string, List<string>> targetDictionary = toBeCached;
|
||||
if (searchdomainManager.GetSearchdomain(jSONEntity.Searchdomain).settings.ParallelEmbeddingsPrefetch)
|
||||
{
|
||||
targetDictionary = toBeCachedParallel;
|
||||
}
|
||||
foreach (JSONDatapoint datapoint in jSONEntity.Datapoints)
|
||||
{
|
||||
foreach (string model in datapoint.Model)
|
||||
{
|
||||
if (!toBeCached.ContainsKey(model))
|
||||
if (!targetDictionary.ContainsKey(model))
|
||||
{
|
||||
toBeCached[model] = [];
|
||||
targetDictionary[model] = [];
|
||||
}
|
||||
toBeCached[model].Add(datapoint.Text);
|
||||
targetDictionary[model].Add(datapoint.Text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var toBeCachedKV in toBeCached)
|
||||
{
|
||||
string model = toBeCachedKV.Key;
|
||||
List<string> uniqueStrings = [.. toBeCachedKV.Value.Distinct()];
|
||||
Datapoint.GetEmbeddings([.. uniqueStrings], [model], aIProvider, embeddingCache);
|
||||
}
|
||||
Parallel.ForEach(toBeCachedParallel, toBeCachedParallelKV =>
|
||||
{
|
||||
string model = toBeCachedParallelKV.Key;
|
||||
List<string> uniqueStrings = [.. toBeCachedParallelKV.Value.Distinct()];
|
||||
Datapoint.GetEmbeddings([.. uniqueStrings], [model], aIProvider, embeddingCache);
|
||||
});
|
||||
// Index/parse the entities
|
||||
ConcurrentQueue<Entity> retVal = [];
|
||||
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = 16 }; // <-- This is needed! Otherwise if we try to index 100+ entities at once, it spawns 100 threads, exploding the SQL pool
|
||||
Parallel.ForEach(jsonEntities, parallelOptions, jSONEntity =>
|
||||
@@ -88,11 +109,11 @@ 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;
|
||||
LRUCache<string, Dictionary<string, float[]>> embeddingCache = searchdomain.embeddingCache;
|
||||
EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache = searchdomain.embeddingCache;
|
||||
Entity? preexistingEntity = entityCache.FirstOrDefault(entity => entity.name == jsonEntity.Name);
|
||||
bool invalidateSearchCache = false;
|
||||
|
||||
@@ -274,10 +295,10 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
|
||||
throw new Exception("jsonDatapoint.Text must not be null at this point");
|
||||
}
|
||||
using SQLHelper helper = searchdomain.helper.DuplicateConnection();
|
||||
LRUCache<string, Dictionary<string, float[]>> embeddingCache = searchdomain.embeddingCache;
|
||||
EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache = searchdomain.embeddingCache;
|
||||
hash ??= Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(jsonDatapoint.Text)));
|
||||
DatabaseHelper.DatabaseInsertDatapoint(helper, jsonDatapoint.Name, jsonDatapoint.Probmethod_embedding, jsonDatapoint.SimilarityMethod, hash, entityId);
|
||||
Dictionary<string, float[]> embeddings = Datapoint.GenerateEmbeddings(jsonDatapoint.Text, [.. jsonDatapoint.Model], searchdomain.aIProvider, embeddingCache);
|
||||
Dictionary<string, float[]> embeddings = Datapoint.GetEmbeddings(jsonDatapoint.Text, [.. jsonDatapoint.Model], searchdomain.aIProvider, embeddingCache);
|
||||
var probMethod_embedding = new ProbMethod(jsonDatapoint.Probmethod_embedding, logger) ?? throw new ProbMethodNotFoundException(jsonDatapoint.Probmethod_embedding);
|
||||
var similarityMethod = new SimilarityMethod(jsonDatapoint.SimilarityMethod, logger) ?? throw new SimilarityMethodNotFoundException(jsonDatapoint.SimilarityMethod);
|
||||
return new Datapoint(jsonDatapoint.Name, probMethod_embedding, similarityMethod, hash, [.. embeddings.Select(kv => (kv.Key, kv.Value))]);
|
||||
@@ -299,4 +320,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);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,11 @@ public static class DatabaseMigrations
|
||||
int initialDatabaseVersion = DatabaseGetVersion(helper);
|
||||
int databaseVersion = initialDatabaseVersion;
|
||||
|
||||
if (databaseVersion == 0)
|
||||
{
|
||||
databaseVersion = Create(helper);
|
||||
}
|
||||
|
||||
var updateMethods = typeof(DatabaseMigrations)
|
||||
.GetMethods(BindingFlags.Public | BindingFlags.Static)
|
||||
.Where(m => m.Name.StartsWith("UpdateFrom") && m.ReturnType == typeof(int))
|
||||
@@ -91,4 +96,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;
|
||||
}
|
||||
}
|
||||
65
src/Server/Migrations/SQLiteMigrations.cs
Normal file
65
src/Server/Migrations/SQLiteMigrations.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System.Data.Common;
|
||||
|
||||
public static class SQLiteMigrations
|
||||
{
|
||||
public static void Migrate(DbConnection conn)
|
||||
{
|
||||
EnableWal(conn);
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
|
||||
cmd.CommandText = "PRAGMA user_version;";
|
||||
var version = Convert.ToInt32(cmd.ExecuteScalar());
|
||||
|
||||
if (version == 0)
|
||||
{
|
||||
CreateV1(conn);
|
||||
SetVersion(conn, 1);
|
||||
version = 1;
|
||||
}
|
||||
|
||||
if (version == 1)
|
||||
{
|
||||
// future migration
|
||||
// UpdateFrom1To2(conn);
|
||||
// SetVersion(conn, 2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnableWal(DbConnection conn)
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "PRAGMA journal_mode = WAL;";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
|
||||
private static void CreateV1(DbConnection conn)
|
||||
{
|
||||
using var tx = conn.BeginTransaction();
|
||||
using var cmd = conn.CreateCommand();
|
||||
|
||||
cmd.CommandText = """
|
||||
CREATE TABLE embedding_cache (
|
||||
cache_key TEXT NOT NULL,
|
||||
model_key TEXT NOT NULL,
|
||||
embedding BLOB NOT NULL,
|
||||
idx INTEGER NOT NULL,
|
||||
PRIMARY KEY (cache_key, model_key)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_index
|
||||
ON embedding_cache(idx);
|
||||
""";
|
||||
|
||||
cmd.ExecuteNonQuery();
|
||||
tx.Commit();
|
||||
}
|
||||
|
||||
private static void SetVersion(DbConnection conn, int version)
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = $"PRAGMA user_version = {version};";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,11 @@ 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; }
|
||||
public required SimpleAuthOptions SimpleAuth { get; set; }
|
||||
public required CacheOptions Cache { get; set; }
|
||||
public required bool UseHttpsRedirection { get; set; }
|
||||
}
|
||||
|
||||
@@ -34,3 +34,16 @@ public class SimpleUser
|
||||
public string Password { get; set; } = "";
|
||||
public string[] Roles { get; set; } = [];
|
||||
}
|
||||
|
||||
public class ConnectionStringsOptions
|
||||
{
|
||||
public required string SQL { get; set; }
|
||||
public string? Cache { get; set; }
|
||||
}
|
||||
|
||||
public class CacheOptions
|
||||
{
|
||||
public required long CacheTopN { get; set; }
|
||||
public bool StoreEmbeddingCache { get; set; } = false;
|
||||
public int? StoreTopN { get; set; }
|
||||
}
|
||||
109
src/Server/Models/SQLHelper.cs
Normal file
109
src/Server/Models/SQLHelper.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
namespace Server.Models;
|
||||
using System.Data.Common;
|
||||
|
||||
public abstract partial class SqlHelper : IDisposable
|
||||
{
|
||||
public DbConnection Connection { get; set; }
|
||||
public DbDataReader? DbDataReader { get; set; }
|
||||
public string ConnectionString { get; set; }
|
||||
public SqlHelper(DbConnection connection, string connectionString)
|
||||
{
|
||||
Connection = connection;
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
|
||||
public abstract SqlHelper DuplicateConnection();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Connection.Close();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public DbDataReader ExecuteSQLCommand(string query, object[] parameters)
|
||||
{
|
||||
lock (Connection)
|
||||
{
|
||||
EnsureConnected();
|
||||
EnsureDbReaderIsClosed();
|
||||
using DbCommand command = Connection.CreateCommand();
|
||||
command.CommandText = query;
|
||||
command.Parameters.AddRange(parameters);
|
||||
DbDataReader = command.ExecuteReader();
|
||||
return DbDataReader;
|
||||
}
|
||||
}
|
||||
|
||||
public void ExecuteQuery<T>(string query, object[] parameters, Func<DbDataReader, T> map)
|
||||
{
|
||||
lock (Connection)
|
||||
{
|
||||
EnsureConnected();
|
||||
EnsureDbReaderIsClosed();
|
||||
|
||||
using var command = Connection.CreateCommand();
|
||||
command.CommandText = query;
|
||||
command.Parameters.AddRange(parameters);
|
||||
|
||||
using var reader = command.ExecuteReader();
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
map(reader);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public int ExecuteSQLNonQuery(string query, object[] parameters)
|
||||
{
|
||||
lock (Connection)
|
||||
{
|
||||
EnsureConnected();
|
||||
EnsureDbReaderIsClosed();
|
||||
using DbCommand command = Connection.CreateCommand();
|
||||
|
||||
command.CommandText = query;
|
||||
command.Parameters.AddRange(parameters);
|
||||
return command.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract int ExecuteSQLCommandGetInsertedID(string query, object[] parameters);
|
||||
|
||||
public bool EnsureConnected()
|
||||
{
|
||||
if (Connection.State != System.Data.ConnectionState.Open)
|
||||
{
|
||||
try
|
||||
{
|
||||
Connection.Close();
|
||||
Connection.Open();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,14 @@ using Server.Helper;
|
||||
using Server.Models;
|
||||
using Server.Services;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Reflection;
|
||||
using System.Configuration;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi;
|
||||
using Shared.Models;
|
||||
using Microsoft.AspNetCore.ResponseCompression;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Server.Migrations;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -29,10 +32,22 @@ builder.Services.AddControllersWithViews()
|
||||
// Add Configuration
|
||||
IConfigurationSection configurationSection = builder.Configuration.GetSection("Embeddingsearch");
|
||||
EmbeddingSearchOptions configuration = configurationSection.Get<EmbeddingSearchOptions>() ?? throw new ConfigurationErrorsException("Unable to start server due to an invalid configration");
|
||||
|
||||
builder.Services.Configure<EmbeddingSearchOptions>(configurationSection);
|
||||
builder.Services.Configure<ApiKeyOptions>(configurationSection);
|
||||
|
||||
// Migrate database
|
||||
var helper = new SQLHelper(new MySql.Data.MySqlClient.MySqlConnection(configuration.ConnectionStrings.SQL), configuration.ConnectionStrings.SQL);
|
||||
DatabaseMigrations.Migrate(helper);
|
||||
|
||||
// Migrate SQLite cache
|
||||
if (configuration.ConnectionStrings.Cache is not null)
|
||||
{
|
||||
|
||||
var SqliteConnection = new SqliteConnection(configuration.ConnectionStrings.Cache);
|
||||
SqliteConnection.Open();
|
||||
SQLiteMigrations.Migrate(SqliteConnection);
|
||||
}
|
||||
|
||||
// Add Localization
|
||||
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
|
||||
builder.Services.Configure<RequestLocalizationOptions>(options =>
|
||||
@@ -48,36 +63,37 @@ builder.Services.AddScoped<LocalizationService>();
|
||||
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
builder.Services.AddOpenApi(options =>
|
||||
{
|
||||
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
||||
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
|
||||
c.IncludeXmlComments(xmlPath);
|
||||
if (configuration.ApiKeys is not null)
|
||||
options.AddDocumentTransformer((document, context, _) =>
|
||||
{
|
||||
c.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme
|
||||
{
|
||||
Description = "ApiKey must appear in header",
|
||||
Type = SecuritySchemeType.ApiKey,
|
||||
Name = "X-API-KEY",
|
||||
In = ParameterLocation.Header,
|
||||
Scheme = "ApiKeyScheme"
|
||||
});
|
||||
var key = new OpenApiSecurityScheme()
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
if (configuration.ApiKeys is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
document.Components ??= new();
|
||||
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
|
||||
|
||||
document.Components.SecuritySchemes["ApiKey"] =
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "ApiKey"
|
||||
},
|
||||
In = ParameterLocation.Header
|
||||
};
|
||||
var requirement = new OpenApiSecurityRequirement
|
||||
{
|
||||
{ key, []}
|
||||
};
|
||||
c.AddSecurityRequirement(requirement);
|
||||
}
|
||||
Type = SecuritySchemeType.ApiKey,
|
||||
Name = "X-API-KEY",
|
||||
In = ParameterLocation.Header,
|
||||
Description = "ApiKey must appear in header"
|
||||
};
|
||||
|
||||
document.Security ??= [];
|
||||
|
||||
// Apply globally
|
||||
document.Security?.Add(
|
||||
new OpenApiSecurityRequirement
|
||||
{
|
||||
[new OpenApiSecuritySchemeReference("ApiKey", document)] = []
|
||||
}
|
||||
);
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(builder.Configuration)
|
||||
@@ -140,6 +156,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 +228,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;
|
||||
}
|
||||
|
||||
@@ -175,11 +242,16 @@ app.Use(async (context, next) =>
|
||||
await next();
|
||||
});
|
||||
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(options =>
|
||||
{
|
||||
options.SwaggerEndpoint("/openapi/v1.json", "API v1");
|
||||
options.RoutePrefix = "swagger";
|
||||
options.EnablePersistAuthorization();
|
||||
options.InjectStylesheet("/swagger-ui/custom.css");
|
||||
options.InjectJavascript("/swagger-ui/custom.js");
|
||||
});
|
||||
app.MapOpenApi("/openapi/v1.json");
|
||||
|
||||
//app.UseElmahExceptionPage(); // Messes with JSON response for API calls. Leaving this here so I don't accidentally put this in again later on.
|
||||
|
||||
if (configuration.ApiKeys is not null)
|
||||
|
||||
@@ -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,97 @@
|
||||
<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>
|
||||
<data name="Query cache size" xml:space="preserve">
|
||||
<value>Query Cache Größe</value>
|
||||
</data>
|
||||
<data name="Embeddings parallel prefetching" xml:space="preserve">
|
||||
<value>Embeddings parallel prefetchen</value>
|
||||
</data>
|
||||
<data name="parallelEmbeddingsPrefetchInfo" xml:space="preserve">
|
||||
<value>Wenn diese Einstellung aktiv ist, wird das Abrufen von Embeddings beim Indizieren von Entities parallelisiert. Deaktiviere diese Einstellung, falls Model-unloading ein Problem ist.</value>
|
||||
</data>
|
||||
<data name="Add result" xml:space="preserve">
|
||||
<value>Ergebnis hinzufügen</value>
|
||||
</data>
|
||||
<data name="Search query was updated successfully" xml:space="preserve">
|
||||
<value>Suchanfrage wurde erfolgreich angepasst</value>
|
||||
</data>
|
||||
<data name="Total RAM usage" xml:space="preserve">
|
||||
<value>RAM Verwendung insgesamt</value>
|
||||
</data>
|
||||
<data name="Total Database size" xml:space="preserve">
|
||||
<value>Datenbankgröße insgesamt</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -243,4 +243,97 @@
|
||||
<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>
|
||||
<data name="Query cache size" xml:space="preserve">
|
||||
<value>Query Cache size</value>
|
||||
</data>
|
||||
<data name="Embeddings parallel prefetching" xml:space="preserve">
|
||||
<value>Embeddings parallel prefetching</value>
|
||||
</data>
|
||||
<data name="parallelEmbeddingsPrefetchInfo" xml:space="preserve">
|
||||
<value>With this setting activated the embeddings retrieval will be parallelized when indexing entities. Disable this setting if model unloading is an issue.</value>
|
||||
</data>
|
||||
<data name="Add result" xml:space="preserve">
|
||||
<value>Add result</value>
|
||||
</data>
|
||||
<data name="Search query was updated successfully" xml:space="preserve">
|
||||
<value>Search query was updated successfully</value>
|
||||
</data>
|
||||
<data name="Total RAM usage" xml:space="preserve">
|
||||
<value>Total RAM usage</value>
|
||||
</data>
|
||||
<data name="Total Database size" xml:space="preserve">
|
||||
<value>Total Database size</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -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,15 +18,15 @@ 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;
|
||||
public EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache;
|
||||
private readonly MySqlConnection connection;
|
||||
public SQLHelper helper;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public Searchdomain(string searchdomain, string connectionString, AIProvider aIProvider, LRUCache<string, Dictionary<string, float[]>> embeddingCache, ILogger logger, string provider = "sqlserver", bool runEmpty = false)
|
||||
public Searchdomain(string searchdomain, string connectionString, AIProvider aIProvider, EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache, ILogger logger, string provider = "sqlserver", bool runEmpty = false)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
_provider = provider.ToLower();
|
||||
@@ -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,18 +188,18 @@ 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;
|
||||
}
|
||||
|
||||
public Dictionary<string, float[]> GetQueryEmbeddings(string query)
|
||||
{
|
||||
bool hasQuery = embeddingCache.TryGet(query, out Dictionary<string, float[]> queryEmbeddings);
|
||||
bool hasQuery = embeddingCache.TryGetValue(query, out Dictionary<string, float[]>? queryEmbeddings);
|
||||
bool allModelsInQuery = queryEmbeddings is not null && modelsInUse.All(model => queryEmbeddings.ContainsKey(model));
|
||||
if (!(hasQuery && allModelsInQuery) || queryEmbeddings is null)
|
||||
{
|
||||
queryEmbeddings = Datapoint.GenerateEmbeddings(query, modelsInUse, aIProvider, embeddingCache);
|
||||
if (!embeddingCache.TryGet(query, out var embeddingCacheForCurrentQuery))
|
||||
queryEmbeddings = Datapoint.GetEmbeddings(query, modelsInUse, aIProvider, embeddingCache);
|
||||
if (!embeddingCache.TryGetValue(query, out var embeddingCacheForCurrentQuery))
|
||||
{
|
||||
embeddingCache.Set(query, queryEmbeddings);
|
||||
}
|
||||
@@ -206,7 +207,7 @@ public class Searchdomain
|
||||
{
|
||||
foreach (KeyValuePair<string, float[]> kvp in queryEmbeddings) // kvp.Key = model, kvp.Value = embedding
|
||||
{
|
||||
if (!embeddingCache.TryGet(kvp.Key, out var _))
|
||||
if (!embeddingCache.TryGetValue(kvp.Key, out var _))
|
||||
{
|
||||
embeddingCacheForCurrentQuery[kvp.Key] = kvp.Value;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,43 +6,48 @@ using Server.Exceptions;
|
||||
using AdaptiveExpressions;
|
||||
using Shared.Models;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Server.Models;
|
||||
using Shared;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Server;
|
||||
|
||||
public class SearchdomainManager
|
||||
public class SearchdomainManager : IDisposable
|
||||
{
|
||||
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 EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache;
|
||||
public long EmbeddingCacheMaxCount;
|
||||
private bool disposed = false;
|
||||
|
||||
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.Cache.CacheTopN;
|
||||
if (options.Value.Cache.StoreEmbeddingCache)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
embeddingCache = CacheHelper.GetEmbeddingStore(options.Value);
|
||||
stopwatch.Stop();
|
||||
_logger.LogInformation("GetEmbeddingStore completed in {ElapsedMilliseconds} ms", stopwatch.ElapsedMilliseconds);
|
||||
} else
|
||||
{
|
||||
embeddingCache = new((int)EmbeddingCacheMaxCount);
|
||||
}
|
||||
connectionString = _options.ConnectionStrings.SQL;
|
||||
connection = new MySqlConnection(connectionString);
|
||||
connection.Open();
|
||||
helper = new SQLHelper(connection, connectionString);
|
||||
try
|
||||
{
|
||||
DatabaseMigrations.Migrate(helper);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical("Unable to migrate the database due to the exception: {ex}", [ex.Message]);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public Searchdomain GetSearchdomain(string searchdomain)
|
||||
@@ -80,12 +85,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 +133,44 @@ public class SearchdomainManager
|
||||
searchdomains[name] = searchdomain;
|
||||
return searchdomain;
|
||||
}
|
||||
|
||||
public bool IsSearchdomainLoaded(string name)
|
||||
{
|
||||
return searchdomains.ContainsKey(name);
|
||||
}
|
||||
|
||||
// Cleanup procedure
|
||||
private async Task Cleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_options.Cache.StoreEmbeddingCache)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
await CacheHelper.UpdateEmbeddingStore(embeddingCache, _options);
|
||||
stopwatch.Stop();
|
||||
_logger.LogInformation("UpdateEmbeddingStore completed in {ElapsedMilliseconds} ms", stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
_logger.LogInformation("SearchdomainManager cleanup completed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during SearchdomainManager cleanup");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true).Wait();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual async Task Dispose(bool disposing)
|
||||
{
|
||||
if (!disposed && disposing)
|
||||
{
|
||||
await Cleanup();
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
@@ -12,21 +12,22 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AdaptiveExpressions" Version="4.23.0" />
|
||||
<PackageReference Include="ElmahCore" Version="2.1.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.3" />
|
||||
<PackageReference Include="MySql.Data" Version="9.2.0" />
|
||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||
<PackageReference Include="AdaptiveExpressions" Version="4.23.1" />
|
||||
<PackageReference Include="ElmahCore" Version="2.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.0" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.2" />
|
||||
<PackageReference Include="MySql.Data" Version="9.6.0" />
|
||||
<PackageReference Include="Npgsql" Version="10.0.1" />
|
||||
<PackageReference Include="OllamaSharp" Version="5.2.2" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.3" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.2" />
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.9.0" />
|
||||
<PackageReference Include="System.Data.Sqlite" Version="1.0.119" />
|
||||
<PackageReference Include="System.Numerics.Tensors" Version="9.0.3" />
|
||||
<PackageReference Include="System.Data.Sqlite" Version="2.0.2" />
|
||||
<PackageReference Include="System.Numerics.Tensors" Version="10.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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,9 +90,9 @@ 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]',
|
||||
'.navbar',
|
||||
'[data-bs-theme="dark"]', '[data-bs-theme="dark"] body', '[data-bs-theme="dark"] .navbar', '[data-bs-theme="dark"] .card', '[data-bs-theme="dark"] .btn',
|
||||
'.col-md-4',
|
||||
'.navbar', '.ms-auto', '.dropdown', '.dropdown-menu',
|
||||
'.visually-hidden', // visually hidden headings
|
||||
'.bi-info-circle-fill', '.text-info', // info icon
|
||||
'.container', '.col-md-6', '.row', '.g-4', '.row>*',
|
||||
@@ -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')
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# How to use CriticalCSS
|
||||
1. Install it here
|
||||
1. Install the dependencies from here
|
||||
```bash
|
||||
npm i -D critical
|
||||
npm install puppeteer
|
||||
78
src/Server/Tools/LocalizationChecker/LocalizationChecker.py
Normal file
78
src/Server/Tools/LocalizationChecker/LocalizationChecker.py
Normal 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()
|
||||
@@ -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>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
@using Server
|
||||
|
||||
@inject LocalizationService T
|
||||
@inject AIProvider AIProvider
|
||||
@model HomeIndexViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Home Page";
|
||||
@@ -25,6 +24,24 @@
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Server -->
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title fs-5">@T["Server"]</h2>
|
||||
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<span>@T["Total RAM usage"]</span>
|
||||
<strong id="serverMemorySize"></strong>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<span>@T["Total Database size"]</span>
|
||||
<strong id="serverDatabaseSize"></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Embedding Cache -->
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
@@ -41,7 +58,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 +122,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>
|
||||
@@ -116,13 +170,6 @@
|
||||
var searchdomains = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Initialize all tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||
});
|
||||
|
||||
|
||||
let searchdomainCount = document.getElementById("searchdomainCount");
|
||||
showThrobber(searchdomainCount);
|
||||
let searchdomainEntityCount = document.getElementById("searchdomainEntityCount");
|
||||
@@ -136,6 +183,21 @@
|
||||
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 serverMemorySize = document.getElementById("serverMemorySize");
|
||||
showThrobber(serverMemorySize);
|
||||
let serverDatabaseSize = document.getElementById("serverDatabaseSize");
|
||||
showThrobber(serverDatabaseSize);
|
||||
|
||||
let healthchecksServer = document.getElementById("healthchecksServer");
|
||||
let healthchecksAiProvider = document.getElementById("healthchecksAiProvider");
|
||||
|
||||
@@ -144,46 +206,40 @@
|
||||
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}%`;
|
||||
serverMemorySize.textContent = NumberOfBytesAsHumanReadable(result.RamTotalSize);
|
||||
hideThrobber(serverMemorySize);
|
||||
serverDatabaseSize.textContent = NumberOfBytesAsHumanReadable(result.DatabaseTotalSize);
|
||||
hideThrobber(serverDatabaseSize);
|
||||
});
|
||||
getHealthCheckStatusAndApply(healthchecksServer, "/healthz/Database");
|
||||
getHealthCheckStatusAndApply(healthchecksAiProvider, "/healthz/AIProvider");
|
||||
@@ -206,8 +262,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());
|
||||
}
|
||||
|
||||
|
||||
@@ -62,11 +62,24 @@
|
||||
<!-- Settings -->
|
||||
<div class="row align-items-center mb-3">
|
||||
<h3>@T["Settings"]</h3>
|
||||
<div class="col-md-6">
|
||||
<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 mt-3">
|
||||
<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 class="col-md-6 mt-3">
|
||||
<input type="checkbox" class="form-check-input" id="searchdomainConfigParallelEmbeddingsPrefetch" />
|
||||
<label class="form-check-label" for="searchdomainConfigParallelEmbeddingsPrefetch">@T["Embeddings parallel prefetching"]</label>
|
||||
<i class="bi bi-info-circle-fill text-info"
|
||||
data-bs-toggle="tooltip"
|
||||
title="@T["parallelEmbeddingsPrefetchInfo"]"></i>
|
||||
</div>
|
||||
</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 +105,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 +116,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 +142,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 +165,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 +213,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 +253,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">
|
||||
@@ -252,12 +265,13 @@
|
||||
|
||||
<!-- Results -->
|
||||
<h3>@T["Results"]</h3>
|
||||
<button class="btn btn-primary btn-sm" onclick="queryUpdateAddResult('', '', null, true)">@T["Add result"]</button>
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 85px;">@T["Score"]</th>
|
||||
<th>@T["Name"]</th>
|
||||
<th>@T["Action"]</th>
|
||||
<th class="text-center">@T["Action"]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="queryUpdateResultsBody"></tbody>
|
||||
@@ -284,8 +298,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 +312,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 +361,24 @@
|
||||
</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 mt-3">
|
||||
<input type="checkbox" class="form-check-input" id="createSearchdomainWithCacheReconciliation" />
|
||||
<label class="form-check-label" for="createSearchdomainWithCacheReconciliation">@T["Enable cache reconciliation"]</label>
|
||||
</div>
|
||||
<div class="col-md-6 mt-3">
|
||||
<input type="checkbox" class="form-check-input" id="createSearchdomainConfigParallelEmbeddingsPrefetch" />
|
||||
<label class="form-check-label" for="createSearchdomainConfigParallelEmbeddingsPrefetch">@T["Embeddings parallel prefetching"]</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
@@ -476,8 +504,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">
|
||||
@@ -656,7 +684,10 @@
|
||||
queriesFilter.addEventListener('input', () => {
|
||||
populateQueriesTable(queriesFilter.value);
|
||||
});
|
||||
selectDomain(0);
|
||||
try
|
||||
{
|
||||
selectDomain(0);
|
||||
} catch (error) {}
|
||||
|
||||
document
|
||||
.getElementById('searchdomainRename')
|
||||
@@ -694,7 +725,9 @@
|
||||
.addEventListener('click', () => {
|
||||
const domainKey = getSelectedDomainKey();
|
||||
const cacheReconciliation = document.getElementById('searchdomainConfigCacheReconciliation').checked;
|
||||
updateSearchdomainConfig(domainKey, { CacheReconciliation: cacheReconciliation});
|
||||
const queryCacheSize = document.getElementById('searchdomainConfigQueryCacheSize').value;
|
||||
const parallelEmbeddingsPrefetch = document.getElementById('searchdomainConfigParallelEmbeddingsPrefetch').checked;
|
||||
updateSearchdomainConfig(domainKey, { CacheReconciliation: cacheReconciliation, QueryCacheSize: queryCacheSize, ParallelEmbeddingsPrefetch: parallelEmbeddingsPrefetch});
|
||||
});
|
||||
|
||||
document
|
||||
@@ -745,7 +778,7 @@
|
||||
"datapoints": datapoints
|
||||
}];
|
||||
showToast("@T["Creating entity"]", "primary");
|
||||
fetch(`/Entity`, {
|
||||
fetch(`/Entities`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -775,9 +808,10 @@
|
||||
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 };
|
||||
// Implement create logic here
|
||||
const parallelEmbeddingsPrefetch = document.getElementById('createSearchdomainConfigParallelEmbeddingsPrefetch').checked;
|
||||
const settings = { CacheReconciliation: cacheReconciliation, QueryCacheSize: queryCacheSize, ParallelEmbeddingsPrefetch: parallelEmbeddingsPrefetch };
|
||||
fetch(`/Searchdomain?searchdomain=${encodeURIComponent(name)}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -869,12 +903,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'
|
||||
@@ -937,7 +971,7 @@
|
||||
}).then(async response => {
|
||||
result = await response.json();
|
||||
if (response.ok && result.Success) {
|
||||
showToast("@T["Searchdomain was created successfully"]", "success");
|
||||
showToast("@T["Search query was updated successfully"]", "success");
|
||||
console.log('Search query was updated successfully');
|
||||
selectDomain(getSelectedDomainKey());
|
||||
} else {
|
||||
@@ -1053,7 +1087,12 @@
|
||||
|
||||
let searchdomainConfigPromise = getSearchdomainConfig(getSelectedDomainKey());
|
||||
let configElementCachereconciliation = document.getElementById('searchdomainConfigCacheReconciliation');
|
||||
let configElementCacheSize = document.getElementById('searchdomainConfigQueryCacheSize');
|
||||
let configElementParallelEmbeddingsPrefetch = document.getElementById('searchdomainConfigParallelEmbeddingsPrefetch');
|
||||
|
||||
showThrobber(document.querySelector('#searchdomainConfigQueryCacheSize'), true);
|
||||
showThrobber(document.querySelector('#searchdomainConfigCacheReconciliation'), true);
|
||||
showThrobber(document.querySelector('#searchdomainConfigParallelEmbeddingsPrefetch'), true);
|
||||
let cacheUtilizationPromise = getSearchdomainCacheUtilization(getSelectedDomainKey());
|
||||
let databaseUtilizationPromise = getSearchdomainDatabaseUtilization(getSelectedDomainKey());
|
||||
|
||||
@@ -1095,10 +1134,15 @@
|
||||
});
|
||||
|
||||
searchdomainConfigPromise.then(searchdomainConfig => {
|
||||
hideThrobber(document.querySelector('#searchdomainConfigCacheReconciliation'), true);
|
||||
hideThrobber(document.querySelector('#searchdomainConfigParallelEmbeddingsPrefetch'), true);
|
||||
|
||||
if (searchdomainConfig != null && searchdomainConfig.Settings != null)
|
||||
{
|
||||
configElementCacheSize.value = searchdomainConfig.Settings.QueryCacheSize;
|
||||
configElementCachereconciliation.checked = searchdomainConfig.Settings.CacheReconciliation;
|
||||
configElementCachereconciliation.disabled = false;
|
||||
configElementParallelEmbeddingsPrefetch.checked = searchdomainConfig.Settings.ParallelEmbeddingsPrefetch;
|
||||
} else {
|
||||
configElementCachereconciliation.disabled = true;
|
||||
showToast("@T["Unable to fetch searchdomain config"]", "danger");
|
||||
@@ -1106,10 +1150,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 +1334,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) {
|
||||
@@ -1434,28 +1495,7 @@
|
||||
</tr>`;
|
||||
} else {
|
||||
query.Results.forEach(r => {
|
||||
const row = document.createElement('tr');
|
||||
row.setAttribute("draggable", true);
|
||||
const tdScore = document.createElement('td');
|
||||
const scoreInput = document.createElement('input');
|
||||
scoreInput.classList.add('form-control');
|
||||
scoreInput.value = r.Score.toFixed(4);
|
||||
tdScore.append(scoreInput);
|
||||
const tdName = document.createElement('td');
|
||||
tdName.classList.add("text-break");
|
||||
tdName.innerText = r.Name;
|
||||
const tdAction = document.createElement('td');
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.classList.add('btn', 'btn-danger', 'btn-sm');
|
||||
deleteButton.innerText = '@Html.Raw(T["Delete"])';
|
||||
deleteButton.onclick = function() {
|
||||
row.remove();
|
||||
};
|
||||
tdAction.append(deleteButton);
|
||||
row.append(tdScore);
|
||||
row.append(tdName);
|
||||
row.append(tdAction);
|
||||
resultsBody.appendChild(row);
|
||||
queryUpdateAddResult(r.Score.toFixed(4), r.Name, resultsBody);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1466,6 +1506,66 @@
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function queryUpdateAddResult(score, name, target=null, insertAtTop=false) {
|
||||
target = target ?? document.getElementById('queryUpdateResultsBody');
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.setAttribute("draggable", true);
|
||||
const tdScore = document.createElement('td');
|
||||
const scoreInput = document.createElement('input');
|
||||
scoreInput.classList.add('form-control');
|
||||
scoreInput.value = score;
|
||||
scoreInput.ariaLabel = "@T["Score"]";
|
||||
tdScore.append(scoreInput);
|
||||
const tdName = document.createElement('td');
|
||||
const tdNameInput = document.createElement('input');
|
||||
tdNameInput.classList.add("form-control");
|
||||
tdNameInput.value = name;
|
||||
tdNameInput.ariaLabel = "@T["Name"]";
|
||||
tdName.append(tdNameInput);
|
||||
const tdAction = document.createElement('td');
|
||||
tdAction.classList.add('text-center');
|
||||
|
||||
const upButton = document.createElement('button');
|
||||
upButton.classList.add('btn', 'btn-primary', 'btn-sm');
|
||||
upButton.innerText = '↑';
|
||||
upButton.onclick = function() {
|
||||
const currentRow = this.closest('tr');
|
||||
const previousRow = currentRow.previousElementSibling;
|
||||
if (previousRow) {
|
||||
target.insertBefore(currentRow, previousRow);
|
||||
}
|
||||
};
|
||||
const downButton = document.createElement('button');
|
||||
downButton.classList.add('btn', 'btn-primary', 'btn-sm', 'mx-1');
|
||||
downButton.innerText = '↓';
|
||||
downButton.onclick = function() {
|
||||
const currentRow = this.closest('tr');
|
||||
const nextRow = currentRow.nextElementSibling;
|
||||
if (nextRow) {
|
||||
target.insertBefore(nextRow, currentRow);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.classList.add('btn', 'btn-danger', 'btn-sm', 'mx-2');
|
||||
deleteButton.innerText = '@Html.Raw(T["Delete"])';
|
||||
deleteButton.onclick = function() {
|
||||
row.remove();
|
||||
};
|
||||
tdAction.append(upButton);
|
||||
tdAction.append(downButton);
|
||||
tdAction.append(deleteButton);
|
||||
row.append(tdScore);
|
||||
row.append(tdName);
|
||||
row.append(tdAction);
|
||||
if (!insertAtTop) {
|
||||
target.appendChild(row);
|
||||
} else {
|
||||
target.insertBefore(row, target.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function NumberOfBytesAsHumanReadable(bytes, decimals = 2) {
|
||||
if (bytes === 0) return '0 B';
|
||||
if (bytes > 1.20892581961*(10**27)) return "∞ B";
|
||||
@@ -1672,7 +1772,7 @@
|
||||
|
||||
// Get the text content from the second cell (index 1) which contains the path
|
||||
const score = parseFloat(cells[0].firstChild.value);
|
||||
const name = cells[1].textContent.trim();
|
||||
const name = cells[1].firstChild.value;
|
||||
|
||||
result.push({
|
||||
"Score": score,
|
||||
|
||||
@@ -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,19 @@
|
||||
<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"))
|
||||
<link rel="preload" href="~/fonts/bootstrap-icons.woff2" as="font" type="font/woff2" crossorigin="anonymous"/>
|
||||
@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 +35,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,11 +48,13 @@
|
||||
};
|
||||
</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>
|
||||
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">
|
||||
<img fetchpriority="high" alt="Logo" src="/logo.png" width="40" height="40" style="width: 40px; height: 40px;">
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
@@ -54,19 +64,34 @@
|
||||
@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>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="Logout">@T["Logout"]</a>
|
||||
@if (User.IsInRole("Admin") || User.IsInRole("Swagger"))
|
||||
{
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
@T["Tools"]
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
|
||||
<a class="dropdown-item" href="/swagger/index.html?ReturnUrl=@(currentUrl)">@T["Swagger"]</a>
|
||||
@if (User.IsInRole("Admin"))
|
||||
{
|
||||
<a class="dropdown-item" href="/elmah?ReturnUrl=@(currentUrl)">@T["Elmah"]</a>
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
<li class="nav-item ms-auto">
|
||||
<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 +116,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>
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
|
||||
"Embeddingsearch": {
|
||||
"ConnectionStrings": {
|
||||
"SQL": "server=localhost;database=embeddingsearch;uid=embeddingsearch;pwd=somepassword!;"
|
||||
"SQL": "server=localhost;database=embeddingsearch;uid=embeddingsearch;pwd=somepassword!;",
|
||||
"Cache": "Data Source=embeddings.db;Mode=ReadWriteCreate;Cache=Shared"
|
||||
},
|
||||
"Elmah": {
|
||||
"LogPath": "~/logs"
|
||||
},
|
||||
"EmbeddingCacheMaxCount": 10000000,
|
||||
"AiProviders": {
|
||||
"ollama": {
|
||||
"handler": "ollama",
|
||||
@@ -46,6 +46,11 @@
|
||||
]
|
||||
},
|
||||
"ApiKeys": ["Some UUID here", "Another UUID here"],
|
||||
"UseHttpsRedirection": true
|
||||
"UseHttpsRedirection": true,
|
||||
"Cache": {
|
||||
"CacheTopN": 100000,
|
||||
"StoreEmbeddingCache": true,
|
||||
"StoreTopN": 20000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
56
src/Server/wwwroot/elmah-ui/custom.css
Normal file
56
src/Server/wwwroot/elmah-ui/custom.css
Normal 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;
|
||||
}
|
||||
17
src/Server/wwwroot/elmah-ui/custom.js
Normal file
17
src/Server/wwwroot/elmah-ui/custom.js
Normal 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);
|
||||
});
|
||||
@@ -48,4 +48,14 @@ function showToast(message, type) {
|
||||
const bsToast = new bootstrap.Toast(toast, { delay: 10000 });
|
||||
bsToast.show();
|
||||
toast.addEventListener('hidden.bs.toast', () => toast.remove());
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Initialize all tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
let retVal = new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
tooltipTriggerEl.role = "tooltip";
|
||||
return retVal;
|
||||
});
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
BIN
src/Server/wwwroot/logo.png
Normal file
BIN
src/Server/wwwroot/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
58
src/Server/wwwroot/swagger-ui/custom.css
Normal file
58
src/Server/wwwroot/swagger-ui/custom.css
Normal 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;
|
||||
}
|
||||
24
src/Server/wwwroot/swagger-ui/custom.js
Normal file
24
src/Server/wwwroot/swagger-ui/custom.js
Normal 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
240
src/Shared/LRUCache.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,10 +95,14 @@ 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, bool parallelEmbeddingsPrefetch = false)
|
||||
{
|
||||
[JsonPropertyName("CacheReconciliation")]
|
||||
public bool CacheReconciliation { get; set; } = cacheReconciliation;
|
||||
[JsonPropertyName("QueryCacheSize")]
|
||||
public int QueryCacheSize { get; set; } = queryCacheSize;
|
||||
[JsonPropertyName("ParallelEmbeddingsPrefetch")]
|
||||
public bool ParallelEmbeddingsPrefetch { get; set; } = parallelEmbeddingsPrefetch;
|
||||
}
|
||||
|
||||
public static class MemorySizes
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -8,14 +8,28 @@ 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; }
|
||||
[JsonPropertyName("DatabaseTotalSize")]
|
||||
public long? DatabaseTotalSize { get; set; }
|
||||
[JsonPropertyName("RamTotalSize")]
|
||||
public long? RamTotalSize { get; set; }
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
Reference in New Issue
Block a user