Add advanced search engine with inverted index, thread pool execution, configuration API, and comprehensive diagnostics

This commit is contained in:
2026-03-23 13:21:20 -04:00
parent af7d1c0d2e
commit b40fcae62f
8 changed files with 885 additions and 106 deletions
+241 -7
View File
@@ -33,12 +33,15 @@
let suggestAbortController = null;
let dropdownActiveIndex = -1;
let dropdownItems = [];
let currentSearchId = 0;
// Advanced search constants
const SEARCH_HISTORY_KEY = "obsigate_search_history";
const MAX_HISTORY_ENTRIES = 50;
const SUGGEST_DEBOUNCE_MS = 150;
const ADVANCED_SEARCH_LIMIT = 50;
const MIN_SEARCH_LENGTH = 2;
const SEARCH_TIMEOUT_MS = 30000;
// ---------------------------------------------------------------------------
// File extension → Lucide icon mapping
@@ -1825,10 +1828,12 @@
if (!openBtn || !closeBtn || !modal) return;
openBtn.addEventListener("click", () => {
openBtn.addEventListener("click", async () => {
modal.classList.add("active");
closeHeaderMenu();
renderConfigFilters();
loadConfigFields();
loadDiagnostics();
safeCreateIcons();
});
@@ -1848,11 +1853,36 @@
patternInput.addEventListener("input", updateRegexPreview);
// Frontend config fields — save to localStorage on change
["cfg-debounce", "cfg-results-per-page", "cfg-min-query", "cfg-timeout"].forEach((id) => {
const input = document.getElementById(id);
if (input) input.addEventListener("change", saveFrontendConfig);
});
// Backend save button
const saveBtn = document.getElementById("cfg-save-backend");
if (saveBtn) saveBtn.addEventListener("click", saveBackendConfig);
// Force reindex
const reindexBtn = document.getElementById("cfg-reindex");
if (reindexBtn) reindexBtn.addEventListener("click", forceReindex);
// Reset defaults
const resetBtn = document.getElementById("cfg-reset-defaults");
if (resetBtn) resetBtn.addEventListener("click", resetConfigDefaults);
// Refresh diagnostics
const diagBtn = document.getElementById("cfg-refresh-diag");
if (diagBtn) diagBtn.addEventListener("click", loadDiagnostics);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && modal.classList.contains("active")) {
closeConfigModal();
}
});
// Load saved frontend config on startup
applyFrontendConfig();
}
function closeConfigModal() {
@@ -1860,6 +1890,177 @@
if (modal) modal.classList.remove("active");
}
// --- Config field helpers ---
const _FRONTEND_CONFIG_KEY = "obsigate-perf-config";
function _getFrontendConfig() {
try { return JSON.parse(localStorage.getItem(_FRONTEND_CONFIG_KEY) || "{}"); }
catch { return {}; }
}
function applyFrontendConfig() {
const cfg = _getFrontendConfig();
if (cfg.debounce_ms) { /* applied dynamically in debounce setTimeout */ }
if (cfg.results_per_page) { /* used as ADVANCED_SEARCH_LIMIT override */ }
if (cfg.min_query_length) { /* used as MIN_SEARCH_LENGTH override */ }
if (cfg.search_timeout_ms) { /* used as SEARCH_TIMEOUT_MS override */ }
}
function _getEffective(key, fallback) {
const cfg = _getFrontendConfig();
return cfg[key] !== undefined ? cfg[key] : fallback;
}
async function loadConfigFields() {
// Frontend fields from localStorage
const cfg = _getFrontendConfig();
_setField("cfg-debounce", cfg.debounce_ms || 300);
_setField("cfg-results-per-page", cfg.results_per_page || 50);
_setField("cfg-min-query", cfg.min_query_length || 2);
_setField("cfg-timeout", cfg.search_timeout_ms || 30000);
// Backend fields from API
try {
const data = await api("/api/config");
_setField("cfg-workers", data.search_workers);
_setField("cfg-max-content", data.max_content_size);
_setField("cfg-title-boost", data.title_boost);
_setField("cfg-tag-boost", data.tag_boost);
_setField("cfg-prefix-exp", data.prefix_max_expansions);
} catch (err) {
console.error("Failed to load backend config:", err);
}
}
function _setField(id, value) {
const el = document.getElementById(id);
if (el && value !== undefined) el.value = value;
}
function _getFieldNum(id, fallback) {
const el = document.getElementById(id);
if (!el) return fallback;
const v = parseFloat(el.value);
return isNaN(v) ? fallback : v;
}
function saveFrontendConfig() {
const cfg = {
debounce_ms: _getFieldNum("cfg-debounce", 300),
results_per_page: _getFieldNum("cfg-results-per-page", 50),
min_query_length: _getFieldNum("cfg-min-query", 2),
search_timeout_ms: _getFieldNum("cfg-timeout", 30000),
};
localStorage.setItem(_FRONTEND_CONFIG_KEY, JSON.stringify(cfg));
showToast("Paramètres client sauvegardés");
}
async function saveBackendConfig() {
const body = {
search_workers: _getFieldNum("cfg-workers", 2),
max_content_size: _getFieldNum("cfg-max-content", 100000),
title_boost: _getFieldNum("cfg-title-boost", 3.0),
tag_boost: _getFieldNum("cfg-tag-boost", 2.0),
prefix_max_expansions: _getFieldNum("cfg-prefix-exp", 50),
};
try {
await fetch("/api/config", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
showToast("Configuration backend sauvegardée");
} catch (err) {
console.error("Failed to save backend config:", err);
showToast("Erreur de sauvegarde");
}
}
async function forceReindex() {
const btn = document.getElementById("cfg-reindex");
if (btn) { btn.disabled = true; btn.textContent = "Réindexation..."; }
try {
await api("/api/index/reload");
showToast("Réindexation terminée");
loadDiagnostics();
await Promise.all([loadVaults(), loadTags()]);
} catch (err) {
console.error("Reindex error:", err);
showToast("Erreur de réindexation");
} finally {
if (btn) { btn.disabled = false; btn.textContent = "Forcer réindexation"; }
}
}
async function resetConfigDefaults() {
// Reset frontend
localStorage.removeItem(_FRONTEND_CONFIG_KEY);
// Reset backend
try {
await fetch("/api/config", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
search_workers: 2, debounce_ms: 300, results_per_page: 50,
min_query_length: 2, search_timeout_ms: 30000, max_content_size: 100000,
title_boost: 3.0, path_boost: 1.5, tag_boost: 2.0, prefix_max_expansions: 50,
snippet_context_chars: 120, max_snippet_highlights: 5,
}),
});
} catch (err) { console.error("Reset config error:", err); }
loadConfigFields();
showToast("Configuration réinitialisée");
}
async function loadDiagnostics() {
const container = document.getElementById("config-diagnostics");
if (!container) return;
container.innerHTML = '<div class="config-diag-loading">Chargement...</div>';
try {
const data = await api("/api/diagnostics");
renderDiagnostics(container, data);
} catch (err) {
container.innerHTML = '<div class="config-diag-loading">Erreur de chargement</div>';
}
}
function renderDiagnostics(container, data) {
container.innerHTML = "";
const sections = [
{ title: "Index", rows: [
["Fichiers indexés", data.index.total_files],
["Tags uniques", data.index.total_tags],
["Vaults", Object.keys(data.index.vaults).join(", ")],
]},
{ title: "Index inversé", rows: [
["Tokens uniques", data.inverted_index.unique_tokens.toLocaleString()],
["Postings total", data.inverted_index.total_postings.toLocaleString()],
["Documents", data.inverted_index.documents],
["Mémoire estimée", data.inverted_index.memory_estimate_mb + " MB"],
["Stale", data.inverted_index.is_stale ? "Oui" : "Non"],
]},
{ title: "Moteur de recherche", rows: [
["Executor actif", data.search_executor.active ? "Oui" : "Non"],
["Workers max", data.search_executor.max_workers],
]},
];
sections.forEach((section) => {
const div = document.createElement("div");
div.className = "config-diag-section";
const title = document.createElement("div");
title.className = "config-diag-section-title";
title.textContent = section.title;
div.appendChild(title);
section.rows.forEach(([label, value]) => {
const row = document.createElement("div");
row.className = "config-diag-row";
row.innerHTML = `<span class="diag-label">${label}</span><span class="diag-value">${value}</span>`;
div.appendChild(row);
});
container.appendChild(div);
});
}
function renderConfigFilters() {
const config = TagFilterService.getConfig();
const filters = config.tagFilters || TagFilterService.defaultFilters;
@@ -1987,13 +2188,13 @@
const vault = document.getElementById("vault-filter").value;
const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
advancedSearchOffset = 0;
if (q.length > 0 || tagFilter) {
if ((q.length >= _getEffective("min_query_length", MIN_SEARCH_LENGTH)) || tagFilter) {
performAdvancedSearch(q, vault, tagFilter);
} else {
} else if (q.length === 0) {
SearchChips.clear();
showWelcome();
}
}, 300);
}, _getEffective("debounce_ms", 300));
});
// --- Focus handler: show history dropdown ---
@@ -2097,17 +2298,21 @@
async function performSearch(query, vaultFilter, tagFilter) {
if (searchAbortController) searchAbortController.abort();
searchAbortController = new AbortController();
const searchId = ++currentSearchId;
showLoading();
let url = `/api/search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}`;
if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
try {
const data = await api(url, { signal: searchAbortController.signal });
if (searchId !== currentSearchId) return;
renderSearchResults(data, query, tagFilter);
} catch (err) {
if (err.name === "AbortError") return;
if (searchId !== currentSearchId) return;
showWelcome();
} finally {
searchAbortController = null;
hideProgressBar();
if (searchId === currentSearchId) searchAbortController = null;
}
}
@@ -2115,6 +2320,7 @@
async function performAdvancedSearch(query, vaultFilter, tagFilter, offset, sort) {
if (searchAbortController) searchAbortController.abort();
searchAbortController = new AbortController();
const searchId = ++currentSearchId;
showLoading();
const ofs = offset !== undefined ? offset : advancedSearchOffset;
@@ -2125,19 +2331,30 @@
const parsed = QueryParser.parse(query);
SearchChips.update(parsed);
let url = `/api/search/advanced?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}&limit=${ADVANCED_SEARCH_LIMIT}&offset=${ofs}&sort=${sortBy}`;
const effectiveLimit = _getEffective("results_per_page", ADVANCED_SEARCH_LIMIT);
let url = `/api/search/advanced?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}&limit=${effectiveLimit}&offset=${ofs}&sort=${sortBy}`;
if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
// Search timeout — abort if server takes too long
const timeoutId = setTimeout(() => {
if (searchAbortController) searchAbortController.abort();
}, _getEffective("search_timeout_ms", SEARCH_TIMEOUT_MS));
try {
const data = await api(url, { signal: searchAbortController.signal });
clearTimeout(timeoutId);
if (searchId !== currentSearchId) return;
advancedSearchTotal = data.total;
advancedSearchOffset = ofs;
renderAdvancedSearchResults(data, query, tagFilter);
} catch (err) {
clearTimeout(timeoutId);
if (err.name === "AbortError") return;
if (searchId !== currentSearchId) return;
showWelcome();
} finally {
searchAbortController = null;
hideProgressBar();
if (searchId === currentSearchId) searchAbortController = null;
}
}
@@ -2209,6 +2426,11 @@
} else {
summaryText.textContent = `${data.total} résultat(s)`;
}
if (data.query_time_ms !== undefined && data.query_time_ms > 0) {
const timeBadge = el("span", { class: "search-time-badge" });
timeBadge.textContent = `(${data.query_time_ms} ms)`;
summaryText.appendChild(timeBadge);
}
header.appendChild(summaryText);
// Sort controls
@@ -2581,6 +2803,7 @@
}
function showWelcome() {
hideProgressBar();
const area = document.getElementById("content-area");
area.innerHTML = `
<div class="welcome">
@@ -2598,6 +2821,17 @@
<div class="loading-spinner"></div>
<div>Recherche en cours...</div>
</div>`;
showProgressBar();
}
function showProgressBar() {
const bar = document.getElementById("search-progress-bar");
if (bar) bar.classList.add("active");
}
function hideProgressBar() {
const bar = document.getElementById("search-progress-bar");
if (bar) bar.classList.remove("active");
}
function goHome() {