codesearch 0.1.14

A fast, intelligent CLI tool with multiple search modes (regex, fuzzy, semantic), code analysis, and dead code detection for popular programming languages
Documentation
//! Fuzzy Search Implementation
//!
//! Provides fuzzy matching and relevance scoring for search results.

use crate::types::{Match, SearchResult};
use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2;
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::Path;

/// Search within a single file using parallel processing
pub fn search_in_file_parallel(
    file_path: &Path,
    matcher: &super::SearchMatcher,
    fuzzy: bool,
    fuzzy_threshold: f64,
    query: &str,
    max_results: usize,
    rank: bool,
    bm25_stats: Option<&super::bm25::Bm25Stats>,
    context_filter: Option<&crate::types::ContextFilter>,
    declaration_filter: Option<&crate::types::DeclarationFilter>,
) -> Result<Vec<SearchResult>, Box<dyn std::error::Error>> {
    let file = fs::File::open(file_path)?;
    let reader = BufReader::new(file);
    let mut results = Vec::new();
    let mut line_count = 0;
    let mut term_freq = 0usize;
    let fuzzy_matcher = SkimMatcherV2::default();

    for line in reader.lines() {
        line_count += 1;
        let line = line?;

        if results.len() >= max_results {
            break;
        }

        // Apply structural context filter before matching
        if let Some(filter) = context_filter {
            let ctx = super::context::classify_line(&line);
            if !super::context::should_include(ctx, Some(filter)) {
                continue;
            }
        }

        if fuzzy {
            if let Some((score, indices)) = fuzzy_matcher.fuzzy_indices(&line, query) {
                if score as f64 >= fuzzy_threshold {
                    term_freq += 1;
                    // Collect characters once to avoid repeated O(n) indexing
                    let line_chars: Vec<char> = line.chars().collect();
                    let mut matches = Vec::new();

                    for &idx in &indices {
                        if matches.is_empty()
                            || idx >= matches.last().map(|m: &Match| m.end).unwrap_or(0)
                        {
                            // Safe character access using the pre-collected vector
                            let text = if idx < line_chars.len() {
                                line_chars[idx].to_string()
                            } else {
                                String::new()
                            };

                            matches.push(Match {
                                start: idx,
                                end: idx + 1,
                                text,
                            });
                        }
                    }

                    let (score_val, relevance) = if rank {
                        let s = calculate_relevance_score(
                            &line,
                            query,
                            line_count,
                            file_path,
                            true,
                            Some(score),
                        );
                        let r = relevance_label(s);
                        (s, r)
                    } else {
                        (score as f64, "Medium".to_string())
                    };

                    if let Some(filter) = declaration_filter {
                        if !super::declarations::should_include_declaration(&line, Some(filter)) {
                            continue;
                        }
                    }

                    results.push(SearchResult {
                        file: file_path.to_string_lossy().into_owned(),
                        line_number: line_count,
                        content: line.clone(),
                        matches,
                        score: score_val,
                        relevance,
                    });
                }
            }
        } else if let Some(mat) = matcher.find(&line) {
            term_freq += 1;
            let (score_val, relevance) = if rank {
                let s = calculate_relevance_score(&line, query, line_count, file_path, false, None);
                let r = relevance_label(s);
                (s, r)
            } else {
                (50.0, "Medium".to_string())
            };

            if let Some(filter) = declaration_filter {
                if !super::declarations::should_include_declaration(&line, Some(filter)) {
                    continue;
                }
            }

            let matches = vec![Match {
                start: mat.start,
                end: mat.end,
                text: line[mat.clone()].to_string(),
            }];

            results.push(SearchResult {
                file: file_path.to_string_lossy().into_owned(),
                line_number: line_count,
                content: line.clone(),
                matches,
                score: score_val,
                relevance,
            });
        }
    }

    // Apply BM25 document-level scoring if stats were provided
    if let Some(stats) = bm25_stats {
        if !results.is_empty() {
            let bm25_score = stats.normalized_score(term_freq, line_count);
            let relevance = relevance_label(bm25_score);
            for r in &mut results {
                r.score = bm25_score;
                r.relevance = relevance.clone();
            }
        }
    }

    Ok(results)
}

fn relevance_label(score: f64) -> String {
    if score >= 80.0 {
        "Very High".to_string()
    } else if score >= 60.0 {
        "High".to_string()
    } else if score >= 40.0 {
        "Medium".to_string()
    } else {
        "Low".to_string()
    }
}

/// Calculate relevance score for a search result
pub fn calculate_relevance_score(
    line: &str,
    query: &str,
    line_number: usize,
    file_path: &Path,
    _is_fuzzy: bool,
    fuzzy_score: Option<i64>,
) -> f64 {
    let mut score = 50.0;

    if line.contains(query) {
        score += 30.0;
    }

    if let Some(fs) = fuzzy_score {
        score += (fs as f64) / 10.0;
    }

    if line_number < 100 {
        score += 5.0;
    }

    if let Some(ext) = file_path.extension().and_then(|e| e.to_str()) {
        match ext {
            "rs" | "py" | "js" | "ts" => score += 10.0,
            "md" | "txt" => score += 5.0,
            _ => {}
        }
    }

    // Test-file dampening: lower score for matches in test / mock / fixture files
    let path_str = file_path.to_string_lossy();
    let is_test_file = path_str.contains("/test")
        || path_str.contains("/spec/")
        || path_str.contains("/mock")
        || path_str.contains("/fixture")
        || path_str.contains("_test.")
        || path_str.contains("_spec.")
        || path_str.contains("test_");
    if is_test_file {
        score *= 0.7;
    }

    // Noise penalty for data blobs / minified files: very long lines
    if line.len() > 500 {
        score *= 0.5;
    }

    let definition_patterns = [
        "fn ",
        "def ",
        "function ",
        "class ",
        "struct ",
        "impl ",
        "trait ",
    ];
    for pattern in &definition_patterns {
        if line.contains(pattern) {
            score += 15.0;
            break;
        }
    }

    score.clamp(0.0, 100.0)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn test_fuzzy_search_performance() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("test.rs");

        // Create a file with multiple lines
        let content = "fn test_function() {\n    let test_var = 123;\n    println!(\"test\");\n}\n";
        fs::write(&file_path, content).unwrap();

        let matcher = crate::search::SearchMatcher::Regex(
            std::sync::Arc::new(regex::Regex::new(r"test").unwrap()),
        );

        // Test fuzzy search
        let result = search_in_file_parallel(&file_path, &matcher, true, 0.0, "test", 100, false, None, None, None);

        assert!(result.is_ok());
        let results = result.unwrap();
        assert!(!results.is_empty());
    }

    #[test]
    fn test_fuzzy_indices_optimization() {
        // This test verifies that the character collection optimization works correctly
        let line = "fn test_function() {";
        let query = "test";

        let matcher = SkimMatcherV2::default();
        if let Some((_score, indices)) = matcher.fuzzy_indices(line, query) {
            // Collect characters once (as done in the optimized version)
            let line_chars: Vec<char> = line.chars().collect();

            // Verify all indices are valid
            for &idx in &indices {
                assert!(
                    idx < line_chars.len(),
                    "Index {} should be less than line length {}",
                    idx,
                    line_chars.len()
                );
                // Verify we can access the character without panicking
                let _c = line_chars[idx];
            }
        }
    }

    #[test]
    fn test_test_file_dampening() {
        let src_score = calculate_relevance_score(
            "fn helper() {}",
            "helper",
            1,
            std::path::Path::new("src/main.rs"),
            false,
            None,
        );
        let test_score = calculate_relevance_score(
            "fn helper() {}",
            "helper",
            1,
            std::path::Path::new("tests/test_main.rs"),
            false,
            None,
        );
        assert!(
            test_score < src_score,
            "Test file score ({}) should be lower than src file score ({})",
            test_score,
            src_score
        );
    }

    #[test]
    fn test_noise_penalty() {
        let normal_line = "fn helper() { println!(\"hello\"); }";
        let minified_line = "a".repeat(600);

        let normal_score = calculate_relevance_score(
            normal_line,
            "helper",
            1,
            std::path::Path::new("src/main.rs"),
            false,
            None,
        );
        let noisy_score = calculate_relevance_score(
            &minified_line,
            "a",
            1,
            std::path::Path::new("dist/bundle.js"),
            false,
            None,
        );
        assert!(
            noisy_score < normal_score,
            "Noisy line score ({}) should be lower than normal line score ({})",
            noisy_score,
            normal_score
        );
    }
}