mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
fuzzy search results order improvements
This commit is contained in:
parent
0f74dcda6c
commit
9450386d57
@ -116,31 +116,25 @@ export function useToolSections(
|
|||||||
});
|
});
|
||||||
const entries = Object.entries(subMap);
|
const entries = Object.entries(subMap);
|
||||||
|
|
||||||
// If a search query is provided, and there are no exact/substring matches across any field,
|
// If a search query is present, always order subcategories by first occurrence in
|
||||||
// preserve the encounter order of subcategories (best matches first) instead of alphabetical.
|
// the ranked filteredTools list so the top-ranked tools' subcategory appears first.
|
||||||
if (searchQuery && searchQuery.trim()) {
|
if (searchQuery && searchQuery.trim()) {
|
||||||
const nq = normalizeForSearch(searchQuery);
|
|
||||||
const hasExact = filteredTools.some(({ item: [id, tool] }) => {
|
|
||||||
const idWords = idToWords(id);
|
|
||||||
return (
|
|
||||||
idWords.includes(nq) ||
|
|
||||||
normalizeForSearch(tool.name).includes(nq) ||
|
|
||||||
normalizeForSearch(tool.description).includes(nq)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (!hasExact) {
|
|
||||||
// Keep original appearance order of subcategories as they occur in filteredTools
|
|
||||||
const order: SubcategoryId[] = [];
|
const order: SubcategoryId[] = [];
|
||||||
filteredTools.forEach(({ item: [_, tool] }) => {
|
filteredTools.forEach(({ item: [_, tool] }) => {
|
||||||
const sc = tool.subcategoryId;
|
const sc = tool.subcategoryId;
|
||||||
if (!order.includes(sc)) order.push(sc);
|
if (!order.includes(sc)) order.push(sc);
|
||||||
});
|
});
|
||||||
return entries
|
return entries
|
||||||
.sort(([a], [b]) => order.indexOf(a as SubcategoryId) - order.indexOf(b as SubcategoryId))
|
.sort(([a], [b]) => {
|
||||||
|
const ai = order.indexOf(a as SubcategoryId);
|
||||||
|
const bi = order.indexOf(b as SubcategoryId);
|
||||||
|
if (ai !== bi) return ai - bi;
|
||||||
|
return (a as SubcategoryId).localeCompare(b as SubcategoryId);
|
||||||
|
})
|
||||||
.map(([subcategoryId, tools]) => ({ subcategoryId, tools } as SubcategoryGroup));
|
.map(([subcategoryId, tools]) => ({ subcategoryId, tools } as SubcategoryGroup));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// No search: alphabetical subcategory ordering
|
||||||
return entries
|
return entries
|
||||||
.sort(([a], [b]) => a.localeCompare(b))
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
.map(([subcategoryId, tools]) => ({ subcategoryId, tools } as SubcategoryGroup));
|
.map(([subcategoryId, tools]) => ({ subcategoryId, tools } as SubcategoryGroup));
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ToolRegistryEntry } from "../data/toolsTaxonomy";
|
import { ToolRegistryEntry } from "../data/toolsTaxonomy";
|
||||||
import { idToWords, scoreMatch, minScoreForQuery } from "./fuzzySearch";
|
import { scoreMatch, minScoreForQuery, normalizeForSearch } from "./fuzzySearch";
|
||||||
|
|
||||||
export interface RankedToolItem {
|
export interface RankedToolItem {
|
||||||
item: [string, ToolRegistryEntry];
|
item: [string, ToolRegistryEntry];
|
||||||
@ -15,47 +15,85 @@ export function filterToolRegistryByQuery(
|
|||||||
return entries.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] }));
|
return entries.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nq = normalizeForSearch(query);
|
||||||
const threshold = minScoreForQuery(query);
|
const threshold = minScoreForQuery(query);
|
||||||
const results: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string; score: number }> = [];
|
|
||||||
|
const exactName: Array<{ id: string; tool: ToolRegistryEntry; pos: number }> = [];
|
||||||
|
const exactSyn: Array<{ id: string; tool: ToolRegistryEntry; text: string; pos: number }> = [];
|
||||||
|
const fuzzyName: Array<{ id: string; tool: ToolRegistryEntry; score: number; text: string }> = [];
|
||||||
|
const fuzzySyn: Array<{ id: string; tool: ToolRegistryEntry; score: number; text: string }> = [];
|
||||||
|
|
||||||
for (const [id, tool] of entries) {
|
for (const [id, tool] of entries) {
|
||||||
let best = 0;
|
const nameNorm = normalizeForSearch(tool.name || '');
|
||||||
let matchedText = '';
|
const pos = nameNorm.indexOf(nq);
|
||||||
|
if (pos !== -1) {
|
||||||
const candidates: string[] = [
|
exactName.push({ id, tool, pos });
|
||||||
idToWords(id),
|
continue;
|
||||||
tool.name || '',
|
|
||||||
tool.description || ''
|
|
||||||
];
|
|
||||||
for (const value of candidates) {
|
|
||||||
if (!value) continue;
|
|
||||||
const s = scoreMatch(query, value);
|
|
||||||
if (s > best) {
|
|
||||||
best = s;
|
|
||||||
matchedText = value;
|
|
||||||
}
|
|
||||||
if (best >= 95) break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(tool.synonyms)) {
|
const syns = Array.isArray(tool.synonyms) ? tool.synonyms : [];
|
||||||
for (const synonym of tool.synonyms) {
|
let matchedExactSyn: { text: string; pos: number } | null = null;
|
||||||
if (!synonym) continue;
|
for (const s of syns) {
|
||||||
const s = scoreMatch(query, synonym);
|
const sn = normalizeForSearch(s);
|
||||||
if (s > best) {
|
const sp = sn.indexOf(nq);
|
||||||
best = s;
|
if (sp !== -1) {
|
||||||
matchedText = synonym;
|
matchedExactSyn = { text: s, pos: sp };
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (best >= 95) break;
|
}
|
||||||
|
if (matchedExactSyn) {
|
||||||
|
exactSyn.push({ id, tool, text: matchedExactSyn.text, pos: matchedExactSyn.pos });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fuzzy name
|
||||||
|
const nameScore = scoreMatch(query, tool.name || '');
|
||||||
|
if (nameScore >= threshold) {
|
||||||
|
fuzzyName.push({ id, tool, score: nameScore, text: tool.name || '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fuzzy synonyms (we'll consider these only if fuzzy name results are weak)
|
||||||
|
let bestSynScore = 0;
|
||||||
|
let bestSynText = '';
|
||||||
|
for (const s of syns) {
|
||||||
|
const synScore = scoreMatch(query, s);
|
||||||
|
if (synScore > bestSynScore) {
|
||||||
|
bestSynScore = synScore;
|
||||||
|
bestSynText = s;
|
||||||
|
}
|
||||||
|
if (bestSynScore >= 95) break;
|
||||||
|
}
|
||||||
|
if (bestSynScore >= threshold) {
|
||||||
|
fuzzySyn.push({ id, tool, score: bestSynScore, text: bestSynText });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (best >= threshold) {
|
// Sort within buckets
|
||||||
results.push({ item: [id, tool] as [string, ToolRegistryEntry], matchedText, score: best });
|
exactName.sort((a, b) => a.pos - b.pos || (a.tool.name || '').length - (b.tool.name || '').length);
|
||||||
}
|
exactSyn.sort((a, b) => a.pos - b.pos || a.text.length - b.text.length);
|
||||||
}
|
fuzzyName.sort((a, b) => b.score - a.score);
|
||||||
|
fuzzySyn.sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
results.sort((a, b) => b.score - a.score);
|
// Concatenate buckets with de-duplication by tool id
|
||||||
return results.map(({ item, matchedText }) => ({ item, matchedText }));
|
const seen = new Set<string>();
|
||||||
|
const ordered: RankedToolItem[] = [];
|
||||||
|
|
||||||
|
const push = (id: string, tool: ToolRegistryEntry, matchedText?: string) => {
|
||||||
|
if (seen.has(id)) return;
|
||||||
|
seen.add(id);
|
||||||
|
ordered.push({ item: [id, tool], matchedText });
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const { id, tool } of exactName) push(id, tool, tool.name);
|
||||||
|
for (const { id, tool, text } of exactSyn) push(id, tool, text);
|
||||||
|
for (const { id, tool, text } of fuzzyName) push(id, tool, text);
|
||||||
|
for (const { id, tool, text } of fuzzySyn) push(id, tool, text);
|
||||||
|
|
||||||
|
if (ordered.length > 0) return ordered;
|
||||||
|
|
||||||
|
// Fallback: return everything unchanged
|
||||||
|
return entries.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user