feat(search): recherche semantique - embeddings vectoriels + hybride RRF (#70)
This commit is contained in:
+25
-1
@@ -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]);
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user