feat(search): recherche semantique - embeddings vectoriels + hybride RRF (#70)

This commit is contained in:
2026-09-12 00:12:20 -04:00
parent 45be3125de
commit 31d4be8015
19 changed files with 1388 additions and 90 deletions
+1
View File
@@ -5235,6 +5235,7 @@ curl -X POST https://votre-serveur.com/webhook \
<button class="results-bar-btn tog" id="rb-case" title="Respecter la casse (Alt-C)">Aa</button>
<button class="results-bar-btn tog" id="rb-word" title="Mot entier (Alt-W)">wd</button>
<button class="results-bar-btn tog" id="rb-regex" title="Expression reguliere (Alt-R)">.*</button>
<button class="results-bar-btn tog" id="rb-semantic" title="Recherche sémantique (Alt-S)">~</button>
</div>
<!-- Offline conflict resolution modal -->
<div class="editor-modal" id="offline-conflict-modal">
+25 -1
View File
@@ -524,6 +524,7 @@ export function initSearch() {
const caseBtn = document.getElementById("rb-case");
const wordBtn = document.getElementById("rb-word");
const regexBtn = document.getElementById("rb-regex");
const semanticBtn = document.getElementById("rb-semantic");
const filterBtn = document.getElementById("search-filter-btn");
const clearBtn = document.getElementById("search-clear-btn");
const filterRow = document.getElementById("search-filter-row");
@@ -540,12 +541,25 @@ export function initSearch() {
wordBtn.classList.toggle("active", state.searchWholeWord);
regexBtn.classList.toggle("active", state.searchRegex);
filterBtn.classList.toggle("active", state.searchFilterVisible);
if (semanticBtn) {
semanticBtn.classList.toggle("active", state.semanticSearch);
semanticBtn.disabled = !state.semanticAvailable;
}
}
// Toggle buttons
caseBtn.addEventListener("click", () => { state.searchCaseSensitive = !state.searchCaseSensitive; _updateToggleUI(); _research(); });
if (wordBtn) wordBtn.addEventListener("click", () => { state.searchWholeWord = !state.searchWholeWord; _updateToggleUI(); _research(); });
if (regexBtn) regexBtn.addEventListener("click", () => { state.searchRegex = !state.searchRegex; _updateToggleUI(); _research(); });
if (semanticBtn) {
semanticBtn.title = t("search.semantic_title");
semanticBtn.addEventListener("click", () => {
if (!state.semanticAvailable) return;
state.semanticSearch = !state.semanticSearch;
_updateToggleUI();
_research();
});
}
if (filterBtn) filterBtn.addEventListener("click", () => { state.searchFilterVisible = !state.searchFilterVisible; if (filterRow) filterRow.style.display = state.searchFilterVisible ? "flex" : "none"; _updateToggleUI(); });
// ── Result navigation (up/down arrows + Enter) ──
@@ -599,6 +613,7 @@ export function initSearch() {
if (e.key === "c" || e.key === "C") { e.preventDefault(); caseBtn.click(); }
else if (e.key === "w" || e.key === "W") { e.preventDefault(); if (wordBtn) wordBtn.click(); }
else if (e.key === "r" || e.key === "R") { e.preventDefault(); if (regexBtn) regexBtn.click(); }
else if (e.key === "s" || e.key === "S") { e.preventDefault(); if (semanticBtn) semanticBtn.click(); }
else if (e.key === "f" || e.key === "F") { e.preventDefault(); if (filterBtn) filterBtn.click(); input.focus(); }
}
});
@@ -788,6 +803,7 @@ export async function performAdvancedSearch(query, vaultFilter, tagFilter, offse
if (parsed.created) url += `&created=${encodeURIComponent(parsed.created)}`;
if (parsed.modified) url += `&modified=${encodeURIComponent(parsed.modified)}`;
if (parsed.size) url += `&size=${encodeURIComponent(parsed.size)}`;
if (state.semanticSearch) url += "&semantic=true";
// Search timeout — abort if server takes too long
const timeoutId = setTimeout(
@@ -803,6 +819,12 @@ export async function performAdvancedSearch(query, vaultFilter, tagFilter, offse
if (searchId !== state.currentSearchId) return;
state.advancedSearchTotal = data.total;
state.advancedSearchOffset = ofs;
state.semanticAvailable = !!data.semantic_available;
const semBtn = document.getElementById("rb-semantic");
if (semBtn) {
semBtn.disabled = !state.semanticAvailable;
semBtn.classList.toggle("active", state.semanticSearch);
}
// Plugin hook: filter search results
data.results = await onSearchFilter(state.vault || "default", query, data.results);
renderAdvancedSearchResults(data, query, tagFilter);
@@ -1092,7 +1114,9 @@ export function renderAdvancedSearchResults(data, query, tagFilter) {
// Score badge
const scoreEl = el("span", { class: "search-result-score", style: "font-size:0.7rem;color:var(--text-muted);margin-left:8px" });
scoreEl.textContent = `score: ${r.score}`;
let scoreText = `score: ${r.score}`;
if (r.semantic_score) scoreText += ` · ${t("search.semantic_score")}: ${r.semantic_score}`;
scoreEl.textContent = scoreText;
const vaultPath = el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path), scoreEl]);
+2
View File
@@ -24,6 +24,8 @@ export const state = {
searchWholeWord: false,
searchRegex: false,
searchFilterVisible: false,
semanticSearch: false,
semanticAvailable: false,
// Search constants
SEARCH_HISTORY_KEY: "obsigate_search_history",
+3
View File
@@ -1398,6 +1398,9 @@
"search.save_search": "Save search",
"search.saved": "Search saved",
"search.sections_all_vaults": "All vaults",
"search.semantic_score": "sim",
"search.semantic_title": "Semantic search (Alt-S)",
"search.semantic_unavailable": "Semantic search unavailable",
"search.simple_search": "Simple search",
"search.smart_scoring": "Smart scoring",
"search.sort_date": "Date",
+3
View File
@@ -1398,6 +1398,9 @@
"search.save_search": "Sauvegarder la recherche",
"search.saved": "Recherche sauvegardée",
"search.sections_all_vaults": "Tous les vaults",
"search.semantic_score": "sim",
"search.semantic_title": "Recherche sémantique (Alt-S)",
"search.semantic_unavailable": "Recherche sémantique indisponible",
"search.simple_search": "Recherche simple",
"search.smart_scoring": "Scoring intelligent",
"search.sort_date": "Date",