mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
titles > Content
This commit is contained in:
+1
-1
@@ -84,7 +84,7 @@
|
|||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"color": "^5.0.0",
|
"color": "^5.0.0",
|
||||||
"dompurify": "^3.2.4",
|
"dompurify": "^3.2.4",
|
||||||
"embeddia": "^1.2.1",
|
"embeddia": "^1.3.0",
|
||||||
"embla-carousel-autoplay": "^8.5.2",
|
"embla-carousel-autoplay": "^8.5.2",
|
||||||
"embla-carousel-svelte": "^8.5.2",
|
"embla-carousel-svelte": "^8.5.2",
|
||||||
"esbuild": "^0.25.3",
|
"esbuild": "^0.25.3",
|
||||||
|
|||||||
@@ -197,11 +197,13 @@ export async function hybridSearch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lexical guardrail: a strong title match is worth a meaningful
|
// Lexical guardrail: title matches must outweigh fuzzy vector/content
|
||||||
// bonus so vector reranking can't quietly drop an exact assessment
|
// overlap so exact titles lead the list.
|
||||||
// title between adjacent keystrokes. Scale is roughly 0..0.18.
|
|
||||||
const lexicalQuality = getLexicalMatchQuality(item, trimmedQuery);
|
const lexicalQuality = getLexicalMatchQuality(item, trimmedQuery);
|
||||||
const lexicalBonus = lexicalQuality > 0 ? lexicalQuality / 80 : 0;
|
let lexicalBonus = lexicalQuality > 0 ? lexicalQuality / 80 : 0;
|
||||||
|
if (lexicalQuality >= 12) lexicalBonus += 0.42;
|
||||||
|
else if (lexicalQuality >= 10) lexicalBonus += 0.24;
|
||||||
|
else if (lexicalQuality >= 8) lexicalBonus += 0.14;
|
||||||
|
|
||||||
const hybridScore =
|
const hybridScore =
|
||||||
(normalizedBm25Score * opts.bm25Weight) +
|
(normalizedBm25Score * opts.bm25Weight) +
|
||||||
|
|||||||
@@ -11,6 +11,57 @@ import {
|
|||||||
STRONG_LEXICAL_THRESHOLD,
|
STRONG_LEXICAL_THRESHOLD,
|
||||||
} from "./lexicalMatch";
|
} from "./lexicalMatch";
|
||||||
|
|
||||||
|
/** Same normalization as lexical matching (trim + lowercase). */
|
||||||
|
function normSearchKey(s: string): string {
|
||||||
|
return s.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exact title tiers so palette navigation (e.g. "Home", "Assessments") always
|
||||||
|
* wins over hybrid-scored body matches. Higher = sort earlier.
|
||||||
|
*/
|
||||||
|
function exactTitleSortTier(r: CombinedResult, queryNorm: string): number {
|
||||||
|
if (!queryNorm) return 0;
|
||||||
|
if (r.type === "command") {
|
||||||
|
const cmd = r.item as StaticCommandItem;
|
||||||
|
if (normSearchKey(cmd.text) !== queryNorm) return 0;
|
||||||
|
return cmd.category === "navigation" ? 3 : 2;
|
||||||
|
}
|
||||||
|
const ix = r.item as IndexItem;
|
||||||
|
if (normSearchKey(ix.text) === queryNorm) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareCombinedSearchResults(
|
||||||
|
a: CombinedResult,
|
||||||
|
b: CombinedResult,
|
||||||
|
queryNorm: string,
|
||||||
|
): number {
|
||||||
|
const tierDiff = exactTitleSortTier(b, queryNorm) - exactTitleSortTier(a, queryNorm);
|
||||||
|
if (tierDiff !== 0) return tierDiff;
|
||||||
|
|
||||||
|
if (a.type === "command" && b.type === "dynamic") {
|
||||||
|
return b.score - a.score - 10;
|
||||||
|
}
|
||||||
|
if (a.type === "dynamic" && b.type === "command") {
|
||||||
|
return b.score - a.score + 10;
|
||||||
|
}
|
||||||
|
return b.score - a.score;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syntheticIndexFromCommand(cmd: StaticCommandItem): IndexItem {
|
||||||
|
return {
|
||||||
|
id: cmd.id,
|
||||||
|
text: cmd.text,
|
||||||
|
category: cmd.category,
|
||||||
|
content: "",
|
||||||
|
dateAdded: 0,
|
||||||
|
metadata: {},
|
||||||
|
actionId: "",
|
||||||
|
renderComponentId: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Search result cache for better performance
|
// Search result cache for better performance
|
||||||
const searchCache = new Map<string, { results: CombinedResult[]; timestamp: number }>();
|
const searchCache = new Map<string, { results: CombinedResult[]; timestamp: number }>();
|
||||||
const CACHE_TTL = 1000 * 60 * 5; // 5 minutes
|
const CACHE_TTL = 1000 * 60 * 5; // 5 minutes
|
||||||
@@ -140,7 +191,19 @@ export function searchCommands(
|
|||||||
return searchResults.map((result: FuseResult<StaticCommandItem>) => {
|
return searchResults.map((result: FuseResult<StaticCommandItem>) => {
|
||||||
const item = result.item;
|
const item = result.item;
|
||||||
const fuseScore = 15 * (1 - (result.score || 0.5));
|
const fuseScore = 15 * (1 - (result.score || 0.5));
|
||||||
const score = fuseScore + (item.priority ?? 0);
|
let score = fuseScore + (item.priority ?? 0);
|
||||||
|
|
||||||
|
// Static palette titles share the same lexical tiers as index titles, but
|
||||||
|
// Fuse scores are tiny versus hybrid dynamic scores — scale title matches
|
||||||
|
// up so "Assessments" / prefix matches stay competitive with body hits.
|
||||||
|
const titleLex = getLexicalMatchQuality(syntheticIndexFromCommand(item), query);
|
||||||
|
if (titleLex >= 12) score += 240;
|
||||||
|
else if (titleLex >= 10) score += 195;
|
||||||
|
else if (titleLex >= 9) score += 165;
|
||||||
|
else if (titleLex >= 8) score += 140;
|
||||||
|
else if (titleLex >= 7) score += 120;
|
||||||
|
else if (titleLex >= 6) score += 100;
|
||||||
|
else if (titleLex > 0) score += titleLex * 14;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
@@ -374,28 +437,14 @@ export async function performSearch(
|
|||||||
// Step 4: Combine command and dynamic results
|
// Step 4: Combine command and dynamic results
|
||||||
const allResults = [...commandResults, ...dynamicResults];
|
const allResults = [...commandResults, ...dynamicResults];
|
||||||
|
|
||||||
// Sort by score (commands typically have higher priority)
|
allResults.sort((a, b) =>
|
||||||
allResults.sort((a, b) => {
|
compareCombinedSearchResults(a, b, trimmedQuery),
|
||||||
// Commands always come first if scores are similar
|
);
|
||||||
if (a.type === "command" && b.type === "dynamic") {
|
|
||||||
return b.score - a.score - 10; // Commands get +10 boost
|
|
||||||
}
|
|
||||||
if (a.type === "dynamic" && b.type === "command") {
|
|
||||||
return b.score - a.score + 10; // Commands get +10 boost
|
|
||||||
}
|
|
||||||
return b.score - a.score;
|
|
||||||
});
|
|
||||||
|
|
||||||
const dedupedResults = dedupeCombinedResultsByCourseNav(allResults);
|
const dedupedResults = dedupeCombinedResultsByCourseNav(allResults);
|
||||||
dedupedResults.sort((a, b) => {
|
dedupedResults.sort((a, b) =>
|
||||||
if (a.type === "command" && b.type === "dynamic") {
|
compareCombinedSearchResults(a, b, trimmedQuery),
|
||||||
return b.score - a.score - 10;
|
);
|
||||||
}
|
|
||||||
if (a.type === "dynamic" && b.type === "command") {
|
|
||||||
return b.score - a.score + 10;
|
|
||||||
}
|
|
||||||
return b.score - a.score;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cache results for queries longer than 2 chars
|
// Cache results for queries longer than 2 chars
|
||||||
if (trimmedQuery.length > 2) {
|
if (trimmedQuery.length > 2) {
|
||||||
|
|||||||
Reference in New Issue
Block a user