10 Commits

10 changed files with 862 additions and 203 deletions

View File

@@ -55,10 +55,7 @@ public class EntityController : ControllerBase
try try
{ {
List<Entity>? entities = _searchdomainHelper.EntitiesFromJSON( List<Entity>? entities = _searchdomainHelper.EntitiesFromJSON(
[], _domainManager,
_domainManager.embeddingCache,
_domainManager.aIProvider,
_domainManager.helper,
_logger, _logger,
JsonSerializer.Serialize(jsonEntities)); JsonSerializer.Serialize(jsonEntities));
if (entities is not null && jsonEntities is not null) if (entities is not null && jsonEntities is not null)
@@ -92,8 +89,13 @@ public class EntityController : ControllerBase
} }
[HttpGet("List")] [HttpGet("List")]
public ActionResult<EntityListResults> List(string searchdomain, bool returnEmbeddings = false) public ActionResult<EntityListResults> List(string searchdomain, bool returnModels = false, bool returnEmbeddings = false)
{ {
if (returnEmbeddings && !returnModels)
{
_logger.LogError("Invalid request for {searchdomain} - embeddings return requested but without models - not possible!", [searchdomain]);
return Ok(new EntityQueryResults() {Results = [], Success = false, Message = "Invalid request" });
}
Searchdomain searchdomain_; Searchdomain searchdomain_;
try try
{ {
@@ -118,12 +120,12 @@ public class EntityController : ControllerBase
List<DatapointResult> datapointResults = []; List<DatapointResult> datapointResults = [];
foreach (Datapoint datapoint in entity.datapoints) foreach (Datapoint datapoint in entity.datapoints)
{ {
if (returnEmbeddings) if (returnModels)
{ {
List<EmbeddingResult> embeddingResults = []; List<EmbeddingResult> embeddingResults = [];
foreach ((string, float[]) embedding in datapoint.embeddings) foreach ((string, float[]) embedding in datapoint.embeddings)
{ {
embeddingResults.Add(new EmbeddingResult() {Model = embedding.Item1, Embeddings = embedding.Item2}); embeddingResults.Add(new EmbeddingResult() {Model = embedding.Item1, Embeddings = returnEmbeddings ? embedding.Item2 : []});
} }
datapointResults.Add(new DatapointResult() {Name = datapoint.name, ProbMethod = datapoint.probMethod.name, SimilarityMethod = datapoint.similarityMethod.name, Embeddings = embeddingResults}); datapointResults.Add(new DatapointResult() {Name = datapoint.name, ProbMethod = datapoint.probMethod.name, SimilarityMethod = datapoint.similarityMethod.name, Embeddings = embeddingResults});
} }
@@ -135,6 +137,7 @@ public class EntityController : ControllerBase
EntityListResult entityListResult = new() EntityListResult entityListResult = new()
{ {
Name = entity.name, Name = entity.name,
ProbMethod = entity.probMethodName,
Attributes = attributeResults, Attributes = attributeResults,
Datapoints = datapointResults Datapoints = datapointResults
}; };

View File

@@ -1,5 +1,6 @@
using System.Text.Json; using System.Text.Json;
using ElmahCore; using ElmahCore;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Server.Exceptions; using Server.Exceptions;
using Server.Helper; using Server.Helper;
@@ -156,6 +157,64 @@ public class SearchdomainController : ControllerBase
return Ok(new SearchdomainSearchesResults() { Searches = searchCache, Success = true }); return Ok(new SearchdomainSearchesResults() { Searches = searchCache, Success = true });
} }
[HttpDelete("Searches")]
public ActionResult<SearchdomainDeleteSearchResult> DeleteSearch(string searchdomain, string query)
{
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 SearchdomainDeleteSearchResult() { 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 SearchdomainDeleteSearchResult() { Success = false, Message = ex.Message });
}
Dictionary<string, DateTimedSearchResult> searchCache = searchdomain_.searchCache;
bool containsKey = searchCache.ContainsKey(query);
if (containsKey)
{
searchCache.Remove(query);
return Ok(new SearchdomainDeleteSearchResult() {Success = true});
}
return Ok(new SearchdomainDeleteSearchResult() {Success = false, Message = "Query not found in search cache"});
}
[HttpPatch("Searches")]
public ActionResult<SearchdomainUpdateSearchResult> UpdateSearch(string searchdomain, string query, [FromBody]List<ResultItem> results)
{
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 SearchdomainUpdateSearchResult() { 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 SearchdomainUpdateSearchResult() { Success = false, Message = ex.Message });
}
Dictionary<string, DateTimedSearchResult> searchCache = searchdomain_.searchCache;
bool containsKey = searchCache.ContainsKey(query);
if (containsKey)
{
DateTimedSearchResult element = searchCache[query];
element.Results = results;
searchCache[query] = element;
return Ok(new SearchdomainUpdateSearchResult() {Success = true});
}
return Ok(new SearchdomainUpdateSearchResult() {Success = false, Message = "Query not found in search cache"});
}
[HttpGet("GetSettings")] [HttpGet("GetSettings")]
public ActionResult<SearchdomainSettingsResults> GetSettings(string searchdomain) public ActionResult<SearchdomainSettingsResults> GetSettings(string searchdomain)
{ {

View File

@@ -1,9 +1,10 @@
namespace Server; namespace Server;
public class Entity(Dictionary<string, string> attributes, Probmethods.probMethodDelegate probMethod, List<Datapoint> datapoints, string name) public class Entity(Dictionary<string, string> attributes, Probmethods.probMethodDelegate probMethod, string probMethodName, List<Datapoint> datapoints, string name)
{ {
public Dictionary<string, string> attributes = attributes; public Dictionary<string, string> attributes = attributes;
public Probmethods.probMethodDelegate probMethod = probMethod; public Probmethods.probMethodDelegate probMethod = probMethod;
public string probMethodName = probMethodName;
public List<Datapoint> datapoints = datapoints; public List<Datapoint> datapoints = datapoints;
public int id; public int id;
public string name = name; public string name = name;

View File

@@ -44,8 +44,12 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
return null; return null;
} }
public List<Entity>? EntitiesFromJSON(List<Entity> entityCache, Dictionary<string, Dictionary<string, float[]>> embeddingCache, AIProvider aIProvider, SQLHelper helper, ILogger logger, string json) public List<Entity>? EntitiesFromJSON(SearchdomainManager searchdomainManager, ILogger logger, string json)
{ {
Dictionary<string, Dictionary<string, float[]>> embeddingCache = searchdomainManager.embeddingCache;
AIProvider aIProvider = searchdomainManager.aIProvider;
SQLHelper helper = searchdomainManager.helper;
List<JSONEntity>? jsonEntities = JsonSerializer.Deserialize<List<JSONEntity>>(json); List<JSONEntity>? jsonEntities = JsonSerializer.Deserialize<List<JSONEntity>>(json);
if (jsonEntities is null) if (jsonEntities is null)
{ {
@@ -72,8 +76,7 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
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 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 => Parallel.ForEach(jsonEntities, parallelOptions, jSONEntity =>
{ {
using var tempHelper = helper.DuplicateConnection(); var entity = EntityFromJSON(searchdomainManager, logger, jSONEntity);
var entity = EntityFromJSON(entityCache, embeddingCache, aIProvider, tempHelper, logger, jSONEntity);
if (entity is not null) if (entity is not null)
{ {
retVal.Enqueue(entity); retVal.Enqueue(entity);
@@ -82,87 +85,188 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
return [.. retVal]; return [.. retVal];
} }
public Entity? EntityFromJSON(List<Entity> entityCache, Dictionary<string, Dictionary<string, float[]>> embeddingCache, AIProvider aIProvider, SQLHelper helper, ILogger logger, JSONEntity jsonEntity) //string json) public Entity? EntityFromJSON(SearchdomainManager searchdomainManager, ILogger logger, JSONEntity jsonEntity) //string json)
{ {
Dictionary<string, Dictionary<string, float[]>> embeddingsLUT = []; // embeddingsLUT: hash -> model -> [embeddingValues * n] SQLHelper helper = searchdomainManager.helper.DuplicateConnection();
int? preexistingEntityID = _databaseHelper.GetEntityID(helper, jsonEntity.Name, jsonEntity.Searchdomain); Searchdomain searchdomain = searchdomainManager.GetSearchdomain(jsonEntity.Searchdomain);
if (preexistingEntityID is not null) List<Entity> entityCache = searchdomain.entityCache;
AIProvider aIProvider = searchdomain.aIProvider;
Dictionary<string, Dictionary<string, float[]>> embeddingCache = searchdomain.embeddingCache;
Entity? preexistingEntity = entityCache.FirstOrDefault(entity => entity.name == jsonEntity.Name);
if (preexistingEntity is not null)
{ {
lock (helper.connection) // TODO change this to helper and do A/B tests (i.e. before/after) int? preexistingEntityID = _databaseHelper.GetEntityID(helper, jsonEntity.Name, jsonEntity.Searchdomain);
if (preexistingEntityID is null)
{ {
Dictionary<string, dynamic> parameters = new() _logger.LogCritical("Unable to index entity {jsonEntity.Name} because it already exists in the searchdomain but not in the database.", [jsonEntity.Name]);
{ throw new Exception($"Unable to index entity {jsonEntity.Name} because it already exists in the searchdomain but not in the database.");
{ "id", preexistingEntityID }
};
System.Data.Common.DbDataReader reader = helper.ExecuteSQLCommand("SELECT e.model, e.embedding, d.hash FROM datapoint d JOIN embedding e ON d.id = e.id_datapoint WHERE d.id_entity = @id", parameters);
while (reader.Read())
{
string model = reader.GetString(0);
long length = reader.GetBytes(1, 0, null, 0, 0);
byte[] embeddingBytes = new byte[length];
reader.GetBytes(1, 0, embeddingBytes, 0, (int)length);
float[] embeddingValues = FloatArrayFromBytes(embeddingBytes);
string hash = reader.GetString(2);
if (!embeddingsLUT.ContainsKey(hash))
{
embeddingsLUT[hash] = [];
}
embeddingsLUT[hash].TryAdd(model, embeddingValues);
}
reader.Close();
} }
_databaseHelper.RemoveEntity(entityCache, helper, jsonEntity.Name, jsonEntity.Searchdomain); // TODO only remove entity if there is actually a change somewhere. Perhaps create 3 datapoint lists to operate with: 1. delete, 2. update, 3. create Dictionary<string, string> attributes = jsonEntity.Attributes;
}
int id_entity = DatabaseHelper.DatabaseInsertEntity(helper, jsonEntity.Name, jsonEntity.Probmethod, _databaseHelper.GetSearchdomainID(helper, jsonEntity.Searchdomain)); // Attribute
foreach (KeyValuePair<string, string> attribute in jsonEntity.Attributes) foreach (KeyValuePair<string, string> attributesKV in preexistingEntity.attributes.ToList())
{
DatabaseHelper.DatabaseInsertAttribute(helper, attribute.Key, attribute.Value, id_entity); // TODO implement bulk insert to reduce number of queries
}
List<Datapoint> datapoints = [];
foreach (JSONDatapoint jsonDatapoint in jsonEntity.Datapoints)
{
string hash = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(jsonDatapoint.Text)));
Dictionary<string, float[]> embeddings = [];
if (embeddingsLUT.ContainsKey(hash))
{ {
Dictionary<string, float[]> hashLUT = embeddingsLUT[hash]; string oldAttributeKey = attributesKV.Key;
foreach (string model in jsonDatapoint.Model) string oldAttribute = attributesKV.Value;
bool newHasAttribute = jsonEntity.Attributes.TryGetValue(oldAttributeKey, out string? newAttribute);
if (newHasAttribute && newAttribute is not null && newAttribute != oldAttribute)
{ {
if (hashLUT.ContainsKey(model)) // Attribute - Updated
Dictionary<string, dynamic> parameters = new()
{ {
embeddings.Add(model, hashLUT[model]); { "newValue", newAttribute },
{ "entityId", preexistingEntityID },
{ "attribute", oldAttributeKey}
};
helper.ExecuteSQLNonQuery("UPDATE attribute SET value=@newValue WHERE id_entity=@entityId AND attribute=@attribute", parameters);
preexistingEntity.attributes[oldAttributeKey] = newAttribute;
} else if (!newHasAttribute)
{
// Attribute - Deleted
Dictionary<string, dynamic> parameters = new()
{
{ "entityId", preexistingEntityID },
{ "attribute", oldAttributeKey}
};
helper.ExecuteSQLNonQuery("DELETE FROM attribute WHERE id_entity=@entityId AND attribute=@attribute", parameters);
preexistingEntity.attributes.Remove(oldAttributeKey);
}
}
foreach (var attributesKV in jsonEntity.Attributes)
{
string newAttributeKey = attributesKV.Key;
string newAttribute = attributesKV.Value;
bool preexistingHasAttribute = preexistingEntity.attributes.TryGetValue(newAttributeKey, out string? preexistingAttribute);
if (!preexistingHasAttribute)
{
// Attribute - New
DatabaseHelper.DatabaseInsertAttribute(helper, newAttributeKey, newAttribute, (int)preexistingEntityID);
preexistingEntity.attributes.Add(newAttributeKey, newAttribute);
}
}
// Datapoint
foreach (Datapoint datapoint in preexistingEntity.datapoints.ToList())
{
bool newEntityHasDatapoint = jsonEntity.Datapoints.Any(x => x.Name == datapoint.name);
if (!newEntityHasDatapoint)
{
// Datapoint - Deleted
Dictionary<string, dynamic> parameters = new()
{
{ "datapointName", datapoint.name },
{ "entityId", preexistingEntityID}
};
helper.ExecuteSQLNonQuery("DELETE e FROM embedding e JOIN datapoint d ON e.id_datapoint=d.id WHERE d.name=@datapointName AND d.id_entity=@entityId", parameters);
helper.ExecuteSQLNonQuery("DELETE FROM datapoint WHERE id_entity=@entityId AND name=@datapointName", parameters);
preexistingEntity.datapoints.Remove(datapoint);
} else
{
JSONDatapoint? newEntityDatapoint = jsonEntity.Datapoints.FirstOrDefault(x => x.Name == datapoint.name);
if (newEntityDatapoint is not null && newEntityDatapoint.Text is not null)
{
// Datapoint - Updated (text)
Dictionary<string, dynamic> parameters = new()
{
{ "datapointName", datapoint.name },
{ "entityId", preexistingEntityID}
};
helper.ExecuteSQLNonQuery("DELETE e FROM embedding e JOIN datapoint d ON e.id_datapoint=d.id WHERE d.name=@datapointName AND d.id_entity=@entityId", parameters);
helper.ExecuteSQLNonQuery("DELETE FROM datapoint WHERE id_entity=@entityId AND name=@datapointName", parameters);
preexistingEntity.datapoints.Remove(datapoint);
Datapoint newDatapoint = DatabaseInsertDatapointWithEmbeddings(helper, searchdomain, newEntityDatapoint, (int)preexistingEntityID);
preexistingEntity.datapoints.Add(newDatapoint);
} }
else if (newEntityDatapoint is not null && (newEntityDatapoint.Probmethod_embedding != datapoint.probMethod.name || newEntityDatapoint.SimilarityMethod != datapoint.similarityMethod.name))
{ {
var additionalEmbeddings = Datapoint.GenerateEmbeddings(jsonDatapoint.Text, [model], aIProvider, embeddingCache); // Datapoint - Updated (probmethod or similaritymethod)
embeddings.Add(model, additionalEmbeddings.First().Value); Dictionary<string, dynamic> parameters = new()
{
{ "probmethod", newEntityDatapoint.Probmethod_embedding },
{ "similaritymethod", newEntityDatapoint.SimilarityMethod },
{ "datapointName", datapoint.name },
{ "entityId", preexistingEntityID}
};
helper.ExecuteSQLNonQuery("UPDATE datapoint SET probmethod_embedding=@probmethod, similaritymethod=@similaritymethod WHERE id_entity=@entityId AND name=@datapointName", parameters);
Datapoint preexistingDatapoint = preexistingEntity.datapoints.First(x => x == datapoint); // The for loop is a copy. This retrieves the original such that it can be updated.
preexistingDatapoint.probMethod = datapoint.probMethod;
preexistingDatapoint.similarityMethod = datapoint.similarityMethod;
} }
} }
} }
else foreach (JSONDatapoint jsonDatapoint in jsonEntity.Datapoints)
{ {
embeddings = Datapoint.GenerateEmbeddings(jsonDatapoint.Text, [.. jsonDatapoint.Model], aIProvider, embeddingCache); bool oldEntityHasDatapoint = preexistingEntity.datapoints.Any(x => x.name == jsonDatapoint.Name);
if (!oldEntityHasDatapoint)
{
// Datapoint - New
Datapoint datapoint = DatabaseInsertDatapointWithEmbeddings(helper, searchdomain, jsonDatapoint, (int)preexistingEntityID);
preexistingEntity.datapoints.Add(datapoint);
}
} }
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);
Datapoint datapoint = new(jsonDatapoint.Name, probMethod_embedding, similarityMethod, hash, [.. embeddings.Select(kv => (kv.Key, kv.Value))]);
int id_datapoint = DatabaseHelper.DatabaseInsertDatapoint(helper, jsonDatapoint.Name, jsonDatapoint.Probmethod_embedding, jsonDatapoint.SimilarityMethod, hash, id_entity); // TODO make this a bulk add action to reduce number of queries
List<(string model, byte[] embedding)> data = [];
foreach ((string, float[]) embedding in datapoint.embeddings)
{
data.Add((embedding.Item1, BytesFromFloatArray(embedding.Item2)));
}
DatabaseHelper.DatabaseInsertEmbeddingBulk(helper, id_datapoint, data);
datapoints.Add(datapoint);
}
var probMethod = Probmethods.GetMethod(jsonEntity.Probmethod) ?? throw new ProbMethodNotFoundException(jsonEntity.Probmethod);
Entity entity = new(jsonEntity.Attributes, probMethod, datapoints, jsonEntity.Name) return preexistingEntity;
}
else
{ {
id = id_entity int id_entity = DatabaseHelper.DatabaseInsertEntity(helper, jsonEntity.Name, jsonEntity.Probmethod, _databaseHelper.GetSearchdomainID(helper, jsonEntity.Searchdomain));
}; foreach (KeyValuePair<string, string> attribute in jsonEntity.Attributes)
entityCache.Add(entity); {
return entity; DatabaseHelper.DatabaseInsertAttribute(helper, attribute.Key, attribute.Value, id_entity); // TODO implement bulk insert to reduce number of queries
}
List<Datapoint> datapoints = [];
foreach (JSONDatapoint jsonDatapoint in jsonEntity.Datapoints)
{
string hash = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(jsonDatapoint.Text)));
Datapoint datapoint = DatabaseInsertDatapointWithEmbeddings(helper, searchdomain, jsonDatapoint, id_entity, hash);
datapoints.Add(datapoint);
}
var probMethod = Probmethods.GetMethod(jsonEntity.Probmethod) ?? throw new ProbMethodNotFoundException(jsonEntity.Probmethod);
Entity entity = new(jsonEntity.Attributes, probMethod, jsonEntity.Probmethod, datapoints, jsonEntity.Name)
{
id = id_entity
};
entityCache.Add(entity);
return entity;
}
}
public Datapoint DatabaseInsertDatapointWithEmbeddings(SQLHelper helper, Searchdomain searchdomain, JSONDatapoint jsonDatapoint, int id_entity, string? hash = null)
{
if (jsonDatapoint.Text is null)
{
throw new Exception("jsonDatapoint.Text must not be null at this point");
}
hash ??= Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(jsonDatapoint.Text)));
Datapoint datapoint = BuildDatapointFromJsonDatapoint(jsonDatapoint, id_entity, searchdomain, hash);
int id_datapoint = DatabaseHelper.DatabaseInsertDatapoint(helper, jsonDatapoint.Name, jsonDatapoint.Probmethod_embedding, jsonDatapoint.SimilarityMethod, hash, id_entity); // TODO make this a bulk add action to reduce number of queries
List<(string model, byte[] embedding)> data = [];
foreach ((string, float[]) embedding in datapoint.embeddings)
{
data.Add((embedding.Item1, BytesFromFloatArray(embedding.Item2)));
}
DatabaseHelper.DatabaseInsertEmbeddingBulk(helper, id_datapoint, data);
return datapoint;
}
public Datapoint BuildDatapointFromJsonDatapoint(JSONDatapoint jsonDatapoint, int entityId, Searchdomain searchdomain, string? hash = null)
{
if (jsonDatapoint.Text is null)
{
throw new Exception("jsonDatapoint.Text must not be null at this point");
}
using SQLHelper helper = searchdomain.helper.DuplicateConnection();
Dictionary<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);
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))]);
} }
} }

View File

@@ -142,7 +142,7 @@ public class Searchdomain
Probmethods.probMethodDelegate? probmethod = Probmethods.GetMethod(probmethodString); Probmethods.probMethodDelegate? probmethod = Probmethods.GetMethod(probmethodString);
if (datapoint_unassigned.TryGetValue(id, out List<Datapoint>? datapoints) && probmethod is not null) if (datapoint_unassigned.TryGetValue(id, out List<Datapoint>? datapoints) && probmethod is not null)
{ {
Entity entity = new(attributes, probmethod, datapoints, name) Entity entity = new(attributes, probmethod, probmethodString, datapoints, name)
{ {
id = id id = id
}; };

View File

@@ -207,26 +207,70 @@
<div class="modal-body"> <div class="modal-body">
<!-- Access times --> <!-- Access times -->
<h3>Access times</h3> <h3>@T["Access times"]</h3>
<ul id="queryAccessTimes" class="list-group mb-4"></ul> <ul id="queryDetailsAccessTimes" class="list-group mb-4"></ul>
<!-- Results --> <!-- Results -->
<h3>Results</h3> <h3>@T["Results"]</h3>
<table class="table table-sm table-striped"> <table class="table table-sm table-striped">
<thead> <thead>
<tr> <tr>
<th>Score</th> <th>@T["Score"]</th>
<th>Name</th> <th>@T["Name"]</th>
</tr> </tr>
</thead> </thead>
<tbody id="queryResultsBody"></tbody> <tbody id="queryDetailsResultsBody"></tbody>
</table> </table>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Close @T["Close"]
</button>
</div>
</div>
</div>
</div>
<!-- Query Update Modal -->
<div class="modal fade" id="queryUpdateModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<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>
</div>
<div class="modal-body">
<!-- Access times -->
<h3>@T["Access times"]</h3>
<ul id="queryUpdateAccessTimes" class="list-group mb-4"></ul>
<!-- Results -->
<h3>@T["Results"]</h3>
<table class="table table-sm table-striped">
<thead>
<tr>
<th style="width: 85px;">@T["Score"]</th>
<th>@T["Name"]</th>
<th>@T["Action"]</th>
</tr>
</thead>
<tbody id="queryUpdateResultsBody"></tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" id="queryUpdateConfirm" data-bs-dismiss="modal">
@T["Update"]
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
@T["Close"]
</button> </button>
</div> </div>
@@ -281,10 +325,10 @@
<div class="modal-footer"> <div class="modal-footer">
<button type="button" id="searchdomainConfirmDelete" class="btn btn-danger" data-bs-dismiss="modal"> <button type="button" id="searchdomainConfirmDelete" class="btn btn-danger" data-bs-dismiss="modal">
Delete @T["Delete"]
</button> </button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Close @T["Close"]
</button> </button>
</div> </div>
@@ -426,6 +470,109 @@
</div> </div>
</div> </div>
<!-- Update entity Modal -->
<div class="modal fade" id="updateEntityModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<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>
</div>
<div class="modal-body">
<!-- Entity base -->
<div class="row g-3">
<div class="col-md-6">
<label for="updateEntityName" class="form-label">@T["Entity name"]</label>
<input type="text" class="form-control mb-3" id="updateEntityName" placeholder="@T["Entity name"]" />
</div>
<div class="col-md-6">
<label for="updateEntityProbMethod" class="form-label">@T["Probmethod"]</label>
<input list="probMethods" id="updateEntityProbMethod" class="form-control" aria-label="@T["Probmethod"]">
<datalist id="probMethods">
@foreach (string probMethod in probMethods)
{
<option value=@probMethod>@probMethod</option>
}
</datalist>
</div>
</div>
<!-- Attributes -->
<div class="row g-3">
<h3 class="fs-5">@T["Attributes"]</h3>
<table>
<thead>
<tr>
<td>@T["Name"]</td>
<td>@T["Value"]</td>
<td class="visually-hidden">@T["Action"]</td>
</tr>
</thead>
<tbody id="updateEntityAttributesContainer"></tbody>
</table>
<button type="button" id="updateEntityAttributesAdd" class="btn btn-primary">@T["Add attribute"]</button>
</div>
<!-- Datapoints -->
<div class="row g-3 mt-3">
<h3 class="fs-5">@T["Datapoints"]</h3>
<table>
<thead>
<tr>
<td>@T["Name"]</td>
<td>@T["Value"]</td>
<td>@T["Probmethod_embedding"]</td>
<td>@T["Similarity method"]</td>
<td>@T["Model"]</td>
<td class="visually-hidden">@T["Action"]</td>
</tr>
</thead>
<tbody id="updateEntityDatapointsContainer"></tbody>
</table>
<button type="button" id="updateEntityDatapointsAdd" class="btn btn-primary">@T["Add datapoint"]</button>
</div>
</div>
<div class="modal-footer">
<button type="button" id="EntityConfirmUpdate" class="btn btn-warning" data-bs-dismiss="modal">
@T["Update"]
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
@T["Close"]
</button>
</div>
</div>
</div>
</div>
<!-- Delete query Modal -->
<div class="modal fade" id="deleteQueryModal" 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="deleteQueryTitle">@T["Delete query"]</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>@Html.Raw(T["GenericDeleteConfirmation", "deleteQueryConfirmationModalName"])</p>
</div>
<div class="modal-footer">
<button type="button" id="queryConfirmDelete" class="btn btn-danger" data-bs-dismiss="modal">
@T["Delete"]
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
@T["Close"]
</button>
</div>
</div>
</div>
</div>
<script> <script>
var domains = JSON.parse('@Html.Raw(System.Text.Json.JsonSerializer.Serialize(domains))'); var domains = JSON.parse('@Html.Raw(System.Text.Json.JsonSerializer.Serialize(domains))');
var probMethods = JSON.parse('@Html.Raw(System.Text.Json.JsonSerializer.Serialize(probMethods))'); var probMethods = JSON.parse('@Html.Raw(System.Text.Json.JsonSerializer.Serialize(probMethods))');
@@ -434,6 +581,60 @@
var queries = null; var queries = null;
var models = JSON.parse('@Html.Raw(System.Text.Json.JsonSerializer.Serialize(models))'); var models = JSON.parse('@Html.Raw(System.Text.Json.JsonSerializer.Serialize(models))');
let draggedRow = null;
document.addEventListener("dragstart", e => {
if (e.target.tagName === "TR") {
draggedRow = e.target;
e.target.classList.add("table-warning");
}
});
document.addEventListener("dragend", e => {
if (e.target.tagName === "TR") {
e.target.classList.remove("table-warning");
}
document.querySelectorAll(".table-warning").forEach(x => x.classList.remove("table-warning"));
});
document.addEventListener("dragenter", e => {
if (e.target.tagName === "TR") {
e.target.classList.add("table-warning");
} else if (e.target.tagName == "TD") {
e.target.parentElement.classList.add("table-warning");
}
});
document.addEventListener("dragleave", e => {
if (e.target.tagName === "TR") {
e.target.classList.remove("table-warning");
} else if (e.target.tagName == "TD") {
e.target.parentElement.classList.remove("table-warning");
}
});
document.addEventListener("dragover", e => {
if (e.target.closest("tr")) {
e.preventDefault();
}
});
document.addEventListener("drop", e => {
const targetRow = e.target.closest("tr");
if (draggedRow && targetRow && draggedRow !== targetRow && targetRow.parentElement.tagName !== "THEAD") {
if (!draggedRow || !targetRow || draggedRow === targetRow) return;
const rect = targetRow.getBoundingClientRect();
const isAfter = e.clientY > rect.top + rect.height / 2;
const parent = targetRow.parentNode;
if (isAfter) {
parent.insertBefore(draggedRow, targetRow.nextSibling);
} else {
parent.insertBefore(draggedRow, targetRow);
}
}
});
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const filterInput = document.getElementById('entitiesFilter'); const filterInput = document.getElementById('entitiesFilter');
@@ -519,113 +720,14 @@
.getElementById('createEntityAttributesAdd') .getElementById('createEntityAttributesAdd')
.addEventListener('click', () => { .addEventListener('click', () => {
var uuid = crypto.randomUUID(); var uuid = crypto.randomUUID();
var entityAttributesContainer = document.getElementById('createEntityAttributesContainer'); addEntityAttribute('createEntityAttributesContainer', uuid);
var tr = document.createElement('tr');
tr.setAttribute('data-uid', uuid);
var tdKey = document.createElement('td');
var keyInput = document.createElement('input');
keyInput.classList.add('form-control')
keyInput.ariaLabel = '@T["Key"]';
tdKey.append(keyInput);
var tdValue = document.createElement('td');
var ValueInput = document.createElement('textarea');
ValueInput.classList.add('form-control')
ValueInput.ariaLabel = '@T["Value"]';
tdValue.append(ValueInput);
var tdAction = document.createElement('td');
var tdButton = document.createElement('button');
tdButton.classList.add('btn', 'btn-danger');
tdButton.textContent = '@T["Remove"]';
tdButton.ariaLabel = '@T["Remove attribute"]';
tdAction.append(tdButton);
tdButton.onclick = function() {
tr.remove();
};
tr.append(tdKey);
tr.append(tdValue);
tr.append(tdAction);
entityAttributesContainer.append(tr);
}); });
document document
.getElementById('createEntityDatapointsAdd') .getElementById('createEntityDatapointsAdd')
.addEventListener('click', () => { .addEventListener('click', () => {
var uuid = crypto.randomUUID(); var uuid = crypto.randomUUID();
var enntityDatapointsContainer = document.getElementById('createEntityDatapointsContainer'); addEntityDatapoint('createEntityDatapointsContainer', uuid, "", "");
var tr = document.createElement('tr');
tr.setAttribute('data-uid', uuid);
var tdName = document.createElement('td');
var nameInput = document.createElement('input');
nameInput.classList.add('form-control')
nameInput.ariaLabel = '@T["Name"]';
tdName.append(nameInput);
var tdValue = document.createElement('td');
var ValueInput = document.createElement('textarea');
ValueInput.classList.add('form-control')
ValueInput.ariaLabel = '@T["Value"]';
tdValue.append(ValueInput);
var tdProbmethodEmbedding = document.createElement('td');
var probMethodSelect = document.createElement('select');
probMethodSelect.classList.add('probmethod');
for (i in probMethods)
{
let probMethodSelectElement = document.createElement('option');
probMethodSelectElement.value = probMethods[i];
probMethodSelectElement.text = probMethods[i];
probMethodSelect.append(probMethodSelectElement);
}
probMethodSelect.classList.add('form-control')
probMethodSelect.ariaLabel = '@T["Probmethod_embedding"]';
tdProbmethodEmbedding.append(probMethodSelect);
var tdSimilarityMethod = document.createElement('td');
var similarityMethodSelect = document.createElement('select');
similarityMethodSelect.classList.add('similarityMethod');
for (i in similarityMethods)
{
let similarityMethodSelectElement = document.createElement('option');
similarityMethodSelectElement.value = similarityMethods[i];
similarityMethodSelectElement.text = similarityMethods[i];
similarityMethodSelect.append(similarityMethodSelectElement);
}
similarityMethodSelect.classList.add('form-control')
similarityMethodSelect.ariaLabel = '@T["Similarity method"]';
tdSimilarityMethod.append(similarityMethodSelect);
var tdModel = document.createElement('td');
var modelSelect = document.createElement('select');
modelSelect.classList.add('model');
modelSelect.multiple = true;
for (i in models)
{
let modelSelectElement = document.createElement('option');
modelSelectElement.value = models[i];
modelSelectElement.text = models[i];
modelSelect.append(modelSelectElement);
}
modelSelect.classList.add('form-control')
modelSelect.ariaLabel = '@T["Probmethod_embedding"]';
tdModel.append(modelSelect);
var tdAction = document.createElement('td');
var tdButton = document.createElement('button');
tdButton.classList.add('btn', 'btn-danger');
tdButton.textContent = '@T["Remove"]';
tdButton.ariaLabel = '@T["Remove attribute"]';
tdAction.append(tdButton);
tdButton.onclick = function() {
tr.remove();
};
tr.append(tdName);
tr.append(tdValue);
tr.append(tdProbmethodEmbedding);
tr.append(tdSimilarityMethod);
tr.append(tdModel);
tr.append(tdAction);
enntityDatapointsContainer.append(tr);
}); });
document document
@@ -734,6 +836,109 @@
console.error('Error deleting entity:', error); console.error('Error deleting entity:', error);
}); });
}); });
document
.getElementById('updateEntityAttributesAdd')
.addEventListener('click', () => {
var uuid = crypto.randomUUID();
addEntityAttribute('updateEntityAttributesContainer', uuid);
});
document
.getElementById('updateEntityDatapointsAdd')
.addEventListener('click', () => {
var uuid = crypto.randomUUID();
addEntityDatapoint('updateEntityDatapointsContainer', uuid, "", "");
});
document
.getElementById('EntityConfirmUpdate')
.addEventListener('click', () => {
var name = document.getElementById('updateEntityName').value;
var probMethod = document.getElementById('updateEntityProbMethod').value;
var attributes = getEntityAttributes('updateEntityAttributesContainer');
var datapoints = getEntityDatapoints('updateEntityDatapointsContainer');
var data = [{
"name": name,
"probmethod": probMethod,
"searchdomain": encodeURIComponent(domains[getSelectedDomainKey()]),
"attributes": attributes,
"datapoints": datapoints
}];
console.log(data);
// TODO add throbber to indicate loading
fetch(`/Entity/Index`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}).then(async response => {
result = await response.json();
if (response.ok && result.Success) {
// TODO add toast
console.log('Entity was created successfully');
selectDomain(getSelectedDomainKey());
} else {
// TODO add toast
console.error('Failed to create entity:', result.Message);
}
}).catch(error => {
console.error('Error creating entity:', error);
});
});
document
.getElementById('queryConfirmDelete')
.addEventListener('click', () => {
let searchdomain = domains[getSelectedDomainKey()];
let query = document.getElementById('deleteQueryConfirmationModalName').textContent;
fetch(`/Searchdomain/Searches?searchdomain=${searchdomain}&query=${query}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(async response => {
result = await response.json();
if (response.ok && result.Success) {
// TODO add toast
console.log('Search query was deleted successfully');
selectDomain(getSelectedDomainKey());
} else {
// TODO add toast
console.error('Failed to delete query:', result.Message);
}
}).catch(error => {
console.error('Error creating entity:', error);
});
});
document
.getElementById('queryUpdateConfirm')
.addEventListener('click', () => {
let searchdomain = domains[getSelectedDomainKey()];
let query = document.getElementById('queryUpdateQueryName').textContent;
let data = getQueryUpdateTableData();
console.log()
fetch(`/Searchdomain/Searches?searchdomain=${searchdomain}&query=${query}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}).then(async response => {
result = await response.json();
if (response.ok && result.Success) {
// TODO add toast
console.log('Search query was deleted successfully');
selectDomain(getSelectedDomainKey());
} else {
// TODO add toast
console.error('Failed to delete query:', result.Message);
}
}).catch(error => {
console.error('Error creating entity:', error);
});
});
}); });
function deleteSearchdomain(domainKey) { function deleteSearchdomain(domainKey) {
@@ -837,7 +1042,7 @@
let databaseUtilizationPromise = getSearchdomainDatabaseUtilization(getSelectedDomainKey()); let databaseUtilizationPromise = getSearchdomainDatabaseUtilization(getSelectedDomainKey());
/* ---------- ENTITIES ---------- */ /* ---------- ENTITIES ---------- */
let entitiesUrl = `/Entity/List?searchdomain=${encodeURIComponent(domainName)}&returnEmbeddings=false`; let entitiesUrl = `/Entity/List?searchdomain=${encodeURIComponent(domainName)}&returnEmbeddings=false&returnModels=true`;
let entitiesCard = document.querySelector("#entitiesTable").parentElement; let entitiesCard = document.querySelector("#entitiesTable").parentElement;
clearEntitiesTable(); clearEntitiesTable();
showEntitiesLoading(entitiesCard); showEntitiesLoading(entitiesCard);
@@ -955,7 +1160,30 @@
updateButton.textContent = '@T["Update"]'; updateButton.textContent = '@T["Update"]';
updateButton.setAttribute("data-index", entities.findIndex(en => en == entity)); updateButton.setAttribute("data-index", entities.findIndex(en => en == entity));
updateButton.addEventListener('click', () => { updateButton.addEventListener('click', () => {
updateEntity(entity); const modal = new bootstrap.Modal(
document.getElementById('updateEntityModal')
);
modal.show();
let updateEntityName = document.getElementById('updateEntityName');
updateEntityName.value = entity.Name;
let updateEntityProbMethod = document.getElementById('updateEntityProbMethod');
updateEntityProbMethod.value = entity.ProbMethod;
let updateEntityAttributesContainer = document.getElementById('updateEntityAttributesContainer');
updateEntityAttributesContainer.textContent = "";
for (i in entity.Attributes)
{
let attribute = entity.Attributes[i];
addEntityAttribute('updateEntityAttributesContainer', attribute.Name, attribute.Name, attribute.Value);
}
let updateEntityDatapointsContainer = document.getElementById('updateEntityDatapointsContainer');
updateEntityDatapointsContainer.textContent = "";
for (i in entity.Datapoints)
{
let datapoint = entity.Datapoints[i];
addEntityDatapoint('updateEntityDatapointsContainer', datapoint.Name, datapoint.Name, null, datapoint.ProbMethod, datapoint.SimilarityMethod, Array.from(datapoint.Embeddings, x => x.Model));
}
}); });
var deleteButton = document.createElement('button'); var deleteButton = document.createElement('button');
@@ -966,7 +1194,7 @@
const modal = new bootstrap.Modal( const modal = new bootstrap.Modal(
document.getElementById('deleteEntityModal') document.getElementById('deleteEntityModal')
); );
modal.show(); modal.show();
let entityConfirmDelete = document.getElementById('EntityConfirmDelete'); let entityConfirmDelete = document.getElementById('EntityConfirmDelete');
entityConfirmDelete.setAttribute('data-name', entity.Name); entityConfirmDelete.setAttribute('data-name', entity.Name);
let deleteEntityConfirmationModalName = document.getElementById('deleteEntityConfirmationModalName'); let deleteEntityConfirmationModalName = document.getElementById('deleteEntityConfirmationModalName');
@@ -1002,14 +1230,37 @@
row.appendChild(nameCell); row.appendChild(nameCell);
const actionCell = document.createElement('td'); const actionCell = document.createElement('td');
const btn = document.createElement('button'); actionCell.classList.add('btn-group');
btn.className = 'btn btn-sm btn-info'; const btnDetails = document.createElement('button');
btn.textContent = '@T["Details"]'; btnDetails.className = 'btn btn-sm btn-info';
btn.addEventListener('click', () => { btnDetails.textContent = '@T["Details"]';
btnDetails.addEventListener('click', () => {
showQueryDetails(query); showQueryDetails(query);
}); });
const btnUpdate = document.createElement('button');
btnUpdate.className = 'btn btn-sm btn-warning';
btnUpdate.textContent = '@T["Update"]';
btnUpdate.addEventListener('click', () => {
showQueryUpdate(query);
});
actionCell.appendChild(btn); const btnDelete = document.createElement('button');
btnDelete.className = 'btn btn-sm btn-danger';
btnDelete.textContent = '@T["Delete"]';
btnDelete.addEventListener('click', () => {
const modal = new bootstrap.Modal(
document.getElementById('deleteQueryModal')
);
modal.show();
let queryConfirmDelete = document.getElementById('queryConfirmDelete');
queryConfirmDelete.setAttribute('data-name', query.Name);
let deleteQueryConfirmationModalName = document.getElementById('deleteQueryConfirmationModalName');
deleteQueryConfirmationModalName.textContent = query.Name;
});
actionCell.appendChild(btnDetails);
actionCell.appendChild(btnUpdate);
actionCell.appendChild(btnDelete);
row.appendChild(actionCell); row.appendChild(actionCell);
tbody.appendChild(row); tbody.appendChild(row);
@@ -1098,7 +1349,7 @@
document.getElementById('queryDetailsQueryName').innerText = query.Name; document.getElementById('queryDetailsQueryName').innerText = query.Name;
/* ---------- Access times ---------- */ /* ---------- Access times ---------- */
const accessList = document.getElementById('queryAccessTimes'); const accessList = document.getElementById('queryDetailsAccessTimes');
accessList.innerHTML = ''; accessList.innerHTML = '';
if (!query.AccessDateTimes || query.AccessDateTimes.length === 0) { if (!query.AccessDateTimes || query.AccessDateTimes.length === 0) {
@@ -1116,7 +1367,7 @@
} }
/* ---------- Results ---------- */ /* ---------- Results ---------- */
const resultsBody = document.getElementById('queryResultsBody'); const resultsBody = document.getElementById('queryDetailsResultsBody');
resultsBody.innerHTML = ''; resultsBody.innerHTML = '';
if (!query.Results || query.Results.length === 0) { if (!query.Results || query.Results.length === 0) {
@@ -1144,6 +1395,73 @@
modal.show(); modal.show();
} }
function showQueryUpdate(query) {
// Title
document.getElementById('queryUpdateQueryName').innerText = query.Name;
/* ---------- Access times ---------- */
const accessList = document.getElementById('queryUpdateAccessTimes');
accessList.innerHTML = '';
if (!query.AccessDateTimes || query.AccessDateTimes.length === 0) {
accessList.innerHTML = `
<li class="list-group-item text-muted text-center">
No access times
</li>`;
} else {
query.AccessDateTimes.forEach(dt => {
const li = document.createElement('li');
li.className = 'list-group-item';
li.textContent = new Date(dt).toLocaleString();
accessList.appendChild(li);
});
}
/* ---------- Results ---------- */
const resultsBody = document.getElementById('queryUpdateResultsBody');
resultsBody.innerHTML = '';
if (!query.Results || query.Results.length === 0) {
resultsBody.innerHTML = `
<tr>
<td colspan="2" class="text-muted text-center">
No results
</td>
</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 = '@T["Delete"]';
deleteButton.onclick = function() {
row.remove();
};
tdAction.append(deleteButton);
row.append(tdScore);
row.append(tdName);
row.append(tdAction);
resultsBody.appendChild(row);
});
}
// Show modal
const modal = new bootstrap.Modal(
document.getElementById('queryUpdateModal')
);
modal.show();
}
function NumberOfBytesAsHumanReadable(bytes, decimals = 2) { function NumberOfBytesAsHumanReadable(bytes, decimals = 2) {
if (bytes === 0) return '0 B'; if (bytes === 0) return '0 B';
if (bytes > 1.20892581961*(10**27)) return "∞ B"; if (bytes > 1.20892581961*(10**27)) return "∞ B";
@@ -1191,7 +1509,10 @@
if (keyInput && valueTextarea && probmethodSelect && modelSelect) { if (keyInput && valueTextarea && probmethodSelect && modelSelect) {
const key = keyInput.value.trim(); const key = keyInput.value.trim();
const value = valueTextarea.value.trim(); var value = valueTextarea.value.trim();
if (value != null && value.length == 0) {
value = null;
}
const probMethod = probmethodSelect.selectedOptions[0].value; const probMethod = probmethodSelect.selectedOptions[0].value;
const similarityMethod = similaritymethodSelect.selectedOptions[0].value; const similarityMethod = similaritymethodSelect.selectedOptions[0].value;
const modelSelection = Array.from(modelSelect.selectedOptions) const modelSelection = Array.from(modelSelect.selectedOptions)
@@ -1213,4 +1534,148 @@
return datapoints; return datapoints;
} }
function addEntityAttribute(containerName = 'createEntityAttributesContainer', uid, key = '', value = '')
{
var container = document.getElementById(containerName);
var tr = document.createElement('tr');
tr.setAttribute('data-uid', uid);
var tdKey = document.createElement('td');
var keyInput = document.createElement('input');
keyInput.classList.add('form-control');
keyInput.ariaLabel = '@T["Key"]';
keyInput.value = key;
tdKey.append(keyInput);
var tdValue = document.createElement('td');
var ValueInput = document.createElement('textarea');
ValueInput.classList.add('form-control');
ValueInput.ariaLabel = '@T["Value"]';
ValueInput.value = value;
tdValue.append(ValueInput);
var tdAction = document.createElement('td');
var tdButton = document.createElement('button');
tdButton.classList.add('btn', 'btn-danger');
tdButton.textContent = '@T["Remove"]';
tdButton.ariaLabel = '@T["Remove attribute"]';
tdAction.append(tdButton);
tdButton.onclick = function() {
tr.remove();
};
tr.append(tdKey);
tr.append(tdValue);
tr.append(tdAction);
container.append(tr);
}
function addEntityDatapoint(containerName = 'createEntityDatapointsContainer', uid, name = null, value = null, probmethod_embedding = null, similarityMethod = null, selectedModels = [])
{
var entityDatapointsContainer = document.getElementById(containerName);
var tr = document.createElement('tr');
tr.setAttribute('data-uid', uid);
var tdName = document.createElement('td');
var nameInput = document.createElement('input');
nameInput.classList.add('form-control')
nameInput.ariaLabel = '@T["Name"]';
if (name != null) nameInput.value = name;
tdName.append(nameInput);
var tdValue = document.createElement('td');
var ValueInput = document.createElement('textarea');
ValueInput.classList.add('form-control')
ValueInput.ariaLabel = '@T["Value"]';
if (value != null) {
ValueInput.value = value;
} else {
nameInput.readOnly = true;
ValueInput.readOnly = true;
}
tdValue.append(ValueInput);
var tdProbmethodEmbedding = document.createElement('td');
var probMethodSelect = document.createElement('select');
probMethodSelect.classList.add('probmethod');
for (i in probMethods)
{
let probMethodSelectElement = document.createElement('option');
probMethodSelectElement.value = probMethods[i];
probMethodSelectElement.text = probMethods[i];
probMethodSelect.append(probMethodSelectElement);
}
probMethodSelect.classList.add('form-control')
probMethodSelect.ariaLabel = '@T["Probmethod_embedding"]';
if (probmethod_embedding != null) probMethodSelect.value = probmethod_embedding;
tdProbmethodEmbedding.append(probMethodSelect);
var tdSimilarityMethod = document.createElement('td');
var similarityMethodSelect = document.createElement('select');
similarityMethodSelect.classList.add('similarityMethod');
for (i in similarityMethods)
{
let similarityMethodSelectElement = document.createElement('option');
similarityMethodSelectElement.value = similarityMethods[i];
similarityMethodSelectElement.text = similarityMethods[i];
similarityMethodSelect.append(similarityMethodSelectElement);
}
similarityMethodSelect.classList.add('form-control')
similarityMethodSelect.ariaLabel = '@T["Similarity method"]';
if (similarityMethod != null) similarityMethodSelect.value = similarityMethod;
tdSimilarityMethod.append(similarityMethodSelect);
var tdModel = document.createElement('td');
var modelSelect = document.createElement('select');
modelSelect.classList.add('model');
modelSelect.multiple = true;
for (i in models)
{
let modelSelectElement = document.createElement('option');
modelSelectElement.value = models[i];
modelSelectElement.text = models[i];
modelSelectElement.selected = selectedModels.findIndex(x => x === models[i] || (x + ":latest") === models[i]) >= 0;
modelSelect.append(modelSelectElement);
}
modelSelect.classList.add('form-control')
modelSelect.ariaLabel = '@T["Probmethod_embedding"]';
if (value == null) {
modelSelect.disabled = true;
}
tdModel.append(modelSelect);
var tdAction = document.createElement('td');
var tdButton = document.createElement('button');
tdButton.classList.add('btn', 'btn-danger');
tdButton.textContent = '@T["Remove"]';
tdButton.ariaLabel = '@T["Remove attribute"]';
tdAction.append(tdButton);
tdButton.onclick = function() {
tr.remove();
};
tr.append(tdName);
tr.append(tdValue);
tr.append(tdProbmethodEmbedding);
tr.append(tdSimilarityMethod);
tr.append(tdModel);
tr.append(tdAction);
entityDatapointsContainer.append(tr);
}
function getQueryUpdateTableData() {
const tbody = document.getElementById('queryUpdateResultsBody');
const rows = tbody.getElementsByTagName('tr');
const result = [];
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const cells = row.getElementsByTagName('td');
// 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();
result.push({
"Score": score,
"Name": name
});
}
return result;
}
</script> </script>

View File

@@ -41,6 +41,8 @@ public class EntityListResult
{ {
[JsonPropertyName("Name")] [JsonPropertyName("Name")]
public required string Name { get; set; } public required string Name { get; set; }
[JsonPropertyName("ProbMethod")]
public required string ProbMethod { get; set; }
[JsonPropertyName("Attributes")] [JsonPropertyName("Attributes")]
public required List<AttributeResult> Attributes { get; set; } public required List<AttributeResult> Attributes { get; set; }
[JsonPropertyName("Datapoints")] [JsonPropertyName("Datapoints")]

View File

@@ -12,7 +12,7 @@ public class JSONEntity
public class JSONDatapoint public class JSONDatapoint
{ {
public required string Name { get; set; } public required string Name { get; set; }
public required string Text { get; set; } public required string? Text { get; set; }
public required string Probmethod_embedding { get; set; } public required string Probmethod_embedding { get; set; }
public required string SimilarityMethod { get; set; } public required string SimilarityMethod { get; set; }
public required string[] Model { get; set; } public required string[] Model { get; set; }

View File

@@ -2,12 +2,19 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Shared.Models; namespace Shared.Models;
public readonly struct ResultItem(float score, string name) public readonly struct ResultItem
{ {
[JsonPropertyName("Score")] [JsonPropertyName("Score")]
public readonly float Score { get; } = score; public readonly float Score { get; }
[JsonPropertyName("Name")] [JsonPropertyName("Name")]
public readonly string Name { get; } = name; public readonly string Name { get; }
[JsonConstructor]
public ResultItem(float score, string name)
{
Score = score;
Name = name;
}
public static long EstimateSize(ResultItem item) public static long EstimateSize(ResultItem item)
{ {

View File

@@ -55,6 +55,24 @@ public class SearchdomainSearchesResults
public required Dictionary<string, DateTimedSearchResult> Searches { get; set; } public required Dictionary<string, DateTimedSearchResult> Searches { get; set; }
} }
public class SearchdomainDeleteSearchResult
{
[JsonPropertyName("Success")]
public required bool Success { get; set; }
[JsonPropertyName("Message")]
public string? Message { get; set; }
}
public class SearchdomainUpdateSearchResult
{
[JsonPropertyName("Success")]
public required bool Success { get; set; }
[JsonPropertyName("Message")]
public string? Message { get; set; }
}
public class SearchdomainSettingsResults public class SearchdomainSettingsResults
{ {
[JsonPropertyName("Success")] [JsonPropertyName("Success")]