Listings Search That Scales: Geo-Search, Filtering, and Relevance
How to build listings search at scale: geo-search, faceted filtering, and relevance ranking, with the build vs buy calls that actually hold up on a live property marketplace.
Most teams do not come to us because their search bar is broken. They come because search quietly became the product, and the stack underneath it cannot keep up. Radius queries crawl, filter combinations time out, and someone in a leadership meeting asks why an obviously worse listing is sitting at position one. If that is where you are, the useful answer is that listings search is three different hard problems wearing one input box, and you solve them in order, not all at once.
We have built and operated this inside real, high-volume property marketplaces (Lamudi, LISTD), so this is written from the calls we actually had to make: when a single Postgres instance genuinely stops being enough, what a dual-database setup costs to run, and how relevance gets pulled apart by monetization and data quality. It is an architecture-level answer, not an Elasticsearch tutorial.
Why listings search breaks differently than product search
E-commerce search optimizes for a mostly static catalog with clean, structured attributes and a clear notion of the best match. Listings search does not get that luxury. Inventory is user-generated, so titles are inconsistent, photos are missing, and the same apartment appears three times from three brokers.
More importantly, location is a first-class query dimension, not a filter you bolt on later. A buyer searching a neighborhood cares about proximity, price, and freshness at the same time, and each of those pulls ranking in a different direction. That combination, geo plus facets plus relevance evaluated together, is what makes listings search its own discipline.
The three problems that compound
Break the work into three problems and the architecture gets clearer:
- Geo-proximity: find listings near a point, inside a bounding box, or within a drawn area.
- Faceted filtering: narrow by price, bedrooms, property type, amenities, and return live counts per facet.
- Relevance: decide what ranks first when many listings match equally well on text.
Each is tractable alone. The pain is that they compound. A radius query is fine until you add six facets and a text match and ask for aggregation counts on top, all under a latency budget. Teams that treat these as separate features ship three subsystems that fight each other. Teams that design for the intersection ship search that holds up.
Build, buy, or defer
The honest default for an early marketplace is Postgres with the PostGIS extension. It handles bounding-box and radius queries, structured filters, and basic text with far less operational overhead than a separate search cluster. Do not reach for a dedicated engine because a blog told you to. Reach for it when combined load, not listing count, forces your hand.
Here is how we frame the tradeoff:
| Concern | Postgres + PostGIS | Dedicated search engine (Elasticsearch, OpenSearch, Typesense) |
|---|---|---|
| Best fit | Hundreds of thousands of listings, simple facets | Millions of listings, many-facet aggregation, tuned relevance |
| Geo | Strong on complex polygons, exact spatial joins | Strong on point and radius, degrades on heavy polygon indexing |
| Faceted counts | Workable, gets expensive across many attributes | Purpose-built aggregations, sub-second at scale |
| Relevance | Basic text match | BM25 out of the box, learning-to-rank on top |
| Scaling model | Single-node, vertical | Horizontal via sharding across nodes |
| Operational cost | You already run it | A second system to sync, monitor, and keep consistent |
The nuance most comparisons miss: PostGIS actually outperforms search engines on complex multipolygon queries, while search engines win decisively on point-radius plus faceted aggregation plus tuned relevance. Choose based on which of those your users actually do.
There is a third path people forget in the build-versus-buy framing, and a fourth that is usually right: a managed service, or deferral. A hosted search service (Algolia, Typesense Cloud, or a managed OpenSearch or Elastic Cloud) lets you rent the cluster operations instead of staffing them, which is a sane trade when you lack in-house search-ops depth and your catalog is moderate. It does not remove the two things that are genuinely yours: the pipeline that syncs listings from your source of truth, and the relevance logic that encodes what your marketplace considers a good result. And more often than teams admit, the right answer this quarter is defer: add the missing index, tune the query, and revisit when the load is real. Whether you migrate at all is the same class of decision as any major re-architecture: do it when the ceiling is real, not anticipated.
What a dedicated search stack actually costs you
The sticker price of a search engine is the least interesting cost. The real ones are structural, and naming them upfront prevents the mid-migration surprise.
- A second system to operate. Provisioning, monitoring, version upgrades, and on-call now cover two datastores instead of one. That is a standing tax on the team, not a one-time setup.
- A sync pipeline you own for good. Keeping the index current with the source of truth is code you write, backfills you run, and drift you debug at 2am when the two stores disagree.
- Eventual consistency as a product cost. When the index lags, users see listings that no longer exist or miss ones that just went live. Support absorbs the difference, so someone has to decide how much lag is acceptable.
- Relevance tuning that is never finished. Ranking is not a feature you ship once. It is an evaluation loop, and learning-to-rank in particular needs a clean pipeline of engagement data before it earns anything.
On effort, calibrate honestly. Useful search on PostGIS is days of work. Standing up a synced search index, backfilling it, and cutting traffic over safely is weeks, and the relevance work after that is continuous. The managed-service route trades most of the operational cost for pricing that scales with records and operations, so the bill rises precisely as you succeed. Neither path is free. You are choosing the currency: engineering time, or a usage invoice.
Geo-search in practice
Elasticsearch models location two ways: geo_point for latitude and longitude pairs, and geo_shape for polygons, lines, and drawn circles. Point plus radius covers the common case (listings near me, listings in this bounding box). Shapes cover drawn search areas and administrative boundaries, and they are where indexing cost and latency climb fast. Between the coarse bounding box and the exact pin sits a useful middle gear: geohash grid aggregations bucket points into cells cheaply, which is how you answer how many listings are in this area for map clustering without paying for per-pin math.
The mistake we see repeatedly is over-investing in pin-level precision before basic facet performance is solved. Most early search traffic is city-level or neighborhood-level. Start with coarse bounding boxes, keep geo mappings lean because nested and geospatial fields inflate query latency, and only add fine-grained radius and search-this-area behavior once user behavior proves the demand. The front-end half of this, map rendering, clustering, and search-this-area interactions, is its own performance problem that we cover in map-based property search; this piece stays on the backend.
Faceted filtering without wrecking SEO
Faceted filtering has a trap that pure system-design guides ignore: every filter combination can generate a unique URL. Multiply a handful of facet categories and you produce millions of combinations, which turns into index bloat, duplicate content, and wasted crawl budget if you let search engines index all of them.
The discipline is deciding which combinations deserve a crawlable page:
- Pages with real search demand (a named city, a property type, a price band) become server-rendered, indexable URLs.
- Everything else stays a client-side refinement with
noindexand canonical tags. - First load is server-rendered HTML for crawlability; subsequent filter changes update client-side for instant response.
Which facets are even filterable is decided upstream, at the schema. If listing attributes are modeled as loose text instead of structured fields, you cannot add a clean facet later without a migration. That coupling is why we treat facet design as a data-model decision first and a search decision second, and why the crawlable-combination question overlaps heavily with your broader programmatic SEO strategy.
Relevance ranking beyond BM25
BM25, the default term-frequency relevance model, is a fine starting point and a poor finishing point. On a listings marketplace, the best text match is routinely the wrong result: it can be stale, unphotographed, or a duplicate. Text relevance alone has no opinion about any of that.
The way modern systems escape this is multi-stage retrieval: cheap candidate generation from the inverted index, then a filtering pass for hard constraints and permissions, then a more expensive re-ranking stage that blends the signals text match cannot express. That final stage is where a listings marketplace earns its ranking:
- Text relevance as the candidate-generation baseline.
- Freshness and staleness decay so listings that have not been updated sink over time.
- Monetization tier so featured and sponsored placements surface, deliberately and transparently.
- Engagement signals via learning-to-rank once you have enough click and contact data to train on.
Two of these are genuinely in tension. Monetization pushes paid listings up, and how you balance that against organic quality is a design decision with revenue attached, which is why it connects directly to how you monetize the marketplace. Freshness only works if your data is trustworthy, because staleness decay assumes stale listings are the problem, not fake ones, which puts a hard dependency on duplicate detection and data quality.
The dual-database pattern
At scale the standard architecture is two stores with clear roles. Postgres, often with PostGIS, stays the transactional source of truth. A denormalized search index (Elasticsearch, OpenSearch, or Typesense) is kept in sync and serves reads. You get transactional consistency where writes happen and sub-second geo, facet, and text search where users query.
The cost is real and worth naming plainly. You now operate a sync pipeline, tolerate eventual consistency between the two stores, and own the failure modes when they drift. Do not adopt this pattern for elegance. Adopt it when read-heavy search traffic and write-heavy listing updates start contending on one node, and the index earns its keep by isolating those workloads. It is one specialized case of the broader synchronization questions every two-sided marketplace has to answer.
Knowing when your stack has hit its ceiling
The signals are consistent across the marketplaces we have worked on. Latency spikes when filter, geo, and text load combine. Faceted counts get slow or get dropped to keep pages responsive. Listing-update writes start blocking search reads. Someone hardcodes a ranking exception because relevance cannot express what the business needs.
None of those is a reason to panic-rebuild. Each is a reason to introduce the next capability deliberately: a search index, then relevance tuning, then learning-to-rank, in that order. The two failure modes sit at opposite ends. One team over-builds a distributed search cluster before it has the load to justify one, paying the second-system tax for nothing. Another nurses a single Postgres instance a year past the point it stopped serving users. Starting today, we would keep search on PostGIS until combined load genuinely bends it, put the dual-database boundary in early on paper so the migration is a switch and not a rewrite, and treat relevance as a signal-blending problem from day one rather than a text-match afterthought.
Frequently asked questions
Do we need Elasticsearch, or can Postgres and PostGIS handle our listing search?
If your catalog is a few hundred thousand listings with straightforward filters and radius search, PostGIS on Postgres carries you a long way with far less operational overhead. Move to a dedicated engine when you need sub-second full-text relevance, faceted aggregations across many attributes at once, or write throughput that starts to strain a single-node transactional database. The trigger is combined load, not listing count alone.
Can we just use a managed service like Algolia instead of running our own search cluster?
Often, yes, and for a moderate catalog it is the pragmatic call. A managed service (Algolia, Typesense Cloud, hosted OpenSearch or Elastic Cloud) rents you the cluster operations you would otherwise staff for. What it does not remove is the sync pipeline from your source of truth and the relevance logic specific to your marketplace, which you own either way. The tradeoff is pricing that usually scales with records indexed and operations served, so the cost curve bends upward exactly as you grow. Buy the operations, keep the judgment.
How do we stop faceted filters from destroying our SEO?
Not every filter combination should be a crawlable, indexable URL. Decide upfront which combinations have real search demand, such as condos in a named city under a price cap, and make those server-rendered pages. Treat the rest as client-side refinements with noindex and canonical tags so you avoid duplicate content and crawl-budget waste.
Why does our most relevant listing feel wrong to users?
Default text relevance like BM25 treats a listing that repeats three bedroom three times as highly relevant, regardless of whether it is stale, unphotographed, or a paid placement. Real marketplace relevance blends text match with freshness signals, monetization tiers, and eventually engagement-based learning-to-rank. If ranking feels wrong, you are almost always missing the non-text signals.
How precise does our geo-search need to be, city-level or exact pin?
Start coarser than you think you need, with city and neighborhood bounding boxes, and only invest in pin-level precision once map pans and search-this-area clicks prove demand. Precise geospatial indexing adds real latency and complexity that is wasted if most searches are city-level anyway. Precision is a decision to defer, not a default to reach for.
When do we need a separate search index instead of querying the main database?
When latency degrades under combined filter, geo, and text load, when you need live faceted counts across large result sets, or when write volume from listing updates contends with read-heavy search traffic. At that point a denormalized search index synced from your source-of-truth database is the standard pattern. It buys you speed and isolation at the cost of eventual consistency and a sync pipeline to operate.