mirror of
https://github.com/qdrant/qdrant.git
synced 2026-09-21 13:37:46 -05:00
Separate GeoHash/GeoHashRaw (#8618)
* GeoHash: convert to tuple struct for consistency * GeoHash: clarify code * Separate `GeoHash`/`GeoHashRaw`
This commit is contained in:
@@ -14,7 +14,7 @@ use crate::types::{GeoBoundingBox, GeoPoint, GeoPolygon, GeoRadius};
|
||||
///
|
||||
/// Geohash string is a base32 encoded string.
|
||||
/// It means that each character can be represented with 5 bits.
|
||||
/// Also, the length of the string is encoded as 4 bits (because max size is `GEOHASH_MAX_LENGTH = 12`).
|
||||
/// Also, the length of the string is encoded as 4 bits (because max size is [`GeoHash::MAX_LENGTH`] = 12).
|
||||
/// So, the packed representation is 64 bits long: 5bits * 12chars + 4bits = 64 bits.
|
||||
///
|
||||
/// Characters are stored in reverse order to keep lexicographical order.
|
||||
@@ -30,24 +30,29 @@ use crate::types::{GeoBoundingBox, GeoPoint, GeoPolygon, GeoRadius};
|
||||
/// Decoded 'd' 'r' '5' 'r' 'u' 'j' '4' '4' '7' 9
|
||||
/// Meaning s[0] s[1] s[2] s[3] s[4] s[5] s[6] s[7] s[8] s[9] s[10] s[11] length
|
||||
/// ```
|
||||
#[repr(C)]
|
||||
#[derive(Default, Clone, Copy, Debug, PartialEq, Hash, Ord, PartialOrd, Eq)]
|
||||
pub struct GeoHash {
|
||||
packed: u64,
|
||||
pub struct GeoHash(u64);
|
||||
|
||||
/// Variation of [`GeoHash`] to serialize/deserialize without validation.
|
||||
///
|
||||
/// Unlike [`GeoHash`], it might contain invalid bit patterns, e.g. `length > GeoHash::MAX_LENGTH`,
|
||||
/// or non-zeroed unused bits in characters.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct GeoHashRaw(pub u64);
|
||||
|
||||
impl GeoHashRaw {
|
||||
/// No-op, unless the raw value contains invalid bits.
|
||||
pub fn normalize(self) -> GeoHash {
|
||||
GeoHash::new_from_parts(self.0, self.0 & GeoHash::LEN_MASK)
|
||||
}
|
||||
}
|
||||
|
||||
// code from geohash crate
|
||||
// the alphabet for the base32 encoding used in geohashing
|
||||
#[rustfmt::skip]
|
||||
const BASE32_CODES: [u8; 32] = [
|
||||
b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7',
|
||||
b'8', b'9', b'b', b'c', b'd', b'e', b'f', b'g',
|
||||
b'h', b'j', b'k', b'm', b'n', b'p', b'q', b'r',
|
||||
b's', b't', b'u', b'v', b'w', b'x', b'y', b'z',
|
||||
];
|
||||
|
||||
/// Max size of geo-hash used for indexing. size=12 is about 6cm2
|
||||
pub const GEOHASH_MAX_LENGTH: usize = 12;
|
||||
impl From<GeoHash> for GeoHashRaw {
|
||||
fn from(hash: GeoHash) -> GeoHashRaw {
|
||||
GeoHashRaw(hash.0)
|
||||
}
|
||||
}
|
||||
|
||||
const LON_RANGE: Range<f64> = -180.0..180.0;
|
||||
const LAT_RANGE: Range<f64> = -90.0..90.0;
|
||||
@@ -58,8 +63,8 @@ impl Index<usize> for GeoHash {
|
||||
|
||||
fn index(&self, i: usize) -> &Self::Output {
|
||||
assert!(i < self.len());
|
||||
let index = (self.packed >> Self::shift_value(i)) & 0b11111;
|
||||
&BASE32_CODES[index as usize]
|
||||
let index = (self.0 >> Self::shift_value(i)) & ((1 << GeoHash::CHAR_BITS) - 1);
|
||||
&GeoHash::BASE32[index as usize]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,24 +96,22 @@ impl Display for GeoHash {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GeoHashIterator {
|
||||
packed_chars: u64,
|
||||
}
|
||||
pub struct GeoHashIterator(u64);
|
||||
|
||||
impl Iterator for GeoHashIterator {
|
||||
type Item = u8;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let len = self.packed_chars & 0b1111;
|
||||
let len = self.0 & GeoHash::LEN_MASK;
|
||||
if len > 0 {
|
||||
// take first character from the packed value
|
||||
let char_index = (self.packed_chars >> 59) & 0b11111;
|
||||
let char_index = self.0 >> (GeoHash::BITS - GeoHash::CHAR_BITS);
|
||||
|
||||
// shift packed value to the left to get the next character
|
||||
self.packed_chars = (self.packed_chars << 5) | (len - 1);
|
||||
self.0 = (self.0 << GeoHash::CHAR_BITS) | (len - 1);
|
||||
|
||||
// get character from the base32 alphabet
|
||||
Some(BASE32_CODES[char_index as usize])
|
||||
Some(GeoHash::BASE32[char_index as usize])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -116,31 +119,48 @@ impl Iterator for GeoHashIterator {
|
||||
}
|
||||
|
||||
impl GeoHash {
|
||||
pub fn new<H>(s: H) -> Result<Self, GeohashError>
|
||||
const BITS: u32 = u64::BITS;
|
||||
|
||||
/// Max length of geo-hash used for indexing. size=12 is about 6cm2
|
||||
const MAX_LEN: usize = 12;
|
||||
|
||||
/// Four least significant bits are length.
|
||||
/// The negation of this mask is the packed characters mask.
|
||||
const LEN_MASK: u64 = 0b1111;
|
||||
|
||||
const LEN_BITS: u32 = 4;
|
||||
|
||||
const CHAR_BITS: u32 = 5;
|
||||
|
||||
/// The alphabet for the base32 encoding used in geohashing.
|
||||
const BASE32: [u8; 32] = *b"0123456789bcdefghjkmnpqrstuvwxyz";
|
||||
|
||||
fn new<H>(s: H) -> Result<Self, GeohashError>
|
||||
where
|
||||
H: AsRef<[u8]>,
|
||||
{
|
||||
let s = s.as_ref();
|
||||
if s.len() > GEOHASH_MAX_LENGTH {
|
||||
if s.len() > GeoHash::MAX_LEN {
|
||||
return Err(GeohashError::InvalidLength(s.len()));
|
||||
}
|
||||
let mut packed: u64 = 0;
|
||||
for (i, c) in s.iter().enumerate() {
|
||||
let index = BASE32_CODES.iter().position(|x| x == c).unwrap() as u64;
|
||||
let index = GeoHash::BASE32.iter().position(|x| x == c).unwrap() as u64;
|
||||
packed |= index << Self::shift_value(i);
|
||||
}
|
||||
packed |= s.len() as u64;
|
||||
Ok(Self { packed })
|
||||
Ok(Self(packed))
|
||||
}
|
||||
|
||||
fn new_from_parts(characters: u64, len: u64) -> GeoHash {
|
||||
let len = len.min(GeoHash::MAX_LEN as u64);
|
||||
let characters_mask = !GeoHash::LEN_MASK
|
||||
<< (GeoHash::BITS - GeoHash::LEN_BITS - GeoHash::CHAR_BITS * len as u32);
|
||||
GeoHash((characters & characters_mask) | len)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> GeoHashIterator {
|
||||
if !self.is_empty() {
|
||||
GeoHashIterator {
|
||||
packed_chars: self.packed,
|
||||
}
|
||||
} else {
|
||||
GeoHashIterator { packed_chars: 0 }
|
||||
}
|
||||
GeoHashIterator(self.0)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
@@ -148,24 +168,12 @@ impl GeoHash {
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
(self.packed & 0b1111) as usize
|
||||
(self.0 & GeoHash::LEN_MASK) as usize
|
||||
}
|
||||
|
||||
pub fn truncate(&self, new_len: usize) -> Self {
|
||||
assert!(new_len <= self.len());
|
||||
if new_len == self.len() {
|
||||
return *self;
|
||||
}
|
||||
if new_len == 0 {
|
||||
return Self { packed: 0 };
|
||||
}
|
||||
|
||||
let mut packed = self.packed;
|
||||
// Clear all bits after `new_len`-th character and clear length bits
|
||||
let shift = Self::shift_value(new_len - 1);
|
||||
packed = (packed >> shift) << shift;
|
||||
packed |= new_len as u64; // set new length
|
||||
Self { packed }
|
||||
GeoHash::new_from_parts(self.0, new_len as u64)
|
||||
}
|
||||
|
||||
pub fn starts_with(&self, other: GeoHash) -> bool {
|
||||
@@ -178,16 +186,15 @@ impl GeoHash {
|
||||
return true;
|
||||
}
|
||||
|
||||
let self_shifted = self.packed >> Self::shift_value(other.len() - 1);
|
||||
let other_shifted = other.packed >> Self::shift_value(other.len() - 1);
|
||||
let self_shifted = self.0 >> Self::shift_value(other.len() - 1);
|
||||
let other_shifted = other.0 >> Self::shift_value(other.len() - 1);
|
||||
self_shifted == other_shifted
|
||||
}
|
||||
|
||||
// Returns the shift value. If we apply this shift to the packed value, we get the value of the `i`-th character.
|
||||
fn shift_value(i: usize) -> usize {
|
||||
assert!(i < GEOHASH_MAX_LENGTH);
|
||||
// first 4 bits is size, then 5 bits per character in reverse order (for lexicographical order)
|
||||
5 * (GEOHASH_MAX_LENGTH - 1 - i) + 4
|
||||
fn shift_value(i: usize) -> u32 {
|
||||
assert!(i < GeoHash::MAX_LEN);
|
||||
GeoHash::LEN_BITS + GeoHash::CHAR_BITS * (GeoHash::MAX_LEN as u32 - 1 - i as u32)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +263,7 @@ fn sphere_neighbor(hash: GeoHash, direction: Direction) -> Result<GeoHash, Geoha
|
||||
}
|
||||
|
||||
pub fn encode_max_precision(lon: f64, lat: f64) -> Result<GeoHash, GeohashError> {
|
||||
let encoded_string = encode((lon, lat).into(), GEOHASH_MAX_LENGTH)?;
|
||||
let encoded_string = encode((lon, lat).into(), GeoHash::MAX_LEN)?;
|
||||
GeoHash::try_from(encoded_string)
|
||||
}
|
||||
|
||||
@@ -393,7 +400,7 @@ fn check_polygon_intersection(geohash: &str, polygon: &Polygon) -> bool {
|
||||
fn create_hashes(
|
||||
mapping_fn: impl Fn(usize) -> Option<Vec<GeoHash>>,
|
||||
) -> OperationResult<Vec<GeoHash>> {
|
||||
(0..=GEOHASH_MAX_LENGTH)
|
||||
(0..=GeoHash::MAX_LEN)
|
||||
.map(mapping_fn)
|
||||
.take_while(|hashes| hashes.is_some())
|
||||
.last()
|
||||
@@ -697,13 +704,46 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::unusual_byte_groupings)]
|
||||
fn geohash_normalize() {
|
||||
let valid_samples: [&[u8]; _] = [
|
||||
b"dr5ru",
|
||||
b"uft56",
|
||||
b"hhbcd",
|
||||
b"uft560000000",
|
||||
b"h",
|
||||
b"hbcd",
|
||||
b"887hh1234567",
|
||||
b"",
|
||||
b"hwx98",
|
||||
b"hbc",
|
||||
b"dr5rukz",
|
||||
];
|
||||
// Normalization should not change valid hashes
|
||||
for s in valid_samples {
|
||||
let hash = GeoHash::new(s).unwrap();
|
||||
assert_eq!(hash, GeoHashRaw::from(hash).normalize());
|
||||
}
|
||||
|
||||
let raw = 0b_00001_00010_00011_00100_00101_00110_00111_01000_01001_01010_01100_01101__0011;
|
||||
let fxd = 0b_00001_00010_00011_00000_00000_00000_00000_00000_00000_00000_00000_00000__0011;
|
||||
// Zero-fill unused chars: ^^^^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^
|
||||
assert_eq!(GeoHashRaw(raw).normalize().0, fxd);
|
||||
|
||||
let raw = 0b_00001_00010_00011_00100_00101_00110_00111_01000_01001_01010_01100_01101__1111;
|
||||
let fxd = 0b_00001_00010_00011_00100_00101_00110_00111_01000_01001_01010_01100_01101__1100;
|
||||
// Truncate length: ^^^^
|
||||
assert_eq!(GeoHashRaw(raw).normalize().0, fxd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geohash_encode_longitude_first() {
|
||||
let center_hash = GeoHash::new(encode(Coord::from(NYC), GEOHASH_MAX_LENGTH).unwrap());
|
||||
let center_hash = GeoHash::new(encode(Coord::from(NYC), GeoHash::MAX_LEN).unwrap());
|
||||
assert_eq!(center_hash.ok(), GeoHash::new(b"dr5ru7c02wnv").ok());
|
||||
let center_hash = GeoHash::new(encode(Coord::from(NYC), 6).unwrap());
|
||||
assert_eq!(center_hash.ok(), GeoHash::new(b"dr5ru7").ok());
|
||||
let center_hash = GeoHash::new(encode(Coord::from(BERLIN), GEOHASH_MAX_LENGTH).unwrap());
|
||||
let center_hash = GeoHash::new(encode(Coord::from(BERLIN), GeoHash::MAX_LEN).unwrap());
|
||||
assert_eq!(center_hash.ok(), GeoHash::new(b"u33dc1v0xupz").ok());
|
||||
let center_hash = GeoHash::new(encode(Coord::from(BERLIN), 6).unwrap());
|
||||
assert_eq!(center_hash.ok(), GeoHash::new(b"u33dc1").ok());
|
||||
@@ -1410,7 +1450,7 @@ mod tests {
|
||||
},
|
||||
radius: OrderedFloat(1000.0),
|
||||
};
|
||||
let hashes = circle_hashes(&circle, GEOHASH_MAX_LENGTH);
|
||||
let hashes = circle_hashes(&circle, GeoHash::MAX_LEN);
|
||||
assert!(hashes.is_ok());
|
||||
|
||||
let circle2 = GeoRadius {
|
||||
@@ -1420,7 +1460,7 @@ mod tests {
|
||||
},
|
||||
radius: OrderedFloat(-1.0),
|
||||
};
|
||||
let hashes2 = circle_hashes(&circle2, GEOHASH_MAX_LENGTH);
|
||||
let hashes2 = circle_hashes(&circle2, GeoHash::MAX_LEN);
|
||||
assert!(hashes2.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ impl From<super::mmap_geo_index::Counts> for Counts {
|
||||
values,
|
||||
} = counts;
|
||||
Self {
|
||||
hash,
|
||||
hash: hash.normalize(),
|
||||
points,
|
||||
values,
|
||||
}
|
||||
@@ -74,7 +74,7 @@ impl ImmutableGeoMapIndex {
|
||||
ids_end,
|
||||
} = item;
|
||||
(
|
||||
hash,
|
||||
hash.normalize(),
|
||||
index.storage.points_map_ids[ids_start as usize..ids_end as usize]
|
||||
.iter()
|
||||
.copied()
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::common::Flusher;
|
||||
use crate::common::mmap_bitslice_buffered_update_wrapper::MmapBitSliceBufferedUpdateWrapper;
|
||||
use crate::common::operation_error::{OperationError, OperationResult};
|
||||
use crate::common::stored_bitslice::MmapBitSlice;
|
||||
use crate::index::field_index::geo_hash::GeoHash;
|
||||
use crate::index::field_index::geo_hash::{GeoHash, GeoHashRaw};
|
||||
use crate::index::field_index::stored_point_to_values::StoredPointToValues;
|
||||
use crate::types::GeoPoint;
|
||||
|
||||
@@ -29,7 +29,7 @@ const STATS_PATH: &str = "mmap_field_index_stats.json";
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(super) struct Counts {
|
||||
pub hash: GeoHash,
|
||||
pub hash: GeoHashRaw,
|
||||
pub points: u32,
|
||||
pub values: u32,
|
||||
}
|
||||
@@ -37,7 +37,7 @@ pub(super) struct Counts {
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(super) struct PointKeyValue {
|
||||
pub hash: GeoHash,
|
||||
pub hash: GeoHashRaw,
|
||||
pub ids_start: u32,
|
||||
pub ids_end: u32,
|
||||
}
|
||||
@@ -133,7 +133,7 @@ impl MmapGeoMapIndex {
|
||||
|
||||
let mut ids_offset = 0;
|
||||
for (i, (hash, ids)) in dynamic_index.points_map.iter().enumerate() {
|
||||
points_map[i].hash = *hash;
|
||||
points_map[i].hash = GeoHashRaw::from(*hash);
|
||||
points_map[i].ids_start = ids_offset as u32;
|
||||
points_map[i].ids_end = (ids_offset + ids.len()) as u32;
|
||||
points_map_ids[ids_offset..ids_offset + ids.len()].copy_from_slice(
|
||||
@@ -162,7 +162,7 @@ impl MmapGeoMapIndex {
|
||||
.zip(counts_per_hash.iter_mut())
|
||||
{
|
||||
if let Some(values) = dynamic_index.values_per_hash.get(hash) {
|
||||
dst.hash = *hash;
|
||||
dst.hash = GeoHashRaw::from(*hash);
|
||||
dst.points = *points as u32;
|
||||
dst.values = *values as u32;
|
||||
}
|
||||
@@ -307,7 +307,7 @@ impl MmapGeoMapIndex {
|
||||
self.storage
|
||||
.counts_per_hash
|
||||
.iter()
|
||||
.map(|counts| (counts.hash, counts.points as usize))
|
||||
.map(|counts| (counts.hash.normalize(), counts.points as usize))
|
||||
}
|
||||
|
||||
pub fn points_of_hash(&self, hash: GeoHash, hw_counter: &HardwareCounterCell) -> usize {
|
||||
@@ -324,7 +324,7 @@ impl MmapGeoMapIndex {
|
||||
if let Ok(index) = self
|
||||
.storage
|
||||
.counts_per_hash
|
||||
.binary_search_by(|x| x.hash.cmp(&hash))
|
||||
.binary_search_by(|x| x.hash.normalize().cmp(&hash))
|
||||
{
|
||||
self.storage.counts_per_hash[index].points as usize
|
||||
} else {
|
||||
@@ -346,7 +346,7 @@ impl MmapGeoMapIndex {
|
||||
if let Ok(index) = self
|
||||
.storage
|
||||
.counts_per_hash
|
||||
.binary_search_by(|x| x.hash.cmp(&hash))
|
||||
.binary_search_by(|x| x.hash.normalize().cmp(&hash))
|
||||
{
|
||||
self.storage.counts_per_hash[index].values as usize
|
||||
} else {
|
||||
@@ -412,11 +412,13 @@ impl MmapGeoMapIndex {
|
||||
let start_index = self
|
||||
.storage
|
||||
.points_map
|
||||
.binary_search_by(|point_key_value| point_key_value.hash.cmp(&geohash))
|
||||
.binary_search_by(|point_key_value| point_key_value.hash.normalize().cmp(&geohash))
|
||||
.unwrap_or_else(|index| index);
|
||||
self.storage.points_map[start_index..]
|
||||
.iter()
|
||||
.take_while(move |point_key_value| point_key_value.hash.starts_with(geohash))
|
||||
.take_while(move |point_key_value| {
|
||||
point_key_value.hash.normalize().starts_with(geohash)
|
||||
})
|
||||
.filter_map(|point_key_value| {
|
||||
Some(
|
||||
self.storage
|
||||
|
||||
Reference in New Issue
Block a user