use crate::constants::{IC_ADJECTIVE, IC_NOUN, IC_VERB};
use chrono::{DateTime, Datelike, NaiveDate, Utc};
use rust_stemmers::{Algorithm, Stemmer};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PosTag {
Noun,
Verb,
Adjective,
ProperNoun,
StopWord,
Other,
}
#[derive(Debug, Clone)]
pub struct TaggedWord {
pub text: String,
pub stem: String,
pub pos: PosTag,
pub position: usize,
}
#[derive(Debug, Clone)]
pub struct SentenceChunk {
pub text: String,
pub sentence_idx: usize,
pub words: Vec<TaggedWord>,
}
impl SentenceChunk {
pub fn nouns(&self) -> Vec<&TaggedWord> {
self.words
.iter()
.filter(|w| matches!(w.pos, PosTag::Noun | PosTag::ProperNoun))
.collect()
}
pub fn verbs(&self) -> Vec<&TaggedWord> {
self.words
.iter()
.filter(|w| w.pos == PosTag::Verb)
.collect()
}
pub fn adjectives(&self) -> Vec<&TaggedWord> {
self.words
.iter()
.filter(|w| w.pos == PosTag::Adjective)
.collect()
}
pub fn content_words(&self) -> Vec<&TaggedWord> {
self.words
.iter()
.filter(|w| {
matches!(
w.pos,
PosTag::Noun | PosTag::ProperNoun | PosTag::Verb | PosTag::Adjective
)
})
.collect()
}
pub fn cooccurrence_pairs(&self) -> Vec<(&str, &str)> {
let content = self.content_words();
let mut pairs = Vec::new();
for i in 0..content.len() {
for j in (i + 1)..content.len() {
pairs.push((content[i].stem.as_str(), content[j].stem.as_str()));
}
}
pairs
}
}
#[derive(Debug, Clone)]
pub struct ChunkExtraction {
pub chunks: Vec<SentenceChunk>,
pub unique_nouns: HashSet<String>,
pub unique_verbs: HashSet<String>,
pub unique_adjectives: HashSet<String>,
pub proper_nouns: HashSet<String>,
}
impl ChunkExtraction {
pub fn all_content_stems(&self) -> HashSet<String> {
let mut all = self.unique_nouns.clone();
all.extend(self.unique_verbs.clone());
all.extend(self.unique_adjectives.clone());
all.extend(self.proper_nouns.clone());
all
}
pub fn all_cooccurrence_pairs(&self) -> Vec<(String, String)> {
let mut all_pairs = Vec::new();
for chunk in &self.chunks {
for (w1, w2) in chunk.cooccurrence_pairs() {
all_pairs.push((w1.to_string(), w2.to_string()));
}
}
all_pairs
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemporalRef {
pub date: NaiveDate,
pub original_text: String,
pub confidence: f32,
pub position: usize,
pub ref_type: TemporalRefType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TemporalRefType {
Absolute,
Relative,
DayOfWeek,
Month,
Year,
}
#[derive(Debug, Clone, Default)]
pub struct TemporalExtraction {
pub refs: Vec<TemporalRef>,
pub earliest: Option<NaiveDate>,
pub latest: Option<NaiveDate>,
}
impl TemporalExtraction {
pub fn has_temporal_refs(&self) -> bool {
!self.refs.is_empty()
}
pub fn date_range(&self) -> Option<(NaiveDate, NaiveDate)> {
match (self.earliest, self.latest) {
(Some(e), Some(l)) => Some((e, l)),
(Some(e), None) => Some((e, e)),
(None, Some(l)) => Some((l, l)),
(None, None) => None,
}
}
}
pub fn extract_temporal_refs(text: &str) -> TemporalExtraction {
let now = Utc::now();
let mut refs = Vec::new();
let mut earliest: Option<NaiveDate> = None;
let mut latest: Option<NaiveDate> = None;
let is_valid_date = |date: &NaiveDate| -> bool {
let year = date.year();
year >= 1900 && year <= 2100
};
if let Ok(parsed) = dateparser::parse(text) {
let date = parsed.date_naive();
if is_valid_date(&date) {
refs.push(TemporalRef {
date,
original_text: text.to_string(),
confidence: 0.8,
position: 0,
ref_type: classify_temporal_ref(text, &date, &now),
});
update_bounds(&mut earliest, &mut latest, date);
}
}
for (pos, sentence) in split_temporal_phrases(text).iter().enumerate() {
if let Ok(parsed) = dateparser::parse(sentence) {
let date = parsed.date_naive();
if !is_valid_date(&date) {
continue;
}
if refs.iter().any(|r| r.date == date) {
continue;
}
refs.push(TemporalRef {
date,
original_text: sentence.to_string(),
confidence: 0.7,
position: pos,
ref_type: classify_temporal_ref(sentence, &date, &now),
});
update_bounds(&mut earliest, &mut latest, date);
}
}
let explicit_dates = extract_explicit_dates(text);
for (date, original, pos) in explicit_dates {
if !is_valid_date(&date) {
continue;
}
if refs.iter().any(|r| r.date == date) {
continue;
}
refs.push(TemporalRef {
date,
original_text: original,
confidence: 0.9,
position: pos,
ref_type: TemporalRefType::Absolute,
});
update_bounds(&mut earliest, &mut latest, date);
}
let relative_dates = extract_relative_dates(text, &now);
for (date, original, pos, ref_type) in relative_dates {
if !is_valid_date(&date) {
continue;
}
if refs.iter().any(|r| r.date == date) {
continue;
}
refs.push(TemporalRef {
date,
original_text: original,
confidence: 0.85,
position: pos,
ref_type,
});
update_bounds(&mut earliest, &mut latest, date);
}
let month_year_dates = extract_month_year_dates(text);
for (date, original, pos) in month_year_dates {
if !is_valid_date(&date) {
continue;
}
if refs.iter().any(|r| r.date == date) {
continue;
}
refs.push(TemporalRef {
date,
original_text: original,
confidence: 0.85,
position: pos,
ref_type: TemporalRefType::Month,
});
update_bounds(&mut earliest, &mut latest, date);
}
refs.sort_by_key(|r| r.position);
TemporalExtraction {
refs,
earliest,
latest,
}
}
fn classify_temporal_ref(text: &str, date: &NaiveDate, now: &DateTime<Utc>) -> TemporalRefType {
let text_lower = text.to_lowercase();
let today = now.date_naive();
if text_lower.contains("yesterday")
|| text_lower.contains("ago")
|| text_lower.contains("last")
|| text_lower.contains("previous")
|| text_lower.contains("before")
|| text_lower.contains("earlier")
{
return TemporalRefType::Relative;
}
let days = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
];
if days.iter().any(|d| text_lower.contains(d)) {
return TemporalRefType::DayOfWeek;
}
let months = [
"january",
"february",
"march",
"april",
"may",
"june",
"july",
"august",
"september",
"october",
"november",
"december",
];
let has_month = months.iter().any(|m| text_lower.contains(m));
let has_day = text.chars().any(|c| c.is_ascii_digit());
if has_month && !has_day {
return TemporalRefType::Month;
}
if text.len() == 4 && text.chars().all(|c| c.is_ascii_digit()) {
return TemporalRefType::Year;
}
let diff = (today - *date).num_days().abs();
if diff <= 7 && text_lower.contains("this") {
return TemporalRefType::Relative;
}
TemporalRefType::Absolute
}
fn split_temporal_phrases(text: &str) -> Vec<String> {
let mut phrases = Vec::new();
let markers = [
" on ", " in ", " at ", " during ", " since ", " until ", " before ", " after ",
" around ", ", ", ". ", "! ", "? ",
];
let current = text.to_string();
for marker in markers {
let parts: Vec<&str> = current.split(marker).collect();
if parts.len() > 1 {
for part in parts {
let trimmed = part.trim();
if !trimmed.is_empty() && trimmed.len() > 3 {
phrases.push(trimmed.to_string());
}
}
break;
}
}
if phrases.is_empty() {
for sentence in text.split('.') {
let trimmed = sentence.trim();
if !trimmed.is_empty() && trimmed.len() > 3 {
phrases.push(trimmed.to_string());
}
}
}
phrases
}
fn extract_explicit_dates(text: &str) -> Vec<(NaiveDate, String, usize)> {
use regex::Regex;
let mut results = Vec::new();
let month_day_year =
Regex::new(r"(?i)(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),?\s+(\d{4})")
.unwrap();
for cap in month_day_year.captures_iter(text) {
let month_str = &cap[1];
let day: u32 = cap[2].parse().unwrap_or(1);
let year: i32 = cap[3].parse().unwrap_or(2000);
let month = month_to_num(month_str);
if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) {
let pos = cap.get(0).map(|m| m.start()).unwrap_or(0);
results.push((date, cap[0].to_string(), pos));
}
}
let iso_date = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
for cap in iso_date.captures_iter(text) {
let year: i32 = cap[1].parse().unwrap_or(2000);
let month: u32 = cap[2].parse().unwrap_or(1);
let day: u32 = cap[3].parse().unwrap_or(1);
if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) {
let pos = cap.get(0).map(|m| m.start()).unwrap_or(0);
results.push((date, cap[0].to_string(), pos));
}
}
let slash_date = Regex::new(r"(\d{1,2})/(\d{1,2})/(\d{4})").unwrap();
for cap in slash_date.captures_iter(text) {
let month: u32 = cap[1].parse().unwrap_or(1);
let day: u32 = cap[2].parse().unwrap_or(1);
let year: i32 = cap[3].parse().unwrap_or(2000);
if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) {
let pos = cap.get(0).map(|m| m.start()).unwrap_or(0);
results.push((date, cap[0].to_string(), pos));
}
}
results
}
fn extract_relative_dates(
text: &str,
now: &DateTime<Utc>,
) -> Vec<(NaiveDate, String, usize, TemporalRefType)> {
use regex::Regex;
let text_lower = text.to_lowercase();
let today = now.date_naive();
let mut results = Vec::new();
if let Some(pos) = text_lower.find("yesterday") {
let date = today - chrono::Duration::days(1);
results.push((
date,
"yesterday".to_string(),
pos,
TemporalRefType::Relative,
));
}
if let Some(pos) = text_lower.find("today") {
results.push((today, "today".to_string(), pos, TemporalRefType::Relative));
}
let ago_re = Regex::new(r"(?i)(\d+)\s+(day|week|month|year)s?\s+ago").unwrap();
for cap in ago_re.captures_iter(text) {
let n: i64 = cap[1].parse().unwrap_or(1);
let unit = cap[2].to_lowercase();
let date = match unit.as_str() {
"day" => today - chrono::Duration::days(n),
"week" => today - chrono::Duration::weeks(n),
"month" => {
today - chrono::Duration::days(n * 30)
}
"year" => today - chrono::Duration::days(n * 365),
_ => continue,
};
let pos = cap.get(0).map(|m| m.start()).unwrap_or(0);
results.push((date, cap[0].to_string(), pos, TemporalRefType::Relative));
}
let last_re = Regex::new(r"(?i)last\s+(week|month|year)").unwrap();
for cap in last_re.captures_iter(text) {
let unit = cap[1].to_lowercase();
let date = match unit.as_str() {
"week" => today - chrono::Duration::weeks(1),
"month" => today - chrono::Duration::days(30),
"year" => today - chrono::Duration::days(365),
_ => continue,
};
let pos = cap.get(0).map(|m| m.start()).unwrap_or(0);
if results.iter().any(|r| r.0 == date) {
continue;
}
results.push((date, cap[0].to_string(), pos, TemporalRefType::Relative));
}
let this_re = Regex::new(r"(?i)this\s+(week|month|year)").unwrap();
for cap in this_re.captures_iter(text) {
let pos = cap.get(0).map(|m| m.start()).unwrap_or(0);
if results.iter().any(|r| r.0 == today) {
continue;
}
results.push((today, cap[0].to_string(), pos, TemporalRefType::Relative));
}
results
}
fn extract_month_year_dates(text: &str) -> Vec<(NaiveDate, String, usize)> {
use regex::Regex;
let mut results = Vec::new();
let month_year_re = Regex::new(
r"(?i)(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{4})"
).unwrap();
for cap in month_year_re.captures_iter(text) {
let month = month_to_num(&cap[1]);
let year: i32 = cap[2].parse().unwrap_or(2000);
if let Some(date) = NaiveDate::from_ymd_opt(year, month, 1) {
let pos = cap.get(0).map(|m| m.start()).unwrap_or(0);
results.push((date, cap[0].to_string(), pos));
}
}
results
}
fn month_to_num(month: &str) -> u32 {
match month.to_lowercase().as_str() {
"january" | "jan" => 1,
"february" | "feb" => 2,
"march" | "mar" => 3,
"april" | "apr" => 4,
"may" => 5,
"june" | "jun" => 6,
"july" | "jul" => 7,
"august" | "aug" => 8,
"september" | "sep" | "sept" => 9,
"october" | "oct" => 10,
"november" | "nov" => 11,
"december" | "dec" => 12,
_ => 1,
}
}
fn update_bounds(
earliest: &mut Option<NaiveDate>,
latest: &mut Option<NaiveDate>,
date: NaiveDate,
) {
match earliest {
Some(e) if date < *e => *earliest = Some(date),
None => *earliest = Some(date),
_ => {}
}
match latest {
Some(l) if date > *l => *latest = Some(date),
None => *latest = Some(date),
_ => {}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TemporalIntent {
WhenQuestion,
SpecificTime,
Ordering,
Duration,
None,
}
pub fn detect_temporal_intent(query: &str) -> TemporalIntent {
let query_lower = query.to_lowercase();
if query_lower.starts_with("when")
|| query_lower.contains(" when ")
|| query_lower.contains("what date")
|| query_lower.contains("what day")
|| query_lower.contains("what time")
{
return TemporalIntent::WhenQuestion;
}
if query_lower.contains("how long")
|| query_lower.contains("how many days")
|| query_lower.contains("how many weeks")
|| query_lower.contains("how many months")
|| query_lower.contains("how many years")
{
return TemporalIntent::Duration;
}
if query_lower.contains("before or after")
|| query_lower.contains("first or")
|| query_lower.contains("earlier or later")
|| query_lower.contains("which came first")
|| query_lower.contains("in what order")
{
return TemporalIntent::Ordering;
}
let time_indicators = [
"yesterday",
"today",
"last week",
"last month",
"last year",
"this week",
"this month",
"this year",
"in january",
"in february",
"in march",
"in april",
"in may",
"in june",
"in july",
"in august",
"in september",
"in october",
"in november",
"in december",
"on monday",
"on tuesday",
"on wednesday",
"on thursday",
"on friday",
"on saturday",
"on sunday",
" ago",
" days ago",
" weeks ago",
" months ago",
" years ago",
];
if time_indicators.iter().any(|t| query_lower.contains(t)) {
return TemporalIntent::SpecificTime;
}
let extraction = extract_temporal_refs(query);
if extraction.has_temporal_refs() {
return TemporalIntent::SpecificTime;
}
TemporalIntent::None
}
pub fn requires_temporal_filtering(query: &str) -> bool {
let intent = detect_temporal_intent(query);
matches!(
intent,
TemporalIntent::SpecificTime | TemporalIntent::Duration | TemporalIntent::Ordering
)
}
pub fn asks_for_temporal_answer(query: &str) -> bool {
matches!(detect_temporal_intent(query), TemporalIntent::WhenQuestion)
}
#[derive(Debug, Clone)]
pub struct TemporalQueryContext {
pub intent: TemporalIntent,
pub extraction: TemporalExtraction,
pub date_range: Option<(NaiveDate, NaiveDate)>,
pub is_filtering_query: bool,
pub is_seeking_query: bool,
}
pub fn analyze_temporal(query: &str) -> TemporalQueryContext {
let extraction = extract_temporal_refs(query);
let query_lower = query.to_lowercase();
let is_when_question = query_lower.starts_with("when")
|| query_lower.contains(" when ")
|| query_lower.contains("what date")
|| query_lower.contains("what day")
|| query_lower.contains("what time");
let intent = if extraction.has_temporal_refs() {
if is_when_question {
TemporalIntent::WhenQuestion
} else {
TemporalIntent::SpecificTime
}
} else {
detect_temporal_intent(query)
};
let date_range = compute_padded_date_range(&extraction);
let is_filtering_query = matches!(
intent,
TemporalIntent::SpecificTime | TemporalIntent::Ordering
) && date_range.is_some();
let is_seeking_query = matches!(
intent,
TemporalIntent::WhenQuestion | TemporalIntent::Duration
);
TemporalQueryContext {
intent,
extraction,
date_range,
is_filtering_query,
is_seeking_query,
}
}
fn compute_padded_date_range(extraction: &TemporalExtraction) -> Option<(NaiveDate, NaiveDate)> {
let (earliest, latest) = extraction.date_range()?;
if extraction.refs.len() == 1 {
let ref_type = extraction.refs[0].ref_type;
match ref_type {
TemporalRefType::Month => {
let start = NaiveDate::from_ymd_opt(earliest.year(), earliest.month(), 1)?;
let end = if earliest.month() == 12 {
NaiveDate::from_ymd_opt(earliest.year() + 1, 1, 1)?
.pred_opt()
.unwrap_or(start)
} else {
NaiveDate::from_ymd_opt(earliest.year(), earliest.month() + 1, 1)?
.pred_opt()
.unwrap_or(start)
};
Some((start, end))
}
TemporalRefType::Year => {
let start = NaiveDate::from_ymd_opt(earliest.year(), 1, 1)?;
let end = NaiveDate::from_ymd_opt(earliest.year(), 12, 31)?;
Some((start, end))
}
TemporalRefType::Absolute | TemporalRefType::DayOfWeek | TemporalRefType::Relative => {
let start = earliest - chrono::Duration::days(1);
let end = latest + chrono::Duration::days(1);
Some((start, end))
}
}
} else {
let start = earliest - chrono::Duration::days(1);
let end = latest + chrono::Duration::days(1);
Some((start, end))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueryType {
Attribute(AttributeQuery),
Temporal,
Exploratory,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttributeQuery {
pub entity: String,
pub attribute: String,
pub attribute_synonyms: Vec<String>,
pub original_query: String,
}
pub fn classify_query(query: &str) -> QueryType {
if let Some(attr_query) = detect_attribute_query(query) {
return QueryType::Attribute(attr_query);
}
if asks_for_temporal_answer(query) {
return QueryType::Temporal;
}
QueryType::Exploratory
}
pub fn detect_attribute_query(query: &str) -> Option<AttributeQuery> {
let query_lower = query.to_lowercase();
let query_trimmed = query_lower.trim().trim_end_matches('?');
if let Some(result) = extract_possessive_pattern(query_trimmed) {
return Some(result);
}
if let Some(result) = extract_of_pattern(query_trimmed) {
return Some(result);
}
if query_lower.starts_with("where does") || query_lower.starts_with("where is") {
if let Some(entity) = extract_entity_after_verb(query_trimmed) {
return Some(AttributeQuery {
entity,
attribute: "location".to_string(),
attribute_synonyms: vec![
"live".to_string(),
"lives".to_string(),
"living".to_string(),
"resides".to_string(),
"located".to_string(),
"address".to_string(),
"home".to_string(),
"place".to_string(),
],
original_query: query.to_string(),
});
}
}
if query_lower.starts_with("how old") {
if let Some(entity) = extract_entity_after_verb(query_trimmed) {
return Some(AttributeQuery {
entity,
attribute: "age".to_string(),
attribute_synonyms: vec![
"age".to_string(),
"years old".to_string(),
"born".to_string(),
"birthday".to_string(),
],
original_query: query.to_string(),
});
}
}
if query_lower.starts_with("is ") {
let status_words = [
"married",
"single",
"divorced",
"engaged",
"dating",
"in a relationship",
];
for status in &status_words {
if query_lower.contains(status) {
let after_is = &query_trimmed[3..]; if let Some(pos) = after_is.find(status) {
let entity = after_is[..pos].trim().to_string();
if !entity.is_empty()
&& entity.chars().next().map_or(false, |c| c.is_alphabetic())
{
return Some(AttributeQuery {
entity: capitalize_first(&entity),
attribute: "relationship_status".to_string(),
attribute_synonyms: vec![
"single".to_string(),
"married".to_string(),
"divorced".to_string(),
"engaged".to_string(),
"dating".to_string(),
"relationship".to_string(),
"partner".to_string(),
"spouse".to_string(),
"status".to_string(),
],
original_query: query.to_string(),
});
}
}
}
}
}
None
}
fn extract_possessive_pattern(query: &str) -> Option<AttributeQuery> {
let possessive_patterns = [
("what is ", "'s "),
("what's ", "'s "),
("what is ", "' "),
("what's ", "' "),
];
for (prefix, possessive) in possessive_patterns {
if let Some(start) = query.find(prefix) {
let after_prefix = &query[start + prefix.len()..];
if let Some(pos_pos) = after_prefix.find(possessive) {
let entity = after_prefix[..pos_pos].trim();
let attribute = after_prefix[pos_pos + possessive.len()..].trim();
if !entity.is_empty() && !attribute.is_empty() {
return Some(AttributeQuery {
entity: capitalize_first(entity),
attribute: normalize_attribute(attribute),
attribute_synonyms: get_attribute_synonyms(attribute),
original_query: query.to_string(),
});
}
}
}
}
None
}
fn extract_of_pattern(query: &str) -> Option<AttributeQuery> {
let prefixes = ["what is the ", "what's the "];
for prefix in prefixes {
if let Some(start) = query.find(prefix) {
let after_prefix = &query[start + prefix.len()..];
if let Some(of_pos) = after_prefix.find(" of ") {
let attribute = after_prefix[..of_pos].trim();
let entity = after_prefix[of_pos + 4..].trim();
if !entity.is_empty() && !attribute.is_empty() {
return Some(AttributeQuery {
entity: capitalize_first(entity),
attribute: normalize_attribute(attribute),
attribute_synonyms: get_attribute_synonyms(attribute),
original_query: query.to_string(),
});
}
}
}
}
None
}
fn extract_entity_after_verb(query: &str) -> Option<String> {
let verbs = [" is ", " does "];
for verb in verbs {
if let Some(pos) = query.find(verb) {
let after_verb = query[pos + verb.len()..].trim();
let stop_words = ["live", "work", "do", "have", "go", "stay", "come"];
let words: Vec<&str> = after_verb.split_whitespace().collect();
let mut entity_words = Vec::new();
for word in words {
if stop_words.contains(&word) {
break;
}
entity_words.push(word);
}
if !entity_words.is_empty() {
return Some(capitalize_first(&entity_words.join(" ")));
}
}
}
None
}
fn normalize_attribute(attr: &str) -> String {
attr.trim()
.to_lowercase()
.replace(' ', "_")
.replace('-', "_")
}
fn get_attribute_synonyms(attribute: &str) -> Vec<String> {
let attr_lower = attribute.to_lowercase();
if attr_lower.contains("relationship")
|| attr_lower.contains("status")
|| attr_lower.contains("marital")
{
return vec![
"single".to_string(),
"married".to_string(),
"divorced".to_string(),
"engaged".to_string(),
"dating".to_string(),
"relationship".to_string(),
"partner".to_string(),
"spouse".to_string(),
"single parent".to_string(),
"status".to_string(),
"marital".to_string(),
];
}
if attr_lower.contains("job")
|| attr_lower.contains("occupation")
|| attr_lower.contains("work")
{
return vec![
"job".to_string(),
"work".to_string(),
"occupation".to_string(),
"profession".to_string(),
"career".to_string(),
"employed".to_string(),
"works as".to_string(),
];
}
if attr_lower.contains("name") {
return vec![
"name".to_string(),
"called".to_string(),
"named".to_string(),
];
}
if attr_lower.contains("age") {
return vec![
"age".to_string(),
"old".to_string(),
"years".to_string(),
"born".to_string(),
"birthday".to_string(),
];
}
vec![attr_lower.clone(), attr_lower.replace('_', " ")]
}
fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
}
pub fn extract_chunks(text: &str) -> ChunkExtraction {
let stemmer = Stemmer::create(Algorithm::English);
let sentences = split_sentences(text);
let mut chunks = Vec::with_capacity(sentences.len());
let mut unique_nouns = HashSet::new();
let mut unique_verbs = HashSet::new();
let mut unique_adjectives = HashSet::new();
let mut proper_nouns = HashSet::new();
for (sentence_idx, sentence) in sentences.iter().enumerate() {
let words = tokenize_with_case(sentence);
let mut tagged_words = Vec::with_capacity(words.len());
for (position, (word, is_capitalized)) in words.iter().enumerate() {
let word_lower = word.to_lowercase();
if word_lower.len() < 2 {
continue;
}
let stem = stemmer.stem(&word_lower).to_string();
let pos = classify_pos_for_chunking(&word_lower, *is_capitalized, position, &words);
match pos {
PosTag::Noun => {
unique_nouns.insert(stem.clone());
}
PosTag::Verb => {
unique_verbs.insert(stem.clone());
}
PosTag::Adjective => {
unique_adjectives.insert(stem.clone());
}
PosTag::ProperNoun => {
proper_nouns.insert(word.clone());
unique_nouns.insert(stem.clone()); }
_ => {}
}
if pos != PosTag::StopWord {
tagged_words.push(TaggedWord {
text: word.clone(),
stem,
pos,
position,
});
}
}
if !tagged_words.is_empty() {
chunks.push(SentenceChunk {
text: sentence.clone(),
sentence_idx,
words: tagged_words,
});
}
}
ChunkExtraction {
chunks,
unique_nouns,
unique_verbs,
unique_adjectives,
proper_nouns,
}
}
fn split_sentences(text: &str) -> Vec<String> {
let mut sentences = Vec::new();
let mut current = String::new();
for ch in text.chars() {
current.push(ch);
if ch == '.' || ch == '!' || ch == '?' || ch == '\n' {
let trimmed = current.trim();
if !trimmed.is_empty() && trimmed.len() > 3 {
let last_word: String = trimmed
.split_whitespace()
.last()
.unwrap_or("")
.chars()
.filter(|c| c.is_alphabetic())
.collect();
let is_abbrev = matches!(
last_word.to_lowercase().as_str(),
"mr" | "mrs"
| "ms"
| "dr"
| "prof"
| "sr"
| "jr"
| "vs"
| "etc"
| "eg"
| "ie"
| "st"
| "ave"
| "rd"
| "blvd"
);
if !is_abbrev || ch == '\n' {
sentences.push(trimmed.to_string());
current.clear();
}
}
}
}
let trimmed = current.trim();
if !trimmed.is_empty() {
sentences.push(trimmed.to_string());
}
sentences
}
fn tokenize_with_case(text: &str) -> Vec<(String, bool)> {
text.split_whitespace()
.map(|w| {
let clean: String = w
.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'')
.to_string();
let is_capitalized = clean
.chars()
.next()
.map(|c| c.is_uppercase())
.unwrap_or(false);
(clean, is_capitalized)
})
.filter(|(w, _)| !w.is_empty())
.collect()
}
fn classify_pos_for_chunking(
word: &str,
is_capitalized: bool,
position: usize,
_context: &[(String, bool)],
) -> PosTag {
if is_stop_word(word) {
return PosTag::StopWord;
}
if is_capitalized && position > 0 {
return PosTag::ProperNoun;
}
if is_verb(word) {
return PosTag::Verb;
}
if is_adjective(word) {
return PosTag::Adjective;
}
if is_noun_for_chunking(word) {
return PosTag::Noun;
}
if word.len() >= 4 {
return PosTag::Noun;
}
PosTag::Other
}
fn is_noun_for_chunking(word: &str) -> bool {
const NOUN_INDICATORS: &[&str] = &[
"memory",
"graph",
"node",
"edge",
"entity",
"embedding",
"vector",
"index",
"query",
"retrieval",
"activation",
"potentiation",
"consolidation",
"decay",
"strength",
"weight",
"threshold",
"importance",
"robot",
"drone",
"sensor",
"lidar",
"camera",
"motor",
"actuator",
"obstacle",
"path",
"waypoint",
"location",
"coordinates",
"position",
"battery",
"power",
"energy",
"voltage",
"current",
"system",
"module",
"component",
"unit",
"device",
"temperature",
"pressure",
"humidity",
"speed",
"velocity",
"signal",
"communication",
"network",
"link",
"connection",
"navigation",
"guidance",
"control",
"steering",
"data",
"information",
"message",
"command",
"response",
"function",
"method",
"class",
"struct",
"interface",
"package",
"library",
"framework",
"api",
"endpoint",
"request",
"error",
"exception",
"bug",
"fix",
"feature",
"test",
"benchmark",
"performance",
"latency",
"throughput",
"cache",
"buffer",
"queue",
"stack",
"heap",
"thread",
"process",
"server",
"client",
"database",
"table",
"column",
"row",
"schema",
"migration",
"deployment",
"container",
"cluster",
"replica",
"person",
"people",
"user",
"agent",
"operator",
"time",
"date",
"day",
"hour",
"minute",
"second",
"area",
"zone",
"region",
"sector",
"space",
"task",
"mission",
"goal",
"objective",
"target",
"warning",
"alert",
"notification",
"level",
"status",
"state",
"condition",
"mode",
"type",
"kind",
"version",
"release",
"update",
"change",
"result",
"output",
"input",
"value",
"key",
"name",
"id",
"identifier",
"sunrise",
"sunset",
"lake",
"mountain",
"beach",
"forest",
"garden",
"park",
"city",
"town",
"village",
"country",
"house",
"home",
"room",
"building",
"street",
"road",
"car",
"bus",
"train",
"plane",
"boat",
"bicycle",
"food",
"drink",
"water",
"coffee",
"tea",
"breakfast",
"lunch",
"dinner",
"meal",
"book",
"movie",
"music",
"song",
"art",
"painting",
"photo",
"picture",
"video",
"game",
"sport",
"team",
"player",
"match",
"race",
"trip",
"vacation",
"holiday",
"weekend",
"morning",
"evening",
"night",
"week",
"month",
"year",
"birthday",
"wedding",
"party",
"event",
"meeting",
"class",
"lesson",
"course",
"school",
"college",
"university",
"job",
"work",
"office",
"company",
"business",
"project",
"plan",
"idea",
"thought",
"feeling",
"emotion",
"love",
"friend",
"family",
"parent",
"child",
"kid",
"baby",
"mother",
"father",
"sister",
"brother",
"wife",
"husband",
"partner",
"group",
"community",
"society",
"culture",
"tradition",
"story",
"history",
"news",
"article",
"blog",
"post",
"comment",
"email",
"letter",
"phone",
"call",
"text",
"chat",
"conversation",
"discussion",
"talk",
"speech",
"presentation",
"question",
"answer",
"problem",
"solution",
"issue",
"challenge",
"opportunity",
"success",
"failure",
"experience",
"skill",
"knowledge",
"wisdom",
"truth",
"fact",
"opinion",
"belief",
"value",
"principle",
"rule",
"law",
"policy",
"decision",
"choice",
"option",
"alternative",
"reason",
"cause",
"effect",
"impact",
"influence",
"power",
"authority",
"responsibility",
"duty",
"right",
"freedom",
"justice",
"peace",
"war",
"conflict",
"agreement",
"contract",
"deal",
"price",
"cost",
"money",
"dollar",
"euro",
"pound",
"budget",
"investment",
"profit",
"loss",
"risk",
"reward",
"benefit",
"advantage",
"disadvantage",
"strength",
"weakness",
"opportunity",
"threat",
"strategy",
"tactic",
"method",
"approach",
"technique",
"tool",
"resource",
"material",
"product",
"service",
"quality",
"quantity",
"size",
"shape",
"color",
"sound",
"smell",
"taste",
"touch",
"sight",
"sense",
"mind",
"body",
"heart",
"soul",
"spirit",
"health",
"illness",
"disease",
"medicine",
"doctor",
"nurse",
"hospital",
"clinic",
"therapy",
"treatment",
"care",
"support",
"help",
"advice",
"guidance",
"counseling",
"coaching",
"mentoring",
"training",
"education",
"learning",
"teaching",
"research",
"study",
"experiment",
"discovery",
"invention",
"innovation",
"technology",
"science",
"math",
"physics",
"chemistry",
"biology",
"psychology",
"sociology",
"philosophy",
"religion",
"spirituality",
"meditation",
"yoga",
"exercise",
"fitness",
"diet",
"nutrition",
"sleep",
"rest",
"relaxation",
"stress",
"anxiety",
"depression",
"happiness",
"joy",
"sadness",
"anger",
"fear",
"surprise",
"disgust",
"trust",
"hope",
"faith",
"courage",
"confidence",
"pride",
"shame",
"guilt",
"regret",
"gratitude",
"empathy",
"compassion",
"kindness",
"generosity",
"honesty",
"integrity",
"loyalty",
"respect",
"tolerance",
"patience",
"persistence",
"determination",
"motivation",
"inspiration",
"creativity",
"imagination",
"curiosity",
"wonder",
"beauty",
"art",
"music",
"dance",
"theater",
"film",
"literature",
"poetry",
"writing",
"reading",
"speaking",
"listening",
"communication",
"expression",
"interpretation",
"understanding",
"meaning",
"purpose",
"goal",
"dream",
"vision",
"mission",
"passion",
"interest",
"hobby",
"activity",
"adventure",
"journey",
"path",
"way",
"direction",
"destination",
"origin",
"beginning",
"end",
"start",
"finish",
"progress",
"growth",
"development",
"evolution",
"transformation",
"change",
"transition",
"shift",
"movement",
"action",
"reaction",
"response",
"behavior",
"habit",
"pattern",
"routine",
"schedule",
"plan",
"strategy",
"tactic",
"approach",
"method",
"process",
"procedure",
"step",
"stage",
"phase",
"cycle",
"circle",
"loop",
"sequence",
"order",
"arrangement",
"organization",
"structure",
"system",
"network",
"connection",
"relationship",
"bond",
"link",
"tie",
"association",
"affiliation",
"membership",
"participation",
"involvement",
"engagement",
"commitment",
"dedication",
"devotion",
"loyalty",
"allegiance",
"support",
"backing",
"endorsement",
"approval",
"acceptance",
"recognition",
"acknowledgment",
"appreciation",
"gratitude",
"thanks",
"praise",
"compliment",
"criticism",
"feedback",
"evaluation",
"assessment",
"judgment",
"opinion",
"view",
"perspective",
"angle",
"aspect",
"dimension",
"element",
"component",
"part",
"piece",
"section",
"segment",
"portion",
"share",
"fraction",
"percentage",
"ratio",
"proportion",
"balance",
"equilibrium",
"harmony",
"unity",
"diversity",
"variety",
"difference",
"similarity",
"comparison",
"contrast",
"distinction",
"separation",
"division",
"classification",
"category",
"class",
"type",
"kind",
"sort",
"species",
"variety",
"version",
"edition",
"model",
"design",
"style",
"format",
"layout",
"arrangement",
"configuration",
"setup",
"installation",
"deployment",
];
if NOUN_INDICATORS.contains(&word) {
return true;
}
if word.ends_with("tion")
|| word.ends_with("sion")
|| word.ends_with("ment")
|| word.ends_with("ness")
|| word.ends_with("ity")
|| word.ends_with("ance")
|| word.ends_with("ence")
|| word.ends_with("age")
|| word.ends_with("ure")
|| word.ends_with("dom")
|| word.ends_with("ship")
|| word.ends_with("hood")
|| word.ends_with("ism")
|| word.ends_with("ist")
{
return true;
}
if (word.ends_with("er") || word.ends_with("or")) && word.len() > 4 {
let without_suffix = &word[..word.len() - 2];
if !without_suffix.ends_with("t")
&& !without_suffix.ends_with("g")
&& !without_suffix.ends_with("d")
{
return true;
}
}
false
}
#[derive(Debug, Clone)]
pub struct FocalEntity {
pub text: String,
pub stem: String,
pub ic_weight: f32,
pub is_compound: bool,
pub negated: bool,
}
#[derive(Debug, Clone)]
pub struct Modifier {
pub text: String,
pub stem: String,
pub ic_weight: f32,
pub negated: bool,
}
#[derive(Debug, Clone)]
pub struct Relation {
pub text: String,
pub stem: String,
pub ic_weight: f32,
pub negated: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QueryIntent {
Needle,
Exploratory,
Hybrid,
}
impl Default for QueryIntent {
fn default() -> Self {
QueryIntent::Hybrid
}
}
#[derive(Debug, Clone)]
pub struct QueryAnalysis {
pub focal_entities: Vec<FocalEntity>,
pub discriminative_modifiers: Vec<Modifier>,
pub relational_context: Vec<Relation>,
pub compound_nouns: Vec<String>,
pub original_query: String,
pub has_negation: bool,
pub intent: QueryIntent,
}
impl QueryAnalysis {
pub fn total_weight(&self) -> f32 {
let entity_weight: f32 = self.focal_entities.iter().map(|e| e.ic_weight).sum();
let modifier_weight: f32 = self
.discriminative_modifiers
.iter()
.map(|m| m.ic_weight)
.sum();
let relation_weight: f32 = self.relational_context.iter().map(|r| r.ic_weight).sum();
let compound_bonus = self.compound_nouns.len() as f32 * 0.5;
entity_weight + modifier_weight + relation_weight + compound_bonus
}
pub fn all_stems(&self) -> HashSet<String> {
let mut stems = HashSet::new();
for e in &self.focal_entities {
stems.insert(e.stem.clone());
}
for m in &self.discriminative_modifiers {
stems.insert(m.stem.clone());
}
for r in &self.relational_context {
stems.insert(r.stem.clone());
}
stems
}
pub fn positive_entity_stems(&self) -> Vec<&str> {
self.focal_entities
.iter()
.filter(|e| !e.negated)
.map(|e| e.stem.as_str())
.collect()
}
pub fn negated_entity_stems(&self) -> Vec<&str> {
self.focal_entities
.iter()
.filter(|e| e.negated)
.map(|e| e.stem.as_str())
.collect()
}
pub fn to_ic_weights(&self) -> std::collections::HashMap<String, f32> {
self.to_ic_weights_with_yake(true)
}
pub fn to_ic_weights_with_yake(
&self,
use_yake: bool,
) -> std::collections::HashMap<String, f32> {
use crate::embeddings::keywords::{KeywordConfig, KeywordExtractor};
let mut weights = std::collections::HashMap::new();
if use_yake {
let config = KeywordConfig {
max_keywords: 5,
ngrams: 2,
min_length: 3,
..Default::default()
};
let extractor = KeywordExtractor::with_config(config);
let keywords = extractor.extract(&self.original_query);
for kw in keywords {
let term = kw.text.to_lowercase();
if term.contains(' ') {
continue;
}
let yake_boost = 1.0 + (kw.importance * 5.0);
weights
.entry(term)
.and_modify(|w: &mut f32| *w = w.max(yake_boost))
.or_insert(yake_boost);
}
}
for entity in &self.focal_entities {
let term = entity.text.to_lowercase();
weights
.entry(term)
.and_modify(|w: &mut f32| *w = w.max(entity.ic_weight))
.or_insert(entity.ic_weight);
if entity.stem != entity.text.to_lowercase() {
weights
.entry(entity.stem.clone())
.and_modify(|w: &mut f32| *w = w.max(entity.ic_weight))
.or_insert(entity.ic_weight);
}
}
for modifier in &self.discriminative_modifiers {
let term = modifier.text.to_lowercase();
weights
.entry(term)
.and_modify(|w: &mut f32| *w = w.max(modifier.ic_weight))
.or_insert(modifier.ic_weight);
if modifier.stem != modifier.text.to_lowercase() {
weights
.entry(modifier.stem.clone())
.and_modify(|w: &mut f32| *w = w.max(modifier.ic_weight))
.or_insert(modifier.ic_weight);
}
}
for relation in &self.relational_context {
let term = relation.text.to_lowercase();
weights
.entry(term)
.and_modify(|w: &mut f32| *w = w.max(relation.ic_weight))
.or_insert(relation.ic_weight);
if relation.stem != relation.text.to_lowercase() {
weights
.entry(relation.stem.clone())
.and_modify(|w: &mut f32| *w = w.max(relation.ic_weight))
.or_insert(relation.ic_weight);
}
}
for compound in &self.compound_nouns {
for word in compound.split_whitespace() {
let term = word.to_lowercase();
weights.entry(term).and_modify(|w: &mut f32| *w *= 1.2);
}
}
weights
}
pub fn keyword_discriminativeness(&self) -> (f32, Vec<String>) {
use crate::embeddings::keywords::{KeywordConfig, KeywordExtractor};
let config = KeywordConfig {
max_keywords: 5,
ngrams: 2,
min_length: 2, ..Default::default()
};
let extractor = KeywordExtractor::with_config(config);
let keywords = extractor.extract(&self.original_query);
let mut max_importance = 0.0f32;
let mut discriminative = Vec::new();
for kw in keywords {
if kw.importance > max_importance {
max_importance = kw.importance;
}
if kw.importance > 0.5 {
discriminative.push(kw.text.to_lowercase());
}
}
(max_importance, discriminative)
}
pub fn to_phrase_boosts(&self) -> Vec<(String, f32)> {
let mut phrases = Vec::new();
for compound in &self.compound_nouns {
phrases.push((compound.to_lowercase(), 2.0));
}
if self.focal_entities.len() >= 2 {
for i in 0..self.focal_entities.len() - 1 {
let e1 = &self.focal_entities[i];
let e2 = &self.focal_entities[i + 1];
if !e1.negated && !e2.negated {
let phrase = format!("{} {}", e1.text.to_lowercase(), e2.text.to_lowercase());
if !self
.compound_nouns
.iter()
.any(|c| c.to_lowercase() == phrase)
{
phrases.push((phrase, 1.5));
}
}
}
}
phrases
}
}
#[derive(Debug)]
struct AnnotatedToken {
text: String,
stem: String,
pos: PartOfSpeech,
negated: bool,
position: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum PartOfSpeech {
Noun,
Adjective,
Verb,
StopWord,
Negation,
Unknown,
}
pub fn analyze_query(query_text: &str) -> QueryAnalysis {
let stemmer = Stemmer::create(Algorithm::English);
let words = tokenize(query_text);
if words.is_empty() {
return QueryAnalysis {
focal_entities: Vec::new(),
discriminative_modifiers: Vec::new(),
relational_context: Vec::new(),
compound_nouns: Vec::new(),
original_query: query_text.to_string(),
has_negation: false,
intent: QueryIntent::Hybrid,
};
}
let annotated = annotate_tokens(&words, &stemmer);
let compound_nouns = detect_compound_nouns(&annotated);
let mut focal_entities = Vec::new();
let mut discriminative_modifiers = Vec::new();
let mut relational_context = Vec::new();
let mut has_negation = false;
let compound_positions: HashSet<usize> = compound_positions(&annotated, &compound_nouns);
for token in &annotated {
if token.pos == PartOfSpeech::Negation {
has_negation = true;
continue;
}
if token.pos == PartOfSpeech::StopWord {
continue;
}
let is_compound = compound_positions.contains(&token.position);
match token.pos {
PartOfSpeech::Noun | PartOfSpeech::Unknown => {
let weight = calculate_term_weight(&token.text, IC_NOUN);
focal_entities.push(FocalEntity {
text: token.text.clone(),
stem: token.stem.clone(),
ic_weight: weight,
is_compound,
negated: token.negated,
});
}
PartOfSpeech::Adjective => {
let weight = calculate_term_weight(&token.text, IC_ADJECTIVE);
discriminative_modifiers.push(Modifier {
text: token.text.clone(),
stem: token.stem.clone(),
ic_weight: weight,
negated: token.negated,
});
}
PartOfSpeech::Verb => {
let weight = calculate_term_weight(&token.text, IC_VERB);
relational_context.push(Relation {
text: token.text.clone(),
stem: token.stem.clone(),
ic_weight: weight,
negated: token.negated,
});
}
_ => {}
}
}
for compound in &compound_nouns {
let stem = stemmer.stem(compound).to_string();
focal_entities.push(FocalEntity {
text: compound.clone(),
stem,
ic_weight: IC_NOUN * 1.5, is_compound: true,
negated: false,
});
}
let intent = detect_query_intent(query_text, &focal_entities, &relational_context);
QueryAnalysis {
focal_entities,
discriminative_modifiers,
relational_context,
compound_nouns,
original_query: query_text.to_string(),
has_negation,
intent,
}
}
fn detect_query_intent(
query_text: &str,
focal_entities: &[FocalEntity],
relational_context: &[Relation],
) -> QueryIntent {
let lower = query_text.to_lowercase();
let needle_starters = [
"what is", "what's", "who is", "who's", "where is", "where's", "when did", "when was",
"which", "how much", "how many", "find", "get me", "show me", "list", "give me",
];
let needle_patterns = [
"'s email",
"'s phone",
"'s address",
"'s name",
"email of",
"phone of",
"address of",
"name of",
"id of",
"password",
"api key",
"token",
];
let exploratory_starters = [
"tell me about",
"explain",
"describe",
"what do we know about",
"summarize",
"overview",
"recap",
"context",
"related to",
"associated with",
"connected to",
"how does",
"how do",
"why does",
"why do",
];
let exploratory_patterns = [
"all about",
"everything about",
"more about",
"history of",
"background",
"related",
];
for starter in needle_starters.iter() {
if lower.starts_with(starter) {
return QueryIntent::Needle;
}
}
for pattern in needle_patterns.iter() {
if lower.contains(pattern) {
return QueryIntent::Needle;
}
}
for starter in exploratory_starters.iter() {
if lower.starts_with(starter) || lower.contains(starter) {
return QueryIntent::Exploratory;
}
}
for pattern in exploratory_patterns.iter() {
if lower.contains(pattern) {
return QueryIntent::Exploratory;
}
}
let entity_count = focal_entities.len();
let relation_count = relational_context.len();
if entity_count > 0 && relation_count == 0 {
QueryIntent::Needle
} else if relation_count > entity_count {
QueryIntent::Exploratory
} else {
QueryIntent::Hybrid
}
}
fn tokenize(text: &str) -> Vec<String> {
text.split_whitespace()
.map(|w| {
w.trim_matches(|c: char| !c.is_alphanumeric())
.to_lowercase()
})
.filter(|w| !w.is_empty())
.collect()
}
fn annotate_tokens(words: &[String], stemmer: &Stemmer) -> Vec<AnnotatedToken> {
let mut annotated = Vec::with_capacity(words.len());
let mut in_negation_scope = false;
let mut negation_distance = 0;
for (i, word) in words.iter().enumerate() {
let stem = stemmer.stem(word).to_string();
let pos = classify_pos(word, i, words);
if pos == PartOfSpeech::Negation {
in_negation_scope = true;
negation_distance = 0;
} else if in_negation_scope {
negation_distance += 1;
if negation_distance > 3 {
in_negation_scope = false;
}
}
let negated = in_negation_scope && pos != PartOfSpeech::Negation;
annotated.push(AnnotatedToken {
text: word.clone(),
stem,
pos,
negated,
position: i,
});
}
annotated
}
fn classify_pos(word: &str, position: usize, context: &[String]) -> PartOfSpeech {
if is_negation(word) {
return PartOfSpeech::Negation;
}
if is_stop_word(word) {
return PartOfSpeech::StopWord;
}
if is_verb(word) {
return PartOfSpeech::Verb;
}
if is_adjective(word) {
return PartOfSpeech::Adjective;
}
if is_noun(word, position, context) {
return PartOfSpeech::Noun;
}
PartOfSpeech::Unknown
}
fn detect_compound_nouns(tokens: &[AnnotatedToken]) -> Vec<String> {
let mut compounds = Vec::new();
const COMPOUND_PATTERNS: &[(&str, &str)] = &[
("machine", "learning"),
("deep", "learning"),
("neural", "network"),
("natural", "language"),
("language", "model"),
("artificial", "intelligence"),
("knowledge", "graph"),
("vector", "database"),
("memory", "system"),
("data", "structure"),
("source", "code"),
("error", "handling"),
("unit", "test"),
("integration", "test"),
("api", "endpoint"),
("web", "server"),
("file", "system"),
("operating", "system"),
("database", "schema"),
("user", "interface"),
("command", "line"),
("version", "control"),
("pull", "request"),
("code", "review"),
("bug", "fix"),
("feature", "request"),
("spreading", "activation"),
("hebbian", "learning"),
("long", "term"),
("short", "term"),
("working", "memory"),
("semantic", "search"),
("graph", "traversal"),
("edge", "device"),
("air", "gapped"),
("support", "group"),
("pride", "parade"),
("poetry", "reading"),
("civil", "rights"),
("human", "rights"),
("social", "media"),
("community", "center"),
("discussion", "group"),
("therapy", "session"),
("art", "therapy"),
("group", "therapy"),
];
for i in 0..tokens.len().saturating_sub(1) {
let t1 = &tokens[i];
let t2 = &tokens[i + 1];
if t1.pos == PartOfSpeech::StopWord || t2.pos == PartOfSpeech::StopWord {
continue;
}
for (w1, w2) in COMPOUND_PATTERNS {
if (t1.stem == *w1 || t1.text == *w1) && (t2.stem == *w2 || t2.text == *w2) {
compounds.push(format!("{} {}", t1.text, t2.text));
break;
}
}
if (t1.pos == PartOfSpeech::Noun || t1.pos == PartOfSpeech::Unknown)
&& (t2.pos == PartOfSpeech::Noun || t2.pos == PartOfSpeech::Unknown)
{
if has_compound_suffix(&t1.text) || has_compound_suffix(&t2.text) {
let compound = format!("{} {}", t1.text, t2.text);
if !compounds.contains(&compound) {
compounds.push(compound);
}
}
}
}
compounds
}
fn has_compound_suffix(word: &str) -> bool {
word.ends_with("tion")
|| word.ends_with("ment")
|| word.ends_with("ing")
|| word.ends_with("ness")
|| word.ends_with("ity")
|| word.ends_with("ance")
|| word.ends_with("ence")
|| word.ends_with("er")
|| word.ends_with("or")
|| word.ends_with("ist")
|| word.ends_with("ism")
}
fn compound_positions(tokens: &[AnnotatedToken], compounds: &[String]) -> HashSet<usize> {
let mut positions = HashSet::new();
for compound in compounds {
let parts: Vec<&str> = compound.split_whitespace().collect();
if parts.len() < 2 {
continue;
}
for i in 0..tokens.len().saturating_sub(parts.len() - 1) {
let mut matches = true;
for (j, part) in parts.iter().enumerate() {
if tokens[i + j].text != *part {
matches = false;
break;
}
}
if matches {
for j in 0..parts.len() {
positions.insert(i + j);
}
}
}
}
positions
}
fn calculate_term_weight(word: &str, base_weight: f32) -> f32 {
let length_factor = if word.len() > 8 {
1.2
} else if word.len() > 5 {
1.1
} else {
1.0
};
let suffix_factor = if word.ends_with("tion")
|| word.ends_with("ment")
|| word.ends_with("ness")
|| word.ends_with("ity")
{
1.1
} else {
1.0
};
base_weight * length_factor * suffix_factor
}
fn is_negation(word: &str) -> bool {
const NEGATIONS: &[&str] = &[
"not",
"no",
"never",
"none",
"nothing",
"neither",
"nobody",
"nowhere",
"without",
"cannot",
"can't",
"won't",
"don't",
"doesn't",
"didn't",
"isn't",
"aren't",
"wasn't",
"weren't",
"hasn't",
"haven't",
"hadn't",
"shouldn't",
"wouldn't",
"couldn't",
"mustn't",
];
NEGATIONS.contains(&word)
}
fn is_noun(word: &str, position: usize, context: &[String]) -> bool {
const NOUN_INDICATORS: &[&str] = &[
"memory",
"graph",
"node",
"edge",
"entity",
"embedding",
"vector",
"index",
"query",
"retrieval",
"activation",
"potentiation",
"consolidation",
"decay",
"strength",
"weight",
"threshold",
"importance",
"robot",
"drone",
"sensor",
"lidar",
"camera",
"motor",
"actuator",
"obstacle",
"path",
"waypoint",
"location",
"coordinates",
"position",
"battery",
"power",
"energy",
"voltage",
"current",
"system",
"module",
"component",
"unit",
"device",
"temperature",
"pressure",
"humidity",
"speed",
"velocity",
"signal",
"communication",
"network",
"link",
"connection",
"navigation",
"guidance",
"control",
"steering",
"data",
"information",
"message",
"command",
"response",
"function",
"method",
"class",
"struct",
"interface",
"module",
"package",
"library",
"framework",
"api",
"endpoint",
"request",
"response",
"error",
"exception",
"bug",
"fix",
"feature",
"test",
"benchmark",
"performance",
"latency",
"throughput",
"cache",
"buffer",
"queue",
"stack",
"heap",
"thread",
"process",
"server",
"client",
"database",
"table",
"column",
"row",
"schema",
"migration",
"deployment",
"container",
"cluster",
"replica",
"person",
"people",
"user",
"agent",
"operator",
"time",
"date",
"day",
"hour",
"minute",
"second",
"area",
"zone",
"region",
"sector",
"space",
"task",
"mission",
"goal",
"objective",
"target",
"warning",
"alert",
"notification",
"level",
"status",
"state",
"condition",
"mode",
"type",
"kind",
"version",
"release",
"update",
"change",
"result",
"output",
"input",
"value",
"key",
"name",
"id",
"identifier",
];
if NOUN_INDICATORS.contains(&word) {
return true;
}
if word.ends_with("tion")
|| word.ends_with("sion")
|| word.ends_with("ment")
|| word.ends_with("ness")
|| word.ends_with("ity")
|| word.ends_with("ance")
|| word.ends_with("ence")
|| word.ends_with("er")
|| word.ends_with("or")
|| word.ends_with("ist")
|| word.ends_with("ism")
|| word.ends_with("age")
|| word.ends_with("ure")
|| word.ends_with("dom")
{
if !(word.ends_with("er") && word.len() < 5) {
return true;
}
}
if position > 0 {
if let Some(prev) = context.get(position - 1) {
let prev = prev.to_lowercase();
if prev == "a" || prev == "an" || prev == "the" || prev == "this" || prev == "that" {
return true;
}
}
}
if position > 0 {
if let Some(prev) = context.get(position - 1) {
if prev.ends_with("'s") || prev.ends_with("s'") {
return true;
}
}
}
false
}
fn is_adjective(word: &str) -> bool {
const ADJECTIVE_INDICATORS: &[&str] = &[
"red",
"blue",
"green",
"yellow",
"orange",
"purple",
"black",
"white",
"gray",
"grey",
"pink",
"brown",
"big",
"small",
"large",
"tiny",
"huge",
"massive",
"mini",
"micro",
"high",
"low",
"tall",
"short",
"long",
"wide",
"narrow",
"hot",
"cold",
"warm",
"cool",
"frozen",
"heated",
"fast",
"slow",
"quick",
"rapid",
"gradual",
"active",
"inactive",
"enabled",
"disabled",
"open",
"closed",
"locked",
"unlocked",
"full",
"empty",
"partial",
"complete",
"valid",
"invalid",
"correct",
"incorrect",
"true",
"false",
"good",
"bad",
"excellent",
"poor",
"optimal",
"suboptimal",
"normal",
"abnormal",
"stable",
"unstable",
"safe",
"unsafe",
"dangerous",
"hazardous",
"new",
"old",
"recent",
"ancient",
"current",
"latest",
"first",
"last",
"next",
"previous",
"primary",
"secondary",
"main",
"important",
"critical",
"minor",
"major",
"autonomous",
"manual",
"automatic",
"remote",
"digital",
"analog",
"electronic",
"mechanical",
"wireless",
"wired",
"connected",
"disconnected",
"local",
"global",
"private",
"public",
"static",
"dynamic",
"mutable",
"immutable",
"sync",
"async",
"concurrent",
"parallel",
"serial",
"sequential",
"optional",
"required",
"default",
"custom",
];
if ADJECTIVE_INDICATORS.contains(&word) {
return true;
}
if word.ends_with("ful")
|| word.ends_with("less")
|| word.ends_with("ous")
|| word.ends_with("ive")
|| word.ends_with("able")
|| word.ends_with("ible")
|| word.ends_with("al")
|| word.ends_with("ic")
|| word.ends_with("ary")
|| word.ends_with("ory")
{
let exceptions = ["animal", "interval", "arrival", "approval"];
if !exceptions.contains(&word) {
return true;
}
}
false
}
fn is_verb(word: &str) -> bool {
const VERB_INDICATORS: &[&str] = &[
"is",
"are",
"was",
"were",
"be",
"been",
"being",
"has",
"have",
"had",
"do",
"does",
"did",
"can",
"could",
"will",
"would",
"shall",
"should",
"may",
"might",
"must",
"go",
"goes",
"went",
"gone",
"going",
"get",
"gets",
"got",
"gotten",
"getting",
"make",
"makes",
"made",
"making",
"take",
"takes",
"took",
"taken",
"taking",
"see",
"sees",
"saw",
"seen",
"seeing",
"give",
"gives",
"gave",
"given",
"giving",
"use",
"uses",
"used",
"using",
"find",
"finds",
"found",
"finding",
"know",
"knows",
"knew",
"known",
"knowing",
"think",
"thinks",
"thought",
"thinking",
"want",
"wants",
"wanted",
"wanting",
"need",
"needs",
"needed",
"needing",
"try",
"tries",
"tried",
"trying",
"detect",
"detects",
"detected",
"detecting",
"observe",
"observes",
"observed",
"observing",
"measure",
"measures",
"measured",
"measuring",
"sense",
"senses",
"sensed",
"sensing",
"scan",
"scans",
"scanned",
"scanning",
"navigate",
"navigates",
"navigated",
"navigating",
"move",
"moves",
"moved",
"moving",
"stop",
"stops",
"stopped",
"stopping",
"start",
"starts",
"started",
"starting",
"reach",
"reaches",
"reached",
"reaching",
"avoid",
"avoids",
"avoided",
"avoiding",
"block",
"blocks",
"blocked",
"blocking",
"create",
"creates",
"created",
"creating",
"delete",
"deletes",
"deleted",
"deleting",
"update",
"updates",
"updated",
"updating",
"read",
"reads",
"reading",
"write",
"writes",
"wrote",
"written",
"writing",
"run",
"runs",
"ran",
"running",
"execute",
"executes",
"executed",
"executing",
"call",
"calls",
"called",
"calling",
"return",
"returns",
"returned",
"returning",
"store",
"stores",
"stored",
"storing",
"load",
"loads",
"loaded",
"loading",
"save",
"saves",
"saved",
"saving",
"fetch",
"fetches",
"fetched",
"fetching",
"send",
"sends",
"sent",
"sending",
"receive",
"receives",
"received",
"receiving",
"connect",
"connects",
"connected",
"connecting",
"disconnect",
"disconnects",
"disconnected",
"disconnecting",
"process",
"processes",
"processed",
"processing",
"handle",
"handles",
"handled",
"handling",
"parse",
"parses",
"parsed",
"parsing",
"compile",
"compiles",
"compiled",
"compiling",
"build",
"builds",
"built",
"building",
"test",
"tests",
"tested",
"testing",
"deploy",
"deploys",
"deployed",
"deploying",
"install",
"installs",
"installed",
"installing",
"configure",
"configures",
"configured",
"configuring",
"initialize",
"initializes",
"initialized",
"initializing",
"shutdown",
"shutdowns",
"terminate",
"terminates",
"terminated",
"terminating",
];
VERB_INDICATORS.contains(&word)
}
fn is_stop_word(word: &str) -> bool {
const STOP_WORDS: &[&str] = &[
"a",
"an",
"the",
"this",
"that",
"these",
"those",
"at",
"in",
"on",
"to",
"for",
"of",
"from",
"by",
"with",
"about",
"into",
"through",
"during",
"before",
"after",
"above",
"below",
"between",
"under",
"over",
"and",
"or",
"but",
"nor",
"so",
"yet",
"both",
"either",
"neither",
"i",
"you",
"he",
"she",
"it",
"we",
"they",
"me",
"him",
"her",
"us",
"them",
"my",
"your",
"his",
"its",
"our",
"their",
"mine",
"yours",
"hers",
"ours",
"theirs",
"who",
"whom",
"whose",
"which",
"what",
"whoever",
"whatever",
"whichever",
"that",
"which",
"who",
"whom",
"whose",
"how",
"when",
"where",
"why",
"just",
"only",
"even",
"also",
"too",
"very",
"really",
"quite",
"rather",
"almost",
"already",
"still",
"always",
"never",
"ever",
"often",
"sometimes",
"usually",
"perhaps",
"maybe",
"probably",
"possibly",
"certainly",
"definitely",
"actually",
"basically",
"essentially",
"simply",
"merely",
"as",
"if",
"then",
"than",
"because",
"although",
"though",
"unless",
"until",
"while",
"whereas",
"whether",
"since",
"some",
"any",
"all",
"each",
"every",
"many",
"much",
"more",
"most",
"few",
"less",
"least",
"other",
"another",
"such",
"same",
"different",
"own",
"several",
];
STOP_WORDS.contains(&word)
}
use crate::graph_memory::{EntityLabel, RelationType};
#[derive(Debug, Clone)]
pub struct OntologicalIntent {
pub expected_labels: Vec<EntityLabel>,
pub relation_types: Vec<RelationType>,
pub confidence: f32,
}
fn verb_stem_to_relation_types(stem: &str) -> Vec<RelationType> {
match stem {
"work" | "collabor" => vec![
RelationType::WorksWith,
RelationType::WorksAt,
RelationType::EmployedBy,
],
"employ" | "hire" => vec![RelationType::EmployedBy, RelationType::WorksAt],
"locat" | "live" | "base" | "resid" | "situat" => {
vec![RelationType::LocatedIn, RelationType::LocatedAt]
}
"learn" | "studi" | "discover" => vec![RelationType::Learned, RelationType::Knows],
"teach" | "mentor" | "instruct" => vec![RelationType::Teaches],
"know" | "familiar" => vec![RelationType::Knows],
"use" | "util" | "adopt" | "leverag" => vec![RelationType::Uses],
"read" | "consult" | "refer" => vec![RelationType::Uses],
"creat" | "build" | "develop" | "design" => {
vec![RelationType::CreatedBy, RelationType::DevelopedBy]
}
"caus" | "result" | "lead" => vec![RelationType::Causes, RelationType::ResultsIn],
"own" | "belong" | "possess" => vec![RelationType::OwnedBy, RelationType::PartOf],
"contain" | "includ" | "consist" => vec![RelationType::Contains, RelationType::PartOf],
"manag" | "supervis" | "oversee" | "direct" | "coordin" => vec![RelationType::Manages],
"assign" | "deleg" | "allocat" => vec![RelationType::AssignedTo],
"approv" | "reject" | "review" | "sign" => vec![RelationType::Approves],
"depend" | "requir" | "need" | "reliant" => {
vec![RelationType::DependsOn, RelationType::Requires]
}
"prefer" | "like" | "favor" | "favour" | "chose" | "choos" | "select" => {
vec![RelationType::Prefers]
}
"recommend" | "suggest" | "advis" | "propos" => vec![RelationType::Recommends],
"write" | "document" | "author" | "draft" | "compil" => {
vec![RelationType::Documents, RelationType::CreatedBy]
}
"implement" | "realiz" | "fulfill" => vec![RelationType::Implements],
"run" | "execut" | "launch" | "start" | "invok" => {
vec![RelationType::Uses, RelationType::Triggers]
}
"deploy" | "releas" | "ship" | "promot" => vec![RelationType::DeploysTo],
"monitor" | "track" | "observ" | "watch" | "alert" => vec![RelationType::Monitors],
"trigger" | "fire" | "emit" => vec![RelationType::Triggers],
"schedul" | "plan" | "arrang" | "organiz" => vec![RelationType::Manages],
"configur" | "tune" | "adjust" => vec![RelationType::Configures],
"migrat" | "upgrad" | "patch" | "updat" => vec![RelationType::SupersededBy],
"replac" | "supersed" | "obsolet" | "deprec" => vec![RelationType::SupersededBy],
"compar" | "differ" | "contrast" | "benchmark" => vec![RelationType::AlternativeTo],
"fix" | "solv" | "resolv" | "debug" | "repair" => vec![RelationType::ResultsIn],
"connect" | "integr" | "link" | "bridg" | "coupl" => {
vec![RelationType::DependsOn, RelationType::Uses]
}
_ => vec![],
}
}
fn question_word_to_labels(query_text: &str) -> Vec<EntityLabel> {
let lower = query_text.to_lowercase();
let trimmed = lower.trim_start();
if trimmed.starts_with("who ") || trimmed.starts_with("whom ") {
vec![
EntityLabel::Person,
EntityLabel::Organization,
EntityLabel::Team,
]
}
else if trimmed.starts_with("where ") {
vec![EntityLabel::Location, EntityLabel::Environment]
}
else if trimmed.starts_with("when ") {
vec![EntityLabel::Date, EntityLabel::Event]
}
else if trimmed.starts_with("how ") {
if trimmed.contains(" deploy")
|| trimmed.contains(" ci")
|| trimmed.contains(" cd")
|| trimmed.contains(" pipeline")
|| trimmed.contains(" build")
|| trimmed.contains(" releas")
{
vec![
EntityLabel::Pipeline,
EntityLabel::Technology,
EntityLabel::Configuration,
]
} else if trimmed.contains(" monitor")
|| trimmed.contains(" metric")
|| trimmed.contains(" alert")
|| trimmed.contains(" observ")
{
vec![EntityLabel::Metric, EntityLabel::Service]
} else if trimmed.contains(" configur")
|| trimmed.contains(" set up")
|| trimmed.contains(" setup")
{
vec![EntityLabel::Configuration, EntityLabel::Technology]
} else {
vec![EntityLabel::Concept, EntityLabel::Technology]
}
}
else if trimmed.starts_with("why ") {
vec![EntityLabel::Event, EntityLabel::Concept]
}
else if (trimmed.starts_with("what ") || trimmed.starts_with("which "))
&& (trimmed.contains(" tool")
|| trimmed.contains(" tech")
|| trimmed.contains(" framework")
|| trimmed.contains(" language")
|| trimmed.contains(" library")
|| trimmed.contains(" stack"))
{
vec![
EntityLabel::Technology,
EntityLabel::Product,
EntityLabel::Module,
]
}
else if (trimmed.starts_with("what ") || trimmed.starts_with("which "))
&& (trimmed.contains(" service")
|| trimmed.contains(" api")
|| trimmed.contains(" endpoint")
|| trimmed.contains(" microservice"))
{
vec![EntityLabel::Service, EntityLabel::Technology]
}
else if (trimmed.starts_with("what ") || trimmed.starts_with("which "))
&& (trimmed.contains(" project")
|| trimmed.contains(" repo")
|| trimmed.contains(" repositor")
|| trimmed.contains(" codebase"))
{
vec![EntityLabel::Project, EntityLabel::Repository]
}
else if (trimmed.starts_with("what ") || trimmed.starts_with("which "))
&& (trimmed.contains(" databas")
|| trimmed.contains(" db")
|| trimmed.contains(" store")
|| trimmed.contains(" cache"))
{
vec![EntityLabel::Database, EntityLabel::Technology]
}
else if (trimmed.starts_with("what ") || trimmed.starts_with("which "))
&& (trimmed.contains(" environ")
|| trimmed.contains(" stage")
|| trimmed.contains(" prod")
|| trimmed.contains(" cluster"))
{
vec![EntityLabel::Environment]
}
else if (trimmed.starts_with("what ") || trimmed.starts_with("which "))
&& (trimmed.contains(" task")
|| trimmed.contains(" ticket")
|| trimmed.contains(" issue")
|| trimmed.contains(" bug"))
{
vec![EntityLabel::Task]
}
else if (trimmed.starts_with("what ") || trimmed.starts_with("which "))
&& (trimmed.contains(" metric")
|| trimmed.contains(" slo")
|| trimmed.contains(" latenc")
|| trimmed.contains(" error rate"))
{
vec![EntityLabel::Metric]
}
else if (trimmed.starts_with("what ") || trimmed.starts_with("which "))
&& (trimmed.contains(" config")
|| trimmed.contains(" flag")
|| trimmed.contains(" setting")
|| trimmed.contains(" param"))
{
vec![EntityLabel::Configuration]
}
else if (trimmed.starts_with("what ") || trimmed.starts_with("which "))
&& (trimmed.contains(" doc")
|| trimmed.contains(" rfc")
|| trimmed.contains(" spec")
|| trimmed.contains(" readme")
|| trimmed.contains(" runbook"))
{
vec![EntityLabel::Document]
}
else if (trimmed.starts_with("what ") || trimmed.starts_with("which "))
&& (trimmed.contains(" skill")
|| trimmed.contains(" abilit")
|| trimmed.contains(" competenc"))
{
vec![EntityLabel::Skill]
}
else if (trimmed.starts_with("what ") || trimmed.starts_with("which "))
&& (trimmed.contains(" compan")
|| trimmed.contains(" organiz")
|| trimmed.contains(" team")
|| trimmed.contains(" squad"))
{
vec![EntityLabel::Organization, EntityLabel::Team]
}
else if (trimmed.starts_with("what ") || trimmed.starts_with("which "))
&& (trimmed.contains(" pipeline")
|| trimmed.contains(" workflow")
|| trimmed.contains(" ci")
|| trimmed.contains(" cd"))
{
vec![EntityLabel::Pipeline]
}
else if trimmed.starts_with("list ")
|| trimmed.starts_with("show ")
|| trimmed.starts_with("find ")
|| trimmed.starts_with("get ")
|| trimmed.starts_with("fetch ")
|| trimmed.starts_with("search ")
{
imperative_object_to_labels(trimmed)
}
else if trimmed.starts_with("compar")
|| trimmed.contains(" vs ")
|| trimmed.contains(" versus ")
|| trimmed.contains(" better than ")
|| trimmed.contains(" differ")
|| trimmed.contains(" alternative")
{
vec![EntityLabel::Technology, EntityLabel::Product]
} else {
vec![]
}
}
fn imperative_object_to_labels(query: &str) -> Vec<EntityLabel> {
if query.contains("service") || query.contains("api") || query.contains("endpoint") {
vec![EntityLabel::Service]
} else if query.contains("project") || query.contains("repo") {
vec![EntityLabel::Project, EntityLabel::Repository]
} else if query.contains("team") || query.contains("squad") || query.contains("group") {
vec![EntityLabel::Team, EntityLabel::Organization]
} else if query.contains("person") || query.contains("people") || query.contains("member") {
vec![EntityLabel::Person]
} else if query.contains("task") || query.contains("ticket") || query.contains("issue") {
vec![EntityLabel::Task]
} else if query.contains("config") || query.contains("flag") || query.contains("setting") {
vec![EntityLabel::Configuration]
} else if query.contains("doc") || query.contains("spec") || query.contains("rfc") {
vec![EntityLabel::Document]
} else if query.contains("pipeline") || query.contains("workflow") || query.contains("ci") {
vec![EntityLabel::Pipeline]
} else if query.contains("metric") || query.contains("slo") || query.contains("alert") {
vec![EntityLabel::Metric]
} else if query.contains("environ") || query.contains("cluster") || query.contains("stage") {
vec![EntityLabel::Environment]
} else if query.contains("databas") || query.contains("db") || query.contains("store") {
vec![EntityLabel::Database]
} else if query.contains("skill") || query.contains("competenc") {
vec![EntityLabel::Skill]
} else if query.contains("technolog") || query.contains("tool") || query.contains("framework") {
vec![EntityLabel::Technology]
} else if query.contains("module") || query.contains("package") || query.contains("crate") {
vec![EntityLabel::Module]
} else {
vec![]
}
}
pub fn infer_ontological_intent(query_text: &str, analysis: &QueryAnalysis) -> OntologicalIntent {
let mut confidence = 0.0_f32;
let expected_labels = question_word_to_labels(query_text);
if !expected_labels.is_empty() {
confidence += 0.4;
}
let mut relation_types = Vec::new();
let mut seen_relations = std::collections::HashSet::new();
for relation in &analysis.relational_context {
for rt in verb_stem_to_relation_types(&relation.stem) {
let key = rt.as_str().to_string();
if seen_relations.insert(key) {
relation_types.push(rt);
}
}
}
if !relation_types.is_empty() {
confidence += 0.4;
}
if !expected_labels.is_empty() && !relation_types.is_empty() {
confidence += 0.2;
}
OntologicalIntent {
expected_labels,
relation_types,
confidence: confidence.min(1.0),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_noun_detection() {
let query = "robot detected obstacle at coordinates";
let analysis = analyze_query(query);
let noun_texts: Vec<String> = analysis
.focal_entities
.iter()
.map(|e| e.text.clone())
.collect();
assert!(noun_texts.contains(&"robot".to_string()));
assert!(noun_texts.contains(&"obstacle".to_string()));
assert!(noun_texts.contains(&"coordinates".to_string()));
}
#[test]
fn test_adjective_detection() {
let query = "red large obstacle in path";
let analysis = analyze_query(query);
let adj_texts: Vec<String> = analysis
.discriminative_modifiers
.iter()
.map(|m| m.text.clone())
.collect();
assert!(adj_texts.contains(&"red".to_string()));
assert!(adj_texts.contains(&"large".to_string()));
}
#[test]
fn test_verb_detection() {
let query = "robot detected obstacle";
let analysis = analyze_query(query);
let verb_texts: Vec<String> = analysis
.relational_context
.iter()
.map(|r| r.text.clone())
.collect();
assert!(verb_texts.contains(&"detected".to_string()));
}
#[test]
fn test_information_content_weights() {
let query = "sensor detected red obstacle";
let analysis = analyze_query(query);
for entity in &analysis.focal_entities {
assert!(entity.ic_weight >= IC_NOUN * 0.9); }
for modifier in &analysis.discriminative_modifiers {
assert!(modifier.ic_weight >= IC_ADJECTIVE * 0.9);
}
for relation in &analysis.relational_context {
assert!(relation.ic_weight >= IC_VERB * 0.9);
}
}
#[test]
fn test_stemming() {
let query = "running detection algorithms";
let analysis = analyze_query(query);
let stems: Vec<String> = analysis
.focal_entities
.iter()
.map(|e| e.stem.clone())
.collect();
assert!(stems.iter().any(|s| s == "detect"));
assert!(stems.iter().any(|s| s == "algorithm"));
}
#[test]
fn test_compound_noun_detection() {
let query = "machine learning neural network";
let analysis = analyze_query(query);
assert!(analysis
.compound_nouns
.contains(&"machine learning".to_string()));
assert!(analysis
.compound_nouns
.contains(&"neural network".to_string()));
}
#[test]
fn test_negation_detection() {
let query = "not working correctly";
let analysis = analyze_query(query);
assert!(analysis.has_negation);
let negated_entities: Vec<&FocalEntity> = analysis
.focal_entities
.iter()
.filter(|e| e.negated)
.collect();
assert!(!negated_entities.is_empty());
}
#[test]
fn test_negation_scope() {
let query = "the sensor is not detecting obstacles properly";
let analysis = analyze_query(query);
assert!(analysis.has_negation);
let negated_verbs: Vec<&Relation> = analysis
.relational_context
.iter()
.filter(|r| r.negated)
.collect();
assert!(negated_verbs.iter().any(|r| r.text == "detecting"));
}
#[test]
fn test_all_stems_helper() {
let query = "fast robot detecting obstacles";
let analysis = analyze_query(query);
let stems = analysis.all_stems();
assert!(stems.contains("robot"));
assert!(stems.contains("fast"));
assert!(stems.contains("detect"));
assert!(stems.contains("obstacl")); }
#[test]
fn test_positive_and_negated_stems() {
let query = "working memory not failed";
let analysis = analyze_query(query);
let positive = analysis.positive_entity_stems();
let _negated = analysis.negated_entity_stems();
assert!(positive.iter().any(|s| s.contains("memori")));
}
#[test]
fn test_empty_query() {
let query = "";
let analysis = analyze_query(query);
assert!(analysis.focal_entities.is_empty());
assert!(analysis.discriminative_modifiers.is_empty());
assert!(analysis.relational_context.is_empty());
assert!(!analysis.has_negation);
}
#[test]
fn test_stop_words_filtered() {
let query = "the a an is are was were";
let analysis = analyze_query(query);
assert!(analysis.focal_entities.is_empty());
assert!(analysis.discriminative_modifiers.is_empty());
assert!(!analysis.relational_context.is_empty());
}
#[test]
fn test_total_weight_calculation() {
let query = "fast robot detecting red obstacles";
let analysis = analyze_query(query);
let weight = analysis.total_weight();
assert!(weight > 0.0);
}
#[test]
fn test_to_ic_weights() {
use crate::constants::{IC_ADJECTIVE, IC_NOUN, IC_VERB};
let query = "fast robot detecting obstacles";
let analysis = analyze_query(query);
let weights = analysis.to_ic_weights();
assert!(!weights.is_empty(), "Weights should not be empty");
let has_noun_weight = weights.values().any(|&w| (w - IC_NOUN).abs() < 0.01);
let has_adj_weight = weights.values().any(|&w| (w - IC_ADJECTIVE).abs() < 0.01);
let has_verb_weight = weights.values().any(|&w| (w - IC_VERB).abs() < 0.01);
assert!(
has_noun_weight || has_adj_weight || has_verb_weight,
"Should have at least one IC weight type. Weights: {:?}",
weights
);
}
#[test]
fn test_to_phrase_boosts() {
let query = "machine learning model for semantic search";
let analysis = analyze_query(query);
let phrases = analysis.to_phrase_boosts();
let has_ml = phrases.iter().any(|(p, _)| p == "machine learning");
let has_ss = phrases.iter().any(|(p, _)| p == "semantic search");
assert!(
has_ml || has_ss,
"Should detect 'machine learning' or 'semantic search' as phrase. Found: {:?}",
phrases
);
for (phrase, boost) in &phrases {
assert!(
*boost >= 1.0,
"Phrase '{}' should have boost >= 1.0, got {}",
phrase,
boost
);
}
}
#[test]
fn test_to_phrase_boosts_support_group() {
let query = "when did she go to the support group";
let analysis = analyze_query(query);
let phrases = analysis.to_phrase_boosts();
let has_support_group = phrases.iter().any(|(p, _)| p == "support group");
assert!(
has_support_group,
"Should detect 'support group' as phrase. Found: {:?}",
phrases
);
}
#[test]
fn test_analyze_temporal_specific_month() {
let ctx = analyze_temporal("what happened in March 2026");
assert_eq!(ctx.intent, TemporalIntent::SpecificTime);
assert!(ctx.is_filtering_query, "should be filtering query");
assert!(!ctx.is_seeking_query);
assert!(ctx.date_range.is_some());
let (start, end) = ctx.date_range.unwrap();
assert_eq!(start.month(), 3);
assert_eq!(start.day(), 1);
assert_eq!(end.month(), 3);
assert_eq!(end.day(), 31);
}
#[test]
fn test_analyze_temporal_when_question() {
let ctx = analyze_temporal("when did the deployment happen");
assert_eq!(ctx.intent, TemporalIntent::WhenQuestion);
assert!(ctx.is_seeking_query, "WhenQuestion should be seeking");
assert!(!ctx.is_filtering_query);
}
#[test]
fn test_analyze_temporal_yesterday() {
let ctx = analyze_temporal("what did we discuss yesterday");
assert_eq!(ctx.intent, TemporalIntent::SpecificTime);
assert!(ctx.is_filtering_query);
assert!(ctx.date_range.is_some());
}
#[test]
fn test_analyze_temporal_no_temporal() {
let ctx = analyze_temporal("tell me about the robot architecture");
assert_eq!(ctx.intent, TemporalIntent::None);
assert!(!ctx.is_filtering_query);
assert!(!ctx.is_seeking_query);
assert!(ctx.date_range.is_none());
}
#[test]
fn test_analyze_temporal_iso_date() {
let ctx = analyze_temporal("what happened on 2026-03-15");
assert_eq!(ctx.intent, TemporalIntent::SpecificTime);
assert!(ctx.is_filtering_query);
let (start, end) = ctx.date_range.unwrap();
assert_eq!(start, NaiveDate::from_ymd_opt(2026, 3, 14).unwrap());
assert_eq!(end, NaiveDate::from_ymd_opt(2026, 3, 16).unwrap());
}
#[test]
fn test_analyze_temporal_when_with_date() {
let ctx = analyze_temporal("when in March 2026 did the incident happen");
assert_eq!(ctx.intent, TemporalIntent::WhenQuestion);
assert!(ctx.is_seeking_query);
assert!(!ctx.is_filtering_query);
}
#[test]
fn test_analyze_temporal_duration() {
let ctx = analyze_temporal("how long ago did we fix the bug");
assert_eq!(ctx.intent, TemporalIntent::Duration);
assert!(ctx.is_seeking_query);
assert!(!ctx.is_filtering_query);
}
#[test]
fn test_compute_padded_date_range_empty() {
let extraction = TemporalExtraction::default();
assert!(compute_padded_date_range(&extraction).is_none());
}
}