Geo and seen-filtering — geohash, S2/H3, Bloom vs exact sets
Spatial indexing essentials and the membership-filter pattern, sized to what you actually need.
Concept → your projects
Spatial indexing (the decision tree):
- Grid/geohash: encode lat/lng into a string prefix — nearby points share prefixes. Query = the cell + 8 neighbors (the classic "search 3×3 grid" pattern); prefix length controls precision (~5 chars ≈ 4.9 km, 6 ≈ 1.2 km, 7 ≈ 150 m). Cheap, index-friendly (B-tree prefix scan), approximate cell boundaries.
- S2 / H3: hierarchical cell systems with better geometry (S2: spherical quadtree, arbitrary rectangles; H3: hexagonal, uniform neighbor distance) — production-grade for global apps.
- Quadtrees: dynamic subdivision; good in-memory; less natural in SQL.
- PostGIS/
earthdistance: real geometry (polygons, distance, containment) — the correct choice when you need actual shapes. - Redis GEO: sorted-set encoding (52-bit geohash in a zset score) with
GEOADD/GEOSEARCH— sub-ms nearby queries for hot paths (the "Redis GEO hot path" from your list: cache the hot radius search, back it with the database).
Point-in-polygon / geofencing: ray casting (count crossings of a ray from the point — odd = inside) is the classic algorithm; winding number is the robust variant; both are O(vertices) per test — fine for modest polygons, which is why geofences are usually few and cached. Real systems precompute bounding boxes (reject fast) then run ray casting on candidates.
Seen filtering (your Bloom-vs-exact question, in its natural habitat): "have I already shown this user this item?" — the canonical feed/dedup filter.
- Exact set (Redis SET / Postgres table): correct, memory ∝ items. Fine to ~millions per user-batch, expensive at massive scale.
- Bloom filter: O(10 bits/item), ~1% false positives — and the failure mode matters: a false positive means "already seen" for something unseen → the item is silently skipped (acceptable for feed dedup, never for authorization). Also: Bloom filters can't be deleted from (no per-item removal) and are per-keyed-set (per-user filters for 100M users = 100M filters — the scale trap; the alternatives are counting Bloom, cuckoo filters (deletable), or a shared filter with per-item TTL semantics).
- Your projects: exact filtering, correctly — traverse's
hiddenUserIds(blocks), dedup by unique constraints, "already solved" viaSolveuniqueness, seen-sets as indexed queries. The right sentence: "I use exact membership everywhere because my sets are small and my false-positive tolerance is zero — Bloom earns its keep when the set exceeds memory and a false positive costs a wasted lookup (feed dedup), never a wrong decision."
Matching/recommendation algorithms (the topic list's tail): rule-based scoring (attribute overlap), collaborative filtering (co-occurrence: users who did X also did Y — computable in SQL at small scale via self-joins), and embedding similarity (your pgvector pattern: embed items/users, nearest neighbors). The systems framing: recommendation quality is a modeling problem; the systems problem is candidate generation (index-friendly retrieval) + scoring + caching, and the honest note for traverse: friend suggestions and problem recommendations would start as SQL co-occurrence queries — no ML infra needed until quality demands it.
Bulletproof your resume
Say these answers aloud before moving on
These are the follow-ups an interviewer will actually ask about your projects.
How would you implement 'problems near me' if traverse added a location feature?
Start with PostGIS or earthdistance in Postgres: store a point per problem, GiST index, ST_DWithin for radius — exact, no new infrastructure, and it handles polygons later. If the query became hot (millions of lookups/s), I'd add Redis GEO as a cache tier for the top radii. Geohash would be the choice if I wanted pure-prefix B-tree indexing without extensions — cheaper index scans, coarser semantics. The decision order is: PostGIS for correctness, geohash for simplicity, Redis GEO for hot-path latency.
Bloom vs exact set for seen-filtering — where's your line?
Bloom when memory binds and a false positive costs a wasted lookup: feed dedup, 'have I crawled this', cache-miss avoidance. Exact when a false positive is a wrong outcome: authorization, access dedup, block filtering, unique constraints. My systems use exact everywhere because the sets are small and the semantics are security-adjacent. The other Bloom trap to name: no deletion and one filter per keyspace — per-user filters don't scale, so production usually pairs a global Bloom (reject obviously-seen) with an exact check (confirm), a two-tier membership test.
Ray casting for point-in-polygon — how does it work and when does it fail?
Cast a ray from the point to infinity; count edge crossings — odd means inside. It fails at degeneracies: the ray passing exactly through a vertex or along an edge double-counts, so implementations use half-open edge rules (count the lower endpoint, not the upper) and handle horizontal edges specially. Practical performance comes from a bounding-box prefilter and caching parsed polygons — geofences are typically few and stable, so the O(vertices) test is fine per request. For thousands of polygons you'd want a spatial index (GiST) rather than testing each.