Enhance: Add exact match boosting to search relevance scoring

FTS5 with Porter stemming treats 'kindness' and 'kind' as the same root word,
which caused stemmed matches to rank equally with exact matches. This adds a
secondary relevance boost on top of BM25 to prioritize exact matches.

Relevance scoring now:
- BM25 base score (from FTS5)
- +100 for exact phrase match in verse text
- +50 per exact word match (e.g., 'kindness' exactly)
- +10 per partial/stemmed match (e.g., 'kind' via stemming)

Example: Searching for 'kindness'
- Verses with 'kindness': BM25 + 150 (phrase + word)
- Verses with 'kind': BM25 + 10 (partial match)

This ensures exact matches appear first while still benefiting from Porter
stemming to find all word variations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-10 20:05:29 -05:00
parent eb35e05ce0
commit 246d849163

View File

@@ -199,17 +199,25 @@ class SearchDatabase {
return reject(err);
}
// Format results
const results = rows.map(row => ({
book: row.book,
chapter: row.chapter,
verse: row.verse_number,
text: row.verse_text,
version: row.version,
highlight: row.highlighted_text,
relevance: -row.rank, // BM25 returns negative scores, negate for consistency
context: [] // Will be populated if requested
}));
// Format results with enhanced relevance scoring
const results = rows.map(row => {
const bm25Score = -row.rank; // BM25 returns negative scores
const exactMatchBoost = this.calculateExactMatchBoost(row.verse_text, query);
return {
book: row.book,
chapter: row.chapter,
verse: row.verse_number,
text: row.verse_text,
version: row.version,
highlight: row.highlighted_text,
relevance: bm25Score + exactMatchBoost,
context: [] // Will be populated if requested
};
});
// Re-sort by enhanced relevance (BM25 + exact match boost)
results.sort((a, b) => b.relevance - a.relevance);
// Add context if requested
if (includeContext && results.length > 0) {
@@ -269,6 +277,39 @@ class SearchDatabase {
return words.join(' AND ');
}
// Calculate exact match boost for better relevance ranking
calculateExactMatchBoost(verseText, query) {
const lowerText = verseText.toLowerCase();
const lowerQuery = query.toLowerCase().replace(/['"]/g, ''); // Remove quotes
let boost = 0;
// Exact phrase match (highest boost) - e.g., "faith hope love"
if (lowerText.includes(lowerQuery)) {
boost += 100;
}
// Exact word match boost - prioritize exact words over stemmed variants
const queryWords = lowerQuery.split(/\s+/).filter(w => w.length > 0);
const textWords = lowerText.split(/\W+/).filter(w => w.length > 0);
for (const queryWord of queryWords) {
// Exact word match (e.g., "kindness" matches "kindness", not just "kind")
if (textWords.includes(queryWord)) {
boost += 50;
} else {
// Partial match (stemmed or substring) - lower boost
for (const textWord of textWords) {
if (textWord.includes(queryWord) || queryWord.includes(textWord)) {
boost += 10;
break; // Only count once per query word
}
}
}
}
return boost;
}
// Get context verses around a target verse
async getContext(book, chapter, verseNumber, version, contextSize = 2) {
const start = Math.max(1, verseNumber - contextSize);