semanticsearch

This commit is contained in:
overcuriousity
2025-08-06 15:06:53 +02:00
parent 5164aa640a
commit 1b59f5585e
9 changed files with 934 additions and 234 deletions

View File

@@ -25,33 +25,55 @@ const sortedTags = Object.entries(tagFrequency)
<div class="filters-container">
<!-- Search Section -->
<div class="filter-section">
<div class="filter-card-compact">
<div class="filter-header-compact">
<h3>🔍 Suche</h3>
</div>
<div class="search-wrapper">
<div class="search-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<div class="filter-section">
<div class="filter-card-compact">
<div class="filter-header-compact">
<h3>🔍 Suche</h3>
</div>
<div class="search-row">
<div class="search-wrapper">
<div class="search-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
</div>
<input
type="text"
id="search-input"
placeholder="Software, Beschreibung oder Tags durchsuchen..."
class="search-input"
/>
<button id="clear-search" class="search-clear hidden" title="Suche löschen">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<!-- Semantic Search Toggle - Inline -->
<div id="semantic-search-container" class="semantic-search-inline hidden">
<label class="semantic-toggle-wrapper" title="Semantische Suche verwendet KI-basierte Bedeutungserkennung statt nur Stichwortvergleich. Findet ähnliche Tools auch wenn andere Begriffe verwendet werden.">
<input type="checkbox" id="semantic-search-enabled" />
<div class="semantic-checkbox-custom"></div>
<span class="semantic-toggle-label">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2h-4"/>
<path d="M9 11V7a3 3 0 0 1 6 0v4"/>
</svg>
Semantisch
</span>
</label>
</div>
</div>
<!-- Status Display -->
<div id="semantic-status" class="semantic-status hidden">
<span class="semantic-results-count"></span>
</div>
<input
type="text"
id="search-input"
placeholder="Software, Beschreibung oder Tags durchsuchen..."
class="search-input"
/>
<button id="clear-search" class="search-clear hidden" title="Suche löschen">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
</div>
</div>
<!-- Primary Filters Section - ONLY Domain and Phase -->
<div class="filter-section">
@@ -289,6 +311,10 @@ const sortedTags = Object.entries(tagFrequency)
const elements = {
searchInput: document.getElementById('search-input'),
clearSearch: document.getElementById('clear-search'),
semanticContainer: document.getElementById('semantic-search-container'),
semanticCheckbox: document.getElementById('semantic-search-enabled'),
semanticStatus: document.getElementById('semantic-status'),
semanticResultsCount: document.querySelector('.semantic-results-count'),
domainSelect: document.getElementById('domain-select'),
phaseSelect: document.getElementById('phase-select'),
typeSelect: document.getElementById('type-select'),
@@ -324,6 +350,54 @@ const sortedTags = Object.entries(tagFrequency)
let selectedTags = new Set();
let selectedPhase = '';
let isTagCloudExpanded = false;
let semanticSearchEnabled = false;
let semanticSearchAvailable = false;
let lastSemanticResults = null;
// Check embeddings availability
async function checkEmbeddingsAvailability() {
try {
const response = await fetch('/api/ai/embeddings-status');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
semanticSearchAvailable = data.embeddings?.enabled && data.embeddings?.initialized;
if (semanticSearchAvailable && elements.semanticContainer) {
elements.semanticContainer.classList.remove('hidden');
}
} catch (error) {
console.error('[EMBEDDINGS] Status check failed:', error.message);
semanticSearchAvailable = false;
}
}
// Semantic search function
async function performSemanticSearch(query) {
if (!semanticSearchAvailable || !query.trim()) {
return null;
}
try {
const response = await fetch('/api/search/semantic', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: query.trim() })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return data.results || [];
} catch (error) {
console.error('[SEMANTIC] Search failed:', error);
return null;
}
}
function toggleCollapsible(toggleBtn, content, storageKey) {
const isCollapsed = toggleBtn.getAttribute('data-collapsed') === 'true';
@@ -494,7 +568,19 @@ const sortedTags = Object.entries(tagFrequency)
: `${count} von ${total} Tools`;
}
function filterTools() {
function updateSemanticStatus(results) {
if (!elements.semanticStatus || !elements.semanticResultsCount) return;
if (semanticSearchEnabled && results?.length > 0) {
elements.semanticStatus.classList.remove('hidden');
elements.semanticResultsCount.textContent = `${results.length} semantische Treffer`;
} else {
elements.semanticStatus.classList.add('hidden');
}
}
// FIXED: Consolidated filtering logic with semantic search support
async function filterTools() {
const searchTerm = elements.searchInput.value.trim().toLowerCase();
const selectedDomain = elements.domainSelect.value;
const selectedPhaseFromSelect = elements.phaseSelect.value;
@@ -508,15 +594,32 @@ const sortedTags = Object.entries(tagFrequency)
const activePhase = selectedPhaseFromSelect || selectedPhase;
const filtered = window.toolsData.filter(tool => {
if (searchTerm && !(
tool.name.toLowerCase().includes(searchTerm) ||
tool.description.toLowerCase().includes(searchTerm) ||
(tool.tags || []).some(tag => tag.toLowerCase().includes(searchTerm))
)) {
return false;
}
let filteredTools = window.toolsData;
let semanticResults = null;
// CONSOLIDATED: Use semantic search if enabled and search term exists
if (semanticSearchEnabled && semanticSearchAvailable && searchTerm) {
semanticResults = await performSemanticSearch(searchTerm);
lastSemanticResults = semanticResults;
if (semanticResults?.length > 0) {
filteredTools = [...semanticResults];
}
} else {
lastSemanticResults = null;
// Traditional text-based search
if (searchTerm) {
filteredTools = window.toolsData.filter(tool =>
tool.name.toLowerCase().includes(searchTerm) ||
tool.description.toLowerCase().includes(searchTerm) ||
(tool.tags || []).some(tag => tag.toLowerCase().includes(searchTerm))
);
}
}
// Apply additional filters to the results
filteredTools = filteredTools.filter(tool => {
if (selectedDomain && !(tool.domains || []).includes(selectedDomain)) {
return false;
}
@@ -560,13 +663,20 @@ const sortedTags = Object.entries(tagFrequency)
return true;
});
const finalResults = searchTerm && window.prioritizeSearchResults
? window.prioritizeSearchResults(filtered, searchTerm)
: filtered;
// FIXED: Preserve semantic order when semantic search is used
const finalResults = semanticSearchEnabled && lastSemanticResults
? filteredTools // Already sorted by semantic similarity
: (searchTerm && window.prioritizeSearchResults
? window.prioritizeSearchResults(filteredTools, searchTerm)
: filteredTools);
updateResultsCounter(finalResults.length);
updateSemanticStatus(lastSemanticResults);
window.dispatchEvent(new CustomEvent('toolsFiltered', { detail: finalResults }));
window.dispatchEvent(new CustomEvent('toolsFiltered', {
detail: finalResults,
semanticSearch: semanticSearchEnabled && !!lastSemanticResults
}));
}
function resetPrimaryFilters() {
@@ -599,12 +709,17 @@ const sortedTags = Object.entries(tagFrequency)
function resetAllFilters() {
elements.searchInput.value = '';
elements.clearSearch.classList.add('hidden');
elements.semanticCheckbox.checked = false;
semanticSearchEnabled = false;
lastSemanticResults = null;
updateSemanticStatus(null);
resetPrimaryFilters();
resetAdvancedFilters();
resetTags();
filterTagCloud();
}
// Event listeners
elements.searchInput.addEventListener('input', (e) => {
const hasValue = e.target.value.length > 0;
elements.clearSearch.classList.toggle('hidden', !hasValue);
@@ -619,16 +734,30 @@ const sortedTags = Object.entries(tagFrequency)
filterTools();
});
// Semantic search checkbox handler
if (elements.semanticCheckbox) {
elements.semanticCheckbox.addEventListener('change', (e) => {
semanticSearchEnabled = e.target.checked;
filterTools();
});
}
[elements.domainSelect, elements.phaseSelect, elements.typeSelect, elements.skillSelect,
elements.platformSelect, elements.licenseSelect, elements.accessSelect].forEach(select => {
select.addEventListener('change', filterTools);
if (select) {
select.addEventListener('change', filterTools);
}
});
[elements.hostedOnly, elements.knowledgebaseOnly].forEach(checkbox => {
checkbox.addEventListener('change', filterTools);
if (checkbox) {
checkbox.addEventListener('change', filterTools);
}
});
elements.tagCloudToggle.addEventListener('click', toggleTagCloud);
if (elements.tagCloudToggle) {
elements.tagCloudToggle.addEventListener('click', toggleTagCloud);
}
elements.tagCloudItems.forEach(item => {
item.addEventListener('click', () => {
@@ -676,6 +805,8 @@ const sortedTags = Object.entries(tagFrequency)
window.clearTagFilters = resetTags;
window.clearAllFilters = resetAllFilters;
// Initialize
checkEmbeddingsAvailability();
initializeCollapsible();
initTagCloud();
filterTagCloud();