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>,
}
const WHOLE_VALUE_OPERATORS: &[&str] = &[
"contains",
"contains_cs",
"not_contains",
"not_contains_cs",
"startswith",
"startswith_cs",
"not_startswith",
"not_startswith_cs",
"endswith",
"endswith_cs",
"not_endswith",
"not_endswith_cs",
"matches",
"not_matches",
"regexp",
"not_regexp",
"regex",
"not_regex",
];
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) {
let wants_unanalyzed = matches!(operator, "eq" | "ne" | "in" | "not_in")
|| WHOLE_VALUE_OPERATORS.contains(&operator);
if wants_unanalyzed
&& 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.keyword"
);
assert_eq!(
mappings.get_query_field("message", "matches"),
"message.keyword"
);
}
#[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"));
}
}