use crate::db::traits::StoreSearch;
use crate::db::SqliteStore;
use crate::search::executor_types::{RankedResult, RankedResults, SearchSource};
use regex::Regex;
use tracing::{debug, instrument};
pub fn normalize_for_exact_match(query: &str) -> String {
let mut normalized = query.to_string();
let re1 = Regex::new(r"([A-Z]+)([A-Z][a-z])").unwrap();
normalized = re1.replace_all(&normalized, "${1}_${2}").to_string();
let re2 = Regex::new(r"([a-z\d])([A-Z]{2,})").unwrap();
normalized = re2.replace_all(&normalized, "${1}_${2}").to_string();
let re3 = Regex::new(r"([a-z\d])([A-Z])").unwrap();
normalized = re3.replace_all(&normalized, "${1}_${2}").to_string();
let re4 = Regex::new(r"[\s\-\.]").unwrap();
normalized = re4.replace_all(&normalized, "_").to_string();
normalized = normalized.to_lowercase();
let re5 = Regex::new(r"_+").unwrap();
normalized = re5.replace_all(&normalized, "_").to_string();
let re6 = Regex::new(r"^_|_$").unwrap();
normalized = re6.replace_all(&normalized, "").to_string();
normalized
}
pub struct FTSExecutor;
impl FTSExecutor {
#[instrument(skip(store), fields(query_len = fts_query.len()))]
pub async fn execute(
store: &SqliteStore,
fts_query: &str,
normalized_query: &str,
repo_id: i64,
worktree_id: Option<i64>,
limit: usize,
) -> Result<RankedResults, FTSError> {
if fts_query.is_empty() {
debug!("Empty FTS query, returning no results");
return Ok(RankedResults::empty(SearchSource::FTS));
}
let fetch_limit = (limit * 3) as i64;
debug!(
"Executing FTS query: '{}' (limit: {}, over-fetch: {})",
fts_query, limit, fetch_limit
);
let hits = store
.search_fts_by_id(
repo_id,
worktree_id,
fts_query,
normalized_query,
fetch_limit,
)
.await
.map_err(|e| FTSError::Database(e.to_string()))?;
let results: Vec<RankedResult> = hits
.into_iter()
.enumerate()
.map(|(i, hit)| {
RankedResult::new_with_exact_match(
hit.chunk_id,
hit.score as f32,
i + 1,
hit.exact_mult.map(|v| v as f32),
)
})
.collect();
debug!("FTS search returned {} results", results.len());
Ok(RankedResults::new(results, SearchSource::FTS))
}
}
#[derive(Debug, thiserror::Error)]
pub enum FTSError {
#[error("Database error: {0}")]
Database(String),
#[error("Invalid FTS query: {0}")]
InvalidQuery(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fts_executor_exists() {
let _executor = FTSExecutor;
}
mod normalize_tests {
use super::*;
#[test]
fn test_simple_camelcase() {
assert_eq!(
normalize_for_exact_match("validateProvider"),
"validate_provider"
);
}
#[test]
fn test_single_word() {
assert_eq!(normalize_for_exact_match("provider"), "provider");
}
#[test]
fn test_multiple_camelcase_transitions() {
assert_eq!(
normalize_for_exact_match("getUserNameFromDatabase"),
"get_user_name_from_database"
);
}
#[test]
fn test_acronym_at_start() {
assert_eq!(normalize_for_exact_match("XMLParser"), "xml_parser");
assert_eq!(normalize_for_exact_match("HTTPClient"), "http_client");
assert_eq!(normalize_for_exact_match("FTPUploader"), "ftp_uploader");
}
#[test]
fn test_acronym_in_middle() {
assert_eq!(
normalize_for_exact_match("validateHTTPRequest"),
"validate_http_request"
);
assert_eq!(
normalize_for_exact_match("sendSMTPMessage"),
"send_smtp_message"
);
assert_eq!(
normalize_for_exact_match("parseJSONData"),
"parse_json_data"
);
}
#[test]
fn test_consecutive_capitals() {
assert_eq!(normalize_for_exact_match("HTTPSHandler"), "https_handler");
assert_eq!(
normalize_for_exact_match("XMLHTTPRequest"),
"xmlhttp_request"
);
assert_eq!(normalize_for_exact_match("SSLContext"), "ssl_context");
}
#[test]
fn test_numbers_with_capitals() {
assert_eq!(normalize_for_exact_match("Base64Encoder"), "base64_encoder");
assert_eq!(normalize_for_exact_match("MD5Hash"), "md5_hash");
assert_eq!(normalize_for_exact_match("SHA256Digest"), "sha256_digest");
}
#[test]
fn test_kebab_case() {
assert_eq!(
normalize_for_exact_match("validate-provider"),
"validate_provider"
);
assert_eq!(
normalize_for_exact_match("user-auth-service-factory"),
"user_auth_service_factory"
);
}
#[test]
fn test_spaces() {
assert_eq!(
normalize_for_exact_match("validate provider"),
"validate_provider"
);
assert_eq!(
normalize_for_exact_match("user auth service"),
"user_auth_service"
);
}
#[test]
fn test_dots() {
assert_eq!(
normalize_for_exact_match("user.auth.service"),
"user_auth_service"
);
}
#[test]
fn test_edge_cases() {
assert_eq!(normalize_for_exact_match(""), "");
assert_eq!(normalize_for_exact_match("HTTP"), "http");
assert_eq!(normalize_for_exact_match("validate"), "validate");
assert_eq!(
normalize_for_exact_match("user__auth___service"),
"user_auth_service"
);
assert_eq!(
normalize_for_exact_match("_privateMethod"),
"private_method"
);
assert_eq!(normalize_for_exact_match("method_"), "method");
}
#[test]
fn test_mixed_separators() {
assert_eq!(
normalize_for_exact_match("user-auth.service Provider"),
"user_auth_service_provider"
);
}
#[test]
fn test_real_world_examples() {
assert_eq!(
normalize_for_exact_match("ValidationErrorHandler"),
"validation_error_handler"
);
assert_eq!(
normalize_for_exact_match("UserAuthFormContainer"),
"user_auth_form_container"
);
assert_eq!(
normalize_for_exact_match("execute_fts_search"),
"execute_fts_search"
);
assert_eq!(
normalize_for_exact_match("user-profile-service"),
"user_profile_service"
);
}
}
}