Compare commits
46 Commits
ad84efb611
...
118-search
| Author | SHA1 | Date | |
|---|---|---|---|
| 41fd8a067e | |||
|
|
047526dc3c | ||
| 329af1c103 | |||
|
|
5869eeabd6 | ||
| 7fffd74f26 | |||
| a9dada01c0 | |||
| 01b0934d6e | |||
| c0189016e8 | |||
| 7d16f90c71 | |||
|
|
d7c248945d | ||
|
|
059bf147dc | ||
| ffe15e211b | |||
| 255395b582 | |||
| 6390dbc9ab | |||
| 7f2a14609f | |||
|
|
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 |
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.
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,3 +20,4 @@ src/Shared/obj
|
|||||||
src/Server/wwwroot/logs/*
|
src/Server/wwwroot/logs/*
|
||||||
src/Server/Tools/CriticalCSS/node_modules
|
src/Server/Tools/CriticalCSS/node_modules
|
||||||
src/Server/Tools/CriticalCSS/package*.json
|
src/Server/Tools/CriticalCSS/package*.json
|
||||||
|
*.db*
|
||||||
|
|||||||
115
README.md
115
README.md
@@ -1,92 +1,59 @@
|
|||||||
# embeddingsearch
|
# embeddingsearch<img src="docs/logo.png" alt="Logo" width="50" align="left">
|
||||||
<img src="https://github.com/LD-Reborn/embeddingsearch/blob/main/logo.png" alt="Logo" width="100">
|
embeddingsearch is a self-hosted semantic search server built on vector embeddings.<br/>It lets you index and semantically search text using modern embedding models.
|
||||||
|
<br/><br/>
|
||||||
|
It's designed to be flexible, extensible, and easy to use.
|
||||||
|
|
||||||
embeddingsearch is a search server that uses Embedding Similarity Search (similiarly to [Magna](https://github.com/yousef-rafat/Magna/tree/main)) to semantically compare a given input to a database of indexed entries.
|
# Project outline
|
||||||
|
<img src="docs/ProjectOutline/ProjectOutlineDiagram.excalidraw.svg" alt="Logo">
|
||||||
|
|
||||||
embeddingsearch offers:
|
## What embeddingsearch offers:
|
||||||
- Privacy and flexibility through self-hosted solutions like:
|
- Privacy and flexibility by allowing one to self-host everything, including:
|
||||||
- ollama
|
- Ollama
|
||||||
- OpenAI-compatible APIs (like LocalAI)
|
- OpenAI-compatible APIs (like LocalAI)
|
||||||
- Great flexibility through deep control over
|
- Astonishing accuracy when using multiple models for single indices
|
||||||
- the amount of datapoints per entity (i.e. the thing you're trying to find)
|
- Ease-of-use and ease-of-implementation
|
||||||
- which models are used (multiple per datapoint possible to improve accuracy)
|
- The server offers a front-end for management and status information, as well as a decorated swagger back-end
|
||||||
- which models are sourced from where (multiple Ollama/OpenAI-compatible sources possible)
|
- The indexer can also be self-hosted and serves as a host for executing indexing scripts
|
||||||
- similarity calculation methods
|
- The client library can be used to develop your own client software that posts queries or creates indices
|
||||||
- aggregation of results (when multiple models are used per datapoint)
|
- Caching & persistency
|
||||||
|
- Generating embeddings is expensive. So why not cache AND store them?
|
||||||
|
- Query results can also be cached.
|
||||||
|
- "Doesn't that eat a lot of precious RAM?" - My own testing showed: embeddings take up around 4200-5200 bytes each depending on the request string size. So around 4-5 GB per million cached embeddings.
|
||||||
|
|
||||||
This repository comes with a
|
This repository comes with a:
|
||||||
- server (accessible via API calls & swagger)
|
- Server
|
||||||
- clientside library (C#)
|
- Client library (C#)
|
||||||
- scripting based indexer service that supports the use of
|
- Scripting based indexer service that supports the use of
|
||||||
- Python
|
- Python
|
||||||
- CSharp (Roslyn)
|
- CSharp (Roslyn - at-runtime evaluation)
|
||||||
- Golang (Planned)
|
- CSharp (Reflection - compiled)
|
||||||
|
- Lua (Planned)
|
||||||
- Javascript (Planned)
|
- Javascript (Planned)
|
||||||
|
|
||||||
# How to set up / use
|
# How to set up
|
||||||
## Server
|
## Server
|
||||||
(Docker now available! See [Docker installation](docs/Server.md#docker-installation))
|
(Docker also available! See [Docker installation](docs/Server.md#docker-installation))
|
||||||
1. Install [ollama](https://ollama.com/download)
|
1. Install the inferencing tool of your choice, (e.g. [ollama](https://ollama.com/download)) and pull a few models that support generating embeddings.
|
||||||
2. Pull a few models using ollama (e.g. `paraphrase-multilingual`, `bge-m3`, `mxbai-embed-large`, `nomic-embed-text`)
|
2. [Install the depencencies](docs/Server.md#installing-the-dependencies)
|
||||||
3. [Install the depencencies](docs/Server.md#installing-the-dependencies)
|
3. [Set up a mysql database](docs/Server.md#mysql-database-setup)
|
||||||
4. [Set up a local mysql database](docs/Server.md#mysql-database-setup)
|
4. [Set up the configuration](docs/Server.md#configuration)
|
||||||
5. [Set up the configuration](docs/Server.md#setup)
|
5. In `src/Server` execute `dotnet build && dotnet run` to start the server
|
||||||
6. In `src/server` execute `dotnet build && dotnet run` to start the server
|
6. (optional) Create a searchdomain using the web interface
|
||||||
7. (optional) [Create a searchdomain using the web interface](docs/Server.md#accessing-the-api)
|
|
||||||
## Client
|
|
||||||
1. Download the package and add it to your project (TODO: NuGet)
|
|
||||||
2. Create a new client by either:
|
|
||||||
1. By injecting IConfiguration (e.g. `services.AddSingleton<Client>();`)
|
|
||||||
2. By specifying the baseUri, apiKey, and searchdomain (e.g. `new Client.Client(baseUri, apiKey, searchdomain)`)
|
|
||||||
## Indexer
|
## Indexer
|
||||||
(Docker now available! See [Docker installation](docs/Indexer.md#docker-installation))
|
(Docker now available! See [Docker installation](docs/Indexer.md#docker-installation))
|
||||||
1. [Install the dependencies](docs/Indexer.md#installing-the-dependencies)
|
1. [Install the dependencies](docs/Indexer.md#installing-the-dependencies)
|
||||||
2. [Set up the server](#server)
|
2. [Configure the indexer](docs/Indexer.md#configuration)
|
||||||
3. [Configure the indexer](docs/Indexer.md#configuration)
|
3. [Set up your indexing script(s)](docs/Indexer.md#scripting)
|
||||||
4. [Set up your indexing script(s)](docs/Indexer.md#scripting)
|
4. In `src/Indexer` execute `dotnet build && dotnet run` to start the indexer
|
||||||
5. Run with `dotnet build && dotnet run` (Or `/usr/bin/dotnet build && /usr/bin/dotnet run`)
|
|
||||||
# Known issues
|
# Known issues
|
||||||
| Issue | Solution |
|
| Issue | Solution |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Unhandled exception. MySql.Data.MySqlClient.MySqlException (0x80004005): Invalid attempt to access a field before calling Read() | The searchdomain you entered does not exist |
|
| System.DllNotFoundException: Could not load libpython3.13.so with flags RTLD_NOW \| RTLD_GLOBAL: libpython3.12.so: cannot open shared object file: No such file or directory | Install python3.13-dev via apt. Also: try running the indexer using `/usr/bin/dotnet` instead of `dotnet` (to make sure dotnet is not running as a snap) |
|
||||||
| Unhandled exception. MySql.Data.MySqlClient.MySqlException (0x80004005): Authentication to host 'localhost' for user 'embeddingsearch' using method 'caching_sha2_password' failed with message: Access denied for user 'embeddingsearch'@'localhost' (using password: YES) | TBD |
|
|
||||||
| System.DllNotFoundException: Could not load libpython3.12.so with flags RTLD_NOW \| RTLD_GLOBAL: libpython3.12.so: cannot open shared object file: No such file or directory | Install python3.12-dev via apt. Also: try running the indexer using `/usr/bin/dotnet` instead of `dotnet` (make sure dotnet is installed via apt) |
|
|
||||||
# To-do
|
|
||||||
- (High priority) Add default indexer
|
|
||||||
- Library
|
|
||||||
- Processing:
|
|
||||||
- Text / Markdown documents: file name, full text, paragraphs
|
|
||||||
- Documents
|
|
||||||
- PDF: file name, full text, headline?, paragraphs, images?
|
|
||||||
- odt/docx: file name, full text, headline?, images?
|
|
||||||
- msg/eml: file name, title, recipients, cc, text
|
|
||||||
- Images: file name, OCR, image description?
|
|
||||||
- Videos?
|
|
||||||
- Presentations (Impress/Powerpoint): file name, full text, first slide title, titles, slide texts
|
|
||||||
- Tables (Calc / Excel): file name, tab/page names?, full text (per tab/page)
|
|
||||||
- Other? (TBD)
|
|
||||||
- Server
|
|
||||||
- ~~Scripting capability (Python; perhaps also lua)~~ (Done with the latest commits)
|
|
||||||
- ~~Intended sourcing possibilities:~~
|
|
||||||
- ~~Local/Remote files (CIFS, SMB, FTP)~~
|
|
||||||
- ~~Database contents (MySQL, MSSQL)~~
|
|
||||||
- ~~Web requests (E.g. manual crawling)~~
|
|
||||||
- ~~Script call management (interval based & event based)~~
|
|
||||||
- Implement [ReaderWriterLock](https://learn.microsoft.com/en-us/dotnet/api/system.threading.readerwriterlockslim?view=net-9.0&redirectedfrom=MSDN) for entityCache to allow for multithreaded read access while retaining single-threaded write access.
|
|
||||||
- NuGet packaging and corresponding README documentation
|
|
||||||
- Add option for query result detail levels. e.g.:
|
|
||||||
- Level 0: `{"Name": "...", "Value": 0.53}`
|
|
||||||
- Level 1: `{"Name": "...", "Value": 0.53, "Datapoints": [{"Name": "title", "Value": 0.65}, {...}]}`
|
|
||||||
- Level 2: `{"Name": "...", "Value": 0.53, "Datapoints": [{"Name": "title", "Value": 0.65, "Embeddings": [{"Model": "bge-m3", "Value": 0.87}, {...}]}, {...}]}`
|
|
||||||
- Add "Click-Through" result evaluation (For each entity: store a list of queries that led to the entity being chosen by the user. Then at query-time choose the best-fitting entry and maybe use it as another datapoint? Or use a separate weight function?)
|
|
||||||
- Reranker/Crossencoder/RAG (or anything else beyond initial retrieval) support
|
|
||||||
- Remove the `id` collumns from the database tables where the table is actually identified (and should be unique by) the name, which should become the new primary key.
|
|
||||||
- Improve performance & latency (Create ready-to-go processes where each contain an n'th share of the entity cache, ready to perform a query. Prepare it after creating the entity cache.)
|
|
||||||
- Implement dynamic invocation based database migrations
|
|
||||||
|
|
||||||
# Future features
|
|
||||||
- Support for other database types (MSSQL, SQLite)
|
|
||||||
|
|
||||||
|
# Planned features and support
|
||||||
|
- Document processor with automatic chunking (e.g.: .md, .pdf, .docx, .xlsx, .png, .mp4)
|
||||||
|
- Indexer front-end
|
||||||
|
- Support for other database types (MSSQL, SQLite, PostgreSQL, MongoDB, Redis)
|
||||||
|
|
||||||
# Community
|
# Community
|
||||||
<a href="https://discord.gg/MUKeZM3k"><img src="https://img.shields.io/badge/Join%20Discord-7289DA?style=flat&logo=discord&logoColor=whiteServer" alt="Discord"></img></a>
|
<a href="https://discord.gg/MUKeZM3k"><img src="https://img.shields.io/badge/Join%20Discord-7289DA?style=flat&logo=discord&logoColor=whiteServer" alt="Discord"></img></a>
|
||||||
@@ -8,15 +8,18 @@ The indexer by default
|
|||||||
- Uses HealthChecks (endpoint: `/healthz`)
|
- Uses HealthChecks (endpoint: `/healthz`)
|
||||||
## Docker installation
|
## Docker installation
|
||||||
(On Linux you might need root privileges, thus use `sudo` where necessary)
|
(On Linux you might need root privileges, thus use `sudo` where necessary)
|
||||||
1. Navigate to the `src` directory
|
1. [Configure the indexer](docs/Indexer.md#configuration)
|
||||||
2. Build the docker container: `docker build -t embeddingsearch-indexer -f Indexer/Dockerfile .`
|
2. [Set up your indexing script(s)](docs/Indexer.md#scripting)
|
||||||
3. Run the docker container: `docker run --net=host -t embeddingsearch-indexer` (the `-t` is optional, but you get more meaningful output. Or use `-d` to run it in the background)
|
3. Navigate to the `src` directory
|
||||||
|
4. Build the docker container: `docker build -t embeddingsearch-indexer -f Indexer/Dockerfile .`
|
||||||
|
5. Run the docker container: `docker run --net=host -t embeddingsearch-indexer` (the `-t` is optional, but you get more meaningful output. Or use `-d` to run it in the background)
|
||||||
## Installing the dependencies
|
## Installing the dependencies
|
||||||
## Ubuntu 24.04
|
## Ubuntu 24.04
|
||||||
1. Install the .NET SDK: `sudo apt update && sudo apt install dotnet-sdk-8.0 -y`
|
1. Install the .NET SDK: `sudo apt update && sudo apt install dotnet-sdk-10.0 -y`
|
||||||
2. Install the python SDK: `sudo apt install python3 python3.12 python3.12-dev`
|
2. Install the python SDK: `sudo apt install python3 python3.13 python3.13-dev`
|
||||||
|
- Note: Python 3.14 is not supported yet
|
||||||
## Windows
|
## Windows
|
||||||
Download the [.NET SDK](https://dotnet.microsoft.com/en-us/download) or follow these steps to use WSL:
|
Download and install the [.NET SDK](https://dotnet.microsoft.com/en-us/download) or follow these steps to use WSL:
|
||||||
1. Install Ubuntu in WSL (`wsl --install` and `wsl --install -d Ubuntu`)
|
1. Install Ubuntu in WSL (`wsl --install` and `wsl --install -d Ubuntu`)
|
||||||
2. Enter your WSL environment `wsl.exe` and configure it
|
2. Enter your WSL environment `wsl.exe` and configure it
|
||||||
3. Update via `sudo apt update && sudo apt upgrade -y && sudo snap refresh`
|
3. Update via `sudo apt update && sudo apt upgrade -y && sudo snap refresh`
|
||||||
@@ -26,15 +29,15 @@ The configuration is located in `src/Indexer` and conforms to the [ASP.NET confi
|
|||||||
|
|
||||||
If you plan to use multiple environments, create any `appsettings.{YourEnvironment}.json` (e.g. `Development`, `Staging`, `Prod`) and set the environment variable `DOTNET_ENVIRONMENT` accordingly on the target machine.
|
If you plan to use multiple environments, create any `appsettings.{YourEnvironment}.json` (e.g. `Development`, `Staging`, `Prod`) and set the environment variable `DOTNET_ENVIRONMENT` accordingly on the target machine.
|
||||||
## Setup
|
## Setup
|
||||||
If you just installed the server and want to configure it:
|
If you just installed the indexer and want to configure it:
|
||||||
1. Open `src/Server/appsettings.Development.json`
|
1. Open `src/Indexer/appsettings.Development.json`
|
||||||
2. If your search server is not on the same machine as the indexer, update "BaseUri" to reflect the URL to the server.
|
2. If your search server is not on the same machine as the indexer, update "BaseUri" to reflect the URL to the server.
|
||||||
3. If your search server requires API keys, (i.e. it's operating outside of the "Development" environment) set `"ApiKey": "<your key here>"` beneath `"BaseUri"` in the `"Embeddingsearch"` section.
|
3. If you configured API keys for the search server, set `"ApiKey": "<your key here>"` beneath `"BaseUri"` in the `"Server"` section.
|
||||||
4. Create your own indexing script(s) in `src/Indexer/Scripts/` and configure their use as
|
4. Create your own indexing script(s) in `src/Indexer/Scripts/` and configure them as shown below
|
||||||
## Structure
|
## Structure
|
||||||
```json
|
```json
|
||||||
"EmbeddingsearchIndexer": {
|
"Indexer": {
|
||||||
"Worker":
|
"Workers":
|
||||||
[ // This is a list; you can have as many "workers" as you want
|
[ // This is a list; you can have as many "workers" as you want
|
||||||
{
|
{
|
||||||
"Name": "example",
|
"Name": "example",
|
||||||
@@ -50,7 +53,12 @@ If you just installed the server and want to configure it:
|
|||||||
"Name": "secondWorker",
|
"Name": "secondWorker",
|
||||||
/* ... */
|
/* ... */
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"ApiKeys": ["YourApiKeysHereForTheIndexer"], // API Keys for if you want to protect the indexer's API
|
||||||
|
"Server": {
|
||||||
|
"BaseUri": "http://localhost:5000", // URL to the embeddingsearch server
|
||||||
|
"ApiKey": "ServerApiKeyHere" // API Key set in the server
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
## Call types
|
## Call types
|
||||||
@@ -71,6 +79,13 @@ If you just installed the server and want to configure it:
|
|||||||
- Parameters:
|
- Parameters:
|
||||||
- Path (e.g. "Scripts/example_content")
|
- Path (e.g. "Scripts/example_content")
|
||||||
# Scripting
|
# Scripting
|
||||||
|
Scripts should be put in `src/Indexer/Scripts/`. If you look there, by default you will find some example scripts that can be taken as reference when building your own.
|
||||||
|
|
||||||
|
For configuration of the scripts see: [Structure](#structure)
|
||||||
|
|
||||||
|
The next few sections explain some core concepts/patterns. If you want to skip to explicit code examples, look here:
|
||||||
|
- [Python](#python)
|
||||||
|
- [Roslyn](#c-roslyn)
|
||||||
## General
|
## General
|
||||||
Scripts need to define the following functions:
|
Scripts need to define the following functions:
|
||||||
- `init()`
|
- `init()`
|
||||||
@@ -186,7 +201,7 @@ from tools import * # Import all tools that are provided for ease of scripting
|
|||||||
|
|
||||||
def init(toolset: Toolset): # defining an init() function with 1 parameter is required.
|
def init(toolset: Toolset): # defining an init() function with 1 parameter is required.
|
||||||
pass # Your code would go here.
|
pass # Your code would go here.
|
||||||
# DO NOT put a main loop here! Why?
|
# Don't put a main loop here! Why?
|
||||||
# This function prevents the application from initializing and maintains exclusive control over the GIL
|
# This function prevents the application from initializing and maintains exclusive control over the GIL
|
||||||
|
|
||||||
def update(toolset: Toolset): # defining an update() function with 1 parameter is required.
|
def update(toolset: Toolset): # defining an update() function with 1 parameter is required.
|
||||||
@@ -261,7 +276,7 @@ public class ExampleScript : Indexer.Models.IScript
|
|||||||
// Required: return an instance of your IScript-extending class
|
// Required: return an instance of your IScript-extending class
|
||||||
return new ExampleScript();
|
return new ExampleScript();
|
||||||
```
|
```
|
||||||
## Golang
|
## Lua
|
||||||
TODO
|
TODO
|
||||||
## Javascript
|
## Javascript
|
||||||
TODO
|
TODO
|
||||||
190
docs/ProjectOutline/ProjectOutlineDiagram.excalidraw.md
Normal file
190
docs/ProjectOutline/ProjectOutlineDiagram.excalidraw.md
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
excalidraw-plugin: parsed
|
||||||
|
tags: [excalidraw]
|
||||||
|
|
||||||
|
---
|
||||||
|
==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠== You can decompress Drawing data with the command palette: 'Decompress current Excalidraw file'. For more info check in plugin settings under 'Saving'
|
||||||
|
|
||||||
|
|
||||||
|
# Excalidraw Data
|
||||||
|
|
||||||
|
## Text Elements
|
||||||
|
Server ^TJzgO4nS
|
||||||
|
|
||||||
|
Indexer ^rgrd8gyy
|
||||||
|
|
||||||
|
embeddingsearch ^jB1B7xr7
|
||||||
|
|
||||||
|
Client ^ZttcBOXC
|
||||||
|
|
||||||
|
embeddings
|
||||||
|
provider ^mEIPhpn1
|
||||||
|
|
||||||
|
✔️ Ollama
|
||||||
|
✔️ OpenAI-compatible
|
||||||
|
(e.g. LocalAI) ^o6rED2fi
|
||||||
|
|
||||||
|
uses ^QkKnkGvS
|
||||||
|
|
||||||
|
Database ^yaSaChsK
|
||||||
|
|
||||||
|
✔️ MySQL
|
||||||
|
⚒️ SQLite
|
||||||
|
⚒️ MSSQL
|
||||||
|
⚒️ PostgreSQL
|
||||||
|
⚒️ MongoDB
|
||||||
|
⚒️ Redis ^LHP4PU6V
|
||||||
|
|
||||||
|
Stores
|
||||||
|
data in ^FP2xPhxz
|
||||||
|
|
||||||
|
Listens on port 5146
|
||||||
|
^CJG2peC6
|
||||||
|
|
||||||
|
Listens on port 5210 ^iLZT5hca
|
||||||
|
|
||||||
|
Workers ^33rXJfFb
|
||||||
|
|
||||||
|
- example.py
|
||||||
|
- example.csx
|
||||||
|
- ... ^e1BVqXa2
|
||||||
|
|
||||||
|
✔️ Front-end
|
||||||
|
✔️ Swagger
|
||||||
|
✔️ Elmah ^6UTNDntp
|
||||||
|
|
||||||
|
⚒️ Front-end
|
||||||
|
✔️ Swagger
|
||||||
|
✔️ Elmah ^tlLF3R27
|
||||||
|
|
||||||
|
✔️ Caches embeddings
|
||||||
|
✔️ Caches queries ^I2lN1U82
|
||||||
|
|
||||||
|
✔️ C# library
|
||||||
|
⚒️ NuGet
|
||||||
|
✔️ Searchdomain CRUD
|
||||||
|
✔️ Entity CRUD
|
||||||
|
✔️ Management operations ^4Ab3XHhK
|
||||||
|
|
||||||
|
Uses ^KvuBRV2K
|
||||||
|
|
||||||
|
Accesses ^ikhSH5rs
|
||||||
|
|
||||||
|
✔️ Multiple provider
|
||||||
|
configuration ^ipkoadg8
|
||||||
|
|
||||||
|
%%
|
||||||
|
## Drawing
|
||||||
|
```compressed-json
|
||||||
|
N4KAkARALgngDgUwgLgAQQQDwMYEMA2AlgCYBOuA7hADTgQBuCpAzoQPYB2KqATLZMzYBXUtiRoIACyhQ4zZAHoFAc0JRJQgEYA6bGwC2CgF7N6hbEcK4OCtptbErHALRY8RMpWdx8Q1TdIEfARcZgRmBShcZQUebQBGAE5tHho6IIR9BA4oZm4AbXAwUDBSiBJuCBghAHUAeQA1QgAzAE000shYRErA7CiOZWCOssxuZwA2HgntCYAGAHYADh4l
|
||||||
|
|
||||||
|
hYAWAGY5ub4iyBhx9YBWY+1d9cSp+Pi5pdvj/jKKEnVueO2Z6eOrpeO544neLrJ6QSQIQjKaTcRI/WanCabY4TdY8TYLdGgiDWIbiVBzLHMKCkNgAawQAGE2Pg2KRKgBieIIJlMkaQTS4bCk5QkoQcYhUml0iSM5litkQZqEfD4ADKsGGEkEHglRJJ5Jqr0k3Dm2ke+wgarJCHlMEV6GVFSxvKhHHCeTQBINbDgnLUhzQtydnQgPOEcAAksQHah8
|
||||||
|
|
||||||
|
gBdLHNchZIPcDhCGVYwj8rCVXBzCW8/l25ghko+7p4zb7AC+hIQCGI3DROySgKWWMYLHYXE9sMbTFYnAAcpwxNwlkt1hN7lck8wACIZKCV7jNAhhLGaYT8gCiwSyORD8cTBqEcGIuBnVc9CwmiR4C3ixyvP0SWKIHFJcYT+AfbC5s7Q8/wYSKZaKfNIAqCQAEcGgWZRNlJAAlAAJCVC16BB+hxYYsTGNBnHiFZtE2RFVniHh9R9D1UGcb5tGHBYM
|
||||||
|
|
||||||
|
R4eINhOOZEk2LEXmIN40CRBY9T+ZiFhIspwUhaFOPWdZtCuHgtimQEuImLE0Lxb0yiNclBVpBlmi07SJQ5Lk/T5AVqQ0iRiWsZhXUCHIJSlGVTXNQ1qStA1VIQTV2O1NA9h9Vz7LxRyVWtYRbXtd4sRdN1YHeHYsQMwNgwKCMDSjXAYxPVAdzfA1k2IVMJFweJMxXYgczzfYungItS3LL9UHiRFYUWeqQQNJsu1bOrlg7Zsez7PEbxRY5gQbbLJ2
|
||||||
|
|
||||||
|
nWqf0XA1l0M9dMmyXICnKoCyhA9AAEFCEkCcAC0AwAIQWNkyiQ/LSBJKhyoAzpinK4D0ogAAxXJlCMGB6HaUETsqtNzrYS6bpLfYkp9fdD2Pd4zwvW96rRbyykfZ80Ey99P3SyaECxSRQgAFSwKAABlkyR1AMf/J4gPKB6cYAKSMZQ6nWDhZUQn7TPxiVMPI4cUh2dYaJ2QXaxa0jxjhvCMTmYd/iF3ZWK1AczmmOZNm2Hha1hTZ4ixiEoSgbgkQ
|
||||||
|
|
||||||
|
UwZzWUgRiWNdThXQeltJ0pdOW5LMjKFSozI4CzcCs/XI2lOUFX8y0q0Jc2NQVrzg/VE1/cqQOir8SRSrC51XWwd1otN31eXikNw0jaMEFjZHXyTFMuexVIgsMxOi93HyK3S4EeEvP4phGn02pbbh1jubr2t7Dh+09H4z2BRJ1jb1axuCCHvwXTHpuKubN0WtBc73A8j1q+jz0va9Yc2eHIBpNG5znrEZ0wfWJFlJgm0zSg8cvyob9IO/I04KBZUI
|
||||||
|
|
||||||
|
Iw8VWd+ckeqlaUZEBJdHxutIgygOoQGCM0b2rUmBQHMAQCBkJoFQBdBKPQORcDJiYIXDKxdnSkEhMmAgj8r7oBfm/A0uAhAYJguEb+eJiRCHnj6R8CA4K6xEnVFIoCpC43xkTJ8p9fzsIRsTF8MpyaAWyg9XA+0jAAHF8AAEUajYBgA0DgzQYBqMmJBKAcwuDnzZugPoAxcSc2rBMM4CxEhDk2IkAETElgsQNGRCidiqITBolrc8Sw5gjgWPLDyB
|
||||||
|
|
||||||
|
sbw8R+OiARQk9bcCGvECSKwmIHxVure8tDjZKQjhbYyVsIA21ts0XSDsDL8ktq7cg7tLILRsr7PyMcnJBxciHNyYdeC5PJI0pUzS44hVzEnH0EVU5RU9DFA0cUgw5xBmUFKaVpFZR9DlPK6BcCbDjtmUKaAVrQHMcWTo10VL12rIsZxQ1gkT0gB3Tg7wxKZPbp2Fs/dB51T+Gsa8QTxxTmnhNM+C9ZobgWjnZad0qaVAABowTqMo7+AArTQx0Ko9
|
||||||
|
|
||||||
|
DOhdCAV1gZYjBpvBuUNd5DSmAfB8Uia5LIRh+ck6N/k+mxswChIiSZk1KEcyma0ICkB5MQJYygYCHDMSi9AF94E+i5s4uIUsLy7G2G47YIsyhePFokRINwRxiSCSOeSBo2IcTqgCc4Q51gqroqk/iOthKUMNlk6xjoumUnyZpYppT9JO0qaZapHsvb1LstHXpgVWmR3cjqw+ho2k9ItH0yuAyQza2TpFMiXpYpZ2mYlPOqUC7pRRtlUuaZ1ibJKt
|
||||||
|
|
||||||
|
swhtdjlby2FsVWw1e6d09BMOxlbeoD1/irZE7jNXLKnggGepNqVlBmmuIFW4U3r3BlvPFMNCXBuPpSsRU0CwcwkAGUuTB74UAoZUBduVMBLv/p/Zh1YrmSg/oA/QwCEnn3AZA6BsDhVlCbEg9wqCoGu0wVibBUQ8GkAIZm4ZJD/DkLnegddWAt20PoWwRhrAf7cFYRIo+eDuHmvePwrGQjL4MunTBmBJLC34FkaUVlD04X7XiIdTApAjr8v8kKmx
|
||||||
|
|
||||||
|
WFgTcWIgCK4EwcLNQBFiLxqJuKbHuKca8YSg1LBmDReYF5jg8AvFcB5gkeGUJOEba1+JbWuutsUkp9tnXFSU9Ad1tTrI+29WaAOEb/XGkDZ5Tpxnuk+vDX6n0NoE4FpjcMlOadxkZymQlVeszIDzPTYskuG60zHDzdXbDNV0oOJ+LDUTdaOp/GDTcjgLy8RETVSrVEGdCDts7RjJci9+0r1DF5iA2LO3b2hnvcdqMp2z3EWep+EhMiaErI4QYYRP
|
||||||
|
|
||||||
|
bYG1NaB+/6MD6Ca8QFryg2uiE68lD+X9IOcVbXMw9QD8AgLq1AB9l6EBwIlLe5B+BltPrgFgj+uC7QfozUQ79pCOB/vq+gRrzXkxDZCCNiUdCGFMMm6gaDxK7TwfiZ6JDBpaX0qwzlg0iNFm4dussh6xxsDrGaPQfQph1gIDqJlgAqnBVcxAaiSGwLtijyFULZOo+Rbe2gFiXi1k3ZVdFlXTYOKc5Iw55g/BVhifCknIDarM1xKJfFYnSerDzOYt
|
||||||
|
|
||||||
|
wuOnBcS45YcrICKR1Ip+1IoVNOsdhpuXgrtOezqXpv2Bmmk2ZUm00z1ZbVhoCs5WzwV7ODM9OFZzYzdVuaTR5wrqaFmkv86s7EExgsFt2adVABzmVheiiOdJmxx4xfeCEmLSX3ha1D9eZYGWst/NqwCvt80B2r1BTde6kLoWwqMAipFeyBXYj+gDQ5mKh04shjvMdcMPsky/eSk+NWZ2CRQ4TQHc8wf4cqDtGQ2B9p1AhRSVmJeqMYXGEiCS2x/j
|
||||||
|
|
||||||
|
zENdTxYtOqgC6SWPaYTdVis53nxsz/EJIXgPuPMTtxrzL7ibwy1Pppc2os3al28vHVqaV4ZTTbsPUa+Sg0qzJuWk+X1x0sGr5D/rHJGhbtGtbnGunImv6Mmp5s7r5q7lmgFvlORgaE7CFk3gICctWiiEsI4qJpLgwE8rcl5LsPFiQYln1O8IOHcPMClt8uNFSinj6L2sQEvMCoOqDBvKVqOhVvXsDhSsnm3mApdhABSEQJ/rZt1mIRIYQFITNjkB
|
||||||
|
|
||||||
|
Nr/Pus0LNsevNqegaEKtthIFeutogptroYKs+gaK+gdvgsdkWpALSGdhdpQuIZIbpiBs9hBiwqQGwh9lwvzj9sRMhnSsIl3iwZIqIqSj3vIpUAALLrB1AwAcAfqRFj7+SWLS6T5YQ/BnD0TDgqwCaGqJBXhsYJIzDIiERBLMQ7x+K757q6iSp+KIg0RDjuJEEX6UKXgzAtzOJBJNyXDqxEE34KZ36aZFK2yK7lLOwmSq7mQ6bXrebf7a6+qm564B
|
||||||
|
|
||||||
|
qAFG4gFGZm7xwhaOZlAjIuZ24wH7hwFO7JT5yfonarTZr5SJBe6W6oA+77LVQuQ4GoBTCXhKpXjL4JYJITKPI9RUENo0EHy8S3D7qZY/IdrCEYZsEcEZ6hhZ43TgoSDPTMCvTvSfRgq+6l5ooYqdBFYlYjq178FErA5YZYEwJCHMEiGCIBGoZBF/gB5yIQ6VD6CrgBgAAKkgcAHAhUeO7MT8aR5ExwB8eERE6IGqeRZOhRaAQSpOqsAIZ4TEqw7O
|
||||||
|
|
||||||
|
EAnO7w/weETOsICwKsREfOCGaA2pcmJssuD+ymT+00ZSLqKuWmUx6uzhPotkWuDkoBd+Bu4cd+xuLpGxUaQyOxNu8aPxZQ7mMyCBZx1h5Qlxayo+lcWytxZJYQtUMsDwg44eaAomypCW0ebYtwfiSQKIjBvylJUJeW6eBWa83Bw6uKhJBKAhHCFJ6Gi2lQ12A2t2zAAAOhwHACSGYLlHSF1iuj1s2YNu2Z2d2SQMBg6eNrul5KoeoSemmYtsYRAG
|
||||||
|
|
||||||
|
IDkBOTeoYfehejtntjgu+mGWSjYT+mQvgKug1n1jdq1h2V2WwD2WuVLqBuBtOW9h4Rhpwl9rwkkn4X9h3mhq3q+aSa+OEUyRIGwBMKQKuBODwFKEka7BzAKc4MiJsHhM4lLC2hKVKXVFMPqlrPgcJkqVLFUV5ERFRB8GPFLGJiEnqd9qgKalasaYMdacMXbBaepq/tae/tMV6k6YZrrmbMseEu6f/pHJ6esWUHZlsZAaMgGfbrAY7uWXMqcVYQeR
|
||||||
|
|
||||||
|
GSgWsquDcduOcdgbVP4jsGiMSb8e1NwCiBQX8VmbwMEkxlsMcCmaNOCdlt2uyCWcvCGVXrwdWfvBOvWX+Y2RIIACjkgA8H+oB1AyipS4AdmBXBWIAcDrQBjOB6D6CuhIKaDBAdmoDpWoAAAUCA2gyg2gqABMH4KCAYAAlMuqeegJFSFfgGFRFUFXUNFbFfFQYElYQClQgGlRldlblflYVfeqVTZFOa9urNukevOagAIjoVuXoatjMcQaQHeigtNS
|
||||||
|
|
||||||
|
YbjmYftnuUpeFEeedieT1lVaFfoOFRwFVY1XFQla1e1Z1eld1XlQVUVVtgNQpA+S9u4Z4SSZ9j4Xwl+TSj+XSf+aEdhkBatA9MiaiR9DBaiv9ETthO4toPcKrKHoiDWjsPmZ4twDRFRBsJ8KrFsH8EQaqZ6EkFRN3D8KsJcBsPcOfp9bcNxIiH8ORUKaJkiH0YTrfkJXkqaYUgrs/mMW/mrp6priJbxSGvxUGqsfMdZosZAOJQ5pJXsQmpMg7q5Q
|
||||||
|
|
||||||
|
6YpX5sge7rgI9BpdwPcQKjwI8XXLVKsAJheCNQgn8WqRiFHtQZ6FxteKLgZZPPZZCbloCqWSCjdCtMipRnBWCmymoqSAANJPjKL0Asw4mlB4k8EEnlY1mO2wYA1kmTou0GhwBsDJhlnlSFDZ6mylBzDlReZgDZ03TYRE1iQMZk35Hjz1RfRgA02zAi4M1IhoiAgF2V7/6exQD7Q5S3Y63lQYD5YEIQDgSQTQTwTHQQD6BsC5SVC0iaBqAT1SibrE
|
||||||
|
|
||||||
|
Bsnp3WSZ450ak2UqyuJjy8So2b2Ih5HniXAAjAgs5t2HJYjZDEDd38i907L90ZDLxD3RGxHxEBiJFfST3T3+Rz0L3f1L2Vir0Z1LSb0YiDj4ErArDOInDDi126gQODiOKrBohjyAjrCX0Mkd0LXrRl7gi4CbUGg314MXQEMKJl4ShBDLgUAp0+hT2MCREkAgNbiajqAwmUJA4/U0md4A1MpgAsoRESAB3B2kih0sy8mCq+0ipiw+JST4SLDERCZc
|
||||||
|
|
||||||
|
YYU3BLCk7MSnBDhMa0bAiEWoBbAkVKpM0ibLC6lmrUXfVlD9EZyuRDHc0sUv4VLsX80KGzH6bOmiV8UmYrEelrHC0y23HbE2H+nQGK2yXK0KVpr7lu5ly4DKLa1IGG3pR0RiRQz4GmVGVoCXDW0AmE0n6Djb0FkQlFmu1p4uVcFlD4lVmx2eVVZ0MnQ9ZCBhB5D9kVXFYtODVKFPlm2TkAJzYLbaHnpoKVArkzh9nm2LVbbLWT0kDEDoRrW7mHZD
|
||||||
|
|
||||||
|
2g1vTg1bV2G7ViHNPhCPbPVuFQYvleHvmUKfmxK/V8OOWYaJ2AXYPg7A2VCaAcCtA4xqKgSygwQQ0WIoRWILMyPpFwhXin1cbNwH3ypixiZw04RDQuInBTC1paqAFC4pDMQAjOJIg716N/afXiz4HWVXhjzMRBIs3ya2NtL2PmmsGWnK6c0cV2lzWOlC1S0i0+MCXmbs2WYS2/79LgG+khNQGuYHHZyVPeaq1JMXGqXYgIQxn5q3G61VRX1PFbyn
|
||||||
|
|
||||||
|
A3j1QBKpl1SXCfGUHmX0QOL8zpnBpglMENmp7sH5Ye2dBe2IkbRbS7QHRoHZ7F7+SezYmAzt1VPR01P4p1PvWN5aXkkt5drBFggXOMrd53O94SAwC4Cyi4AUiSDMCB1fPQDSOjDjCEFQvTD8xL67CLAYXYT5ESyyp2IYhk5ngeI+gE11RaxUSFPdwohjzn1U36l+4CI2MmkTFc2Us9rUtsW0uuP2lzJzGePC2uRunstLHGhMt/5iXm4SWxpSVhM+
|
||||||
|
|
||||||
|
jBkiuShiuhbq1xM0yJObvJMR54tKr4REFfEGkXi5OvLqw7ACYbB/x2Ums+VmscOROQDVM161OVaCFBtcONNiEThHi4AchhDlU9b/tRBAdIDbrKF7qjUDNaGzqXxLn6HdRTNLkYKrU+jmEbVq2na/pbMOFgeAehCQcuFgYvWHNvUcJwbU2/bcMA6XMhvXP+syKRtCPoAExwRsnrBsko4TANApsT4GhcwIU4RUSOIfCXhMTIg5No00aoizBTDSw1r4
|
||||||
|
|
||||||
|
Qgn6NqNw0ZGikn5C7IgWMfm9PWOs0DEcv35dtMVaSjFWkDu2kC1f4eM8XMvju+Mmczs8sLtOYCv7HhOHFyVFY+YxNbtpjJsyuYEBsJkNxCl3A8bx3zVZN+75EXt4j6v1g4SglJ6lNPsWtrtvungeWft1nftXNUb+VBWREwCyhqIEwdmABJZEFRV0TDODV6V7KPV016gKvUSDyCaJV215EZwMoGwBOPtG14wo4K0+gTIQ4ZFWV61xwLV6gPV2oB1X
|
||||||
|
|
||||||
|
N817N/Nx11AF12t6V/14N8Nyt6gKN5ll0zuq9jXWNv0xoYMwh0tjM8h5M0YTM+hzuW+ss0Q7h8ee09N+Vz1wd4t41wd5ES179+t2wJ14ENt6gH14MHtyN5WMd09a4U+e9u9d4a22c/4fR+G4xyDmEax8Bba9tHtIdCm661DfBThHEEiDWmsGk0owl7J8TuqQ0X4kEqrICGi2pz4sEjWWJKcPxLZTSp9eJFLDcIancNTgo7JnRTkgxZzeZ6po47zS
|
||||||
|
|
||||||
|
4zZ245KCOw57O946HGy0AaGv48y4ExAYu/LYGZAKu/AScdE+9xKxrQTLu/K9WAbcWulLHjeHvSe5QQbMxIl5DMRDcOWsvsa4Waa6wc5ZwRvVaxieYqm/yX7SDWyTwJgByZgEYOiu67iVil6++z6/lyEcx8pcnRlz6GnRnZa6UMXZ0LnWAPnTdIXZX6UNhNz3YrDHzzeDZVcqUCL4asCCCZL/PscFg2AEVkSJ3XfYNn3Qic/QtEPSPVBLBNK/3VPT
|
||||||
|
|
||||||
|
PRIMmA4CRwiUAyvWvZQg3zX+cILDhFsPkdqS2kew4gg8hVJCOFJLsNqZcIOEP0cpADfePw/XcU/YPQ9E8y828x8xPcvz/qkB56MxWYsvRYa787ouoI+ssEIix4GIWLTevhFEz0QcIGqYiB3yf7BxO6pDf6OQxw5lASG+DEIBQzRTX18ANDBppAAYYIAmG2/Mvmw0kAcNQ+7eHhr+WDb0kBGFMNjk9ET7J9JAqfATmm0gDCcxMyQEcIfi4jjwtYwS
|
||||||
|
|
||||||
|
AtvRCQoC8aaPRMSOiHxodJzweoJVOJjKI3gdSenShP8CNKy8TOFLEYjzSs5ds6WtnB0hrx1yOcACuvcWqO0N7ztZaJvW3ArRXZK012/nG3sBEjLYgv66BYqKF3DLhdIYoeK8DhX3Sns/cwTWLs8htq8A1GTbZYGl2drF8e04fWEvJVfbZ9cuH7Wss3mqwcCMMxXKhBgkCAjlwYuAVAMmBA5iF5QtIcIB2RqF1DTEF3U7o2lg5Xd4Ov7W7iMwkBjM
|
||||||
|
|
||||||
|
7y81VDjM2PQDY/mZQLDm90qCbQieDrCULYTw7tMmhVQ1oQB3aF7Mker2FHlRw+ro9aOrArHiwIToF8gaOeCQJEX0AwR6A5XCYAk0kZYlyeQnRDHYmv5L4midiFEMvnjRFt6IkDeYOiFVjnh9GcQPIkxA2CyRaIzRamosAkhRDAQWpLYLsHiEds5eZnBxlS1YrONrONSellxVc62oJ2evYSgby17spXBQTOWh4LN6ZwImPgjdmSRWRxM6gDvfur7n
|
||||||
|
|
||||||
|
1qKt922TOxMOGPae8LanoE4AIkzJJCyK6ICLEH3S6nCIA0JLLpHzwzR9x8wgh5qv1JCSBZQcEY4CwHT4V5M+blGOrnyKFnCCBR8byqUKxCl8chWdKAQgwLpfQ9+FEIxpCLgbAipItdbCPCPyIfAkRN4FEXRCH4j8ogC1N/oMDNEYB+QYY5QBGNH64NiBhDCMUQLIYkDfoZA4hhQP+hUDqSJwx9vQxvK0DmGO/ZgIwOYF/kLhNrcoBqK1E6jxuBYG
|
||||||
|
|
||||||
|
PoJ3+bcwkKHGFnGIORqYhGeHwaAcxHVh/AbK1wM8PozWCylHEjiEcCximD6Dqw7bIzmS0jimDmK2IpxuMQKRWC1ejLckaqAcFi0/GXLL0nO02JuCPOS7QVt52FaW8Va1vCMSyLTCNBd28ZZ4vzGmDaMxIGrIiL72rTdF+RdI4PiU1lHyj3a2XfIZ1Dy4mjA2JQn9qIQcLrRsAYgXMLszaY9ZYJ8EzplBx6YZw1Cl3capNWGaPohhC0EYRtk3KDD0
|
||||||
|
|
||||||
|
AEw+Zhv2mHrVZhVwm4XcNlAPClh21ewnMLgn2hEJpHR8rsKOao8TmiGKxqGzYF/UG8oOfHmqPQAUgaYyiHgIgApCe4nhjY9NjRj1QbA822wdSfxGmByCrwspITCjQcQ71K2zwJFuJFVj5EVgQLJxLRSF6ttDBMvGXBiIKQK9LONLSwYOwZa2CFiFIpzo4L3HOCKRRvPlhAF2K0iZKPnF9uu2vHit/BkrXAGyQfFhdni2pVEWsA9Hm04ulOT8RNSk
|
||||||
|
|
||||||
|
ipcuM6Qh9paMy5ATLxnrSsjnzrwxci+so8oRACJhEhsgzAVAJwFQBp0FqE1YEBMA7INCHCdUmcO7CakcAWptIKAO1OHBdT0Jw1TCXOU0ILkhmiHO7rNQMILVHupE6AKYUw7UTLCEY5YZ9x6y9SGpA0oaW1KGhjSOh1+fZsjx4n7C0e1FDHt+SEkMcqSuPQGmJMuHoBCABMHaDjGODY5cAQguPk2JBGzB+IUsO2kNH+BjhGeTfbiERBWDXA6IqqQX
|
||||||
|
|
||||||
|
sZN17wibg5k0eIagbbBoWiM4owQ5JMGMUsRvbHEauKqSq8h27jbinYO8k7izMpI6dluLALuc/SnnTwUGW8ElTRWUUvdrbziZqIEpYQ54jhCpy7AacGrVSVlOBAnAQRC+Ypg5UY6ASKmnM4rCBLKzGjKpFoqCbHx6mZY+pjU5qa1JGkoCMwSEsQvtP6kGzhp2U24Cd2g5kEehOExcgtLWwocVp+Elai9wsJHZtpzE/DpUHNn6zBphs62SbM4nkc0A
|
||||||
|
|
||||||
|
ewkIjdI/JHDBJuYwqfsPOGvTKxyiOoJQFJCrgYIZ076CXhSKE4BSFFc4NqTPwrBUQKIeIWRFMnG1y5rccnDZTBFFsGaFwVVlsC0nYt0eY8TQXyJzJSwMQ7YeyWzSnZqRCZPbdkH21xFuTyZHk+ztTO3Gi06ZTgzXm52PEszTxXnLwQyOVm+CbxAQ3AJ8xC7e4ORDxbkS7wHAHwN8UwDOLENLlZSRcOEW9vlJD55ishbtJWccQrLV4Ch6sidABXDJ
|
||||||
|
|
||||||
|
VTn5ccwIo9IQAVi2UqsUgBChpjNBHoiKBScIIgCioRxEnDEPVD8SiY0QGFWtneGvZDg0Q1EUPGp2CS8xy6fiEFvcAElSBPqV+QzqS07ZOSiZY8kmXzSnmEjGZrpZzkPKjj7ivGlIo8dSPcHSUhWRxXIZFJdw8yYpGtHGALOUrhCyClwZiLC0FFxcy2WU4cPgSIjz45Z2YxWRH3fmlTP5oEwoRrMK6McapNQWkOSF1GmyHC5i0gJYrrGKEuhMHToW
|
||||||
|
|
||||||
|
NRmkTUnZq0+7r8TGGrTnuL6Tad7OinBTfZ7TWxfYu2FkcDmEcq6VHL4m+FzmD07Hk9N/k4Zk5bKZkPtAaCgQIUuACuNoQbHwLRUZ4PUPcn7HN0EZvwgcOoxcRAjFghqFYNMCMkc4OkRETjA4igZNR8KLbaitZJoX0UCZ8vBhXKPHmky3ULCwWmwpM4kjF5s8pmSvP5Zry2Z5vDmXoq5liLmRu8lHNIsDxth0mTEMFtci94GlpehlRIXkwMa7BS2Z
|
||||||
|
|
||||||
|
4Ign+PllUkdFOQqOmVK/kVSvKJiqkjVOcCoAsAqUHwDlTgAwAOyXyn5YlWCC6BmAmAIFagG0AwruplQYFZgF+VgqAVUKkFX8vBWQqXA0K2FRNO6EuK4Os0m7kh0WmuySJ7staRhyolLMtpQSnaTtXaYIqkV/ywFVirRVgrsAEKqFTCu0ARKuJr1f6tHNOaxycxQCpJf9STlcDGS4kiABMBRw4xuwE4HIJSu9qwUAZSk8iCODwg3gtYw4T5EKTfFQ
|
||||||
|
|
||||||
|
yA+EsD4N3BzJDgbgoSRFmyyIjJB/ROwIaNME1gfj25PS2cbQsckOozBSvCwWuPcmsLuFY7WmYbj8lLy5l/Ck8abzCkXjVloixAuIpUoa0IU2ypVuFhFySpz26UqtJqxiE6skheZSLM1BuUyiAFco7IWWSeUGK1Zry+ppkOgmVBIqj0EkDkFcD8g6qC3CgNEGUBMA2164Q6qNmkIDkxCDaptVABbXEA21soDtcoC7WkAe1+APtbbIwkOy3FuE+aZ4
|
||||||
|
|
||||||
|
pJUPcyV6CdaVSte40r41dKliSV1QCNqP4Y6idVOpnVzqF1iPSJZdMo6xKaOFC/7KKvQygKHoUAfAATEeibAYIl4f6XNSKVnA0mzEJIC+MNSVLlJMwZxBJgvCGoKaTSlUi0shZ2qz6jqoUs6psmuq8Zg87XqZ3oWjzhlTClXviOsHDsZ5Xkueay13EudJlh4n0lbgEXLt2Zm8mNdvKCW3j8o6JDYrGU0qCyt4SQBpWeCHGZrSCNFTsacvrSvIPgyN
|
||||||
|
|
||||||
|
bItFnvZPyE5L88propEU5dDF38mtdVJ6zzdT1zam+hes7XdrjqQVXtbgH7ViVJulQXTSOvPUmb21Rm2dfZrM0WbvMQ1PFX0ygCuLru/Q4lS7M3VLVfFO6yADMP3VklD1fsiQDZrPUGb7Nk6xzdevM28rw5z5B9QnUFX8SEl8crWc9Myjvq10PAfAN2HiAo4VgAG6Gi4nOBM01FSIFxPbQLZIVzwQuGngJjRZC5lS1beiFTwiwXh8CpwRYPuhxnZM
|
||||||
|
|
||||||
|
M46IgZZiMI16QVxzC0jRuM8mS0aZ884NbRoDUuC+FxvCNaFKEW+dQyfghNXEx2jJqeRdUaGB0W1ZCjeAKwSWYfmk7EQjWxapTU5VfmqaK17lIxW8sglFc9qQVCkJyHBCNShyrZNtT9o6zhBUAoENhCQg4kDqvu3237aDoB2Xl7NwOv7WDoh3yEHFbm7pmd2XxYSvNBK9xXNIGHkqvF65ZaVuu3L+LqVgSg9SEq+2oBkd8O88i2UR2RUGdjU8HUwH
|
||||||
|
|
||||||
|
R3JaolqWgVXEq+pZbX1Ja3Lbc0lVKiCeEAdYOtE0CbAIUcESQMF3yUqi1VIg8YKfhKVC5T+GA9WBQrIifAD8ZOD4AqRzJgj4RdEeqICGU5Rcx404s8dfjnF0LPVS44mVNpI0f4KZ6vCjfNqo068aNnCokcELW1BSQpgi88cIr85MiA2nGtZLAsD28bYxzxU4FJB/E5rzttGDMrmvOX1Q6wSQD4lotrWlqntjyrPs8o03Vqv2H20xXTopD0hUARAT
|
||||||
|
|
||||||
|
QOQFIAsr5u3YIQMog7QTr7sHWYgAYAOz06YIKOCcD2pyDuh+9g+ttZEWsDRBSyTUxAOQCQScAMd7KKzcepr1162qje5vUFVb3t6oAne9rJIB72HVkwY+ofc5pH2wBT9E+qfV2uXiz6mAR4FsEvtx12zMKy6nzdBL81zViJgW8lX4sWZ7rqd4W2nUOu+21769m+trjvo71xau9h+3vSfopAD6z9kVVcBfpgBX77Nk+87LfoWj3759T+7nfer51PrB
|
||||||
|
|
||||||
|
dtJYBSJLx5i77mb0iAFChhTwpY99YkvGTyoAClu4yQNYKJmSm1a0sRBMiEoz1DAgVO/wE6SrDBGidmtKIY/FeCbjdLeEGgmQYiDRa99lglq+3e6rG0EavVy45XniPd3TyqZlG4kRwrw0B7vSvLRjRttD0bzwpjI7mRstinYB2RCJTkc720oNxs9rPche+MRCIaxR5yxxPgTJwZN89AEsteXxoM5yfaKu6VYHXoBCB9oMEBoDwGTYR1h+JeytXwTj
|
||||||
|
|
||||||
|
o/ybmf8zWVc2tGZ0boe/avrXwNElG7oFESQ6grVS7w5DtdRQzmVnw6qARCwIMdgNDE91wxj9Kfl/zTBKJVEGiLRDoj0QGI/EygYxNnLKCADZ6wAgBv3S34QCwGN0GokxEVL4Q6IiNIUpfzFwbHrg2xy8FgOIZRjujMY3oz6Gn45Ah6UOGHHDgRxI5Uc6OTHNjmVXUDf6cxkAYvUIDgCd+KxqvnDTaJC57a2pZtLIKgGAnjdIJgEMHgmDHGcGS2BM
|
||||||
|
|
||||||
|
btuTF4DUxkNNgxmMoEF6aBdAiASWLUBMD8ssol9eQbFX5aJA8RxI8kdSPlaC5GNEET6JQVNxBwkG3gA4gU5XBhNp/Hosvmrb8wqtxjAEZvm+G27eAbq/pZwsXEWdzBrk31eMrs5GHvdJh3yctv8nLzw1q8yNVtoinsb410e7EAFJCEFpHxRtE+msCPwat8Iks8eFXR3wKb/xJah5eWsyOvbNNFe7MTVJRxoSJug6hwl6ah2OLX9BnTHXjt6GErfN
|
||||||
|
|
||||||
|
MzYYRM28VuzoE5EqYSFoCVD16D+eQvBsxWE9Z/TS+p7Heu4lpbrmGW+JZjyF0PamOok6g1G3elwBSQbAQhsoCWC0nXh6RLjMTQahKpzwDq/g7YiQrRCdSUkLQYhs62iY4a/EJEQEixlUVeE3cHDcZ0lMjydDLuvQ5PJm0e7NxK2hbdRoXkhrZlgehjXVBpE2GWNdhreZHvDL6ncAlE6WkabjKJTEy3GNnl8lE0dRYBkskFrcG7h3s20GQ8I0XudO
|
||||||
|
|
||||||
|
GjvW5egrpXo+V07IiCYJBH8paljleyHZbBFKGUAiBH9tyaxfWtK7gXCAkF68reSc1wXIQiFhfdMYPRY7ksOO6aR/u1lLkozS0nxX/uC3Lkkzu2iLTDqh7oXML0F4zbhYQsEHkLYcnnZHPS3867pdHEs/wyBgZG6AuAOAHAHlCbxJ+XQcEFkEqCQJoQTwBgPIQoD7QRl02gwyMGXIiAvYAYGcPoHlALjGKLIcUKpewD6WZ+RlzS8Rvl7mXWQll6y9
|
||||||
|
|
||||||
|
caMuPQ5t3LZy+dBsuZATLm5nUHqC8sGWjLflzlmqaCs+WbhVIkMMpD0veXXLmQBqqzMDJxXgrmQR6GRb6GpXIrGV4i6cgisJX9AFCSi4ROjMhaXLUAQy75ZDGImUxiY8VtlcKtoHiAuAigPgPRO6WrL8Vyq0Zdas4wY+TsTqxVaqv6BG1aaGCNSDxBN5DQ2AEkDKCTXjIeYVwZrbe3qhNRVLzAWa9SHwDtA0AE4ouTJqT3TA0BqlowGwAMByXrkB
|
||||||
|
|
||||||
|
ANhAFZohZF/cRyRqz1cyATWq4BaXhfyF0s8gSAQZ2K99eIDygEAu2cM5nBIB9dcoaBwDsEEyGg3RlH/H0PtGpAPQOUHITKnRFCTJCMb6N6gPiD1BlUsQjCBCzVQKQo3cAaN7YDjZrCU2KbuN44GVTBxPXQrSOOAARb41kpY1jCFMCQh6Pw3CB52dqulH4vLkiAwN3nViHOyKXol+Z4QJ+rwQUcMMh1OxUwG7Bpp5bWIRW5YshsC35bDNuwHCh+bM
|
||||||
|
|
||||||
|
BZQ52OAODYQBa3obLA7EP0EICMAcY51/AJdedYxwMg1tni5h2aYYIir5iJOgUcY5RgDAN8YIK7Y6g5bQgS2a27bftvlnHrY3KG3alXKXxIi2QIQG+rF2e7dmOyIGCWCAA===
|
||||||
|
```
|
||||||
|
%%
|
||||||
2
docs/ProjectOutline/ProjectOutlineDiagram.excalidraw.svg
Normal file
2
docs/ProjectOutline/ProjectOutlineDiagram.excalidraw.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 36 KiB |
@@ -1,21 +1,21 @@
|
|||||||
# Overview
|
# Overview
|
||||||
The server by default
|
The server by default
|
||||||
- runs on port 5146
|
- runs on port 5146
|
||||||
- Uses Swagger UI in development mode (`/swagger/index.html`)
|
- Uses Swagger UI (`/swagger/index.html`)
|
||||||
- Ignores API keys when in development mode
|
|
||||||
- Uses Elmah error logging (endpoint: `/elmah`, local files: `~/logs`)
|
- Uses Elmah error logging (endpoint: `/elmah`, local files: `~/logs`)
|
||||||
- Uses serilog logging (local files: `~/logs`)
|
- Uses serilog logging (local files: `~/logs`)
|
||||||
- Uses HealthChecks (endpoint: `/healthz`)
|
- Uses HealthChecks (endpoint: `/healthz`)
|
||||||
## Docker installation
|
## Docker installation
|
||||||
(On Linux you might need root privileges, thus use `sudo` where necessary)
|
(On Linux you might need root privileges. Use `sudo` where necessary)
|
||||||
1. Navigate to the `src/server` directory
|
1. [Set up the configuration](docs/Server.md#setup)
|
||||||
2. Build the docker container: `docker build -t embeddingsearch-server -f /Dockerfile .`
|
2. Navigate to the `src` directory
|
||||||
3. Run the docker container: `docker run --net=host -t embeddingsearch-server` (the `-t` is optional, but you get more meaningful output. Or use `-d` to run it in the background)
|
3. Build the docker container: `docker build -t embeddingsearch-server -f Server/Dockerfile .`
|
||||||
|
4. Run the docker container: `docker run --net=host -t embeddingsearch-server` (the `-t` is optional, but you get more meaningful output. Or use `-d` to run it in the background)
|
||||||
# Installing the dependencies
|
# Installing the dependencies
|
||||||
## Ubuntu 24.04
|
## Ubuntu 24.04
|
||||||
1. Install the .NET SDK: `sudo apt update && sudo apt install dotnet-sdk-8.0 -y`
|
1. Install the .NET SDK: `sudo apt update && sudo apt install dotnet-sdk-10.0 -y`
|
||||||
## Windows
|
## Windows
|
||||||
Download the [.NET SDK](https://dotnet.microsoft.com/en-us/download) or follow these steps to use WSL:
|
Download and install the [.NET SDK](https://dotnet.microsoft.com/en-us/download) or follow these steps to use WSL:
|
||||||
1. Install Ubuntu in WSL (`wsl --install` and `wsl --install -d Ubuntu`)
|
1. Install Ubuntu in WSL (`wsl --install` and `wsl --install -d Ubuntu`)
|
||||||
2. Enter your WSL environment `wsl.exe` and configure it
|
2. Enter your WSL environment `wsl.exe` and configure it
|
||||||
3. Update via `sudo apt update && sudo apt upgrade -y && sudo snap refresh`
|
3. Update via `sudo apt update && sudo apt upgrade -y && sudo snap refresh`
|
||||||
@@ -30,6 +30,9 @@ Download the [.NET SDK](https://dotnet.microsoft.com/en-us/download) or follow t
|
|||||||
`CREATE DATABASE embeddingsearch; use embeddingsearch;`
|
`CREATE DATABASE embeddingsearch; use embeddingsearch;`
|
||||||
4. Create the user (replace "somepassword! with a secure password):
|
4. Create the user (replace "somepassword! with a secure password):
|
||||||
`CREATE USER 'embeddingsearch'@'%' identified by "somepassword!"; GRANT ALL ON embeddingsearch.* TO embeddingsearch; FLUSH PRIVILEGES;`
|
`CREATE USER 'embeddingsearch'@'%' identified by "somepassword!"; GRANT ALL ON embeddingsearch.* TO embeddingsearch; FLUSH PRIVILEGES;`
|
||||||
|
- Caution: The symbol "%" in the command means that this user can be logged into from outside of the machine.
|
||||||
|
- Replace `'%'` with `'localhost'` or with the IP of your embeddingsearch server machine if that is a concern.
|
||||||
|
5. Exit mysql: `exit`
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
## Environments
|
## Environments
|
||||||
@@ -43,34 +46,39 @@ If you just installed the server and want to configure it:
|
|||||||
3. Check the "AiProviders" section. If your Ollama/LocalAI/etc. instance does not run locally, update the "baseURL" to point to the correct URL.
|
3. Check the "AiProviders" section. If your Ollama/LocalAI/etc. instance does not run locally, update the "baseURL" to point to the correct URL.
|
||||||
4. If you plan on using the server in production:
|
4. If you plan on using the server in production:
|
||||||
1. Set the environment variable `DOTNET_ENVIRONMENT` to something that is not "Development". (e.g. "Prod")
|
1. Set the environment variable `DOTNET_ENVIRONMENT` to something that is not "Development". (e.g. "Prod")
|
||||||
2. Rename the `appsettings.Development.json` - replace "Development" with whatever you chose. (e.g. "Prod")
|
2. Rename the `appsettings.Development.json` - replace "Development" with what you chose for `DOTNET_ENVIRONMENT`
|
||||||
3. Set API keys in the "ApiKeys" section (generate keys using the `uuid` command on Linux)
|
3. Set API keys in the "ApiKeys" section (generate keys using the `uuid` command on Linux)
|
||||||
## Structure
|
## Structure
|
||||||
```json
|
```json
|
||||||
"Embeddingsearch": {
|
"Embeddingsearch": {
|
||||||
"ConnectionStrings": {
|
"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" // Name of the sqlite cache file
|
||||||
},
|
},
|
||||||
"Elmah": {
|
"Elmah": {
|
||||||
"AllowedHosts": [ // Specify which IP addresses can access /elmah
|
"LogPath": "~/logs" // Where the logs are stored
|
||||||
"127.0.0.1",
|
|
||||||
"::1",
|
|
||||||
"172.17.0.1"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"AiProviders": {
|
"AiProviders": {
|
||||||
"ollama": { // Name of the provider. Used when defining models for a datapoint, e.g. "ollama:mxbai-embed-large"
|
"ollama": { // Name for the provider. Used when defining models for a datapoint, e.g. "ollama:mxbai-embed-large"
|
||||||
"handler": "ollama", // The type of API located at baseURL
|
"handler": "ollama", // The type of API located at baseURL
|
||||||
"baseURL": "http://localhost:11434" // Location of the API
|
"baseURL": "http://localhost:11434", // Location of the API
|
||||||
|
"Allowlist": [".*"], // Allow- and Denylist. Filter out non-embeddings models using regular expressions
|
||||||
|
"Denylist": ["qwen3-coder:latest", "qwen3:0.6b", "deepseek-v3.1:671b-cloud", "qwen3-vl", "deepseek-ocr"]
|
||||||
},
|
},
|
||||||
"localAI": {
|
"localAI": { // e.g. model name: "localAI:bert-embeddings"
|
||||||
"handler": "openai",
|
"handler": "openai",
|
||||||
"baseURL": "http://localhost:8080",
|
"baseURL": "http://localhost:8080",
|
||||||
"ApiKey": "Some API key here"
|
"ApiKey": "Some API key here",
|
||||||
|
"Allowlist": [".*"],
|
||||||
|
"Denylist": ["cross-encoder", "..."]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ApiKeys": ["Some UUID here", "Another UUID here"], // Restrict access in non-development environments to the server's API using your own generated API keys
|
"ApiKeys": ["Some UUID here", "Another UUID here"], // (optional) Restrict access using API keys
|
||||||
"UseHttpsRedirection": true // tbh I don't even know why this is still here. // TODO implement HttpsRedirection or remove this line
|
"Cache": {
|
||||||
|
"CacheTopN": 10000, // Only cache this number of queries. (Eviction policy: LRU)
|
||||||
|
"StoreEmbeddingCache": true, // If set to true, the SQLite database will be used to store the embeddings
|
||||||
|
"StoreTopN": 10000 // Only write the top n number of queries to the SQLite database
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
## AiProviders
|
## AiProviders
|
||||||
@@ -91,9 +99,9 @@ One can even specify multiple Ollama instances and name them however one pleases
|
|||||||
```
|
```
|
||||||
### handler
|
### handler
|
||||||
Currently two handlers are implemented for embeddings generation:
|
Currently two handlers are implemented for embeddings generation:
|
||||||
- ollama
|
- `ollama`
|
||||||
- requests embeddings from `/api/embed`
|
- requests embeddings from `/api/embed`
|
||||||
- localai
|
- `openai`
|
||||||
- requests embeddings from `/v1/embeddings`
|
- requests embeddings from `/v1/embeddings`
|
||||||
### baseURL
|
### baseURL
|
||||||
Specified by `scheme://host:port`. E.g.: `"baseUrl": "http://localhost:11434"`
|
Specified by `scheme://host:port`. E.g.: `"baseUrl": "http://localhost:11434"`
|
||||||
@@ -105,7 +113,7 @@ Any specified absolute path will be disregarded. (e.g. "http://x.x.x.x/any/subro
|
|||||||
|
|
||||||
# API
|
# API
|
||||||
## Accessing the api
|
## Accessing the api
|
||||||
Once started, the server's API can be comfortably be viewed and manipulated via swagger.
|
Once started, the server's API can be viewed and manipulated via swagger.
|
||||||
|
|
||||||
By default it is accessible under: `http://localhost:5146/swagger/index.html`
|
By default it is accessible under: `http://localhost:5146/swagger/index.html`
|
||||||
|
|
||||||
@@ -114,7 +122,7 @@ To make an API request from within swagger:
|
|||||||
2. Click the "Try it out" button. The input fields (if there are any for your action) should now be editable.
|
2. Click the "Try it out" button. The input fields (if there are any for your action) should now be editable.
|
||||||
3. Fill in the necessary information
|
3. Fill in the necessary information
|
||||||
4. Click "Execute"
|
4. Click "Execute"
|
||||||
## Restricting access
|
## Authorization
|
||||||
API keys do **not** get checked in Development environment!
|
Being logged in has priority over API Key requirement (if api keys are set).
|
||||||
|
|
||||||
Set up a non-development environment as described in [Configuration>Setup](#setup) to enable API key authentication.
|
So being logged in automatically authorizes endpoint usage.
|
||||||
BIN
docs/logo.png
Normal file
BIN
docs/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -121,13 +121,13 @@ public class Client
|
|||||||
}), new StringContent(settings, Encoding.UTF8, "application/json"));
|
}), new StringContent(settings, Encoding.UTF8, "application/json"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SearchdomainSearchesResults> SearchdomainGetQueriesAsync(string searchdomain)
|
public async Task<SearchdomainQueriesResults> SearchdomainGetQueriesAsync(string searchdomain)
|
||||||
{
|
{
|
||||||
Dictionary<string, string> parameters = new()
|
Dictionary<string, string> parameters = new()
|
||||||
{
|
{
|
||||||
{"searchdomain", searchdomain}
|
{"searchdomain", searchdomain}
|
||||||
};
|
};
|
||||||
return await FetchUrlAndProcessJson<SearchdomainSearchesResults>(HttpMethod.Get, GetUrl($"{baseUri}/Searchdomain", "Queries", parameters));
|
return await FetchUrlAndProcessJson<SearchdomainQueriesResults>(HttpMethod.Get, GetUrl($"{baseUri}/Searchdomain", "Queries", parameters));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<EntityQueryResults> SearchdomainQueryAsync(string query)
|
public async Task<EntityQueryResults> SearchdomainQueryAsync(string query)
|
||||||
@@ -190,13 +190,13 @@ public class Client
|
|||||||
return await FetchUrlAndProcessJson<SearchdomainUpdateResults>(HttpMethod.Put, GetUrl($"{baseUri}/Searchdomain", "Settings", parameters), content);
|
return await FetchUrlAndProcessJson<SearchdomainUpdateResults>(HttpMethod.Put, GetUrl($"{baseUri}/Searchdomain", "Settings", parameters), content);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SearchdomainSearchCacheSizeResults> SearchdomainGetQueryCacheSizeAsync(string searchdomain)
|
public async Task<SearchdomainQueryCacheSizeResults> SearchdomainGetQueryCacheSizeAsync(string searchdomain)
|
||||||
{
|
{
|
||||||
Dictionary<string, string> parameters = new()
|
Dictionary<string, string> parameters = new()
|
||||||
{
|
{
|
||||||
{"searchdomain", searchdomain}
|
{"searchdomain", searchdomain}
|
||||||
};
|
};
|
||||||
return await FetchUrlAndProcessJson<SearchdomainSearchCacheSizeResults>(HttpMethod.Get, GetUrl($"{baseUri}/Searchdomain/QueryCache", "Size", parameters));
|
return await FetchUrlAndProcessJson<SearchdomainQueryCacheSizeResults>(HttpMethod.Get, GetUrl($"{baseUri}/Searchdomain/QueryCache", "Size", parameters));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SearchdomainInvalidateCacheResults> SearchdomainClearQueryCache(string searchdomain)
|
public async Task<SearchdomainInvalidateCacheResults> SearchdomainClearQueryCache(string searchdomain)
|
||||||
@@ -222,9 +222,9 @@ public class Client
|
|||||||
return await FetchUrlAndProcessJson<ServerGetModelsResult>(HttpMethod.Get, GetUrl($"{baseUri}/Server", "Models", []));
|
return await FetchUrlAndProcessJson<ServerGetModelsResult>(HttpMethod.Get, GetUrl($"{baseUri}/Server", "Models", []));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ServerGetEmbeddingCacheSizeResult> ServerGetEmbeddingCacheSizeAsync()
|
public async Task<ServerGetStatsResult> ServerGetStatsAsync()
|
||||||
{
|
{
|
||||||
return await FetchUrlAndProcessJson<ServerGetEmbeddingCacheSizeResult>(HttpMethod.Get, GetUrl($"{baseUri}/Server/EmbeddingCache", "Size", []));
|
return await FetchUrlAndProcessJson<ServerGetStatsResult>(HttpMethod.Get, GetUrl($"{baseUri}/Server/Stats", "Size", []));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<T> FetchUrlAndProcessJson<T>(HttpMethod httpMethod, string url, HttpContent? content = null)
|
private async Task<T> FetchUrlAndProcessJson<T>(HttpMethod httpMethod, string url, HttpContent? content = null)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
FROM ubuntu:24.04 AS ubuntu
|
FROM ubuntu:25.10 AS ubuntu
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get install -y python3.12 python3.12-venv python3.12-dev dotnet-sdk-8.0
|
RUN apt-get install -y python3.13 python3.13-venv python3.13-dev dotnet-sdk-10.0
|
||||||
RUN apt-get clean
|
RUN apt-get clean
|
||||||
COPY . /src/
|
COPY . /src/
|
||||||
ENV ASPNETCORE_ENVIRONMENT Docker
|
ENV ASPNETCORE_ENVIRONMENT Docker
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ElmahCore" Version="2.1.2" />
|
<PackageReference Include="ElmahCore" Version="2.1.2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.14" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.14.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.2" />
|
||||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.0" />
|
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.1" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.0" />
|
||||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.3" />
|
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.2" />
|
||||||
<PackageReference Include="Python" Version="3.13.3" />
|
<PackageReference Include="Python" Version="3.14.2" />
|
||||||
<PackageReference Include="Pythonnet" Version="3.0.5" />
|
<PackageReference Include="Pythonnet" Version="3.0.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -80,8 +80,6 @@ else
|
|||||||
app.UseMiddleware<Shared.ApiKeyMiddleware>();
|
app.UseMiddleware<Shared.ApiKeyMiddleware>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// app.UseHttpsRedirection();
|
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"ApiKeys": ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"],
|
"ApiKeys": ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"],
|
||||||
"Server": {
|
"Server": {
|
||||||
"BaseUri": "http://localhost:5146",
|
"BaseUri": "http://localhost:5146",
|
||||||
"ApiKey": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
|
"ApiKey": "APIKeyForTheServer"
|
||||||
}
|
},
|
||||||
|
"PythonRuntime": "libpython3.13.so"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,26 +5,8 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Kestrel":{
|
"Indexer": {
|
||||||
"Endpoints": {
|
"Workers":
|
||||||
"http":{
|
|
||||||
"Url": "http://0.0.0.0:5120"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Embeddingsearch": {
|
|
||||||
"BaseUri": "http://172.17.0.1:5146",
|
|
||||||
"ApiKeys": ["b54ea868-496e-11f0-9cc7-f79f06b160e5", "bbdeedf0-496e-11f0-9744-97e28c221f67"]
|
|
||||||
},
|
|
||||||
"EmbeddingsearchIndexer": {
|
|
||||||
"Elmah": {
|
|
||||||
"AllowedHosts": [
|
|
||||||
"127.0.0.1",
|
|
||||||
"::1",
|
|
||||||
"172.17.0.1"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Worker":
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"Name": "pythonExample",
|
"Name": "pythonExample",
|
||||||
@@ -36,6 +18,12 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"ApiKeys": ["APIKeyOfYourChoice", "AnotherOneIfYouLike"],
|
||||||
|
"Server": {
|
||||||
|
"BaseUri": "http://172.17.0.1:5146",
|
||||||
|
"ApiKey": "APIKeyForTheServer"
|
||||||
|
},
|
||||||
|
"PythonRuntime": "libpython3.13.so"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
Uri uri = new(modelUri);
|
||||||
string provider = uri.Scheme;
|
string provider = uri.Scheme;
|
||||||
@@ -103,13 +108,13 @@ public class AIProvider
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
JObject responseContentJson = JObject.Parse(responseContent);
|
JObject responseContentJson = JObject.Parse(responseContent);
|
||||||
JToken? responseContentTokens = responseContentJson.SelectToken(embeddingsJsonPath);
|
List<JToken>? responseContentTokens = [.. responseContentJson.SelectTokens(embeddingsJsonPath)];
|
||||||
if (responseContentTokens is null)
|
if (responseContentTokens is null)
|
||||||
{
|
{
|
||||||
_logger.LogError("Unable to select tokens using JSONPath {embeddingsJsonPath} for string: {responseContent}.", [embeddingsJsonPath, responseContent]);
|
_logger.LogError("Unable to select tokens using JSONPath {embeddingsJsonPath} for string: {responseContent}.", [embeddingsJsonPath, responseContent]);
|
||||||
throw new JSONPathSelectionException(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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -116,12 +116,14 @@ public class EntityController : ControllerBase
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogError("Unable to deserialize an entity");
|
_logger.LogError("Unable to deserialize an entity");
|
||||||
|
ElmahCore.ElmahExtensions.RaiseError(new Exception("Unable to deserialize an entity"));
|
||||||
return Ok(new EntityIndexResult() { Success = false, Message = "Unable to deserialize an entity"});
|
return Ok(new EntityIndexResult() { Success = false, Message = "Unable to deserialize an entity"});
|
||||||
}
|
}
|
||||||
} catch (Exception ex)
|
} catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (ex.InnerException is not null) ex = ex.InnerException;
|
if (ex.InnerException is not null) ex = ex.InnerException;
|
||||||
_logger.LogError("Unable to index the provided entities. {ex.Message} - {ex.StackTrace}", [ex.Message, ex.StackTrace]);
|
_logger.LogError("Unable to index the provided entities. {ex.Message} - {ex.StackTrace}", [ex.Message, ex.StackTrace]);
|
||||||
|
ElmahCore.ElmahExtensions.RaiseError(ex);
|
||||||
return Ok(new EntityIndexResult() { Success = false, Message = ex.Message });
|
return Ok(new EntityIndexResult() { Success = false, Message = ex.Message });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +144,11 @@ public class EntityController : ControllerBase
|
|||||||
if (entity_ is null)
|
if (entity_ is null)
|
||||||
{
|
{
|
||||||
_logger.LogError("Unable to delete the entity {entityName} in {searchdomain} - it was not found under the specified name", [entityName, searchdomain]);
|
_logger.LogError("Unable to delete the entity {entityName} in {searchdomain} - it was not found under the specified name", [entityName, searchdomain]);
|
||||||
|
ElmahCore.ElmahExtensions.RaiseError(
|
||||||
|
new Exception(
|
||||||
|
$"Unable to delete the entity {entityName} in {searchdomain} - it was not found under the specified name"
|
||||||
|
)
|
||||||
|
);
|
||||||
return Ok(new EntityDeleteResults() {Success = false, Message = "Entity not found"});
|
return Ok(new EntityDeleteResults() {Success = false, Message = "Entity not found"});
|
||||||
}
|
}
|
||||||
searchdomain_.ReconciliateOrInvalidateCacheForDeletedEntity(entity_);
|
searchdomain_.ReconciliateOrInvalidateCacheForDeletedEntity(entity_);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http.HttpResults;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Server.Exceptions;
|
using Server.Exceptions;
|
||||||
using Server.Helper;
|
using Server.Helper;
|
||||||
|
using Shared;
|
||||||
using Shared.Models;
|
using Shared.Models;
|
||||||
|
|
||||||
namespace Server.Controllers;
|
namespace Server.Controllers;
|
||||||
@@ -54,6 +55,10 @@ public class SearchdomainController : ControllerBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (settings.QueryCacheSize <= 0)
|
||||||
|
{
|
||||||
|
settings.QueryCacheSize = 1_000_000; // TODO get rid of this magic number
|
||||||
|
}
|
||||||
int id = _domainManager.CreateSearchdomain(searchdomain, settings);
|
int id = _domainManager.CreateSearchdomain(searchdomain, settings);
|
||||||
return Ok(new SearchdomainCreateResults(){Id = id, Success = true});
|
return Ok(new SearchdomainCreateResults(){Id = id, Success = true});
|
||||||
} catch (Exception)
|
} catch (Exception)
|
||||||
@@ -134,13 +139,13 @@ public class SearchdomainController : ControllerBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="searchdomain">Name of the searchdomain</param>
|
/// <param name="searchdomain">Name of the searchdomain</param>
|
||||||
[HttpGet("Queries")]
|
[HttpGet("Queries")]
|
||||||
public ActionResult<SearchdomainSearchesResults> GetQueries([Required]string searchdomain)
|
public ActionResult<SearchdomainQueriesResults> GetQueries([Required]string searchdomain)
|
||||||
{
|
{
|
||||||
(Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_domainManager, searchdomain, _logger);
|
(Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_domainManager, searchdomain, _logger);
|
||||||
if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new SearchdomainUpdateResults(){Success = false, Message = message});
|
if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new SearchdomainUpdateResults(){Success = false, Message = message});
|
||||||
Dictionary<string, DateTimedSearchResult> searchCache = searchdomain_.searchCache;
|
Dictionary<string, DateTimedSearchResult> searchCache = searchdomain_.queryCache.AsDictionary();
|
||||||
|
|
||||||
return Ok(new SearchdomainSearchesResults() { Searches = searchCache, Success = true });
|
return Ok(new SearchdomainQueriesResults() { Searches = searchCache, Success = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -175,7 +180,7 @@ public class SearchdomainController : ControllerBase
|
|||||||
{
|
{
|
||||||
(Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_domainManager, searchdomain, _logger);
|
(Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_domainManager, searchdomain, _logger);
|
||||||
if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new SearchdomainUpdateResults(){Success = false, Message = message});
|
if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new SearchdomainUpdateResults(){Success = false, Message = message});
|
||||||
Dictionary<string, DateTimedSearchResult> searchCache = searchdomain_.searchCache;
|
EnumerableLruCache<string, DateTimedSearchResult> searchCache = searchdomain_.queryCache;
|
||||||
bool containsKey = searchCache.ContainsKey(query);
|
bool containsKey = searchCache.ContainsKey(query);
|
||||||
if (containsKey)
|
if (containsKey)
|
||||||
{
|
{
|
||||||
@@ -196,7 +201,7 @@ public class SearchdomainController : ControllerBase
|
|||||||
{
|
{
|
||||||
(Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_domainManager, searchdomain, _logger);
|
(Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_domainManager, searchdomain, _logger);
|
||||||
if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new SearchdomainUpdateResults(){Success = false, Message = message});
|
if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new SearchdomainUpdateResults(){Success = false, Message = message});
|
||||||
Dictionary<string, DateTimedSearchResult> searchCache = searchdomain_.searchCache;
|
EnumerableLruCache<string, DateTimedSearchResult> searchCache = searchdomain_.queryCache;
|
||||||
bool containsKey = searchCache.ContainsKey(query);
|
bool containsKey = searchCache.ContainsKey(query);
|
||||||
if (containsKey)
|
if (containsKey)
|
||||||
{
|
{
|
||||||
@@ -237,6 +242,7 @@ public class SearchdomainController : ControllerBase
|
|||||||
};
|
};
|
||||||
searchdomain_.helper.ExecuteSQLNonQuery("UPDATE searchdomain set settings = @settings WHERE id = @id", parameters);
|
searchdomain_.helper.ExecuteSQLNonQuery("UPDATE searchdomain set settings = @settings WHERE id = @id", parameters);
|
||||||
searchdomain_.settings = request;
|
searchdomain_.settings = request;
|
||||||
|
searchdomain_.queryCache.Capacity = request.QueryCacheSize;
|
||||||
return Ok(new SearchdomainUpdateResults(){Success = true});
|
return Ok(new SearchdomainUpdateResults(){Success = true});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,15 +251,17 @@ public class SearchdomainController : ControllerBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="searchdomain">Name of the searchdomain</param>
|
/// <param name="searchdomain">Name of the searchdomain</param>
|
||||||
[HttpGet("QueryCache/Size")]
|
[HttpGet("QueryCache/Size")]
|
||||||
public ActionResult<SearchdomainSearchCacheSizeResults> GetSearchCacheSize([Required]string searchdomain)
|
public ActionResult<SearchdomainQueryCacheSizeResults> GetQueryCacheSize([Required]string searchdomain)
|
||||||
{
|
{
|
||||||
if (!SearchdomainHelper.IsSearchdomainLoaded(_domainManager, searchdomain))
|
if (!SearchdomainHelper.IsSearchdomainLoaded(_domainManager, searchdomain))
|
||||||
{
|
{
|
||||||
return Ok(new SearchdomainSearchCacheSizeResults() { QueryCacheSizeBytes = 0, Success = true });
|
return Ok(new SearchdomainQueryCacheSizeResults() { SizeBytes = 0, ElementCount = 0, ElementMaxCount = 0, Success = true });
|
||||||
}
|
}
|
||||||
(Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_domainManager, searchdomain, _logger);
|
(Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_domainManager, searchdomain, _logger);
|
||||||
if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new SearchdomainUpdateResults(){Success = false, Message = message});
|
if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new SearchdomainUpdateResults(){Success = false, Message = message});
|
||||||
return Ok(new SearchdomainSearchCacheSizeResults() { QueryCacheSizeBytes = searchdomain_.GetSearchCacheSize(), Success = true });
|
int elementCount = searchdomain_.queryCache.Count;
|
||||||
|
int ElementMaxCount = searchdomain_.settings.QueryCacheSize;
|
||||||
|
return Ok(new SearchdomainQueryCacheSizeResults() { SizeBytes = searchdomain_.GetSearchCacheSize(), ElementCount = elementCount, ElementMaxCount = ElementMaxCount, Success = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -278,7 +286,7 @@ public class SearchdomainController : ControllerBase
|
|||||||
{
|
{
|
||||||
(Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_domainManager, searchdomain, _logger);
|
(Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_domainManager, searchdomain, _logger);
|
||||||
if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new SearchdomainUpdateResults(){Success = false, Message = message});
|
if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new SearchdomainUpdateResults(){Success = false, Message = message});
|
||||||
long sizeInBytes = DatabaseHelper.GetSearchdomainDatabaseSize(searchdomain_.helper, searchdomain);
|
long EmbeddingCacheUtilization = DatabaseHelper.GetSearchdomainDatabaseSize(searchdomain_.helper, searchdomain);
|
||||||
return Ok(new SearchdomainGetDatabaseSizeResult() { SearchdomainDatabaseSizeBytes = sizeInBytes, Success = true });
|
return Ok(new SearchdomainGetDatabaseSizeResult() { SearchdomainDatabaseSizeBytes = EmbeddingCacheUtilization, Success = true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
namespace Server.Controllers;
|
namespace Server.Controllers;
|
||||||
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text.Json;
|
|
||||||
using AdaptiveExpressions;
|
|
||||||
using ElmahCore;
|
using ElmahCore;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Server.Exceptions;
|
|
||||||
using Server.Helper;
|
using Server.Helper;
|
||||||
using Server.Models;
|
using Server.Models;
|
||||||
|
using Shared;
|
||||||
using Shared.Models;
|
using Shared.Models;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
@@ -61,25 +58,23 @@ public class ServerController : ControllerBase
|
|||||||
long size = 0;
|
long size = 0;
|
||||||
long elementCount = 0;
|
long elementCount = 0;
|
||||||
long embeddingsCount = 0;
|
long embeddingsCount = 0;
|
||||||
LRUCache<string, Dictionary<string, float[]>> embeddingCache = _searchdomainManager.embeddingCache;
|
EnumerableLruCache<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)
|
foreach (KeyValuePair<string, Dictionary<string, float[]>> kv in embeddingCache)
|
||||||
{
|
{
|
||||||
if (!embeddingCache.TryGet(key, out var entry))
|
string key = kv.Key;
|
||||||
continue;
|
Dictionary<string, float[]> entry = kv.Value;
|
||||||
|
|
||||||
// estimate size
|
|
||||||
size += EstimateEntrySize(key, entry);
|
size += EstimateEntrySize(key, entry);
|
||||||
elementCount++;
|
elementCount++;
|
||||||
embeddingsCount += entry.Keys.Count;
|
embeddingsCount += entry.Keys.Count;
|
||||||
}
|
}
|
||||||
var sqlHelper = DatabaseHelper.GetSQLHelper(_options.Value);
|
var sqlHelper = DatabaseHelper.GetSQLHelper(_options.Value);
|
||||||
|
var databaseTotalSize = DatabaseHelper.GetTotalDatabaseSize(sqlHelper);
|
||||||
Task<long> entityCountTask = DatabaseHelper.CountEntities(sqlHelper);
|
Task<long> entityCountTask = DatabaseHelper.CountEntities(sqlHelper);
|
||||||
long queryCacheUtilization = 0;
|
long queryCacheUtilization = 0;
|
||||||
|
long queryCacheElementCount = 0;
|
||||||
|
long queryCacheMaxElementCountAll = 0;
|
||||||
|
long queryCacheMaxElementCountLoadedSearchdomainsOnly = 0;
|
||||||
foreach (string searchdomain in _searchdomainManager.ListSearchdomains())
|
foreach (string searchdomain in _searchdomainManager.ListSearchdomains())
|
||||||
{
|
{
|
||||||
if (SearchdomainHelper.IsSearchdomainLoaded(_searchdomainManager, searchdomain))
|
if (SearchdomainHelper.IsSearchdomainLoaded(_searchdomainManager, searchdomain))
|
||||||
@@ -87,10 +82,35 @@ public class ServerController : ControllerBase
|
|||||||
(Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_searchdomainManager, searchdomain, _logger);
|
(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});
|
if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new ServerGetStatsResult(){Success = false, Message = message});
|
||||||
queryCacheUtilization += searchdomain_.GetSearchCacheSize();
|
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;
|
long entityCount = await entityCountTask;
|
||||||
return new ServerGetStatsResult() { Success = true, EntityCount = entityCount, QueryCacheUtilization = queryCacheUtilization, SizeInBytes = size, MaxElementCount = _searchdomainManager.EmbeddingCacheMaxCount, ElementCount = elementCount, EmbeddingsCount = embeddingsCount};
|
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)
|
} catch (Exception ex)
|
||||||
{
|
{
|
||||||
ElmahExtensions.RaiseError(ex);
|
ElmahExtensions.RaiseError(ex);
|
||||||
|
|||||||
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,4 @@
|
|||||||
using AdaptiveExpressions;
|
using Shared;
|
||||||
using OllamaSharp;
|
|
||||||
using OllamaSharp.Models;
|
|
||||||
|
|
||||||
namespace Server;
|
namespace Server;
|
||||||
|
|
||||||
@@ -26,36 +24,103 @@ public class Datapoint
|
|||||||
return probMethod.method(probabilities);
|
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)
|
foreach (string model in models)
|
||||||
{
|
{
|
||||||
bool embeddingCacheHasModel = embeddingCache.TryGet(model, out var embeddingCacheForModel);
|
List<string> toBeGenerated = [];
|
||||||
if (embeddingCacheHasModel && embeddingCacheForModel.ContainsKey(content))
|
embeddings[model] = [];
|
||||||
|
foreach (string value in content)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toBeGenerated.Count == 0)
|
||||||
{
|
{
|
||||||
retVal[model] = embeddingCacheForModel[content];
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var response = aIProvider.GenerateEmbeddings(model, [content]);
|
IEnumerable<float[]> generatedEmbeddings = GenerateEmbeddings([.. toBeGenerated], model, aIProvider, embeddingCache);
|
||||||
if (response is not null)
|
if (generatedEmbeddings.Count() != toBeGenerated.Count)
|
||||||
{
|
{
|
||||||
retVal[model] = response;
|
throw new Exception("Requested embeddings count and generated embeddings count mismatched!");
|
||||||
if (!embeddingCacheHasModel)
|
}
|
||||||
|
for (int i = 0; i < toBeGenerated.Count; i++)
|
||||||
{
|
{
|
||||||
embeddingCacheForModel = [];
|
embeddings[model][toBeGenerated.ElementAt(i)] = generatedEmbeddings.ElementAt(i);
|
||||||
}
|
}
|
||||||
if (!embeddingCacheForModel.ContainsKey(content))
|
}
|
||||||
|
return embeddings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<float[]> GenerateEmbeddings(string[] content, string model, AIProvider aIProvider, EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache)
|
||||||
{
|
{
|
||||||
embeddingCacheForModel[content] = response;
|
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");
|
||||||
return retVal;
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN dotnet restore ./Server.csproj
|
RUN dotnet restore Server/Server.csproj
|
||||||
RUN dotnet publish ./Server.csproj -c Release -o /output
|
RUN dotnet publish Server/Server.csproj -c Release -o /output
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /output .
|
COPY --from=build /output .
|
||||||
ENV ASPNETCORE_ENVIRONMENT Docker
|
ENV ASPNETCORE_ENVIRONMENT Docker
|
||||||
|
|||||||
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,6 @@
|
|||||||
using System.Configuration;
|
|
||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using MySql.Data.MySqlClient;
|
using MySql.Data.MySqlClient;
|
||||||
using Server.Exceptions;
|
using Server.Exceptions;
|
||||||
using Server.Models;
|
using Server.Models;
|
||||||
@@ -39,6 +39,19 @@ public class DatabaseHelper(ILogger<DatabaseHelper> logger)
|
|||||||
helper.ExecuteSQLNonQuery(query.ToString(), parameters);
|
helper.ExecuteSQLNonQuery(query.ToString(), parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int DatabaseInsertEmbeddingBulk(SQLHelper helper, List<(string hash, string model, byte[] embedding)> data)
|
||||||
|
{
|
||||||
|
return helper.BulkExecuteNonQuery(
|
||||||
|
"INSERT INTO embedding (id_datapoint, model, embedding) SELECT d.id, @model, @embedding FROM datapoint d WHERE d.hash = @hash",
|
||||||
|
data.Select(element => new object[] {
|
||||||
|
new MySqlParameter("@model", element.model),
|
||||||
|
new MySqlParameter("@embedding", element.embedding),
|
||||||
|
new MySqlParameter("@hash", element.hash)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static int DatabaseInsertSearchdomain(SQLHelper helper, string name, SearchdomainSettings settings = new())
|
public static int DatabaseInsertSearchdomain(SQLHelper helper, string name, SearchdomainSettings settings = new())
|
||||||
{
|
{
|
||||||
Dictionary<string, dynamic> parameters = new()
|
Dictionary<string, dynamic> parameters = new()
|
||||||
@@ -71,6 +84,32 @@ public class DatabaseHelper(ILogger<DatabaseHelper> logger)
|
|||||||
return helper.ExecuteSQLCommandGetInsertedID("INSERT INTO attribute (attribute, value, id_entity) VALUES (@attribute, @value, @id_entity)", parameters);
|
return helper.ExecuteSQLCommandGetInsertedID("INSERT INTO attribute (attribute, value, id_entity) VALUES (@attribute, @value, @id_entity)", parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int DatabaseInsertAttributes(SQLHelper helper, List<(string attribute, string value, int id_entity)> values) //string[] attribute, string value, int id_entity)
|
||||||
|
{
|
||||||
|
return helper.BulkExecuteNonQuery(
|
||||||
|
"INSERT INTO attribute (attribute, value, id_entity) VALUES (@attribute, @value, @id_entity)",
|
||||||
|
values.Select(element => new object[] {
|
||||||
|
new MySqlParameter("@attribute", element.attribute),
|
||||||
|
new MySqlParameter("@value", element.value),
|
||||||
|
new MySqlParameter("@id_entity", element.id_entity)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int DatabaseInsertDatapoints(SQLHelper helper, List<(string name, ProbMethodEnum probmethod_embedding, SimilarityMethodEnum similarityMethod, string hash)> values, int id_entity)
|
||||||
|
{
|
||||||
|
return helper.BulkExecuteNonQuery(
|
||||||
|
"INSERT INTO datapoint (name, probmethod_embedding, similaritymethod, hash, id_entity) VALUES (@name, @probmethod_embedding, @similaritymethod, @hash, @id_entity)",
|
||||||
|
values.Select(element => new object[] {
|
||||||
|
new MySqlParameter("@name", element.name),
|
||||||
|
new MySqlParameter("@probmethod_embedding", element.probmethod_embedding),
|
||||||
|
new MySqlParameter("@similaritymethod", element.similarityMethod),
|
||||||
|
new MySqlParameter("@hash", element.hash),
|
||||||
|
new MySqlParameter("@id_entity", id_entity)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static int DatabaseInsertDatapoint(SQLHelper helper, string name, ProbMethodEnum probmethod_embedding, SimilarityMethodEnum similarityMethod, string hash, int id_entity)
|
public static int DatabaseInsertDatapoint(SQLHelper helper, string name, ProbMethodEnum probmethod_embedding, SimilarityMethodEnum similarityMethod, string hash, int id_entity)
|
||||||
{
|
{
|
||||||
Dictionary<string, dynamic> parameters = new()
|
Dictionary<string, dynamic> parameters = new()
|
||||||
@@ -143,7 +182,7 @@ public class DatabaseHelper(ILogger<DatabaseHelper> logger)
|
|||||||
|
|
||||||
helper.ExecuteSQLNonQuery("DELETE embedding.* FROM embedding JOIN datapoint dp ON id_datapoint = dp.id JOIN entity ON id_entity = entity.id WHERE entity.id_searchdomain = @searchdomain", parameters);
|
helper.ExecuteSQLNonQuery("DELETE embedding.* FROM embedding JOIN datapoint dp ON id_datapoint = dp.id JOIN entity ON id_entity = entity.id WHERE entity.id_searchdomain = @searchdomain", parameters);
|
||||||
helper.ExecuteSQLNonQuery("DELETE datapoint.* FROM datapoint JOIN entity ON id_entity = entity.id WHERE entity.id_searchdomain = @searchdomain", parameters);
|
helper.ExecuteSQLNonQuery("DELETE datapoint.* FROM datapoint JOIN entity ON id_entity = entity.id WHERE entity.id_searchdomain = @searchdomain", parameters);
|
||||||
helper.ExecuteSQLNonQuery("DELETE attribute.* FROM attribute JOIN entity ON id_entity = entity.id WHERE entity.id_searchdomain = @searchdomain", parameters);
|
helper.ExecuteSQLNonQuery("DELETE FROM attribute WHERE id_entity IN (SELECT entity.id FROM entity WHERE id_searchdomain = @searchdomain)", parameters);
|
||||||
return helper.ExecuteSQLNonQuery("DELETE FROM entity WHERE entity.id_searchdomain = @searchdomain", parameters);
|
return helper.ExecuteSQLNonQuery("DELETE FROM entity WHERE entity.id_searchdomain = @searchdomain", parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,6 +262,21 @@ public class DatabaseHelper(ILogger<DatabaseHelper> logger)
|
|||||||
return result;
|
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)
|
public static async Task<long> CountEntities(SQLHelper helper)
|
||||||
{
|
{
|
||||||
DbDataReader searchdomainSumReader = helper.ExecuteSQLCommand("SELECT COUNT(*) FROM entity;", []);
|
DbDataReader searchdomainSumReader = helper.ExecuteSQLCommand("SELECT COUNT(*) FROM entity;", []);
|
||||||
@@ -244,4 +298,22 @@ public class DatabaseHelper(ILogger<DatabaseHelper> logger)
|
|||||||
searchdomainSumReader.Close();
|
searchdomainSumReader.Close();
|
||||||
return result;
|
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 System.Data.Common;
|
||||||
using MySql.Data.MySqlClient;
|
using MySql.Data.MySqlClient;
|
||||||
|
|
||||||
@@ -6,6 +7,7 @@ namespace Server.Helper;
|
|||||||
public class SQLHelper:IDisposable
|
public class SQLHelper:IDisposable
|
||||||
{
|
{
|
||||||
public MySqlConnection connection;
|
public MySqlConnection connection;
|
||||||
|
public DbDataReader? dbDataReader;
|
||||||
public string connectionString;
|
public string connectionString;
|
||||||
public SQLHelper(MySqlConnection connection, string connectionString)
|
public SQLHelper(MySqlConnection connection, string connectionString)
|
||||||
{
|
{
|
||||||
@@ -30,13 +32,15 @@ public class SQLHelper:IDisposable
|
|||||||
lock (connection)
|
lock (connection)
|
||||||
{
|
{
|
||||||
EnsureConnected();
|
EnsureConnected();
|
||||||
|
EnsureDbReaderIsClosed();
|
||||||
using MySqlCommand command = connection.CreateCommand();
|
using MySqlCommand command = connection.CreateCommand();
|
||||||
command.CommandText = query;
|
command.CommandText = query;
|
||||||
foreach (KeyValuePair<string, dynamic> parameter in parameters)
|
foreach (KeyValuePair<string, dynamic> parameter in parameters)
|
||||||
{
|
{
|
||||||
command.Parameters.AddWithValue($"@{parameter.Key}", parameter.Value);
|
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)
|
lock (connection)
|
||||||
{
|
{
|
||||||
EnsureConnected();
|
EnsureConnected();
|
||||||
|
EnsureDbReaderIsClosed();
|
||||||
using MySqlCommand command = connection.CreateCommand();
|
using MySqlCommand command = connection.CreateCommand();
|
||||||
|
|
||||||
command.CommandText = query;
|
command.CommandText = query;
|
||||||
@@ -61,6 +66,7 @@ public class SQLHelper:IDisposable
|
|||||||
lock (connection)
|
lock (connection)
|
||||||
{
|
{
|
||||||
EnsureConnected();
|
EnsureConnected();
|
||||||
|
EnsureDbReaderIsClosed();
|
||||||
using MySqlCommand command = connection.CreateCommand();
|
using MySqlCommand command = connection.CreateCommand();
|
||||||
|
|
||||||
command.CommandText = query;
|
command.CommandText = query;
|
||||||
@@ -74,6 +80,33 @@ public class SQLHelper:IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool EnsureConnected()
|
public bool EnsureConnected()
|
||||||
{
|
{
|
||||||
if (connection.State != System.Data.ConnectionState.Open)
|
if (connection.State != System.Data.ConnectionState.Open)
|
||||||
@@ -83,11 +116,29 @@ public class SQLHelper:IDisposable
|
|||||||
connection.Close();
|
connection.Close();
|
||||||
connection.Open();
|
connection.Open();
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
throw; // TODO add logging here
|
ElmahCore.ElmahExtensions.RaiseError(ex);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
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 System.Text.Json;
|
||||||
using AdaptiveExpressions;
|
using AdaptiveExpressions;
|
||||||
using Server.Exceptions;
|
using Server.Exceptions;
|
||||||
|
using Shared;
|
||||||
using Shared.Models;
|
using Shared.Models;
|
||||||
|
|
||||||
namespace Server.Helper;
|
namespace Server.Helper;
|
||||||
@@ -15,7 +16,7 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
|
|||||||
|
|
||||||
public static byte[] BytesFromFloatArray(float[] floats)
|
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();
|
var floatArray = floats.ToArray();
|
||||||
Buffer.BlockCopy(floatArray, 0, byteArray, 0, byteArray.Length);
|
Buffer.BlockCopy(floatArray, 0, byteArray, 0, byteArray.Length);
|
||||||
return byteArray;
|
return byteArray;
|
||||||
@@ -23,7 +24,7 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
|
|||||||
|
|
||||||
public static float[] FloatArrayFromBytes(byte[] bytes)
|
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);
|
Buffer.BlockCopy(bytes, 0, floatArray, 0, bytes.Length);
|
||||||
return floatArray;
|
return floatArray;
|
||||||
}
|
}
|
||||||
@@ -47,7 +48,7 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
|
|||||||
|
|
||||||
public List<Entity>? EntitiesFromJSON(SearchdomainManager searchdomainManager, ILogger logger, string json)
|
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;
|
AIProvider aIProvider = searchdomainManager.aIProvider;
|
||||||
SQLHelper helper = searchdomainManager.helper;
|
SQLHelper helper = searchdomainManager.helper;
|
||||||
|
|
||||||
@@ -57,22 +58,42 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// toBeCached: model -> [datapoint.text * n]
|
// Prefetch embeddings
|
||||||
Dictionary<string, List<string>> toBeCached = [];
|
Dictionary<string, List<string>> toBeCached = [];
|
||||||
|
Dictionary<string, List<string>> toBeCachedParallel = [];
|
||||||
foreach (JSONEntity jSONEntity in jsonEntities)
|
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 (JSONDatapoint datapoint in jSONEntity.Datapoints)
|
||||||
{
|
{
|
||||||
foreach (string model in datapoint.Model)
|
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 = [];
|
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
|
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 =>
|
||||||
@@ -88,11 +109,11 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
|
|||||||
|
|
||||||
public Entity? EntityFromJSON(SearchdomainManager searchdomainManager, ILogger logger, JSONEntity jsonEntity) //string json)
|
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);
|
Searchdomain searchdomain = searchdomainManager.GetSearchdomain(jsonEntity.Searchdomain);
|
||||||
List<Entity> entityCache = searchdomain.entityCache;
|
List<Entity> entityCache = searchdomain.entityCache;
|
||||||
AIProvider aIProvider = searchdomain.aIProvider;
|
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);
|
Entity? preexistingEntity = entityCache.FirstOrDefault(entity => entity.name == jsonEntity.Name);
|
||||||
bool invalidateSearchCache = false;
|
bool invalidateSearchCache = false;
|
||||||
|
|
||||||
@@ -224,18 +245,29 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
int id_entity = DatabaseHelper.DatabaseInsertEntity(helper, jsonEntity.Name, jsonEntity.Probmethod, _databaseHelper.GetSearchdomainID(helper, jsonEntity.Searchdomain));
|
int id_entity = DatabaseHelper.DatabaseInsertEntity(helper, jsonEntity.Name, jsonEntity.Probmethod, _databaseHelper.GetSearchdomainID(helper, jsonEntity.Searchdomain));
|
||||||
|
List<(string attribute, string value, int id_entity)> toBeInsertedAttributes = [];
|
||||||
foreach (KeyValuePair<string, string> attribute in jsonEntity.Attributes)
|
foreach (KeyValuePair<string, string> attribute in jsonEntity.Attributes)
|
||||||
{
|
{
|
||||||
DatabaseHelper.DatabaseInsertAttribute(helper, attribute.Key, attribute.Value, id_entity); // TODO implement bulk insert to reduce number of queries
|
toBeInsertedAttributes.Add(new() {
|
||||||
|
attribute = attribute.Key,
|
||||||
|
value = attribute.Value,
|
||||||
|
id_entity = id_entity
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
DatabaseHelper.DatabaseInsertAttributes(helper, toBeInsertedAttributes);
|
||||||
|
|
||||||
List<Datapoint> datapoints = [];
|
List<Datapoint> datapoints = [];
|
||||||
|
List<(JSONDatapoint datapoint, string hash)> toBeInsertedDatapoints = [];
|
||||||
foreach (JSONDatapoint jsonDatapoint in jsonEntity.Datapoints)
|
foreach (JSONDatapoint jsonDatapoint in jsonEntity.Datapoints)
|
||||||
{
|
{
|
||||||
string hash = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(jsonDatapoint.Text)));
|
string hash = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(jsonDatapoint.Text)));
|
||||||
Datapoint datapoint = DatabaseInsertDatapointWithEmbeddings(helper, searchdomain, jsonDatapoint, id_entity, hash);
|
toBeInsertedDatapoints.Add(new()
|
||||||
datapoints.Add(datapoint);
|
{
|
||||||
|
datapoint = jsonDatapoint,
|
||||||
|
hash = hash
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
List<Datapoint> datapoint = DatabaseInsertDatapointsWithEmbeddings(helper, searchdomain, toBeInsertedDatapoints, id_entity);
|
||||||
|
|
||||||
var probMethod = Probmethods.GetMethod(jsonEntity.Probmethod) ?? throw new ProbMethodNotFoundException(jsonEntity.Probmethod);
|
var probMethod = Probmethods.GetMethod(jsonEntity.Probmethod) ?? throw new ProbMethodNotFoundException(jsonEntity.Probmethod);
|
||||||
Entity entity = new(jsonEntity.Attributes, probMethod, jsonEntity.Probmethod.ToString(), datapoints, jsonEntity.Name)
|
Entity entity = new(jsonEntity.Attributes, probMethod, jsonEntity.Probmethod.ToString(), datapoints, jsonEntity.Name)
|
||||||
@@ -249,6 +281,38 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Datapoint> DatabaseInsertDatapointsWithEmbeddings(SQLHelper helper, Searchdomain searchdomain, List<(JSONDatapoint datapoint, string hash)> values, int id_entity)
|
||||||
|
{
|
||||||
|
List<Datapoint> result = [];
|
||||||
|
List<(string name, ProbMethodEnum probmethod_embedding, SimilarityMethodEnum similarityMethod, string hash)> toBeInsertedDatapoints = [];
|
||||||
|
List<(string hash, string model, byte[] embedding)> toBeInsertedEmbeddings = [];
|
||||||
|
foreach ((JSONDatapoint datapoint, string hash) value in values)
|
||||||
|
{
|
||||||
|
Datapoint datapoint = BuildDatapointFromJsonDatapoint(value.datapoint, id_entity, searchdomain, value.hash);
|
||||||
|
toBeInsertedDatapoints.Add(new()
|
||||||
|
{
|
||||||
|
name = datapoint.name,
|
||||||
|
probmethod_embedding = datapoint.probMethod.probMethodEnum,
|
||||||
|
similarityMethod = datapoint.similarityMethod.similarityMethodEnum,
|
||||||
|
hash = value.hash
|
||||||
|
});
|
||||||
|
foreach ((string, float[]) embedding in datapoint.embeddings)
|
||||||
|
{
|
||||||
|
toBeInsertedEmbeddings.Add(new()
|
||||||
|
{
|
||||||
|
hash = value.hash,
|
||||||
|
model = embedding.Item1,
|
||||||
|
embedding = BytesFromFloatArray(embedding.Item2)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
result.Add(datapoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseHelper.DatabaseInsertDatapoints(helper, toBeInsertedDatapoints, id_entity);
|
||||||
|
DatabaseHelper.DatabaseInsertEmbeddingBulk(helper, toBeInsertedEmbeddings);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public Datapoint DatabaseInsertDatapointWithEmbeddings(SQLHelper helper, Searchdomain searchdomain, JSONDatapoint jsonDatapoint, int id_entity, string? hash = null)
|
public Datapoint DatabaseInsertDatapointWithEmbeddings(SQLHelper helper, Searchdomain searchdomain, JSONDatapoint jsonDatapoint, int id_entity, string? hash = null)
|
||||||
{
|
{
|
||||||
if (jsonDatapoint.Text is null)
|
if (jsonDatapoint.Text is null)
|
||||||
@@ -274,10 +338,10 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
|
|||||||
throw new Exception("jsonDatapoint.Text must not be null at this point");
|
throw new Exception("jsonDatapoint.Text must not be null at this point");
|
||||||
}
|
}
|
||||||
using SQLHelper helper = searchdomain.helper.DuplicateConnection();
|
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)));
|
hash ??= Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(jsonDatapoint.Text)));
|
||||||
DatabaseHelper.DatabaseInsertDatapoint(helper, jsonDatapoint.Name, jsonDatapoint.Probmethod_embedding, jsonDatapoint.SimilarityMethod, hash, entityId);
|
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 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);
|
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))]);
|
return new Datapoint(jsonDatapoint.Name, probMethod_embedding, similarityMethod, hash, [.. embeddings.Select(kv => (kv.Key, kv.Value))]);
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ public static class DatabaseMigrations
|
|||||||
int initialDatabaseVersion = DatabaseGetVersion(helper);
|
int initialDatabaseVersion = DatabaseGetVersion(helper);
|
||||||
int databaseVersion = initialDatabaseVersion;
|
int databaseVersion = initialDatabaseVersion;
|
||||||
|
|
||||||
|
if (databaseVersion == 0)
|
||||||
|
{
|
||||||
|
databaseVersion = Create(helper);
|
||||||
|
}
|
||||||
|
|
||||||
var updateMethods = typeof(DatabaseMigrations)
|
var updateMethods = typeof(DatabaseMigrations)
|
||||||
.GetMethods(BindingFlags.Public | BindingFlags.Static)
|
.GetMethods(BindingFlags.Public | BindingFlags.Static)
|
||||||
.Where(m => m.Name.StartsWith("UpdateFrom") && m.ReturnType == typeof(int))
|
.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", []);
|
helper.ExecuteSQLNonQuery("ALTER TABLE datapoint ADD COLUMN similaritymethod VARCHAR(512) NULL DEFAULT 'Cosine' AFTER probmethod_embedding", []);
|
||||||
return 4;
|
return 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int UpdateFrom4(SQLHelper helper)
|
||||||
|
{
|
||||||
|
helper.ExecuteSQLNonQuery("UPDATE searchdomain SET settings = JSON_SET(settings, '$.QueryCacheSize', 1000000) WHERE JSON_EXTRACT(settings, '$.QueryCacheSize') is NULL;", []); // Set QueryCacheSize to a default of 1000000
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,9 +8,9 @@ public class EmbeddingSearchOptions : ApiKeyOptions
|
|||||||
{
|
{
|
||||||
public required ConnectionStringsOptions ConnectionStrings { get; set; }
|
public required ConnectionStringsOptions ConnectionStrings { get; set; }
|
||||||
public ElmahOptions? Elmah { get; set; }
|
public ElmahOptions? Elmah { get; set; }
|
||||||
public required long EmbeddingCacheMaxCount { get; set; }
|
|
||||||
public required Dictionary<string, AiProvider> AiProviders { get; set; }
|
public required Dictionary<string, AiProvider> AiProviders { get; set; }
|
||||||
public required SimpleAuthOptions SimpleAuth { get; set; }
|
public required SimpleAuthOptions SimpleAuth { get; set; }
|
||||||
|
public required CacheOptions Cache { get; set; }
|
||||||
public required bool UseHttpsRedirection { get; set; }
|
public required bool UseHttpsRedirection { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,4 +38,12 @@ public class SimpleUser
|
|||||||
public class ConnectionStringsOptions
|
public class ConnectionStringsOptions
|
||||||
{
|
{
|
||||||
public required string SQL { get; set; }
|
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,13 +9,14 @@ using Server.Helper;
|
|||||||
using Server.Models;
|
using Server.Models;
|
||||||
using Server.Services;
|
using Server.Services;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Reflection;
|
|
||||||
using System.Configuration;
|
using System.Configuration;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi;
|
||||||
using Shared.Models;
|
using Shared.Models;
|
||||||
using Microsoft.AspNetCore.ResponseCompression;
|
using Microsoft.AspNetCore.ResponseCompression;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Server.Migrations;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -31,10 +32,28 @@ builder.Services.AddControllersWithViews()
|
|||||||
// Add Configuration
|
// Add Configuration
|
||||||
IConfigurationSection configurationSection = builder.Configuration.GetSection("Embeddingsearch");
|
IConfigurationSection configurationSection = builder.Configuration.GetSection("Embeddingsearch");
|
||||||
EmbeddingSearchOptions configuration = configurationSection.Get<EmbeddingSearchOptions>() ?? throw new ConfigurationErrorsException("Unable to start server due to an invalid configration");
|
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<EmbeddingSearchOptions>(configurationSection);
|
||||||
builder.Services.Configure<ApiKeyOptions>(configurationSection);
|
builder.Services.Configure<ApiKeyOptions>(configurationSection);
|
||||||
|
|
||||||
|
// Configure Kestrel
|
||||||
|
builder.WebHost.ConfigureKestrel(options =>
|
||||||
|
{
|
||||||
|
options.Limits.MaxRequestBodySize = configuration.MaxRequestBodySize ?? 50 * 1024 * 1024;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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
|
// Add Localization
|
||||||
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
|
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
|
||||||
builder.Services.Configure<RequestLocalizationOptions>(options =>
|
builder.Services.Configure<RequestLocalizationOptions>(options =>
|
||||||
@@ -50,36 +69,37 @@ builder.Services.AddScoped<LocalizationService>();
|
|||||||
|
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen(c =>
|
builder.Services.AddOpenApi(options =>
|
||||||
{
|
{
|
||||||
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
options.AddDocumentTransformer((document, context, _) =>
|
||||||
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
|
|
||||||
c.IncludeXmlComments(xmlPath);
|
|
||||||
if (configuration.ApiKeys is not null)
|
|
||||||
{
|
{
|
||||||
c.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme
|
if (configuration.ApiKeys is null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
document.Components ??= new();
|
||||||
|
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
|
||||||
|
|
||||||
|
document.Components.SecuritySchemes["ApiKey"] =
|
||||||
|
new OpenApiSecurityScheme
|
||||||
{
|
{
|
||||||
Description = "ApiKey must appear in header",
|
|
||||||
Type = SecuritySchemeType.ApiKey,
|
Type = SecuritySchemeType.ApiKey,
|
||||||
Name = "X-API-KEY",
|
Name = "X-API-KEY",
|
||||||
In = ParameterLocation.Header,
|
In = ParameterLocation.Header,
|
||||||
Scheme = "ApiKeyScheme"
|
Description = "ApiKey must appear in header"
|
||||||
});
|
|
||||||
var key = new OpenApiSecurityScheme()
|
|
||||||
{
|
|
||||||
Reference = new OpenApiReference
|
|
||||||
{
|
|
||||||
Type = ReferenceType.SecurityScheme,
|
|
||||||
Id = "ApiKey"
|
|
||||||
},
|
|
||||||
In = ParameterLocation.Header
|
|
||||||
};
|
};
|
||||||
var requirement = new OpenApiSecurityRequirement
|
|
||||||
|
document.Security ??= [];
|
||||||
|
|
||||||
|
// Apply globally
|
||||||
|
document.Security?.Add(
|
||||||
|
new OpenApiSecurityRequirement
|
||||||
{
|
{
|
||||||
{ key, []}
|
[new OpenApiSecuritySchemeReference("ApiKey", document)] = []
|
||||||
};
|
|
||||||
c.AddSecurityRequirement(requirement);
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Log.Logger = new LoggerConfiguration()
|
Log.Logger = new LoggerConfiguration()
|
||||||
.ReadFrom.Configuration(builder.Configuration)
|
.ReadFrom.Configuration(builder.Configuration)
|
||||||
@@ -228,13 +248,16 @@ app.Use(async (context, next) =>
|
|||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI(options =>
|
app.UseSwaggerUI(options =>
|
||||||
{
|
{
|
||||||
|
options.SwaggerEndpoint("/openapi/v1.json", "API v1");
|
||||||
|
options.RoutePrefix = "swagger";
|
||||||
options.EnablePersistAuthorization();
|
options.EnablePersistAuthorization();
|
||||||
options.InjectStylesheet("/swagger-ui/custom.css");
|
options.InjectStylesheet("/swagger-ui/custom.css");
|
||||||
options.InjectJavascript("/swagger-ui/custom.js");
|
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.
|
//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)
|
if (configuration.ApiKeys is not null)
|
||||||
|
|||||||
@@ -300,4 +300,40 @@
|
|||||||
<data name="Unable to fetch searchdomain database utilization" xml:space="preserve">
|
<data name="Unable to fetch searchdomain database utilization" xml:space="preserve">
|
||||||
<value>Searchdomain Datenbank-Auslastung konnte nicht abgerufen werden</value>
|
<value>Searchdomain Datenbank-Auslastung konnte nicht abgerufen werden</value>
|
||||||
</data>
|
</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>
|
</root>
|
||||||
@@ -300,4 +300,40 @@
|
|||||||
<data name="Unable to fetch searchdomain database utilization" xml:space="preserve">
|
<data name="Unable to fetch searchdomain database utilization" xml:space="preserve">
|
||||||
<value>Unable to fetch searchdomain database utilization</value>
|
<value>Unable to fetch searchdomain database utilization</value>
|
||||||
</data>
|
</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>
|
</root>
|
||||||
@@ -4,6 +4,7 @@ using System.Text.Json;
|
|||||||
using ElmahCore.Mvc.Logger;
|
using ElmahCore.Mvc.Logger;
|
||||||
using MySql.Data.MySqlClient;
|
using MySql.Data.MySqlClient;
|
||||||
using Server.Helper;
|
using Server.Helper;
|
||||||
|
using Shared;
|
||||||
using Shared.Models;
|
using Shared.Models;
|
||||||
using AdaptiveExpressions;
|
using AdaptiveExpressions;
|
||||||
|
|
||||||
@@ -17,15 +18,15 @@ public class Searchdomain
|
|||||||
public string searchdomain;
|
public string searchdomain;
|
||||||
public int id;
|
public int id;
|
||||||
public SearchdomainSettings settings;
|
public SearchdomainSettings settings;
|
||||||
public Dictionary<string, DateTimedSearchResult> searchCache; // Key: query, Value: Search results for that query (with timestamp)
|
public EnumerableLruCache<string, DateTimedSearchResult> queryCache; // Key: query, Value: Search results for that query (with timestamp)
|
||||||
public List<Entity> entityCache;
|
public List<Entity> entityCache;
|
||||||
public List<string> modelsInUse;
|
public List<string> modelsInUse;
|
||||||
public LRUCache<string, Dictionary<string, float[]>> embeddingCache;
|
public EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache;
|
||||||
private readonly MySqlConnection connection;
|
private readonly MySqlConnection connection;
|
||||||
public SQLHelper helper;
|
public SQLHelper helper;
|
||||||
private readonly ILogger _logger;
|
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;
|
_connectionString = connectionString;
|
||||||
_provider = provider.ToLower();
|
_provider = provider.ToLower();
|
||||||
@@ -33,12 +34,12 @@ public class Searchdomain
|
|||||||
this.aIProvider = aIProvider;
|
this.aIProvider = aIProvider;
|
||||||
this.embeddingCache = embeddingCache;
|
this.embeddingCache = embeddingCache;
|
||||||
this._logger = logger;
|
this._logger = logger;
|
||||||
searchCache = [];
|
|
||||||
entityCache = [];
|
entityCache = [];
|
||||||
connection = new MySqlConnection(connectionString);
|
connection = new MySqlConnection(connectionString);
|
||||||
connection.Open();
|
connection.Open();
|
||||||
helper = new SQLHelper(connection, connectionString);
|
helper = new SQLHelper(connection, connectionString);
|
||||||
settings = GetSettings();
|
settings = GetSettings();
|
||||||
|
queryCache = new(settings.QueryCacheSize);
|
||||||
modelsInUse = []; // To make the compiler shut up - it is set in UpdateSearchDomain() don't worry // yeah, about that...
|
modelsInUse = []; // To make the compiler shut up - it is set in UpdateSearchDomain() don't worry // yeah, about that...
|
||||||
if (!runEmpty)
|
if (!runEmpty)
|
||||||
{
|
{
|
||||||
@@ -163,7 +164,7 @@ public class Searchdomain
|
|||||||
|
|
||||||
public List<(float, string)> Search(string query, int? topN = null)
|
public List<(float, string)> Search(string query, int? topN = null)
|
||||||
{
|
{
|
||||||
if (searchCache.TryGetValue(query, out DateTimedSearchResult cachedResult))
|
if (queryCache.TryGetValue(query, out DateTimedSearchResult cachedResult))
|
||||||
{
|
{
|
||||||
cachedResult.AccessDateTimes.Add(DateTime.Now);
|
cachedResult.AccessDateTimes.Add(DateTime.Now);
|
||||||
return [.. cachedResult.Results.Select(r => (r.Score, r.Name))];
|
return [.. cachedResult.Results.Select(r => (r.Score, r.Name))];
|
||||||
@@ -187,18 +188,18 @@ public class Searchdomain
|
|||||||
[.. sortedResults.Select(r =>
|
[.. sortedResults.Select(r =>
|
||||||
new ResultItem(r.Item1, r.Item2 ))]
|
new ResultItem(r.Item1, r.Item2 ))]
|
||||||
);
|
);
|
||||||
searchCache[query] = new DateTimedSearchResult(DateTime.Now, searchResult);
|
queryCache.Set(query, new DateTimedSearchResult(DateTime.Now, searchResult));
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, float[]> GetQueryEmbeddings(string query)
|
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));
|
bool allModelsInQuery = queryEmbeddings is not null && modelsInUse.All(model => queryEmbeddings.ContainsKey(model));
|
||||||
if (!(hasQuery && allModelsInQuery) || queryEmbeddings is null)
|
if (!(hasQuery && allModelsInQuery) || queryEmbeddings is null)
|
||||||
{
|
{
|
||||||
queryEmbeddings = Datapoint.GenerateEmbeddings(query, modelsInUse, aIProvider, embeddingCache);
|
queryEmbeddings = Datapoint.GetEmbeddings(query, modelsInUse, aIProvider, embeddingCache);
|
||||||
if (!embeddingCache.TryGet(query, out var embeddingCacheForCurrentQuery))
|
if (!embeddingCache.TryGetValue(query, out var embeddingCacheForCurrentQuery))
|
||||||
{
|
{
|
||||||
embeddingCache.Set(query, queryEmbeddings);
|
embeddingCache.Set(query, queryEmbeddings);
|
||||||
}
|
}
|
||||||
@@ -206,7 +207,7 @@ public class Searchdomain
|
|||||||
{
|
{
|
||||||
foreach (KeyValuePair<string, float[]> kvp in queryEmbeddings) // kvp.Key = model, kvp.Value = embedding
|
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;
|
embeddingCacheForCurrentQuery[kvp.Key] = kvp.Value;
|
||||||
}
|
}
|
||||||
@@ -218,7 +219,7 @@ public class Searchdomain
|
|||||||
|
|
||||||
public void UpdateModelsInUse()
|
public void UpdateModelsInUse()
|
||||||
{
|
{
|
||||||
modelsInUse = GetModels([.. entityCache]);
|
modelsInUse = GetModels(entityCache.ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float EvaluateEntityAgainstQueryEmbeddings(Entity entity, Dictionary<string, float[]> queryEmbeddings)
|
private static float EvaluateEntityAgainstQueryEmbeddings(Entity entity, Dictionary<string, float[]> queryEmbeddings)
|
||||||
@@ -277,22 +278,14 @@ public class Searchdomain
|
|||||||
|
|
||||||
public SearchdomainSettings GetSettings()
|
public SearchdomainSettings GetSettings()
|
||||||
{
|
{
|
||||||
Dictionary<string, dynamic> parameters = new()
|
return DatabaseHelper.GetSearchdomainSettings(helper, searchdomain);
|
||||||
{
|
|
||||||
["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 ReconciliateOrInvalidateCacheForNewOrUpdatedEntity(Entity entity)
|
public void ReconciliateOrInvalidateCacheForNewOrUpdatedEntity(Entity entity)
|
||||||
{
|
{
|
||||||
if (settings.CacheReconciliation)
|
if (settings.CacheReconciliation)
|
||||||
{
|
{
|
||||||
foreach (KeyValuePair<string, DateTimedSearchResult> element in searchCache)
|
foreach (var element in queryCache)
|
||||||
{
|
{
|
||||||
string query = element.Key;
|
string query = element.Key;
|
||||||
DateTimedSearchResult searchResult = element.Value;
|
DateTimedSearchResult searchResult = element.Value;
|
||||||
@@ -322,7 +315,7 @@ public class Searchdomain
|
|||||||
{
|
{
|
||||||
if (settings.CacheReconciliation)
|
if (settings.CacheReconciliation)
|
||||||
{
|
{
|
||||||
foreach (KeyValuePair<string, DateTimedSearchResult> element in searchCache)
|
foreach (KeyValuePair<string, DateTimedSearchResult> element in queryCache)
|
||||||
{
|
{
|
||||||
string query = element.Key;
|
string query = element.Key;
|
||||||
DateTimedSearchResult searchResult = element.Value;
|
DateTimedSearchResult searchResult = element.Value;
|
||||||
@@ -337,18 +330,18 @@ public class Searchdomain
|
|||||||
|
|
||||||
public void InvalidateSearchCache()
|
public void InvalidateSearchCache()
|
||||||
{
|
{
|
||||||
searchCache = [];
|
queryCache = new(settings.QueryCacheSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
public long GetSearchCacheSize()
|
public long GetSearchCacheSize()
|
||||||
{
|
{
|
||||||
long sizeInBytes = 0;
|
long EmbeddingCacheUtilization = 0;
|
||||||
foreach (var entry in searchCache)
|
foreach (var entry in queryCache)
|
||||||
{
|
{
|
||||||
sizeInBytes += sizeof(int); // string length prefix
|
EmbeddingCacheUtilization += sizeof(int); // string length prefix
|
||||||
sizeInBytes += entry.Key.Length * sizeof(char); // string characters
|
EmbeddingCacheUtilization += entry.Key.Length * sizeof(char); // string characters
|
||||||
sizeInBytes += entry.Value.EstimateSize();
|
EmbeddingCacheUtilization += entry.Value.EstimateSize();
|
||||||
}
|
}
|
||||||
return sizeInBytes;
|
return EmbeddingCacheUtilization;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ using Shared.Models;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Server.Models;
|
using Server.Models;
|
||||||
|
using Shared;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace Server;
|
namespace Server;
|
||||||
|
|
||||||
public class SearchdomainManager
|
public class SearchdomainManager : IDisposable
|
||||||
{
|
{
|
||||||
private Dictionary<string, Searchdomain> searchdomains = [];
|
private Dictionary<string, Searchdomain> searchdomains = [];
|
||||||
private readonly ILogger<SearchdomainManager> _logger;
|
private readonly ILogger<SearchdomainManager> _logger;
|
||||||
@@ -21,8 +23,9 @@ public class SearchdomainManager
|
|||||||
private readonly string connectionString;
|
private readonly string connectionString;
|
||||||
private MySqlConnection connection;
|
private MySqlConnection connection;
|
||||||
public SQLHelper helper;
|
public SQLHelper helper;
|
||||||
public LRUCache<string, Dictionary<string, float[]>> embeddingCache;
|
public EnumerableLruCache<string, Dictionary<string, float[]>> embeddingCache;
|
||||||
public long EmbeddingCacheMaxCount;
|
public long EmbeddingCacheMaxCount;
|
||||||
|
private bool disposed = false;
|
||||||
|
|
||||||
public SearchdomainManager(ILogger<SearchdomainManager> logger, IOptions<EmbeddingSearchOptions> options, AIProvider aIProvider, DatabaseHelper databaseHelper)
|
public SearchdomainManager(ILogger<SearchdomainManager> logger, IOptions<EmbeddingSearchOptions> options, AIProvider aIProvider, DatabaseHelper databaseHelper)
|
||||||
{
|
{
|
||||||
@@ -30,21 +33,21 @@ public class SearchdomainManager
|
|||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
this.aIProvider = aIProvider;
|
this.aIProvider = aIProvider;
|
||||||
_databaseHelper = databaseHelper;
|
_databaseHelper = databaseHelper;
|
||||||
EmbeddingCacheMaxCount = _options.EmbeddingCacheMaxCount;
|
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);
|
embeddingCache = new((int)EmbeddingCacheMaxCount);
|
||||||
|
}
|
||||||
connectionString = _options.ConnectionStrings.SQL;
|
connectionString = _options.ConnectionStrings.SQL;
|
||||||
connection = new MySqlConnection(connectionString);
|
connection = new MySqlConnection(connectionString);
|
||||||
connection.Open();
|
connection.Open();
|
||||||
helper = new SQLHelper(connection, connectionString);
|
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)
|
public Searchdomain GetSearchdomain(string searchdomain)
|
||||||
@@ -82,13 +85,19 @@ public class SearchdomainManager
|
|||||||
{
|
{
|
||||||
DbDataReader reader = helper.ExecuteSQLCommand("SELECT name FROM searchdomain", []);
|
DbDataReader reader = helper.ExecuteSQLCommand("SELECT name FROM searchdomain", []);
|
||||||
List<string> results = [];
|
List<string> results = [];
|
||||||
|
try
|
||||||
|
{
|
||||||
while (reader.Read())
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
results.Add(reader.GetString(0));
|
results.Add(reader.GetString(0));
|
||||||
}
|
}
|
||||||
reader.Close();
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
reader.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int CreateSearchdomain(string searchdomain, SearchdomainSettings settings)
|
public int CreateSearchdomain(string searchdomain, SearchdomainSettings settings)
|
||||||
@@ -129,4 +138,39 @@ public class SearchdomainManager
|
|||||||
{
|
{
|
||||||
return searchdomains.ContainsKey(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">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -12,21 +12,22 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AdaptiveExpressions" Version="4.23.0" />
|
<PackageReference Include="AdaptiveExpressions" Version="4.23.1" />
|
||||||
<PackageReference Include="ElmahCore" Version="2.1.2" />
|
<PackageReference Include="ElmahCore" Version="2.1.2" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
<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="Serilog.Sinks.File" Version="7.0.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.0" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.3" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.2" />
|
||||||
<PackageReference Include="MySql.Data" Version="9.2.0" />
|
<PackageReference Include="MySql.Data" Version="9.6.0" />
|
||||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
<PackageReference Include="Npgsql" Version="10.0.1" />
|
||||||
<PackageReference Include="OllamaSharp" Version="5.2.2" />
|
<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.SqlClient" Version="4.9.0" />
|
||||||
<PackageReference Include="System.Data.Sqlite" Version="1.0.119" />
|
<PackageReference Include="System.Data.Sqlite" Version="2.0.2" />
|
||||||
<PackageReference Include="System.Numerics.Tensors" Version="9.0.3" />
|
<PackageReference Include="System.Numerics.Tensors" Version="10.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -91,8 +91,8 @@ 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
|
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: [
|
forceInclude: [
|
||||||
'[data-bs-theme="dark"]', '[data-bs-theme="dark"] body', '[data-bs-theme="dark"] .navbar', '[data-bs-theme="dark"] .card', '[data-bs-theme="dark"] .btn',
|
'[data-bs-theme="dark"]', '[data-bs-theme="dark"] body', '[data-bs-theme="dark"] .navbar', '[data-bs-theme="dark"] .card', '[data-bs-theme="dark"] .btn',
|
||||||
'.navbar',
|
|
||||||
'.col-md-4',
|
'.col-md-4',
|
||||||
|
'.navbar', '.ms-auto', '.dropdown', '.dropdown-menu',
|
||||||
'.visually-hidden', // visually hidden headings
|
'.visually-hidden', // visually hidden headings
|
||||||
'.bi-info-circle-fill', '.text-info', // info icon
|
'.bi-info-circle-fill', '.text-info', // info icon
|
||||||
'.container', '.col-md-6', '.row', '.g-4', '.row>*',
|
'.container', '.col-md-6', '.row', '.g-4', '.row>*',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# How to use CriticalCSS
|
# How to use CriticalCSS
|
||||||
1. Install it here
|
1. Install the dependencies from here
|
||||||
```bash
|
```bash
|
||||||
npm i -D critical
|
npm i -D critical
|
||||||
npm install puppeteer
|
npm install puppeteer
|
||||||
@@ -8,4 +8,3 @@ npm install puppeteer
|
|||||||
```bash
|
```bash
|
||||||
node CriticalCSSGenerator.js
|
node CriticalCSSGenerator.js
|
||||||
```
|
```
|
||||||
3. Move the `.css` files from the current directory to the `CriticalCSS/` folder (overwrite existing files)
|
|
||||||
@@ -24,6 +24,24 @@
|
|||||||
|
|
||||||
<div class="row g-4">
|
<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 -->
|
<!-- Embedding Cache -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card shadow-sm h-100">
|
<div class="card shadow-sm h-100">
|
||||||
@@ -104,6 +122,43 @@
|
|||||||
<span>@T["Total query cache utilization"]</span>
|
<span>@T["Total query cache utilization"]</span>
|
||||||
<strong id="totalQuerycacheUtilization"></strong>
|
<strong id="totalQuerycacheUtilization"></strong>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,13 +170,6 @@
|
|||||||
var searchdomains = null;
|
var searchdomains = null;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
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");
|
let searchdomainCount = document.getElementById("searchdomainCount");
|
||||||
showThrobber(searchdomainCount);
|
showThrobber(searchdomainCount);
|
||||||
let searchdomainEntityCount = document.getElementById("searchdomainEntityCount");
|
let searchdomainEntityCount = document.getElementById("searchdomainEntityCount");
|
||||||
@@ -135,6 +183,21 @@
|
|||||||
let embeddingcacheEmbeddingCount = document.getElementById("embeddingcacheEmbeddingCount");
|
let embeddingcacheEmbeddingCount = document.getElementById("embeddingcacheEmbeddingCount");
|
||||||
showThrobber(embeddingcacheEmbeddingCount);
|
showThrobber(embeddingcacheEmbeddingCount);
|
||||||
let embeddingcacheElementCountProgressBar = document.getElementById("embeddingcacheElementCountProgressBar");
|
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 healthchecksServer = document.getElementById("healthchecksServer");
|
||||||
let healthchecksAiProvider = document.getElementById("healthchecksAiProvider");
|
let healthchecksAiProvider = document.getElementById("healthchecksAiProvider");
|
||||||
|
|
||||||
@@ -145,23 +208,38 @@
|
|||||||
searchdomainCount.textContent = searchdomains.length;
|
searchdomainCount.textContent = searchdomains.length;
|
||||||
});
|
});
|
||||||
getServerStats().then(result => {
|
getServerStats().then(result => {
|
||||||
let utilization = result.SizeInBytes;
|
let utilization = result.EmbeddingCacheUtilization;
|
||||||
let maxElementCount = result.MaxElementCount;
|
let embeddingCacheMaxElementCount = result.EmbeddingCacheMaxElementCount;
|
||||||
let elementCount = result.ElementCount;
|
let embeddingCacheElementCount = result.ElementCount;
|
||||||
let embeddingCount = result.EmbeddingsCount;
|
let embeddingCount = result.EmbeddingsCount;
|
||||||
let entityCount = result.EntityCount;
|
let entityCount = result.EntityCount;
|
||||||
let queryCacheUtilization = result.QueryCacheUtilization;
|
let queryCacheUtilization = result.QueryCacheUtilization;
|
||||||
|
let queryCacheElementCount = result.QueryCacheElementCount;
|
||||||
|
let queryCacheMaxElementCountAll = result.QueryCacheMaxElementCountAll;
|
||||||
|
let queryCacheMaxElementCountLoadedSearchdomainsOnly = result.QueryCacheMaxElementCountLoadedSearchdomainsOnly;
|
||||||
hideThrobber(embeddingcacheSize);
|
hideThrobber(embeddingcacheSize);
|
||||||
embeddingcacheSize.textContent = NumberOfBytesAsHumanReadable(utilization);
|
embeddingcacheSize.textContent = NumberOfBytesAsHumanReadable(utilization);
|
||||||
hideThrobber(embeddingcacheElementCount);
|
hideThrobber(embeddingcacheElementCount);
|
||||||
embeddingcacheElementCount.textContent = `${elementCount.toLocaleString()} / ${maxElementCount.toLocaleString()}`;
|
embeddingcacheElementCount.textContent = `${embeddingCacheElementCount.toLocaleString()} / ${embeddingCacheMaxElementCount.toLocaleString()}`;
|
||||||
hideThrobber(embeddingcacheEmbeddingCount);
|
hideThrobber(embeddingcacheEmbeddingCount);
|
||||||
embeddingcacheEmbeddingCount.textContent = embeddingCount;
|
embeddingcacheEmbeddingCount.textContent = embeddingCount;
|
||||||
embeddingcacheElementCountProgressBar.style.width = `${elementCount / maxElementCount * 100}%`;
|
embeddingcacheElementCountProgressBar.style.width = `${embeddingCacheElementCount / embeddingCacheMaxElementCount * 100}%`;
|
||||||
hideThrobber(searchdomainEntityCount);
|
hideThrobber(searchdomainEntityCount);
|
||||||
searchdomainEntityCount.textContent = entityCount;
|
searchdomainEntityCount.textContent = entityCount;
|
||||||
hideThrobber(totalQuerycacheUtilization);
|
hideThrobber(totalQuerycacheUtilization);
|
||||||
totalQuerycacheUtilization.textContent = NumberOfBytesAsHumanReadable(queryCacheUtilization);
|
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(healthchecksServer, "/healthz/Database");
|
||||||
getHealthCheckStatusAndApply(healthchecksAiProvider, "/healthz/AIProvider");
|
getHealthCheckStatusAndApply(healthchecksAiProvider, "/healthz/AIProvider");
|
||||||
|
|||||||
@@ -62,11 +62,24 @@
|
|||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<div class="row align-items-center mb-3">
|
<div class="row align-items-center mb-3">
|
||||||
<h3>@T["Settings"]</h3>
|
<h3>@T["Settings"]</h3>
|
||||||
<div class="col-md-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" />
|
<input type="checkbox" class="form-check-input" id="searchdomainConfigCacheReconciliation" />
|
||||||
<label class="form-check-label" for="searchdomainConfigCacheReconciliation">@T["Cache reconciliation"]</label>
|
<label class="form-check-label" for="searchdomainConfigCacheReconciliation">@T["Cache reconciliation"]</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 mt-3 mt-md-0">
|
<div 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>
|
<button class="btn btn-warning w-100" id="searchdomainConfigUpdate">@T["Update"]</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -252,12 +265,13 @@
|
|||||||
|
|
||||||
<!-- Results -->
|
<!-- Results -->
|
||||||
<h3>@T["Results"]</h3>
|
<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">
|
<table class="table table-sm table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 85px;">@T["Score"]</th>
|
<th style="width: 85px;">@T["Score"]</th>
|
||||||
<th>@T["Name"]</th>
|
<th>@T["Name"]</th>
|
||||||
<th>@T["Action"]</th>
|
<th class="text-center">@T["Action"]</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="queryUpdateResultsBody"></tbody>
|
<tbody id="queryUpdateResultsBody"></tbody>
|
||||||
@@ -347,11 +361,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
<div class="row align-items-center mb-3">
|
||||||
|
<div class="col-md-12">
|
||||||
<label for="createSearchdomainName" class="form-label">@T["Searchdomain name"]</label>
|
<label for="createSearchdomainName" class="form-label">@T["Searchdomain name"]</label>
|
||||||
<input type="text" class="form-control mb-3" id="createSearchdomainName" placeholder="@T["Searchdomain name"]" />
|
<input type="text" class="form-control mb-3" id="createSearchdomainName" placeholder="@T["Searchdomain name"]" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label class="form-check-label mb-2" for="createSearchdomainQueryCacheSize">@T["Query cache size"]:</label>
|
||||||
|
<input type="number" class="form-control" id="createSearchdomainQueryCacheSize" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-7 mt-3">
|
||||||
<input type="checkbox" class="form-check-input" id="createSearchdomainWithCacheReconciliation" />
|
<input type="checkbox" class="form-check-input" id="createSearchdomainWithCacheReconciliation" />
|
||||||
<label class="form-check-label" for="createSearchdomainWithCacheReconciliation">@T["Enable cache reconciliation"]</label>
|
<label class="form-check-label" for="createSearchdomainWithCacheReconciliation">@T["Enable cache reconciliation"]</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div 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">
|
<div class="modal-footer">
|
||||||
<button type="button" id="searchdomainConfirmCreate" class="btn btn-primary" data-bs-dismiss="modal">
|
<button type="button" id="searchdomainConfirmCreate" class="btn btn-primary" data-bs-dismiss="modal">
|
||||||
@@ -656,7 +684,10 @@
|
|||||||
queriesFilter.addEventListener('input', () => {
|
queriesFilter.addEventListener('input', () => {
|
||||||
populateQueriesTable(queriesFilter.value);
|
populateQueriesTable(queriesFilter.value);
|
||||||
});
|
});
|
||||||
|
try
|
||||||
|
{
|
||||||
selectDomain(0);
|
selectDomain(0);
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
document
|
document
|
||||||
.getElementById('searchdomainRename')
|
.getElementById('searchdomainRename')
|
||||||
@@ -694,7 +725,9 @@
|
|||||||
.addEventListener('click', () => {
|
.addEventListener('click', () => {
|
||||||
const domainKey = getSelectedDomainKey();
|
const domainKey = getSelectedDomainKey();
|
||||||
const cacheReconciliation = document.getElementById('searchdomainConfigCacheReconciliation').checked;
|
const cacheReconciliation = document.getElementById('searchdomainConfigCacheReconciliation').checked;
|
||||||
updateSearchdomainConfig(domainKey, { CacheReconciliation: cacheReconciliation});
|
const queryCacheSize = document.getElementById('searchdomainConfigQueryCacheSize').value;
|
||||||
|
const parallelEmbeddingsPrefetch = document.getElementById('searchdomainConfigParallelEmbeddingsPrefetch').checked;
|
||||||
|
updateSearchdomainConfig(domainKey, { CacheReconciliation: cacheReconciliation, QueryCacheSize: queryCacheSize, ParallelEmbeddingsPrefetch: parallelEmbeddingsPrefetch});
|
||||||
});
|
});
|
||||||
|
|
||||||
document
|
document
|
||||||
@@ -745,7 +778,7 @@
|
|||||||
"datapoints": datapoints
|
"datapoints": datapoints
|
||||||
}];
|
}];
|
||||||
showToast("@T["Creating entity"]", "primary");
|
showToast("@T["Creating entity"]", "primary");
|
||||||
fetch(`/Entity`, {
|
fetch(`/Entities`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -775,9 +808,10 @@
|
|||||||
document.getElementById('createSearchdomainModal')
|
document.getElementById('createSearchdomainModal')
|
||||||
);
|
);
|
||||||
const name = document.getElementById('createSearchdomainName').value;
|
const name = document.getElementById('createSearchdomainName').value;
|
||||||
|
const queryCacheSize = document.getElementById('createSearchdomainQueryCacheSize').value;
|
||||||
const cacheReconciliation = document.getElementById('createSearchdomainWithCacheReconciliation').checked;
|
const cacheReconciliation = document.getElementById('createSearchdomainWithCacheReconciliation').checked;
|
||||||
const settings = { CacheReconciliation: cacheReconciliation };
|
const parallelEmbeddingsPrefetch = document.getElementById('createSearchdomainConfigParallelEmbeddingsPrefetch').checked;
|
||||||
// Implement create logic here
|
const settings = { CacheReconciliation: cacheReconciliation, QueryCacheSize: queryCacheSize, ParallelEmbeddingsPrefetch: parallelEmbeddingsPrefetch };
|
||||||
fetch(`/Searchdomain?searchdomain=${encodeURIComponent(name)}`, {
|
fetch(`/Searchdomain?searchdomain=${encodeURIComponent(name)}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -869,12 +903,12 @@
|
|||||||
var data = [{
|
var data = [{
|
||||||
"name": name,
|
"name": name,
|
||||||
"probmethod": probMethod,
|
"probmethod": probMethod,
|
||||||
"searchdomain": encodeURIComponent(domains[getSelectedDomainKey()]),
|
"searchdomain": domains[getSelectedDomainKey()],
|
||||||
"attributes": attributes,
|
"attributes": attributes,
|
||||||
"datapoints": datapoints
|
"datapoints": datapoints
|
||||||
}];
|
}];
|
||||||
showToast("@T["Updating entity"]", "primary");
|
showToast("@T["Updating entity"]", "primary");
|
||||||
fetch(`/Entity`, {
|
fetch(`/Entities`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -937,7 +971,7 @@
|
|||||||
}).then(async response => {
|
}).then(async response => {
|
||||||
result = await response.json();
|
result = await response.json();
|
||||||
if (response.ok && result.Success) {
|
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');
|
console.log('Search query was updated successfully');
|
||||||
selectDomain(getSelectedDomainKey());
|
selectDomain(getSelectedDomainKey());
|
||||||
} else {
|
} else {
|
||||||
@@ -1053,7 +1087,12 @@
|
|||||||
|
|
||||||
let searchdomainConfigPromise = getSearchdomainConfig(getSelectedDomainKey());
|
let searchdomainConfigPromise = getSearchdomainConfig(getSelectedDomainKey());
|
||||||
let configElementCachereconciliation = document.getElementById('searchdomainConfigCacheReconciliation');
|
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 cacheUtilizationPromise = getSearchdomainCacheUtilization(getSelectedDomainKey());
|
||||||
let databaseUtilizationPromise = getSearchdomainDatabaseUtilization(getSelectedDomainKey());
|
let databaseUtilizationPromise = getSearchdomainDatabaseUtilization(getSelectedDomainKey());
|
||||||
|
|
||||||
@@ -1095,10 +1134,15 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
searchdomainConfigPromise.then(searchdomainConfig => {
|
searchdomainConfigPromise.then(searchdomainConfig => {
|
||||||
|
hideThrobber(document.querySelector('#searchdomainConfigCacheReconciliation'), true);
|
||||||
|
hideThrobber(document.querySelector('#searchdomainConfigParallelEmbeddingsPrefetch'), true);
|
||||||
|
|
||||||
if (searchdomainConfig != null && searchdomainConfig.Settings != null)
|
if (searchdomainConfig != null && searchdomainConfig.Settings != null)
|
||||||
{
|
{
|
||||||
|
configElementCacheSize.value = searchdomainConfig.Settings.QueryCacheSize;
|
||||||
configElementCachereconciliation.checked = searchdomainConfig.Settings.CacheReconciliation;
|
configElementCachereconciliation.checked = searchdomainConfig.Settings.CacheReconciliation;
|
||||||
configElementCachereconciliation.disabled = false;
|
configElementCachereconciliation.disabled = false;
|
||||||
|
configElementParallelEmbeddingsPrefetch.checked = searchdomainConfig.Settings.ParallelEmbeddingsPrefetch;
|
||||||
} else {
|
} else {
|
||||||
configElementCachereconciliation.disabled = true;
|
configElementCachereconciliation.disabled = true;
|
||||||
showToast("@T["Unable to fetch searchdomain config"]", "danger");
|
showToast("@T["Unable to fetch searchdomain config"]", "danger");
|
||||||
@@ -1106,10 +1150,11 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
cacheUtilizationPromise.then(cacheUtilization => {
|
cacheUtilizationPromise.then(cacheUtilization => {
|
||||||
if (cacheUtilization != null && cacheUtilization.QueryCacheSizeBytes != null)
|
hideThrobber(document.querySelector('#searchdomainConfigQueryCacheSize'), true);
|
||||||
|
if (cacheUtilization != null && cacheUtilization.SizeBytes != null)
|
||||||
{
|
{
|
||||||
document.querySelector('#cacheUtilization').innerText =
|
document.querySelector('#cacheUtilization').innerText =
|
||||||
`${NumberOfBytesAsHumanReadable(cacheUtilization.QueryCacheSizeBytes)}`;
|
`${NumberOfBytesAsHumanReadable(cacheUtilization.SizeBytes)}`;
|
||||||
} else {
|
} else {
|
||||||
showToast("@T["Unable to fetch searchdomain cache utilization"]", "danger");
|
showToast("@T["Unable to fetch searchdomain cache utilization"]", "danger");
|
||||||
console.error('Failed to fetch searchdomain cache utilization');
|
console.error('Failed to fetch searchdomain cache utilization');
|
||||||
@@ -1289,15 +1334,31 @@
|
|||||||
domainItem.classList.add('list-group-item-danger');
|
domainItem.classList.add('list-group-item-danger');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showThrobber(element = null) {
|
function showThrobber(element = null, direct = false) {
|
||||||
if (element == null) element = document;
|
if (element == null) element = document;
|
||||||
|
if (direct) {
|
||||||
|
let spinner = document.createElement('div');
|
||||||
|
spinner.classList.add('spinner');
|
||||||
|
spinner.style.position = "absolute";
|
||||||
|
spinner.style.marginTop = "0.5rem";
|
||||||
|
spinner.style.marginLeft = "0.5rem";
|
||||||
|
element.style.opacity = "0.25";
|
||||||
|
element.parentElement.insertBefore(spinner, element);
|
||||||
|
} else {
|
||||||
element.querySelector('.spinner').classList.remove('d-none');
|
element.querySelector('.spinner').classList.remove('d-none');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideThrobber(element = null) {
|
}
|
||||||
|
|
||||||
|
function hideThrobber(element = null, direct = false) {
|
||||||
if (element == null) element = document;
|
if (element == null) element = document;
|
||||||
|
if (direct) {
|
||||||
|
element.previousElementSibling.remove()
|
||||||
|
element.style.opacity = "1";
|
||||||
|
} else {
|
||||||
element.querySelector('.spinner').classList.add('d-none');
|
element.querySelector('.spinner').classList.add('d-none');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showEntityDetails(entity) {
|
function showEntityDetails(entity) {
|
||||||
// Title
|
// Title
|
||||||
@@ -1434,28 +1495,7 @@
|
|||||||
</tr>`;
|
</tr>`;
|
||||||
} else {
|
} else {
|
||||||
query.Results.forEach(r => {
|
query.Results.forEach(r => {
|
||||||
const row = document.createElement('tr');
|
queryUpdateAddResult(r.Score.toFixed(4), r.Name, resultsBody);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1466,6 +1506,66 @@
|
|||||||
modal.show();
|
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) {
|
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";
|
||||||
@@ -1672,7 +1772,7 @@
|
|||||||
|
|
||||||
// Get the text content from the second cell (index 1) which contains the path
|
// Get the text content from the second cell (index 1) which contains the path
|
||||||
const score = parseFloat(cells[0].firstChild.value);
|
const score = parseFloat(cells[0].firstChild.value);
|
||||||
const name = cells[1].textContent.trim();
|
const name = cells[1].firstChild.value;
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
"Score": score,
|
"Score": score,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<meta name="description" content="Embeddingsearch server" />
|
<meta name="description" content="Embeddingsearch server" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>@ViewData["Title"] - embeddingsearch</title>
|
<title>@ViewData["Title"] - embeddingsearch</title>
|
||||||
|
<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"))
|
@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="preload" href="~/lib/bootstrap/dist/css/bootstrap.min.css" as="style"/>
|
||||||
@@ -51,7 +52,9 @@
|
|||||||
<header>
|
<header>
|
||||||
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light 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">
|
<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"
|
<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">
|
aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
@@ -68,17 +71,20 @@
|
|||||||
</li>
|
</li>
|
||||||
@if (User.IsInRole("Admin") || User.IsInRole("Swagger"))
|
@if (User.IsInRole("Admin") || User.IsInRole("Swagger"))
|
||||||
{
|
{
|
||||||
<li class="nav-item">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link text" href="/swagger/index.html?ReturnUrl=@(currentUrl)">@T["Swagger"]</a>
|
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
</li>
|
@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"))
|
@if (User.IsInRole("Admin"))
|
||||||
{
|
{
|
||||||
<li class="nav-item">
|
<a class="dropdown-item" href="/elmah?ReturnUrl=@(currentUrl)">@T["Elmah"]</a>
|
||||||
<a class="nav-link text" href="/elmah?ReturnUrl=@(currentUrl)">@T["Elmah"]</a>
|
}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
<li class="nav-item">
|
<li class="nav-item ms-auto">
|
||||||
<a class="nav-link text" asp-area="" asp-controller="Account" asp-action="Logout">@T["Logout"]</a>
|
<a class="nav-link text" asp-area="" asp-controller="Account" asp-action="Logout">@T["Logout"]</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,12 @@
|
|||||||
|
|
||||||
"Embeddingsearch": {
|
"Embeddingsearch": {
|
||||||
"ConnectionStrings": {
|
"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": {
|
"Elmah": {
|
||||||
"LogPath": "~/logs"
|
"LogPath": "~/logs"
|
||||||
},
|
},
|
||||||
"EmbeddingCacheMaxCount": 10000000,
|
|
||||||
"AiProviders": {
|
"AiProviders": {
|
||||||
"ollama": {
|
"ollama": {
|
||||||
"handler": "ollama",
|
"handler": "ollama",
|
||||||
@@ -46,6 +46,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ApiKeys": ["Some UUID here", "Another UUID here"],
|
"ApiKeys": ["Some UUID here", "Another UUID here"],
|
||||||
"UseHttpsRedirection": true
|
"UseHttpsRedirection": true,
|
||||||
|
"Cache": {
|
||||||
|
"CacheTopN": 100000,
|
||||||
|
"StoreEmbeddingCache": true,
|
||||||
|
"StoreTopN": 20000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,27 +15,41 @@
|
|||||||
"UseSwagger": true,
|
"UseSwagger": true,
|
||||||
"Embeddingsearch": {
|
"Embeddingsearch": {
|
||||||
"ConnectionStrings": {
|
"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": {
|
"Elmah": {
|
||||||
"AllowedHosts": [
|
"LogPath": "~/logs"
|
||||||
"127.0.0.1",
|
|
||||||
"::1",
|
|
||||||
"172.17.0.1"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"AiProviders": {
|
"AiProviders": {
|
||||||
"ollama": {
|
"ollama": {
|
||||||
"handler": "ollama",
|
"handler": "ollama",
|
||||||
"baseURL": "http://localhost:11434"
|
"baseURL": "http://localhost:11434",
|
||||||
|
"Allowlist": [".*"],
|
||||||
|
"Denylist": ["qwen3-coder:latest", "qwen3:0.6b", "qwen3-vl", "deepseek-ocr"]
|
||||||
},
|
},
|
||||||
"localAI": {
|
"localAI": {
|
||||||
"handler": "openai",
|
"handler": "openai",
|
||||||
"baseURL": "http://localhost:8080",
|
"baseURL": "http://localhost:8080",
|
||||||
"ApiKey": "Some API key here"
|
"ApiKey": "Some API key here",
|
||||||
|
"Allowlist": [".*"],
|
||||||
|
"Denylist": ["cross-encoder", "jina-reranker-v1-tiny-en", "whisper-small"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ApiKeys": ["Some UUID here", "Another UUID here"],
|
"SimpleAuth": {
|
||||||
"UseHttpsRedirection": true
|
"Users": [
|
||||||
|
{
|
||||||
|
"Username": "admin",
|
||||||
|
"Password": "UnsafePractice.67",
|
||||||
|
"Roles": ["Admin"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ApiKeys": ["APIKeyOfYourChoice", "AnotherOneIfYouLike"],
|
||||||
|
"Cache": {
|
||||||
|
"CacheTopN": 10000,
|
||||||
|
"StoreEmbeddingCache": true,
|
||||||
|
"StoreTopN": 10000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,5 +16,8 @@
|
|||||||
"Application": "Embeddingsearch.Server"
|
"Application": "Embeddingsearch.Server"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Embeddingsearch": {
|
||||||
|
"MaxRequestBodySize": 524288000
|
||||||
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,3 +80,7 @@ td.btn-group {
|
|||||||
display: revert;
|
display: revert;
|
||||||
min-width: 15rem;
|
min-width: 15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="light"] img[alt="Logo"] {
|
||||||
|
filter: invert(100%);
|
||||||
|
}
|
||||||
@@ -49,3 +49,13 @@ function showToast(message, type) {
|
|||||||
bsToast.show();
|
bsToast.show();
|
||||||
toast.addEventListener('hidden.bs.toast', () => toast.remove());
|
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
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")]
|
[JsonPropertyName("CacheReconciliation")]
|
||||||
public bool CacheReconciliation { get; set; } = 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
|
public static class MemorySizes
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Shared;
|
||||||
|
|
||||||
namespace Shared.Models;
|
namespace Shared.Models;
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ public class SearchdomainDeleteResults : SuccesMessageBaseModel
|
|||||||
public required int DeletedEntities { get; set; }
|
public required int DeletedEntities { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SearchdomainSearchesResults : SuccesMessageBaseModel
|
public class SearchdomainQueriesResults : SuccesMessageBaseModel
|
||||||
{
|
{
|
||||||
[JsonPropertyName("Searches")]
|
[JsonPropertyName("Searches")]
|
||||||
public required Dictionary<string, DateTimedSearchResult> Searches { get; set; }
|
public required Dictionary<string, DateTimedSearchResult> Searches { get; set; }
|
||||||
@@ -41,10 +42,14 @@ public class SearchdomainSettingsResults : SuccesMessageBaseModel
|
|||||||
public required SearchdomainSettings? Settings { get; set; }
|
public required SearchdomainSettings? Settings { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SearchdomainSearchCacheSizeResults : SuccesMessageBaseModel
|
public class SearchdomainQueryCacheSizeResults : SuccesMessageBaseModel
|
||||||
{
|
{
|
||||||
[JsonPropertyName("QueryCacheSizeBytes")]
|
[JsonPropertyName("ElementCount")]
|
||||||
public required long? QueryCacheSizeBytes { get; set; }
|
public required int? ElementCount { get; set; }
|
||||||
|
[JsonPropertyName("ElementMaxCount")]
|
||||||
|
public required int? ElementMaxCount { get; set; }
|
||||||
|
[JsonPropertyName("SizeBytes")]
|
||||||
|
public required long? SizeBytes { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SearchdomainInvalidateCacheResults : SuccesMessageBaseModel {}
|
public class SearchdomainInvalidateCacheResults : SuccesMessageBaseModel {}
|
||||||
|
|||||||
@@ -10,16 +10,26 @@ public class ServerGetModelsResult : SuccesMessageBaseModel
|
|||||||
|
|
||||||
public class ServerGetStatsResult : SuccesMessageBaseModel
|
public class ServerGetStatsResult : SuccesMessageBaseModel
|
||||||
{
|
{
|
||||||
[JsonPropertyName("SizeInBytes")]
|
[JsonPropertyName("EmbeddingCacheUtilization")]
|
||||||
public long? SizeInBytes { get; set; }
|
public long? EmbeddingCacheUtilization { get; set; }
|
||||||
[JsonPropertyName("MaxElementCount")]
|
[JsonPropertyName("EmbeddingCacheMaxElementCount")]
|
||||||
public long? MaxElementCount { get; set; }
|
public long? EmbeddingCacheMaxElementCount { get; set; }
|
||||||
[JsonPropertyName("ElementCount")]
|
[JsonPropertyName("ElementCount")]
|
||||||
public long? ElementCount { get; set; }
|
public long? EmbeddingCacheElementCount { get; set; }
|
||||||
[JsonPropertyName("EmbeddingsCount")]
|
[JsonPropertyName("EmbeddingsCount")]
|
||||||
public long? EmbeddingsCount { get; set; }
|
public long? EmbeddingsCount { get; set; }
|
||||||
[JsonPropertyName("EntityCount")]
|
[JsonPropertyName("EntityCount")]
|
||||||
public long? EntityCount { get; set; }
|
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")]
|
[JsonPropertyName("QueryCacheUtilization")]
|
||||||
public long? QueryCacheUtilization { get; set; }
|
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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user