|
|
@@ -64,14 +64,45 @@ double field_score(const std::string& field, const std::string& pattern) {
|
|
|
return subsequence_score(lower, pattern);
|
|
|
}
|
|
|
|
|
|
+// Check if pattern starts with "tag:" prefix for exact tag filtering
|
|
|
+bool has_tag_prefix(const std::string& pattern, std::string& tag_query) {
|
|
|
+ const std::string prefix = "tag:";
|
|
|
+ if (pattern.size() > prefix.size() &&
|
|
|
+ ascii_lower(pattern.substr(0, prefix.size())) == prefix) {
|
|
|
+ tag_query = ascii_lower(pattern.substr(prefix.size()));
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
double book_score(const Book& book, const std::string& pattern) {
|
|
|
if (pattern.empty())
|
|
|
return 0.0;
|
|
|
+
|
|
|
+ // Handle "tag:tagname" syntax for exact tag filtering
|
|
|
+ std::string tag_query;
|
|
|
+ if (has_tag_prefix(pattern, tag_query)) {
|
|
|
+ if (tag_query.empty())
|
|
|
+ return 0.0;
|
|
|
+ for (const auto& tag : book.tags()) {
|
|
|
+ auto lower_tag = ascii_lower(tag);
|
|
|
+ // Exact match or substring match within tag
|
|
|
+ if (lower_tag == tag_query || lower_tag.find(tag_query) != std::string::npos)
|
|
|
+ return 100.0; // High score for tag match
|
|
|
+ }
|
|
|
+ return -1.0; // No matching tag
|
|
|
+ }
|
|
|
+
|
|
|
+ // Regular fuzzy search across all fields
|
|
|
double best = -1.0;
|
|
|
best = std::max(best, field_score(book.title(), pattern));
|
|
|
best = std::max(best, field_score(book.author(), pattern));
|
|
|
best = std::max(best, field_score(book.filePath(), pattern));
|
|
|
best = std::max(best, field_score(book.id(), pattern));
|
|
|
+ // Also search tags
|
|
|
+ for (const auto& tag : book.tags()) {
|
|
|
+ best = std::max(best, field_score(tag, pattern));
|
|
|
+ }
|
|
|
return best;
|
|
|
}
|
|
|
|