use super::*;
use crate::error::{DbError, DbResult};
use crate::storage::geo::{haversine_distance, GeoIndex, GeoIndexStats, GeoPoint};
use serde_json::Value;
impl Collection {
pub fn get_all_geo_indexes(&self) -> Vec<GeoIndex> {
self.index_meta()
.expect("Column family should exist")
.geo
.clone()
}
pub(crate) fn get_geo_index(&self, name: &str) -> Option<GeoIndex> {
self.index_meta()?
.geo
.iter()
.find(|i| i.name == name)
.cloned()
}
pub fn create_geo_index(&self, name: String, field: String) -> DbResult<GeoIndexStats> {
if self.get_geo_index(&name).is_some() {
return Err(DbError::InvalidDocument(format!(
"Geo Index '{}' already exists",
name
)));
}
let index = GeoIndex {
name: name.clone(),
field: field.clone(),
precision: 6,
};
let index_bytes = serde_json::to_vec(&index)?;
{
let db = &self.db;
let cf = db
.cf_handle(&self.name)
.expect("Column family should exist");
db.put_cf(&cf, Self::geo_meta_key(&name), &index_bytes)
.map_err(|e| {
DbError::InternalError(format!("Failed to create geo index: {}", e))
})?;
}
self.invalidate_index_meta();
let docs = self.all();
let db = &self.db;
let cf = db
.cf_handle(&self.name)
.expect("Column family should exist");
let mut count = 0;
for doc in &docs {
let doc_value = doc.to_value();
if let Some(val) =
crate::storage::index::extract_field_value(&doc_value, &field).as_object()
{
if val.contains_key("lat") || val.contains_key("latitude") {
let entry_key = Self::geo_entry_key(&name, &doc.key);
let geo_data = serde_json::to_vec(&doc_value[&field])?;
db.put_cf(&cf, entry_key, &geo_data).map_err(|e| {
DbError::InternalError(format!("Failed to build geo index: {}", e))
})?;
count += 1;
}
}
}
Ok(GeoIndexStats {
name,
field,
precision: 6,
indexed_documents: count,
geohash_buckets: 0,
})
}
pub fn drop_geo_index(&self, name: &str) -> DbResult<()> {
if self.get_geo_index(name).is_none() {
return Err(DbError::InvalidDocument(format!(
"Geo Index '{}' not found",
name
)));
}
let db = &self.db;
let cf = db
.cf_handle(&self.name)
.expect("Column family should exist");
db.delete_cf(&cf, Self::geo_meta_key(name))
.map_err(|e| DbError::InternalError(format!("Failed to drop geo index: {}", e)))?;
self.invalidate_index_meta();
let prefix = format!("{}{}:", GEO_PREFIX, name);
let iter = db.prefix_iterator_cf(&cf, prefix.as_bytes());
for result in iter.flatten() {
let (key, _) = result;
if key.starts_with(prefix.as_bytes()) {
db.delete_cf(&cf, &key).map_err(|e| {
DbError::InternalError(format!("Failed to drop geo index entry: {}", e))
})?;
} else {
break;
}
}
Ok(())
}
pub fn list_geo_indexes(&self) -> Vec<GeoIndexStats> {
self.get_all_geo_indexes()
.iter()
.map(|idx| {
let db = &self.db;
let cf = db
.cf_handle(&self.name)
.expect("Column family should exist");
let prefix = format!("{}{}:", GEO_PREFIX, idx.name);
let count = db
.prefix_iterator_cf(&cf, prefix.as_bytes())
.take_while(|r| {
r.as_ref()
.is_ok_and(|(k, _)| k.starts_with(prefix.as_bytes()))
})
.count();
GeoIndexStats {
name: idx.name.clone(),
field: idx.field.clone(),
precision: idx.precision,
indexed_documents: count,
geohash_buckets: 0,
}
})
.collect()
}
pub fn geo_near(
&self,
field: &str,
lat: f64,
lon: f64,
limit: usize,
) -> Option<Vec<(Document, f64)>> {
let indexes = self.get_all_geo_indexes();
let index = indexes.iter().find(|idx| idx.field == field)?;
let db = &self.db;
let cf = db
.cf_handle(&self.name)
.expect("Column family should exist");
let prefix = format!("{}{}:", GEO_PREFIX, index.name);
let iter = db.prefix_iterator_cf(&cf, prefix.as_bytes());
let mut matches = Vec::new();
for result in iter.flatten() {
let (key, value) = result;
if !key.starts_with(prefix.as_bytes()) {
break;
}
if let Ok(point_val) = serde_json::from_slice::<Value>(&value) {
if let Some(target) = GeoPoint::from_value(&point_val) {
let dist = haversine_distance(&GeoPoint::new(lat, lon), &target);
let key_str = String::from_utf8_lossy(&key);
let doc_key = key_str.strip_prefix(&prefix).unwrap_or("");
if !doc_key.is_empty() {
matches.push((doc_key.to_string(), dist));
}
}
}
}
matches.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
matches.truncate(limit);
let doc_keys: Vec<String> = matches.iter().map(|(k, _)| k.clone()).collect();
let docs = self.get_many(&doc_keys);
let mut results = Vec::new();
for (key, dist) in matches {
if let Some(doc) = docs.iter().find(|d| d.key == key) {
results.push((doc.clone(), dist));
}
}
Some(results)
}
pub fn geo_within(
&self,
field: &str,
lat: f64,
lon: f64,
radius: f64,
) -> Option<Vec<(Document, f64)>> {
let indexes = self.get_all_geo_indexes();
let index = indexes.iter().find(|idx| idx.field == field)?;
let db = &self.db;
let cf = db
.cf_handle(&self.name)
.expect("Column family should exist");
let prefix = format!("{}{}:", GEO_PREFIX, index.name);
let iter = db.prefix_iterator_cf(&cf, prefix.as_bytes());
let mut matches = Vec::new();
for result in iter.flatten() {
let (key, value) = result;
if !key.starts_with(prefix.as_bytes()) {
break;
}
if let Ok(point_val) = serde_json::from_slice::<Value>(&value) {
if let Some(target) = GeoPoint::from_value(&point_val) {
let dist = haversine_distance(&GeoPoint::new(lat, lon), &target);
if dist <= radius {
let key_str = String::from_utf8_lossy(&key);
let doc_key = key_str.strip_prefix(&prefix).unwrap_or("");
if !doc_key.is_empty() {
matches.push((doc_key.to_string(), dist));
}
}
}
}
}
let doc_keys: Vec<String> = matches.iter().map(|(k, _)| k.clone()).collect();
let docs = self.get_many(&doc_keys);
let mut results = Vec::new();
for (key, dist) in matches {
if let Some(doc) = docs.iter().find(|d| d.key == key) {
results.push((doc.clone(), dist));
}
}
Some(results)
}
}