--- title: "Proximity Service" description: "Find nearby businesses with geospatial indexes (even grids, geohash, quadtrees, Google S2) plus caching and replication." icon: map-pin --- ## Introduction A **proximity service** is designed to find nearby locations, such as restaurants, hotels, gas stations, and other businesses. This functionality is used in applications like **Google Maps** and **Yelp** to help users discover places within a defined radius. ## Step 1: Understanding the Problem and Establishing Scope ### Functional Requirements 1. **Search for businesses** based on user location (latitude, longitude) and search radius. 2. **Allow business owners** to add, update, or delete businesses (not real-time). 3. **Provide detailed business information** when requested. ### Non-Functional Requirements - **Low latency**: Users should get quick responses. - **Data privacy**: Compliance with GDPR and CCPA regulations. - **High availability**: Handle peak-hour spikes in busy locations. ### Back-of-the-Envelope Estimation - **100 million daily active users**. - **200 million businesses** in the system. - **Search QPS Calculation**: - Users make **5 searches per day**. - **Search QPS** = (100M × 5) / 86,400 ≈ **5,000 QPS**. --- ## Step 2: High-Level Design ### API Design #### Search Nearby Businesses GET /v1/search/nearby - **Request Parameters**: - `latitude`: User’s location latitude. - `longitude`: User’s location longitude. - `radius`: Search radius (default: 5000m). #### Business APIs | API Endpoint | Description | |-----------------------------------|--------------------------------------------------| | `GET /v1/businesses/{id}` | Fetch detailed business info | | `POST /v1/businesses` | Add a new business | | `PUT /v1/businesses/{id}` | Update business details | | `DELETE /v1/businesses/{id}` | Remove a business from the system | ### Data Model - Since the read volume is high because two features are very commonly used, a realtional database such as MySQL is a good fit. - Search for nearby businesses - View the detailed information of a business ### Data Schema - Key Database tables are the business table and the geospatial index table - The business table consists the detailed information about a business. ### High-Level System Architecture The system comprises of two parts: Location based service (LBS) and business related service. ![HLD](./images/proximity-service/high-level-design.png) - **Location-Based Service (LBS)**: - Processes location-based search queries. - Read-heavy service with no write requests. - QPS is high especially during peak hours in dense areas and the system is stateless. - **Business Service**: Deals with two types of requests. - Business owners create, update or delete businesses. - Customers view detailed information about a business. - **Load Balancer**: Routes traffic to LBS and Business service. - **Database Cluster**: - Uses **primary-replica architecture** for read-heavy workloads. - There might be some discrepancy between data read b/w data read by LBS and data written by by primary database. - This incosistency is not an issue beacuase the business information is not updated in real-time. --- ## Step 3: Algorithms for Fetching Nearby Businesses ### Option 1: Two-Dimensional Search (Naive Approach) ![2D](./images/proximity-service/2d-search.png) The most intuitive way is to draw a circle with pre-defined radius and find all the businesses within the circle. **SQL Query:** ```sql SELECT business_id, latitude, longitude FROM business WHERE (latitude BETWEEN :lat - radius AND :lat + radius) AND (longitude BETWEEN :long - radius AND :long + radius); ``` **Problems:** - **Inefficient**: Requires scanning the entire database. - **Limited by one-dimensional indexes** (latitude/longitude). A potiential improvement is to build index on logitude and latitude columns, alhtough this is slighlty better but still vry slow. ### Better Approach - The problem with last approach is that the database index can only increase search speed in one dimension. - An optimal apporach is to reprsent the two-dimensional data into one dimension using geospatial indexing. - Hash: Even grid, Geo Hash - Tree: Quadtree, Google S2, RTree ![2D](./images/proximity-service/geospatial-index-types.png) ### Option 2: Evenly Divided Grid ![Even Grid](./images/proximity-service/even-grid.png) - **Divides the world into fixed-size grids**. - **Issue**: Uneven business distribution (high density in cities, sparse in rural areas). ### Option 3: Geohash - Divide the planet into four quadrants along with the prime meridian and equator. And then divide each grid into four smaller grids. - Each grids can be represented by altering b/w longitude and latitude bit. - Repeat this subdivision ![Geohash](./images/proximity-service/geohash.png) ![Geohash](./images/proximity-service/geohash-1.png) - **Encodes latitude and longitude into a single alphanumeric string**. It has 12 precisions (levels) - **Hierarchical grid structure** allows for efficient searching. - The right precision is chosen by using the minimal geohash length according to the table. ![Geohash Radius](./images/proximity-service/geohash-radius-mapping.png) - Geohash guarantees that the longer a shared prefix is between two geohashes, the closer they are. - **Challenges**: ![Boundary Issue](./images/proximity-service/boundary-issue.png) - **Boundary issues** (businesses close to grid edges may get excluded). - Two locations can be very close but have no shared prefix at all (can be on other side of equator) - Two locations can have a long shared prefix but belong to different geohashes. - Solution: Need to search neighboring grids. ### Option 4: Quadtree A quadtree is a tree data structure that recursively divides a two-dimensional space into four quadrants, with each internal node having exactly four children, representing the four sub-regions of the space. - The quadtree is an in-memory data structure and it runs on each LBS server and built on server startup time. ![Quadtree](./images/proximity-service/quadtree.png) - The root node is recursively broken down into 4 quadrants until no nodes are left with more than x number of businesses (100 in this case). ![Building Quadtree](./images/proximity-service/building-quadtree.png) - The quadtree index doen't take too much memory (typically in GBs) and can easily fit in one server. - Since tge time complexity to build the tree is nlogn, it might take a few minutes to build the tree. - **Efficient for k-nearest search queries** (e.g., find the closest gas station). ![Real World Quadtree](./images/proximity-service/realworld-quadtree.png) #### Operational considerations - For around 200 million businesses, it might take few minutes to build a quadtree at the server start time. - While the quadtree is built it cannot serve traffic, therefore a new release should be rolled out incrementally to a subset of servers. - When updating a business or adding a new the easiest approach is to incrementally rebuild the quadtree. (Leading to a lot of cache invalidation) - Also possible to update the quadtree on the fly but more complex to implement. (Needs locking mechanism) ### Option 5: Google S2 It maps a sphere to a !D index based on Hilbert curve.Two points that are close to each other on the Hilbert curve are close in 1D space. ![Hilbert curve](./images/proximity-service/hilbert-curve.png) ![Geofence](./images/proximity-service/geofence.png) - **Divides the earth into small cells using a Hilbert curve**. - Great for geofencing becuase it can cover arbitrary areas with varying levels. - Geofencing also allows to define parameters that surround the area of interest. - Aother advantage if instead of having a fixed level of precision, we can specify min,max level and max cells in S2. ## Tradeoff Comparison #### Geohash - Easy to use and implement- No need to build/rebuild a tree - Supports fixed radius results - Updating the index is easy. - Cannot dynamically adjust the grid size based on population density. #### Quadtree - Slightly harder to implement. - Supports fetching k-nearest businesses. - Can dynamically adjust the grid size based on population desnsity. - Updating the index is more complicated as might need to rebuild the whole tree. --- ## Step 4: Scaling the Database and Caching Strategy ### Scaling the Business Table - **Sharding by business ID** ensures even data distribution. - We have separate rows for each business in the table. | Geohash | Business ID | |---------|------------| | 9q9hvu | 343 | | 9q9hvu | 347 | | 9q9hvu | 112 | ### Scaling the Geospatial Index - Might not be a good fit for the geohash table. In this case everything can fit in a single server so there's no tehcnical reason for sharding. - A better approach is to have read-replicas to help with read loads. --- ### Cache Strategy The most obvious cache key choice is the location coordinate, however it has a few issues: - Location coordinates from gps are not accurate. - A user can move casuing the location coordinate to change. - A better key is the geohash. | Cache Key | Cache Value | |------------|------------| | `geohash` | List of business IDs in that grid | | `business_id` | Business details (name, address, reviews, etc.) | --- ## Step 5: Deployment Strategy and Final Architecture ### Region and Availability Zones - Deploy LBS and Business Service **across multiple regions**. ### Handling Real-Time Updates - **Business updates are batch processed daily**. ### Final System Architecture ![Final Design](./images/proximity-service/final-design.png) This final algorithm looks like this: ## Steps to Retrieve Nearby Businesses 1. **User Request:** - A user searches for restaurants within **500 meters**. - The client sends **latitude (37.776720), longitude (-122.416730), and radius (500m)** to the **load balancer**. 2. **Request Forwarding:** - The **load balancer (LB)** forwards the request to the **Location-Based Service (LBS)**. 3. **Geohash Calculation:** - LBS determines the **geohash length** matching the radius. - Using a reference table, **500m corresponds to geohash length = 6**. 4. **Fetching Neighboring Geohashes:** - LBS calculates **neighboring geohashes** to include nearby areas. - The result is a list: ``` [my_geohash, neighbor1_geohash, neighbor2_geohash, ..., neighbor8_geohash] ``` 5. **Fetching Business IDs from Redis:** - For each geohash in the list, LBS queries the **Geohash Redis server** to fetch **business IDs**. - Parallel queries are used to minimize latency. 6. **Retrieving & Ranking Businesses:** - LBS fetches **full business details** from the **Business Info Redis server**. - Businesses are **sorted by distance** from the user’s location. - The **ranked results** are sent back to the client. ## Key Optimizations - **Parallel Redis Calls**: Reduces response time. - **Geohash Indexing**: Ensures efficient spatial queries. - **Caching**: Speeds up lookup and retrieval of business data. This method ensures **low-latency, scalable** retrieval of businesses near a user’s location. --- ### Choosing the Best Indexing Method | Indexing Method | Pros | Cons | |----------------|------|------| | **Geohash** | Easy to implement, efficient for proximity search | Boundary issues, fixed grid size | | **Quadtree** | Dynamically adjusts to density, supports k-nearest queries | More complex, requires tree rebalancing | | **Google S2** | Advanced geofencing, used in Google Maps | Harder to implement | --- ## References 1. [Geohash Algorithm](https://www.movable-type.co.uk/scripts/geohash.html) 2. [Quadtree Indexing](https://en.wikipedia.org/wiki/Quadtree) 3. [Google S2 Geometry](https://s2geometry.io/)