use std::cmp::{max, min};
use std::path::PathBuf;
use crate::common::condition_checker::{CheckItem, ConditionChecker, Partitioner, Rest, Select};
use crate::common::counter::hardware_counter::HardwareCounterCell;
use crate::common::types::PointOffsetType;
use crate::common::universal_io::UserData;
use super::GEO_QUERY_MAX_REGION;
use crate::segment::common::operation_error::{OperationError, OperationResult};
use crate::segment::index::field_index::geo_hash::{
GeoHash, circle_hashes, common_hash_prefix, geo_hash_to_box, polygon_hashes,
polygon_hashes_estimation, rectangle_hashes,
};
use crate::segment::index::field_index::stat_tools::estimate_multi_value_selection_cardinality;
use crate::segment::index::field_index::{CardinalityEstimation, PayloadBlockCondition, PrimaryCondition};
use crate::segment::index::payload_config::StorageType;
use crate::segment::telemetry::PayloadIndexTelemetry;
use crate::segment::types::{CheckGeoPoint, FieldCondition, GeoPoint, PayloadKeyType};
pub trait GeoIndexRead {
fn points_count(&self) -> usize;
fn points_values_count(&self) -> usize;
fn max_values_per_point(&self) -> usize;
fn points_of_hash(
&self,
hash: GeoHash,
hw_counter: &HardwareCounterCell,
) -> OperationResult<usize>;
fn values_of_hash(
&self,
hash: GeoHash,
hw_counter: &HardwareCounterCell,
) -> OperationResult<usize>;
fn check_values_any(
&self,
idx: PointOffsetType,
hw_counter: &HardwareCounterCell,
check_fn: &dyn Fn(&GeoPoint) -> bool,
) -> OperationResult<bool>;
fn for_each_matching_value<I, F, M, U>(
&self,
items: I,
hw_counter: &HardwareCounterCell,
check_fn: F,
mut on_match: M,
) -> OperationResult<()>
where
U: UserData,
I: Iterator<Item = (U, PointOffsetType)>,
F: Fn(&GeoPoint) -> bool,
M: FnMut(U, bool),
{
for (tag, idx) in items {
on_match(tag, self.check_values_any(idx, hw_counter, &check_fn)?);
}
Ok(())
}
fn values_count(&self, idx: PointOffsetType) -> usize;
fn get_values(&self, idx: PointOffsetType) -> Option<Box<dyn Iterator<Item = GeoPoint> + '_>>;
fn iterator(
&self,
values: Vec<GeoHash>,
) -> OperationResult<Box<dyn Iterator<Item = PointOffsetType> + '_>>;
fn points_per_hash_filtered(
&self,
filter: &dyn Fn(&(GeoHash, usize)) -> bool,
) -> OperationResult<Vec<(GeoHash, usize)>>;
fn get_storage_type(&self) -> StorageType;
fn ram_usage_bytes(&self) -> usize;
fn is_on_disk(&self) -> bool;
fn populate(&self) -> OperationResult<()>;
fn clear_cache(&self) -> OperationResult<()>;
fn files(&self) -> Vec<PathBuf>;
fn immutable_files(&self) -> Vec<PathBuf>;
fn telemetry_index_type(&self) -> &'static str;
fn values_is_empty(&self, idx: PointOffsetType) -> bool {
self.values_count(idx) == 0
}
fn match_cardinality(
&self,
values: &[GeoHash],
hw_counter: &HardwareCounterCell,
) -> OperationResult<CardinalityEstimation> {
let max_values_per_point = self.max_values_per_point();
if max_values_per_point == 0 {
return Ok(CardinalityEstimation::exact(0));
}
let Some(common_hash) = common_hash_prefix(values) else {
return Ok(CardinalityEstimation::exact(0));
};
let total_points = self.points_of_hash(common_hash, hw_counter)?;
let total_values = self.values_of_hash(common_hash, hw_counter)?;
let (sum, maximum_per_hash) = values
.iter()
.map(|®ion| self.points_of_hash(region, hw_counter))
.try_fold((0, 0), |(sum, maximum), count| {
let count = count?;
OperationResult::Ok((sum + count, max(maximum, count)))
})?;
let min_hits_by_value_groups = sum / max_values_per_point;
let point_duplications = total_values - total_points;
let possible_non_duplicated = sum.saturating_sub(point_duplications);
let estimation_min = max(
max(min_hits_by_value_groups, possible_non_duplicated),
maximum_per_hash,
);
let estimation_max = min(sum, total_points);
let estimation_exp =
estimate_multi_value_selection_cardinality(total_points, total_values, sum).round()
as usize;
Ok(CardinalityEstimation {
primary_clauses: vec![],
min: estimation_min,
exp: min(estimation_max, max(estimation_min, estimation_exp)),
max: estimation_max,
})
}
fn large_hashes(
&self,
threshold: usize,
) -> OperationResult<Box<dyn Iterator<Item = (GeoHash, usize)>>> {
let filter = |pair: &(GeoHash, usize)| pair.1 > threshold && !pair.0.is_empty();
let mut large_regions = self.points_per_hash_filtered(&filter)?;
large_regions.sort_by(|a, b| b.cmp(a));
let mut edge_region = vec![];
let mut current_region = GeoHash::default();
for (region, size) in large_regions {
if !current_region.starts_with(region) {
current_region = region;
edge_region.push((region, size));
}
}
Ok(Box::new(edge_region.into_iter()))
}
fn get_telemetry_data(&self) -> PayloadIndexTelemetry {
PayloadIndexTelemetry {
field_name: None,
points_count: self.points_count(),
points_values_count: self.points_values_count(),
histogram_bucket_size: None,
index_type: self.telemetry_index_type(),
}
}
}
pub(super) fn filter<'a, G: GeoIndexRead + ?Sized>(
geo: &'a G,
condition: &FieldCondition,
hw_counter: &'a HardwareCounterCell,
) -> OperationResult<Option<Box<dyn Iterator<Item = PointOffsetType> + 'a>>> {
if let Some(geo_bounding_box) = &condition.geo_bounding_box {
let geo_hashes = rectangle_hashes(geo_bounding_box, GEO_QUERY_MAX_REGION)?;
let geo_condition_copy = *geo_bounding_box;
return Ok(Some(Box::new(geo.iterator(geo_hashes)?.filter(
move |&point| {
geo.check_values_any(point, hw_counter, &|geo_point| {
geo_condition_copy.check_point(geo_point)
})
.unwrap_or(false) },
))));
}
if let Some(geo_radius) = &condition.geo_radius {
let geo_hashes = circle_hashes(geo_radius, GEO_QUERY_MAX_REGION)?;
let geo_condition_copy = *geo_radius;
return Ok(Some(Box::new(geo.iterator(geo_hashes)?.filter(
move |&point| {
geo.check_values_any(point, hw_counter, &|geo_point| {
geo_condition_copy.check_point(geo_point)
})
.unwrap_or(false) },
))));
}
if let Some(geo_polygon) = &condition.geo_polygon {
let geo_hashes = polygon_hashes(geo_polygon, GEO_QUERY_MAX_REGION)?;
let geo_condition_copy = geo_polygon.convert();
return Ok(Some(Box::new(geo.iterator(geo_hashes)?.filter(
move |&point| {
geo.check_values_any(point, hw_counter, &|geo_point| {
geo_condition_copy.check_point(geo_point)
})
.unwrap_or(false) },
))));
}
Ok(None)
}
pub(super) fn estimate_cardinality<G: GeoIndexRead + ?Sized>(
geo: &G,
condition: &FieldCondition,
hw_counter: &HardwareCounterCell,
) -> OperationResult<Option<CardinalityEstimation>> {
if let Some(geo_bounding_box) = &condition.geo_bounding_box {
let Some(geo_hashes) = rectangle_hashes(geo_bounding_box, GEO_QUERY_MAX_REGION).ok() else {
return Ok(None);
};
let mut estimation = geo.match_cardinality(&geo_hashes, hw_counter)?;
estimation
.primary_clauses
.push(PrimaryCondition::Condition(Box::new(condition.clone())));
return Ok(Some(estimation));
}
if let Some(geo_radius) = &condition.geo_radius {
let Some(geo_hashes) = circle_hashes(geo_radius, GEO_QUERY_MAX_REGION).ok() else {
return Ok(None);
};
let mut estimation = geo.match_cardinality(&geo_hashes, hw_counter)?;
estimation
.primary_clauses
.push(PrimaryCondition::Condition(Box::new(condition.clone())));
return Ok(Some(estimation));
}
if let Some(geo_polygon) = &condition.geo_polygon {
let (exterior_hashes, interior_hashes) =
polygon_hashes_estimation(geo_polygon, GEO_QUERY_MAX_REGION);
let mut exterior_estimation = geo.match_cardinality(&exterior_hashes, hw_counter)?;
for interior in &interior_hashes {
let interior_estimation = geo.match_cardinality(interior, hw_counter)?;
exterior_estimation.min = exterior_estimation
.min
.saturating_sub(interior_estimation.max);
exterior_estimation.max = max(
exterior_estimation.min,
exterior_estimation
.max
.saturating_sub(interior_estimation.min),
);
exterior_estimation.exp = max(
exterior_estimation
.exp
.saturating_sub(interior_estimation.exp),
exterior_estimation.min,
);
}
exterior_estimation
.primary_clauses
.push(PrimaryCondition::Condition(Box::new(condition.clone())));
return Ok(Some(exterior_estimation));
}
Ok(None)
}
pub(super) fn for_each_payload_block<G: GeoIndexRead + ?Sized>(
geo: &G,
threshold: usize,
key: PayloadKeyType,
f: &mut dyn FnMut(PayloadBlockCondition) -> OperationResult<()>,
) -> OperationResult<()> {
geo.large_hashes(threshold)?
.try_for_each(|(geo_hash, size)| {
f(PayloadBlockCondition {
condition: FieldCondition::new_geo_bounding_box(
key.clone(),
geo_hash_to_box(geo_hash),
),
cardinality: size,
})
})
}
pub struct GeoConditionChecker<'a, G: ?Sized, F> {
geo: &'a G,
hw_counter: HardwareCounterCell,
filter: F,
}
impl<'a, G: ?Sized, F> GeoConditionChecker<'a, G, F> {
pub(super) fn new(geo: &'a G, hw_counter: HardwareCounterCell, filter: F) -> Self {
Self {
geo,
hw_counter,
filter,
}
}
}
impl<G, P> ConditionChecker for GeoConditionChecker<'_, G, P>
where
G: GeoIndexRead + ?Sized,
P: CheckGeoPoint,
{
type Error = OperationError;
fn check(&self, point_id: PointOffsetType) -> OperationResult<bool> {
self.geo
.check_values_any(point_id, &self.hw_counter, &|value| {
self.filter.check_point(value)
})
}
fn check_batched<K: CheckItem>(
&self,
ids: &mut [K],
select: Select,
_rest: Rest,
) -> OperationResult<usize> {
let p = Partitioner::new(ids);
self.geo.for_each_matching_value(
p.iter().map(|item| (item, item.point_id())),
&self.hw_counter,
|value| self.filter.check_point(value),
|item, matched| p.write(item, matched == select.is_match()),
)?;
Ok(p.finish())
}
}