use std::sync::Arc;
use tokio::sync::RwLock;
use rmcp::schemars;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::cache::{CrateCache, storage::CacheStorage};
use crate::search::config::{
DEFAULT_FUZZY_DISTANCE, DEFAULT_SEARCH_LIMIT, MAX_FUZZY_DISTANCE, MAX_SEARCH_LIMIT,
};
use crate::search::outputs::{SearchErrorOutput, SearchItemsFuzzyOutput};
use crate::search::{FuzzySearchOptions, FuzzySearcher, SearchIndexer, SearchResult};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SearchItemsFuzzyParams {
#[schemars(description = "The name of the crate")]
pub crate_name: String,
#[schemars(description = "The version of the crate")]
pub version: String,
#[schemars(description = "The search query")]
pub query: String,
#[schemars(description = "Enable fuzzy matching for typo tolerance")]
pub fuzzy_enabled: Option<bool>,
#[schemars(description = "Edit distance for fuzzy matching (0-2)")]
pub fuzzy_distance: Option<u8>,
#[schemars(description = "Maximum number of results to return")]
pub limit: Option<usize>,
#[schemars(description = "Filter by item kind")]
pub kind_filter: Option<String>,
#[schemars(
description = "For workspace crates, specify the member path (e.g., 'crates/rmcp')"
)]
pub member: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SearchTools {
cache: Arc<RwLock<CrateCache>>,
}
impl SearchTools {
pub fn new(cache: Arc<RwLock<CrateCache>>) -> Self {
Self { cache }
}
async fn has_search_index(
&self,
crate_name: &str,
version: &str,
member: Option<&str>,
) -> bool {
let cache = self.cache.read().await;
cache.storage.has_search_index(crate_name, version, member)
}
async fn perform_search(
&self,
params: SearchItemsFuzzyParams,
storage: CacheStorage,
) -> Result<Vec<SearchResult>, anyhow::Error> {
let indexer = SearchIndexer::new_for_crate(
¶ms.crate_name,
¶ms.version,
&storage,
params.member.as_deref(),
)?;
let fuzzy_searcher = FuzzySearcher::from_indexer(&indexer)?;
let fuzzy_distance = params.fuzzy_distance.unwrap_or(DEFAULT_FUZZY_DISTANCE);
if fuzzy_distance > MAX_FUZZY_DISTANCE {
return Err(anyhow::anyhow!(
"Fuzzy distance must be between 0 and {}",
MAX_FUZZY_DISTANCE
));
}
let limit = params.limit.unwrap_or(DEFAULT_SEARCH_LIMIT);
if limit > MAX_SEARCH_LIMIT {
return Err(anyhow::anyhow!(
"Limit must not exceed {}",
MAX_SEARCH_LIMIT
));
}
let options = FuzzySearchOptions {
fuzzy_enabled: params.fuzzy_enabled.unwrap_or(true),
fuzzy_distance,
limit,
kind_filter: params.kind_filter.clone(),
crate_filter: Some(params.crate_name.clone()),
member_filter: params.member.clone(),
};
fuzzy_searcher.search(¶ms.query, &options)
}
pub async fn search_items_fuzzy(
&self,
params: SearchItemsFuzzyParams,
) -> Result<SearchItemsFuzzyOutput, SearchErrorOutput> {
let query = params.query.clone();
let fuzzy_enabled = params.fuzzy_enabled.unwrap_or(true);
let crate_name = params.crate_name.clone();
let version = params.version.clone();
let member = params.member.clone();
let result = async {
{
let cache = self.cache.read().await;
let has_docs = cache.has_docs(
¶ms.crate_name,
¶ms.version,
params.member.as_deref(),
);
if has_docs
&& self
.has_search_index(
¶ms.crate_name,
¶ms.version,
params.member.as_deref(),
)
.await
{
let storage = cache.storage.clone();
drop(cache);
return self.perform_search(params, storage).await;
}
}
{
let cache = self.cache.write().await;
let has_docs = cache.has_docs(
¶ms.crate_name,
¶ms.version,
params.member.as_deref(),
);
if !has_docs {
cache
.ensure_crate_or_member_docs(
¶ms.crate_name,
¶ms.version,
params.member.as_deref(),
)
.await?;
}
}
let cache = self.cache.read().await;
let storage = cache.storage.clone();
drop(cache);
if !self
.has_search_index(
¶ms.crate_name,
¶ms.version,
params.member.as_deref(),
)
.await
{
let cache = self.cache.write().await;
cache
.create_search_index(
¶ms.crate_name,
¶ms.version,
params.member.as_deref(),
)
.await?;
}
self.perform_search(params, storage).await
}
.await;
match result {
Ok(results) => {
let total = results.len();
Ok(SearchItemsFuzzyOutput {
results: results
.into_iter()
.map(|r| crate::search::outputs::SearchResult {
score: r.score,
item_id: r.item_id,
name: r.name,
path: r.path,
kind: r.kind,
crate_name: r.crate_name,
version: r.version,
visibility: r.visibility,
doc_preview: None, member: r.member,
})
.collect(),
query,
total_results: total,
fuzzy_enabled,
crate_name,
version,
member,
})
}
Err(e) => Err(SearchErrorOutput::new(format!("Search failed: {e}"))),
}
}
}