use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchDocument {
pub id: String,
pub content: String,
pub title: Option<String>,
pub fields: HashMap<String, serde_json::Value>,
pub metadata: DocumentMetadata,
pub timestamp: DateTime<Utc>,
pub tags: Vec<String>,
pub language: Option<String>,
pub boost: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentMetadata {
pub content_type: Option<String>,
pub file_size: Option<u64>,
pub source: Option<String>,
pub author: Option<String>,
pub category: Option<String>,
pub keywords: Vec<String>,
pub permissions: Vec<String>,
pub attributes: HashMap<String, String>,
}
impl Default for DocumentMetadata {
fn default() -> Self {
Self {
content_type: None,
file_size: None,
source: None,
author: None,
category: None,
keywords: Vec::new(),
permissions: Vec::new(),
attributes: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchQuery {
pub text: Option<String>,
pub fields: Vec<FieldQuery>,
pub filters: Vec<Filter>,
pub sort: Vec<SortField>,
pub pagination: Pagination,
pub options: SearchOptions,
pub facets: Vec<FacetConfig>,
pub aggregations: Vec<Aggregation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldQuery {
pub field: String,
pub query: String,
pub operator: QueryOperator,
pub boost: Option<f32>,
pub fuzziness: Option<f32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum QueryOperator {
And,
Or,
Not,
Phrase,
Prefix,
Wildcard,
Fuzzy,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Filter {
pub field: String,
pub value: FilterValue,
pub operator: FilterOperator,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FilterValue {
String(String),
Number(f64),
Boolean(bool),
Array(Vec<serde_json::Value>),
Range(FilterRange),
Exists(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FilterRange {
pub gte: Option<f64>,
pub gt: Option<f64>,
pub lte: Option<f64>,
pub lt: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FilterOperator {
Equals,
NotEquals,
In,
NotIn,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
Exists,
NotExists,
Contains,
StartsWith,
EndsWith,
GeoWithin,
GeoDistance,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SortField {
pub field: String,
pub order: SortOrder,
pub mode: Option<SortMode>,
pub unit: Option<String>,
pub location: Option<GeoPoint>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SortOrder {
Asc,
Desc,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SortMode {
Min,
Max,
Sum,
Avg,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeoPoint {
pub lat: f64,
pub lon: f64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Pagination {
pub page: u32,
pub size: u32,
pub offset: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchOptions {
pub fuzzy: bool,
pub fuzziness: f32,
pub minimum_should_match: String,
pub highlight: bool,
pub highlight_config: Option<HighlightConfig>,
pub snippets: bool,
pub snippet_length: usize,
pub suggestions: bool,
pub suggestion_limit: usize,
pub boost_query: Option<Box<SearchQuery>>,
pub timeout_ms: Option<u64>,
pub track_total_hits: bool,
pub explain: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HighlightConfig {
pub pre_tag: String,
pub post_tag: String,
pub fields: Vec<String>,
pub fragment_size: usize,
pub number_of_fragments: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FacetConfig {
pub field: String,
pub facet_type: FacetType,
pub size: u32,
pub filter: Option<Box<Filter>>,
pub sort: Option<FacetSort>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FacetType {
Terms,
Range,
DateRange,
Histogram,
Stats,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FacetSort {
Count,
Key,
Alpha,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Aggregation {
pub name: String,
pub agg_type: AggregationType,
pub field: String,
pub params: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AggregationType {
Terms,
DateHistogram,
Histogram,
Stats,
ExtendedStats,
Min,
Max,
Sum,
Avg,
Cardinality,
Percentiles,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResults {
pub total_hits: u64,
pub max_score: Option<f64>,
pub hits: Vec<SearchHit>,
pub aggregations: HashMap<String, serde_json::Value>,
pub facets: HashMap<String, FacetResult>,
pub suggestions: Vec<Suggestion>,
pub metadata: SearchMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchHit {
pub document: SearchDocument,
pub score: Option<f64>,
pub highlights: HashMap<String, Vec<String>>,
pub snippets: HashMap<String, String>,
pub sort_values: Vec<serde_json::Value>,
pub matched_queries: Vec<String>,
pub inner_hits: HashMap<String, Vec<SearchHit>>,
pub explanation: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FacetResult {
pub buckets: Vec<FacetBucket>,
pub metadata: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FacetBucket {
pub key: serde_json::Value,
pub count: u64,
pub sub_aggregations: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Suggestion {
pub text: String,
pub suggestion_type: SuggestionType,
pub score: f64,
pub info: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SuggestionType {
Term,
Phrase,
Completion,
DidYouMean,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchMetadata {
pub query_time_ms: u64,
pub total_shards: u32,
pub successful_shards: u32,
pub failed_shards: u32,
pub scroll_id: Option<String>,
pub backend_metadata: HashMap<String, serde_json::Value>,
}
impl SearchDocument {
pub fn builder() -> SearchDocumentBuilder {
SearchDocumentBuilder::new()
}
pub fn get_field<T>(&self, field: &str) -> Option<T>
where
T: for<'de> Deserialize<'de>,
{
self.fields.get(field)
.and_then(|v| serde_json::from_value(v.clone()).ok())
}
pub fn has_field(&self, field: &str) -> bool {
self.fields.contains_key(field)
}
pub fn field_names(&self) -> Vec<String> {
self.fields.keys().cloned().collect()
}
}
pub struct SearchDocumentBuilder {
document: SearchDocument,
}
impl SearchDocumentBuilder {
pub fn new() -> Self {
Self {
document: SearchDocument {
id: Uuid::new_v4().to_string(),
content: String::new(),
title: None,
fields: HashMap::new(),
metadata: DocumentMetadata::default(),
timestamp: Utc::now(),
tags: Vec::new(),
language: None,
boost: 1.0,
},
}
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.document.id = id.into();
self
}
pub fn content(mut self, content: impl Into<String>) -> Self {
self.document.content = content.into();
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.document.title = Some(title.into());
self
}
pub fn field<T: Serialize>(mut self, name: impl Into<String>, value: T) -> Result<Self, serde_json::Error> {
let json_value = serde_json::to_value(value)?;
self.document.fields.insert(name.into(), json_value);
Ok(self)
}
pub fn json_field(mut self, name: impl Into<String>, value: serde_json::Value) -> Self {
self.document.fields.insert(name.into(), value);
self
}
pub fn metadata(mut self, metadata: DocumentMetadata) -> Self {
self.document.metadata = metadata;
self
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.document.tags.push(tag.into());
self
}
pub fn tags(mut self, tags: Vec<String>) -> Self {
self.document.tags.extend(tags);
self
}
pub fn language(mut self, language: impl Into<String>) -> Self {
self.document.language = Some(language.into());
self
}
pub fn boost(mut self, boost: f32) -> Self {
self.document.boost = boost;
self
}
pub fn build(self) -> SearchDocument {
self.document
}
}
impl Default for SearchDocumentBuilder {
fn default() -> Self {
Self::new()
}
}
impl SearchQuery {
pub fn builder() -> SearchQueryBuilder {
SearchQueryBuilder::new()
}
}
pub struct SearchQueryBuilder {
query: SearchQuery,
}
impl SearchQueryBuilder {
pub fn new() -> Self {
Self {
query: SearchQuery {
text: None,
fields: Vec::new(),
filters: Vec::new(),
sort: Vec::new(),
pagination: Pagination {
page: 1,
size: 20,
offset: None,
},
options: SearchOptions {
fuzzy: false,
fuzziness: 0.7,
minimum_should_match: "75%".to_string(),
highlight: true,
highlight_config: None,
snippets: true,
snippet_length: 200,
suggestions: false,
suggestion_limit: 10,
boost_query: None,
timeout_ms: None,
track_total_hits: true,
explain: false,
},
facets: Vec::new(),
aggregations: Vec::new(),
},
}
}
pub fn text(mut self, text: impl Into<String>) -> Self {
self.query.text = Some(text.into());
self
}
pub fn field_query(mut self, field: impl Into<String>, query: impl Into<String>) -> Self {
self.query.fields.push(FieldQuery {
field: field.into(),
query: query.into(),
operator: QueryOperator::And,
boost: None,
fuzziness: None,
});
self
}
pub fn filter(mut self, filter: Filter) -> Self {
self.query.filters.push(filter);
self
}
pub fn field_filter(mut self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
self.query.filters.push(Filter {
field: field.into(),
value: value.into(),
operator: FilterOperator::Equals,
});
self
}
pub fn sort(mut self, field: impl Into<String>, order: SortOrder) -> Self {
self.query.sort.push(SortField {
field: field.into(),
order,
mode: None,
unit: None,
location: None,
});
self
}
pub fn pagination(mut self, page: u32, size: u32) -> Self {
self.query.pagination = Pagination {
page,
size,
offset: None,
};
self
}
pub fn limit(mut self, size: u32) -> Self {
self.query.pagination.size = size;
self
}
pub fn fuzzy(mut self, enabled: bool) -> Self {
self.query.options.fuzzy = enabled;
self
}
pub fn fuzziness(mut self, fuzziness: f32) -> Self {
self.query.options.fuzziness = fuzziness;
self
}
pub fn highlight(mut self, enabled: bool) -> Self {
self.query.options.highlight = enabled;
self
}
pub fn facet(mut self, facet: FacetConfig) -> Self {
self.query.facets.push(facet);
self
}
pub fn aggregation(mut self, aggregation: Aggregation) -> Self {
self.query.aggregations.push(aggregation);
self
}
pub fn build(self) -> SearchQuery {
self.query
}
}
impl Default for SearchQueryBuilder {
fn default() -> Self {
Self::new()
}
}