use crate::Result;
#[cfg(any(feature = "keywords-yake", feature = "keywords-rake"))]
use crate::plugins::registry::get_post_processor_registry;
#[cfg(any(feature = "keywords-yake", feature = "keywords-rake"))]
use once_cell::sync::OnceCell;
#[cfg(any(feature = "keywords-yake", feature = "keywords-rake"))]
use std::sync::Arc;
pub mod config;
pub mod processor;
pub mod types;
#[cfg(feature = "keywords-yake")]
mod yake;
#[cfg(feature = "keywords-rake")]
mod rake;
pub use config::{KeywordConfig, NgramRange};
pub use processor::KeywordExtractor;
#[cfg(feature = "keywords-rake")]
pub use config::RakeParams;
#[cfg(feature = "keywords-yake")]
pub use config::YakeParams;
pub use types::{Keyword, KeywordAlgorithm};
pub fn extract_keywords(text: &str, config: &KeywordConfig) -> Result<Vec<Keyword>> {
config.validate()?;
match config.algorithm {
#[cfg(feature = "keywords-yake")]
KeywordAlgorithm::Yake => yake::extract_keywords_yake(text, config),
#[cfg(feature = "keywords-rake")]
KeywordAlgorithm::Rake => rake::extract_keywords_rake(text, config),
#[cfg(not(any(feature = "keywords-yake", feature = "keywords-rake")))]
_ => Err(crate::XbergError::Other(
"No keyword extraction algorithm feature enabled".to_string(),
)),
}
}
#[cfg(any(feature = "keywords-yake", feature = "keywords-rake"))]
static PROCESSOR_INITIALIZED: OnceCell<()> = OnceCell::new();
#[cfg(any(feature = "keywords-yake", feature = "keywords-rake"))]
pub(crate) fn ensure_initialized() -> Result<()> {
PROCESSOR_INITIALIZED.get_or_try_init(register_keyword_processor)?;
let registry = get_post_processor_registry();
let already_registered = registry
.read()
.list()
.iter()
.any(|name| name == processor::KEYWORD_PROCESSOR_NAME);
if !already_registered {
register_keyword_processor()?;
}
Ok(())
}
#[cfg_attr(alef, alef(skip))]
#[cfg(any(feature = "keywords-yake", feature = "keywords-rake"))]
pub(crate) fn register_keyword_processor() -> Result<()> {
crate::plugins::processor::register_post_processor_if_absent(Arc::new(KeywordExtractor)).map(|_| ())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(any(feature = "keywords-yake", feature = "keywords-rake"))]
fn post_processor_registry_lists(name: &str) -> bool {
get_post_processor_registry()
.read()
.list()
.iter()
.any(|registered| registered == name)
}
#[cfg(any(feature = "keywords-yake", feature = "keywords-rake"))]
#[test]
#[serial_test::serial]
fn keyword_processor_recovers_after_a_post_processor_registry_guard_cycle() {
ensure_initialized().expect("first call must register the keyword processor");
assert!(
post_processor_registry_lists("keyword-extraction"),
"keyword processor must be present immediately after ensure_initialized"
);
{
use crate::plugins::registry::test_support::PostProcessorRegistryGuard;
let _guard = PostProcessorRegistryGuard::acquire();
}
ensure_initialized().expect("ensure_initialized must remain Ok once already initialized");
assert!(
post_processor_registry_lists("keyword-extraction"),
"keyword processor must still be registered after a PostProcessorRegistryGuard cycle"
);
}
#[test]
fn test_extract_keywords_default_algorithm() {
let text = "Rust programming language provides memory safety and performance.";
let config = KeywordConfig::default();
let keywords = extract_keywords(text, &config).unwrap();
assert!(!keywords.is_empty(), "Should extract keywords");
assert!(keywords.len() <= config.max_keywords);
}
#[cfg(feature = "keywords-yake")]
#[test]
fn test_extract_keywords_yake() {
let text = "Natural language processing using Rust is efficient and safe.";
let config = KeywordConfig::yake();
let keywords = extract_keywords(text, &config).unwrap();
assert!(!keywords.is_empty());
assert_eq!(keywords[0].algorithm, KeywordAlgorithm::Yake);
}
#[cfg(feature = "keywords-rake")]
#[test]
fn test_extract_keywords_rake() {
let text = "Natural language processing using Rust is efficient and safe.";
let config = KeywordConfig::rake();
let keywords = extract_keywords(text, &config).unwrap();
assert!(!keywords.is_empty());
assert_eq!(keywords[0].algorithm, KeywordAlgorithm::Rake);
}
#[cfg(feature = "keywords-rake")]
#[test]
fn should_reject_directly_constructed_invalid_ngram_range() {
let config = KeywordConfig {
ngram_range: NgramRange { min: 0, max: 0 },
..KeywordConfig::rake()
};
let error = extract_keywords("Rust keyword extraction", &config).expect_err("invalid range must fail");
match error {
crate::XbergError::Validation { message, .. } => {
assert_eq!(message, "ngram range minimum must be at least 1, got 0");
}
other => panic!("expected validation error, got {other:?}"),
}
}
#[cfg(all(feature = "keywords-yake", feature = "keywords-rake"))]
#[test]
fn test_compare_algorithms() {
let text = "Machine learning and artificial intelligence are transforming technology. \
Deep learning models require substantial computational resources.";
let yake_config = KeywordConfig::yake().with_max_keywords(5);
let yake_keywords = extract_keywords(text, &yake_config).unwrap();
let rake_config = KeywordConfig::rake().with_max_keywords(5);
let rake_keywords = extract_keywords(text, &rake_config).unwrap();
assert!(!yake_keywords.is_empty());
assert!(!rake_keywords.is_empty());
assert!(yake_keywords.iter().all(|k| k.algorithm == KeywordAlgorithm::Yake));
assert!(rake_keywords.iter().all(|k| k.algorithm == KeywordAlgorithm::Rake));
println!(
"YAKE keywords: {:?}",
yake_keywords.iter().map(|k| &k.text).collect::<Vec<_>>()
);
println!(
"RAKE keywords: {:?}",
rake_keywords.iter().map(|k| &k.text).collect::<Vec<_>>()
);
}
}