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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user