90 lines
2.5 KiB
TypeScript
90 lines
2.5 KiB
TypeScript
import { useMemo } from 'react';
|
|
|
|
import { isEmpty } from './string.util';
|
|
|
|
import type { DocsPageHeading, SearchablePage } from '../interface';
|
|
|
|
const PARTIAL_MATCH_WORD_LENGTH_THRESHOLD = 5;
|
|
const WHOLE_WORD_MATCH_FAVOR_WEIGHT = 2;
|
|
const TITLE_FAVOR_WEIGHT = 15;
|
|
|
|
export interface SearchScore {
|
|
entry: SearchablePage;
|
|
metaScore: number;
|
|
score: number;
|
|
isExactTitleMatch: boolean;
|
|
matchedHeader: DocsPageHeading | undefined;
|
|
}
|
|
|
|
function getSearchScore(words: string[], entry: SearchablePage): SearchScore {
|
|
let score = 0;
|
|
let metaScore = 0;
|
|
|
|
for (const word of words) {
|
|
score +=
|
|
(entry.title.match(new RegExp(`\\b${word}\\b`, 'gi')) ?? []).length *
|
|
TITLE_FAVOR_WEIGHT *
|
|
WHOLE_WORD_MATCH_FAVOR_WEIGHT;
|
|
score +=
|
|
(entry.textContent.match(new RegExp(`\\b${word}\\b`, 'gi')) ?? []).length *
|
|
WHOLE_WORD_MATCH_FAVOR_WEIGHT;
|
|
|
|
if (word.length >= PARTIAL_MATCH_WORD_LENGTH_THRESHOLD) {
|
|
score += (entry.title.match(new RegExp(`${word}`, 'gi')) ?? []).length * TITLE_FAVOR_WEIGHT;
|
|
score += (entry.textContent.match(new RegExp(`${word}`, 'gi')) ?? []).length;
|
|
}
|
|
}
|
|
|
|
const exactMatchFavorWeight = words.length;
|
|
const exactSearch = words.join(' ').toLowerCase();
|
|
const isExactTitleMatch = entry.title.toLowerCase().includes(exactSearch);
|
|
|
|
const exactTitleMatchScore =
|
|
(isExactTitleMatch ? 1 : 0) *
|
|
TITLE_FAVOR_WEIGHT *
|
|
exactMatchFavorWeight *
|
|
WHOLE_WORD_MATCH_FAVOR_WEIGHT;
|
|
|
|
if (isExactTitleMatch) {
|
|
metaScore += 1;
|
|
}
|
|
|
|
score += exactTitleMatchScore;
|
|
score +=
|
|
(entry.textContent.match(new RegExp(`\\b${exactSearch}\\b`, 'gi')) ?? []).length *
|
|
exactMatchFavorWeight *
|
|
WHOLE_WORD_MATCH_FAVOR_WEIGHT;
|
|
|
|
return {
|
|
score,
|
|
metaScore,
|
|
entry,
|
|
isExactTitleMatch: exactTitleMatchScore > 0,
|
|
matchedHeader: entry.headings.find(header => header.title.toLowerCase().includes(exactSearch)),
|
|
};
|
|
}
|
|
|
|
export function useSearchScores(query: string | null, entries: SearchablePage[]): SearchScore[] {
|
|
return useMemo(() => {
|
|
if (!query || isEmpty(query.trim())) {
|
|
return [];
|
|
}
|
|
|
|
const queryWords = query.split(' ').filter(word => word.trim().length > 0);
|
|
|
|
const scores = entries
|
|
.map(entry => getSearchScore(queryWords, entry))
|
|
.filter(result => result.score > 0);
|
|
|
|
scores.sort((a, b) => {
|
|
if (a.metaScore !== b.metaScore) {
|
|
return b.metaScore - a.metaScore;
|
|
}
|
|
|
|
return b.score - a.score;
|
|
});
|
|
|
|
return scores;
|
|
}, [entries, query]);
|
|
}
|