Merge pull request #39 from LD-Reborn/28-create-a-front-end---recent-queries

28 create a front end   recent queries
This commit is contained in:
LD50
2025-12-19 12:28:18 +01:00
committed by GitHub
7 changed files with 671 additions and 42 deletions

View File

@@ -1,6 +1,8 @@
using System.Text.Json;
using ElmahCore;
using Microsoft.AspNetCore.Mvc;
using Server.Exceptions;
using Server.Helper;
using Shared.Models;
namespace Server.Controllers;
@@ -98,14 +100,39 @@ public class SearchdomainController : ControllerBase
{
_logger.LogError("Unable to update searchdomain {searchdomain} - not found", [searchdomain]);
return Ok(new SearchdomainUpdateResults() { Success = false, Message = $"Unable to update searchdomain {searchdomain} - not found" });
} catch (Exception)
} catch (Exception ex)
{
_logger.LogError("Unable to update searchdomain {searchdomain}", [searchdomain]);
_logger.LogError("Unable to update searchdomain {searchdomain} - Exception: {ex.Message} - {ex.StackTrace}", [searchdomain, ex.Message, ex.StackTrace]);
return Ok(new SearchdomainUpdateResults() { Success = false, Message = $"Unable to update searchdomain {searchdomain}" });
}
return Ok(new SearchdomainUpdateResults(){Success = true});
}
[HttpPost("UpdateSettings")]
public ActionResult<SearchdomainUpdateResults> UpdateSettings(string searchdomain, [FromBody] SearchdomainSettings request)
{
try
{
Searchdomain searchdomain_ = _domainManager.GetSearchdomain(searchdomain);
Dictionary<string, dynamic> parameters = new()
{
{"settings", JsonSerializer.Serialize(request)},
{"id", searchdomain_.id}
};
searchdomain_.helper.ExecuteSQLNonQuery("UPDATE searchdomain set settings = @settings WHERE id = @id", parameters);
searchdomain_.settings = request;
} catch (SearchdomainNotFoundException)
{
_logger.LogError("Unable to update settings for searchdomain {searchdomain} - not found", [searchdomain]);
return Ok(new SearchdomainUpdateResults() { Success = false, Message = $"Unable to update settings for searchdomain {searchdomain} - not found" });
} catch (Exception ex)
{
_logger.LogError("Unable to update settings for searchdomain {searchdomain} - Exception: {ex.Message} - {ex.StackTrace}", [searchdomain, ex.Message, ex.StackTrace]);
return Ok(new SearchdomainUpdateResults() { Success = false, Message = $"Unable to update settings for searchdomain {searchdomain}" });
}
return Ok(new SearchdomainUpdateResults(){Success = true});
}
[HttpGet("GetSearches")]
public ActionResult<SearchdomainSearchesResults> GetSearches(string searchdomain)
{
@@ -128,4 +155,96 @@ public class SearchdomainController : ControllerBase
return Ok(new SearchdomainSearchesResults() { Searches = searchCache, Success = true });
}
[HttpGet("GetSettings")]
public ActionResult<SearchdomainSettingsResults> GetSettings(string searchdomain)
{
Searchdomain searchdomain_;
try
{
searchdomain_ = _domainManager.GetSearchdomain(searchdomain);
}
catch (SearchdomainNotFoundException)
{
_logger.LogError("Unable to retrieve the searchdomain {searchdomain} - it likely does not exist yet", [searchdomain]);
return Ok(new SearchdomainSettingsResults() { Settings = null, Success = false, Message = "Searchdomain not found" });
}
catch (Exception ex)
{
_logger.LogError("Unable to retrieve the searchdomain {searchdomain} - {ex.Message} - {ex.StackTrace}", [searchdomain, ex.Message, ex.StackTrace]);
return Ok(new SearchdomainSettingsResults() { Settings = null, Success = false, Message = ex.Message });
}
SearchdomainSettings settings = searchdomain_.settings;
return Ok(new SearchdomainSettingsResults() { Settings = settings, Success = true });
}
[HttpGet("GetSearchCacheSize")]
public ActionResult<SearchdomainSearchCacheSizeResults> GetSearchCacheSize(string searchdomain)
{
Searchdomain searchdomain_;
try
{
searchdomain_ = _domainManager.GetSearchdomain(searchdomain);
}
catch (SearchdomainNotFoundException)
{
_logger.LogError("Unable to retrieve the searchdomain {searchdomain} - it likely does not exist yet", [searchdomain]);
return Ok(new SearchdomainSearchCacheSizeResults() { SearchCacheSizeBytes = null, Success = false, Message = "Searchdomain not found" });
}
catch (Exception ex)
{
_logger.LogError("Unable to retrieve the searchdomain {searchdomain} - {ex.Message} - {ex.StackTrace}", [searchdomain, ex.Message, ex.StackTrace]);
return Ok(new SearchdomainSearchCacheSizeResults() { SearchCacheSizeBytes = null, Success = false, Message = ex.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() { SearchCacheSizeBytes = sizeInBytes, Success = true });
}
[HttpGet("ClearSearchCache")]
public ActionResult<SearchdomainInvalidateCacheResults> InvalidateSearchCache(string searchdomain)
{
try
{
Searchdomain searchdomain_ = _domainManager.GetSearchdomain(searchdomain);
searchdomain_.InvalidateSearchCache();
} catch (SearchdomainNotFoundException)
{
_logger.LogError("Unable to invalidate search cache for searchdomain {searchdomain} - not found", [searchdomain]);
return Ok(new SearchdomainInvalidateCacheResults() { Success = false, Message = $"Unable to invalidate search cache for searchdomain {searchdomain} - not found" });
} catch (Exception ex)
{
_logger.LogError("Unable to invalidate search cache for searchdomain {searchdomain} - Exception: {ex.Message} - {ex.StackTrace}", [searchdomain, ex.Message, ex.StackTrace]);
return Ok(new SearchdomainInvalidateCacheResults() { Success = false, Message = $"Unable to invalidate search cache for searchdomain {searchdomain}" });
}
return Ok(new SearchdomainInvalidateCacheResults(){Success = true});
}
[HttpGet("GetDatabaseSize")]
public ActionResult<SearchdomainGetDatabaseSizeResult> GetDatabaseSize(string searchdomain)
{
Searchdomain searchdomain_;
try
{
searchdomain_ = _domainManager.GetSearchdomain(searchdomain);
}
catch (SearchdomainNotFoundException)
{
_logger.LogError("Unable to retrieve the searchdomain {searchdomain} - it likely does not exist yet", [searchdomain]);
return Ok(new SearchdomainGetDatabaseSizeResult() { SearchdomainDatabaseSizeBytes = null, Success = false, Message = "Searchdomain not found" });
}
catch (Exception ex)
{
_logger.LogError("Unable to retrieve the searchdomain {searchdomain} - {ex.Message} - {ex.StackTrace}", [searchdomain, ex.Message, ex.StackTrace]);
return Ok(new SearchdomainGetDatabaseSizeResult() { SearchdomainDatabaseSizeBytes = null, Success = false, Message = ex.Message });
}
long sizeInBytes = DatabaseHelper.GetSearchdomainDatabaseSize(searchdomain_.helper, searchdomain);
return Ok(new SearchdomainGetDatabaseSizeResult() { SearchdomainDatabaseSizeBytes = sizeInBytes, Success = true });
}
}

View File

@@ -1,6 +1,7 @@
using System.Data.Common;
using System.Text;
using Server.Exceptions;
using Shared.Models;
namespace Server.Helper;
@@ -27,12 +28,12 @@ public class DatabaseHelper(ILogger<DatabaseHelper> logger)
helper.ExecuteSQLNonQuery(query.ToString(), parameters);
}
public static int DatabaseInsertSearchdomain(SQLHelper helper, string name)
public static int DatabaseInsertSearchdomain(SQLHelper helper, string name, SearchdomainSettings settings = new())
{
Dictionary<string, dynamic> parameters = new()
{
{ "name", name },
{ "settings", "{}"} // TODO add settings. It's not used yet, but maybe it's needed someday...
{ "settings", settings}
};
return helper.ExecuteSQLCommandGetInsertedID("INSERT INTO searchdomain (name, settings) VALUES (@name, @settings)", parameters);
}
@@ -176,4 +177,38 @@ public class DatabaseHelper(ILogger<DatabaseHelper> logger)
return result;
}
}
public static long GetSearchdomainDatabaseSize(SQLHelper helper, string searchdomain)
{
Dictionary<string, dynamic> parameters = new()
{
{ "searchdomain", searchdomain}
};
DbDataReader searchdomainSumReader = helper.ExecuteSQLCommand("SELECT SUM(LENGTH(id) + LENGTH(name) + LENGTH(settings)) AS total_bytes FROM embeddingsearch.searchdomain WHERE name=@searchdomain", parameters);
bool success = searchdomainSumReader.Read();
long result = success && !searchdomainSumReader.IsDBNull(0) ? searchdomainSumReader.GetInt64(0) : 0;
searchdomainSumReader.Close();
DbDataReader entitySumReader = helper.ExecuteSQLCommand("SELECT SUM(LENGTH(e.id) + LENGTH(e.name) + LENGTH(e.probmethod) + LENGTH(e.id_searchdomain)) AS total_bytes FROM embeddingsearch.entity e JOIN embeddingsearch.searchdomain s ON e.id_searchdomain = s.id WHERE s.name=@searchdomain", parameters);
success = entitySumReader.Read();
result += success && !entitySumReader.IsDBNull(0) ? entitySumReader.GetInt64(0) : 0;
entitySumReader.Close();
DbDataReader datapointSumReader = helper.ExecuteSQLCommand("SELECT SUM(LENGTH(d.id) + LENGTH(d.name) + LENGTH(d.probmethod_embedding) + LENGTH(d.similaritymethod) + LENGTH(d.id_entity) + LENGTH(d.hash)) AS total_bytes FROM embeddingsearch.datapoint d JOIN embeddingsearch.entity e ON d.id_entity = e.id JOIN embeddingsearch.searchdomain s ON e.id_searchdomain = s.id WHERE s.name=@searchdomain", parameters);
success = datapointSumReader.Read();
result += success && !datapointSumReader.IsDBNull(0) ? datapointSumReader.GetInt64(0) : 0;
datapointSumReader.Close();
DbDataReader embeddingSumReader = helper.ExecuteSQLCommand("SELECT SUM(LENGTH(em.id) + LENGTH(em.id_datapoint) + LENGTH(em.model) + LENGTH(em.embedding)) AS total_bytes FROM embeddingsearch.embedding em JOIN embeddingsearch.datapoint d ON em.id_datapoint = d.id JOIN embeddingsearch.entity e ON d.id_entity = e.id JOIN embeddingsearch.searchdomain s ON e.id_searchdomain = s.id WHERE s.name=@searchdomain", parameters);
success = embeddingSumReader.Read();
result += success && !embeddingSumReader.IsDBNull(0) ? embeddingSumReader.GetInt64(0) : 0;
embeddingSumReader.Close();
DbDataReader attributeSumReader = helper.ExecuteSQLCommand("SELECT SUM(LENGTH(a.id) + LENGTH(a.id_entity) + LENGTH(a.attribute) + LENGTH(a.value)) AS total_bytes FROM embeddingsearch.attribute a JOIN embeddingsearch.entity e ON a.id_entity = e.id JOIN embeddingsearch.searchdomain s ON e.id_searchdomain = s.id WHERE s.name=@searchdomain", parameters);
success = attributeSumReader.Read();
result += success && !attributeSumReader.IsDBNull(0) ? attributeSumReader.GetInt64(0) : 0;
attributeSumReader.Close();
return result;
}
}

View File

@@ -1,5 +1,6 @@
using System.Data;
using System.Data.Common;
using System.Text.Json;
using ElmahCore.Mvc.Logger;
using MySql.Data.MySqlClient;
using Server.Helper;
@@ -14,6 +15,7 @@ public class Searchdomain
public AIProvider aIProvider;
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 List<Entity> entityCache;
public List<string> modelsInUse;
@@ -36,6 +38,7 @@ public class Searchdomain
connection = new MySqlConnection(connectionString);
connection.Open();
helper = new SQLHelper(connection, connectionString);
settings = GetSettings();
modelsInUse = []; // To make the compiler shut up - it is set in UpdateSearchDomain() don't worry // yeah, about that...
if (!runEmpty)
{
@@ -229,6 +232,19 @@ public class Searchdomain
return this.id;
}
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);
}
public void InvalidateSearchCache()
{
searchCache = [];

View File

@@ -14,12 +14,14 @@
}
<div class="container-fluid mt-4">
<h1 class="visually-hidden">embeddingsearch</h1>
<div class="row">
<!-- Sidebar -->
<div class="col-md-4 sidebar">
<div class="col-md-4 sidebar" role="complementary">
<div class="card">
<div class="card-body p-2">
<h2 class="visually-hidden">@T["Searchdomain selection"]</h2>
<ul class="list-group list-group-flush mb-2" style="max-height: 60vh; overflow-y: auto;">
@foreach (var domain in domains)
{
@@ -28,55 +30,63 @@
</li>
}
</ul>
<button class="btn btn-primary w-100">Add</button>
<button id="searchdomainCreate" class="btn btn-primary w-100">@T["Create"]</button>
</div>
</div>
</div>
<!-- Main Content -->
<div class="col col-md-8">
<div class="col col-md-8" role="main">
<div class="card section-card">
<div class="card-body">
<h2 class="visually-hidden">@T["Searchdomain information and settings"]</h2>
<h3 class="visually-hidden">@T["Actions"]</h3>
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0 text-nowrap overflow-auto">Searchdomain1</h4>
<p id="searchdomainName" class="mb-0 text-nowrap overflow-auto fs-3">Searchdomain</p>
<div class="col-md-3 text-end w-auto">
<button class="btn btn-warning btn-sm me-2">Rename</button>
<button class="btn btn-danger btn-sm">Delete</button>
<button id="searchdomainRename" class="btn btn-warning btn-sm me-2">@T["Rename"]</button>
<button id="searchdomainDelete" class="btn btn-danger btn-sm">@T["Delete"]</button>
</div>
</div>
<!-- Settings -->
<div class="row align-items-center mb-3">
<h3>@T["Settings"]</h3>
<div class="col-md-6">
<label class="form-label">Settings</label>
<input
type="text"
class="form-control"
placeholder="JSON-string"
disabled
/>
<input type="checkbox" class="form-check-input" id="searchdomainConfigCacheReconciliation" />
<label class="form-check-label" for="searchdomainConfigCacheReconciliation">@T["Cache reconsiliation"]</label>
</div>
<div class="col-md-2 mt-3 mt-md-0">
<button class="btn btn-warning w-100">Update</button>
<button class="btn btn-warning w-100" id="searchdomainConfigUpdate">@T["Update"]</button>
</div>
</div>
<h3 class="visually-hidden">@T["Search cache"]</h3>
<!-- Cache -->
<div class="d-flex align-items-center mb-4">
<div class="me-3">
<strong>Cache utilization:</strong> 2.47MiB
<strong>@T["Search cache utilization"]:</strong> <span id="cacheUtilization">0.00MiB</span>
</div>
<button class="btn btn-primary btn-sm">Reset</button>
<button id="cacheClear" class="btn btn-warning btn-sm">@T["Clear"]</button>
</div>
<h3 class="visually-hidden">@T["Database size"]</h3>
<!-- Database size -->
<div class="d-flex align-items-center mb-4">
<div class="me-3">
<strong>@T["Database size"]:</strong> <span id="databaseUtilization">0.00MiB</span>
</div>
<button id="cacheClear" class="btn btn-warning btn-sm">@T["Clear"]</button>
</div>
<!-- Recent Queries -->
<div class="card section-card mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<strong>Recent queries</strong>
<h3>Recent queries</h3>
<input
type="text"
class="form-control form-control-sm w-25"
@@ -101,7 +111,7 @@
<div class="card section-card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<strong>Entities</strong>
<h3>Entities</h3>
<input
id="entitiesFilter"
type="text"
@@ -134,19 +144,19 @@
<div class="modal fade" id="entityDetailsModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="entityDetailsTitle">Entity Details</h5>
<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>
</div>
<div class="modal-body">
<!-- Attributes -->
<h6>Attributes</h6>
<h3 class="fs-4">@T["Attributes"]</h3>
<table class="table table-sm table-bordered mb-4">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th>@T["Key"]</th>
<th>@T["Value"]</th>
</tr>
</thead>
<tbody id="entityAttributesBody">
@@ -154,13 +164,13 @@
</table>
<!-- Datapoints -->
<h6>Datapoints</h6>
<h3 class="fs-4">@T["Datapoints"]</h3>
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Name</th>
<th>ProbMethod</th>
<th>SimilarityMethod</th>
<th>@T["Name"]</th>
<th>@T["ProbMethod"]</th>
<th>@T["SimilarityMethod"]</th>
</tr>
</thead>
<tbody id="entityDatapointsBody">
@@ -170,7 +180,7 @@
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Close
@T["Close"]
</button>
</div>
</div>
@@ -182,19 +192,19 @@
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="queryDetailsTitle">Query Details</h5>
<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>
</div>
<div class="modal-body">
<!-- Access times -->
<h6>Access times</h6>
<h3>Access times</h3>
<ul id="queryAccessTimes" class="list-group mb-4"></ul>
<!-- Results -->
<h6>Results</h6>
<h3>Results</h3>
<table class="table table-sm table-striped">
<thead>
<tr>
@@ -217,6 +227,92 @@
</div>
</div>
<!-- Rename searchdomain Modal -->
<div class="modal fade" id="renameSearchdomainModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-m modal-dialog-scrollable">
<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>
</div>
<div class="modal-body">
<!-- New name -->
<div class="mb-3">
<label for="renameSearchdomainNewName" class="form-label">New name</label>
<input type="text" class="form-control" id="renameSearchdomainNewName" />
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" onclick="renameSearchdomain(getSelectedDomainKey(), document.getElementById('renameSearchdomainNewName').value)" data-bs-dismiss="modal">
Rename
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Close
</button>
</div>
</div>
</div>
</div>
<!-- Delete searchdomain Modal -->
<div class="modal fade" id="deleteSearchdomainModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-m modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h2 class="modal-title" id="deleteSearchdomainTitle">@T["Delete searchdomain"]</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>@T["Are you sure you want to delete this searchdomain? This action cannot be undone."]</p>
</div>
<div class="modal-footer">
<button type="button" id="searchdomainConfirmDelete" class="btn btn-danger" data-bs-dismiss="modal">
Delete
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Close
</button>
</div>
</div>
</div>
</div>
<!-- Create searchdomain Modal -->
<div class="modal fade" id="createSearchdomainModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-m modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h2 class="modal-title" id="createSearchdomainTitle">@T["Create searchdomain"]</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</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 reconsiliation"]</label>
</div>
<div class="modal-footer">
<button type="button" id="searchdomainConfirmCreate" class="btn btn-primary" data-bs-dismiss="modal">
@T["Create"]
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
@T["Close"]
</button>
</div>
</div>
</div>
</div>
<script>
@@ -245,8 +341,185 @@
queriesFilter.addEventListener('input', () => {
populateQueriesTable(queriesFilter.value);
});
selectDomain(0);
document
.getElementById('searchdomainRename')
.addEventListener('click', () => {
const modal = new bootstrap.Modal(
document.getElementById('renameSearchdomainModal')
);
// Fill in searchdomain current name
const domainKey = getSelectedDomainKey();
document.getElementById(
'renameSearchdomainNewName'
).value = domains[domainKey];
modal.show();
});
document
.getElementById('searchdomainDelete')
.addEventListener('click', () => {
const modal = new bootstrap.Modal(
document.getElementById('deleteSearchdomainModal')
);
modal.show();
});
document
.getElementById('searchdomainConfirmDelete')
.addEventListener('click', () => {
const domainKey = getSelectedDomainKey();
deleteSearchdomain(domainKey);
selectDomain(0);
});
document
.getElementById('searchdomainConfigUpdate')
.addEventListener('click', () => {
const domainKey = getSelectedDomainKey();
const cacheReconciliation = document.getElementById('searchdomainConfigCacheReconciliation').checked;
updateSearchdomainConfig(domainKey, { CacheReconciliation: cacheReconciliation});
});
document
.getElementById('searchdomainCreate')
.addEventListener('click', () => {
const modal = new bootstrap.Modal(
document.getElementById('createSearchdomainModal')
);
modal.show();
});
document
.getElementById('searchdomainConfirmCreate')
.addEventListener('click', () => {
const modal = new bootstrap.Modal(
document.getElementById('createSearchdomainModal')
);
const name = document.getElementById('createSearchdomainName').value;
const cacheReconciliation = document.getElementById('createSearchdomainWithCacheReconciliation').checked;
const settings = { CacheReconciliation: cacheReconciliation };
// Implement create logic here
fetch(`/Searchdomain/Create?searchdomain=${encodeURIComponent(name)}&settings=${JSON.stringify(settings)}`, {
method: 'GET'
}).then(response => {
if (response.ok) {
// TODO add toast
console.log('Searchdomain created successfully');
// Reload the page to show the new searchdomain
location.reload();
} else {
// TODO add toast
console.error('Failed to create searchdomain');
}
}).catch(error => {
console.error('Error creating searchdomain:', error);
});
});
document
.getElementById('cacheClear')
.addEventListener('click', () => {
const domainKey = getSelectedDomainKey();
fetch(`/Searchdomain/ClearSearchCache?searchdomain=${encodeURIComponent(domains[domainKey])}`, {
method: 'GET'
}).then(response => {
if (response.ok) {
// TODO add toast
console.log('Searchdomain cache cleared successfully');
// Update cache utilization display
document.querySelector('#cacheUtilization').innerText = '0.00MiB';
} else {
// TODO add toast
console.error('Failed to clear searchdomain cache');
}
}).catch(error => {
console.error('Error clearing searchdomain cache:', error);
});
});
});
function deleteSearchdomain(domainKey) {
// Implement delete logic here
fetch(`/Searchdomain/Delete?searchdomain=${encodeURI(domains[domainKey])}`, {
method: 'GET'
}).then(response => {
if (response.ok) {
// TODO add toast
// Remove from sidebar
var domainItem = document.getElementById('sidebar_domain_' + domainKey);
domainItem.remove();
console.log('Searchdomain deleted successfully');
} else {
// TODO add toast
console.error('Failed to delete searchdomain');
}
}).catch(error => {
console.error('Error deleting searchdomain:', error);
});
}
function renameSearchdomain(domainKey, newName) {
// Implement rename logic here
fetch(`/Searchdomain/Update?searchdomain=${encodeURI(domains[domainKey])}&newName=${newName}`, {
method: 'GET'
}).then(response => {
if (response.ok) {
// TODO add toast
// Update sidebar and header name
var domainItem = document.getElementById('sidebar_domain_' + domainKey);
domainItem.innerText = newName;
document.querySelector('.section-card h3').innerText = newName;
domains[domainKey] = newName;
console.log('Searchdomain renamed successfully');
} else {
// TODO add toast
console.error('Failed to rename searchdomain');
}
}).catch(error => {
console.error('Error renaming searchdomain:', error);
});
}
function updateSearchdomainConfig(domainKey, newSettings) {
// Implement update logic here
fetch(`/Searchdomain/UpdateSettings?searchdomain=${encodeURIComponent(domains[domainKey])}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newSettings)
}).then(response => {
if (response.ok) {
// TODO add toast
console.log('Searchdomain settings updated successfully');
} else {
// TODO add toast
console.error('Failed to update searchdomain settings');
}
}).catch(error => {
console.error('Error updating searchdomain settings:', error);
});
}
function getSelectedDomainKey() {
return document.querySelector('.domain-item.active').id.split("_")[2] - 0;
}
function getSearchdomainConfig(domainKey) {
return fetch(`/Searchdomain/GetSettings?searchdomain=${encodeURIComponent(domains[domainKey])}`)
.then(r => r.json());
}
function getSearchdomainCacheUtilization(domainKey) {
return fetch(`/Searchdomain/GetSearchCacheSize?searchdomain=${encodeURIComponent(domains[domainKey])}`)
.then(r => r.json());
}
function getSearchdomainDatabaseUtilization(domainKey) {
return fetch(`/Searchdomain/GetDatabaseSize?searchdomain=${encodeURIComponent(domains[domainKey])}`)
.then(r => r.json());
}
function selectDomain(domainKey) {
document.querySelectorAll('.domain-item').forEach(item => {
item.classList.remove('active');
@@ -256,7 +529,13 @@
selectedItem.classList.add('active');
var domainName = domains[domainKey];
document.querySelector('.section-card h4').innerText = domainName;
document.querySelector('#searchdomainName').innerText = domainName;
let searchdomainConfigPromise = getSearchdomainConfig(getSelectedDomainKey());
let configElementCacheReconsiliation = document.getElementById('searchdomainConfigCacheReconciliation');
let cacheUtilizationPromise = getSearchdomainCacheUtilization(getSelectedDomainKey());
let databaseUtilizationPromise = getSearchdomainDatabaseUtilization(getSelectedDomainKey());
/* ---------- ENTITIES ---------- */
let entitiesUrl = `/Entity/List?searchdomain=${encodeURIComponent(domainName)}&returnEmbeddings=false`;
@@ -294,6 +573,41 @@
console.error('Error fetching queries:', err);
hideQueriesLoading(queriesCard);
});
searchdomainConfigPromise.then(searchdomainConfig => {
if (searchdomainConfig != null && searchdomainConfig.Settings != null)
{
console.log(searchdomainConfig);
configElementCacheReconsiliation.checked = searchdomainConfig.Settings.CacheReconciliation;
configElementCacheReconsiliation.disabled = false;
} else {
//configElement.value = 'Error fetching searchdomain config';
configElementCacheReconsiliation.disabled = true;
// TODO add toast
console.error('Failed to fetch searchdomain config');
}
});
cacheUtilizationPromise.then(cacheUtilization => {
if (cacheUtilization != null && cacheUtilization.SearchCacheSizeBytes != null)
{
document.querySelector('#cacheUtilization').innerText =
`${(cacheUtilization.SearchCacheSizeBytes / (1024 * 1024)).toFixed(2)}MiB`;
} else {
// TODO add toast
console.error('Failed to fetch searchdomain cache utilization');
}
});
databaseUtilizationPromise.then(databaseUtilization => {
if (databaseUtilization != null && databaseUtilization.SearchdomainDatabaseSizeBytes != null)
{
document.querySelector('#databaseUtilization').innerText =
`${(databaseUtilization.SearchdomainDatabaseSizeBytes / (1024 * 1024)).toFixed(2)}MiB`;
} else {
// TODO add toast
console.error('Failed to fetch searchdomain database utilization');
}
});
}
function clearEntitiesTable() {
@@ -313,6 +627,7 @@
const normalizedFilter = filterText.toLowerCase();
let isFirstEntity = true;
entities
.filter(e => e.Name.toLowerCase().includes(normalizedFilter))
.forEach(entity => {
@@ -320,11 +635,15 @@
var nameCell = document.createElement('td');
nameCell.textContent = entity.Name;
if (isFirstEntity) {
nameCell.classList.add('w-100'); // Otherwise the table doesn't use the full width
isFirstEntity = false;
}
row.appendChild(nameCell);
var actionCell = document.createElement('td');
var detailsButton = document.createElement('button');
detailsButton.className = 'btn btn-primary btn-sm';
detailsButton.className = 'btn btn-info btn-sm';
detailsButton.textContent = 'Details';
detailsButton.setAttribute("data-index", entities.findIndex(en => en == entity));
detailsButton.addEventListener('click', () => {
@@ -341,6 +660,7 @@
function populateQueriesTable(filterText = '') {
if (!queries) return;
const tbody = document.querySelector('#queriesTable tbody');
tbody.innerHTML = '';
@@ -358,7 +678,7 @@
const actionCell = document.createElement('td');
const btn = document.createElement('button');
btn.className = 'btn btn-sm btn-primary';
btn.className = 'btn btn-sm btn-info';
btn.textContent = '@T["Details"]';
btn.addEventListener('click', () => {
showQueryDetails(query);
@@ -369,6 +689,9 @@
tbody.appendChild(row);
});
}
function flagSearchdomainAsErroneous(domainKey) {
@@ -447,8 +770,7 @@
function showQueryDetails(query) {
// Title
document.getElementById('queryDetailsTitle').innerText =
`Query: ${query.Name}`;
document.getElementById('queryDetailsQueryName').innerText = query.Name;
/* ---------- Access times ---------- */
const accessList = document.getElementById('queryAccessTimes');

View File

@@ -44,4 +44,8 @@ body {
.d-none {
display: none;
}
}
.modal-title {
font-size: 1.25rem;
}

View File

@@ -8,6 +8,25 @@ public readonly struct ResultItem(float score, string name)
public readonly float Score { get; } = score;
[JsonPropertyName("Name")]
public readonly string Name { get; } = name;
public static long EstimateSize(ResultItem item)
{
long size = 0;
// string object
if (item.Name != null)
{
size += MemorySizes.ObjectHeader;
size += sizeof(int); // string length
size += item.Name.Length * sizeof(char);
size = Align(size);
}
return size;
}
private static long Align(long size)
=> (size + 7) & ~7; // 8-byte alignment
}
public struct DateTimedSearchResult(DateTime dateTime, List<ResultItem> results)
@@ -16,4 +35,72 @@ public struct DateTimedSearchResult(DateTime dateTime, List<ResultItem> results)
public List<DateTime> AccessDateTimes { get; set; } = [dateTime];
[JsonPropertyName("Results")]
public List<ResultItem> Results { get; set; } = results;
public long EstimateSize()
{
long size = 0;
size += EstimateDateTimeList(AccessDateTimes);
size += EstimateResultItemList(Results);
return size;
}
private static long EstimateDateTimeList(List<DateTime>? list)
{
if (list == null)
return 0;
long size = 0;
// List object
size += MemorySizes.ObjectHeader;
size += MemorySizes.Reference; // reference to array
// Internal array
size += MemorySizes.ArrayHeader;
size += list.Capacity * sizeof(long); // DateTime = 8 bytes
return size;
}
private static long EstimateResultItemList(List<ResultItem>? list)
{
if (list == null)
return 0;
long size = 0;
// List object
size += MemorySizes.ObjectHeader;
size += MemorySizes.Reference;
// Internal array of structs
size += MemorySizes.ArrayHeader;
int resultItemInlineSize = sizeof(float) + IntPtr.Size; // float + string reference
size += list.Capacity * resultItemInlineSize;
// Heap allocations referenced by ResultItem
foreach (var item in list)
size += ResultItem.EstimateSize(item);
return size;
}
}
public struct SearchdomainSettings(bool cacheReconciliation = false)
{
[JsonPropertyName("CacheReconciliation")]
public bool CacheReconciliation { get; set; } = cacheReconciliation;
}
internal static class MemorySizes
{
public static readonly int PointerSize = IntPtr.Size;
public static readonly int ObjectHeader = PointerSize * 2;
public static readonly int Reference = PointerSize;
public static readonly int ArrayHeader = Align(ObjectHeader + sizeof(int));
public static int Align(int size)
=> (size + PointerSize - 1) & ~(PointerSize - 1);
}

View File

@@ -54,3 +54,49 @@ public class SearchdomainSearchesResults
[JsonPropertyName("Searches")]
public required Dictionary<string, DateTimedSearchResult> Searches { get; set; }
}
public class SearchdomainSettingsResults
{
[JsonPropertyName("Success")]
public required bool Success { get; set; }
[JsonPropertyName("Message")]
public string? Message { get; set; }
[JsonPropertyName("Settings")]
public required SearchdomainSettings? Settings { get; set; }
}
public class SearchdomainSearchCacheSizeResults
{
[JsonPropertyName("Success")]
public required bool Success { get; set; }
[JsonPropertyName("Message")]
public string? Message { get; set; }
[JsonPropertyName("SearchCacheSizeBytes")]
public required long? SearchCacheSizeBytes { get; set; }
}
public class SearchdomainInvalidateCacheResults
{
[JsonPropertyName("Success")]
public required bool Success { get; set; }
[JsonPropertyName("Message")]
public string? Message { get; set; }
}
public class SearchdomainGetDatabaseSizeResult
{
[JsonPropertyName("Success")]
public required bool Success { get; set; }
[JsonPropertyName("Message")]
public string? Message { get; set; }
[JsonPropertyName("SearchdomainDatabaseSizeBytes")]
public required long? SearchdomainDatabaseSizeBytes { get; set; }
}