use super::error::Result;
use serde_json::Value as JsonValue;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FieldType {
Keyword,
Text,
Long,
Double,
Boolean,
Date,
Ip,
Object,
Nested,
}
#[derive(Debug, Clone)]
pub struct FieldMapping {
pub field_type: FieldType,
pub subfields: HashMap<String, FieldType>,
}
#[derive(Debug, Clone)]
pub struct FieldMappings {
mappings: HashMap<String, FieldMapping>,
}
impl FieldMappings {
pub fn new() -> Self {
Self {
mappings: HashMap::new(),
}
}
pub fn from_opensearch_response(response: JsonValue) -> Result<Self> {
let mut mappings = HashMap::new();
let properties = if let Some(index_obj) = response.as_object() {
if let Some((_index_name, index_data)) = index_obj.iter().next() {
if let Some(mappings_obj) = index_data.get("mappings") {
mappings_obj.get("properties")
} else {
None
}
} else {
None
}
} else {
None
};
if let Some(properties) = properties {
if let Some(props_obj) = properties.as_object() {
for (field_name, field_def) in props_obj {
if let Some(mapping) = Self::parse_field_definition(field_def) {
mappings.insert(field_name.clone(), mapping);
}
}
}
}
Ok(Self { mappings })
}
pub fn from_properties(properties: HashMap<String, JsonValue>) -> Self {
let mut mappings = HashMap::new();
for (field_name, field_def) in properties {
if let Some(mapping) = Self::parse_field_definition(&field_def) {
mappings.insert(field_name, mapping);
}
}
Self { mappings }
}
fn parse_field_definition(field_def: &JsonValue) -> Option<FieldMapping> {
let field_type_str = field_def.get("type")?.as_str()?;
let field_type = Self::parse_field_type(field_type_str)?;
let mut subfields = HashMap::new();
if let Some(fields) = field_def.get("fields") {
if let Some(fields_obj) = fields.as_object() {
for (subfield_name, subfield_def) in fields_obj {
if let Some(subfield_type_str) =
subfield_def.get("type").and_then(|v| v.as_str())
{
if let Some(subfield_type) = Self::parse_field_type(subfield_type_str) {
subfields.insert(subfield_name.clone(), subfield_type);
}
}
}
}
}
Some(FieldMapping {
field_type,
subfields,
})
}
fn parse_field_type(type_str: &str) -> Option<FieldType> {
match type_str {
"keyword" => Some(FieldType::Keyword),
"text" => Some(FieldType::Text),
"long" | "integer" | "short" | "byte" => Some(FieldType::Long),
"double" | "float" | "half_float" | "scaled_float" => Some(FieldType::Double),
"boolean" => Some(FieldType::Boolean),
"date" => Some(FieldType::Date),
"ip" => Some(FieldType::Ip),
"object" => Some(FieldType::Object),
"nested" => Some(FieldType::Nested),
_ => None, }
}
pub fn get_query_field(&self, field: &str, operator: &str) -> String {
if let Some(mapping) = self.mappings.get(field) {
if matches!(
operator,
"eq" | "ne" | "in" | "contains" | "startswith" | "endswith" | "matches"
) && mapping.field_type == FieldType::Text
&& mapping.subfields.contains_key("keyword")
{
return format!("{}.keyword", field);
}
}
field.to_string()
}
pub fn should_use_term_query(&self, field: &str) -> bool {
if let Some(mapping) = self.mappings.get(field) {
matches!(
mapping.field_type,
FieldType::Keyword
| FieldType::Long
| FieldType::Double
| FieldType::Boolean
| FieldType::Date
| FieldType::Ip
)
} else {
true
}
}
pub fn get_field_type(&self, field: &str) -> Option<&FieldType> {
self.mappings.get(field).map(|m| &m.field_type)
}
pub fn add_mapping(&mut self, field: String, mapping: FieldMapping) {
self.mappings.insert(field, mapping);
}
pub fn len(&self) -> usize {
self.mappings.len()
}
pub fn is_empty(&self) -> bool {
self.mappings.is_empty()
}
}
impl Default for FieldMappings {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_mappings() {
let mappings = FieldMappings::new();
assert_eq!(mappings.get_query_field("test", "eq"), "test");
assert!(mappings.should_use_term_query("test"));
}
#[test]
fn test_text_field_with_keyword() {
let mut mappings = FieldMappings::new();
let mut subfields = HashMap::new();
subfields.insert("keyword".to_string(), FieldType::Keyword);
mappings.add_mapping(
"message".to_string(),
FieldMapping {
field_type: FieldType::Text,
subfields,
},
);
assert_eq!(mappings.get_query_field("message", "eq"), "message.keyword");
assert_eq!(mappings.get_query_field("message", "contains"), "message");
}
#[test]
fn test_keyword_field() {
let mut mappings = FieldMappings::new();
mappings.add_mapping(
"status".to_string(),
FieldMapping {
field_type: FieldType::Keyword,
subfields: HashMap::new(),
},
);
assert_eq!(mappings.get_query_field("status", "eq"), "status");
assert!(mappings.should_use_term_query("status"));
}
}