Listing Search That Survives Stale Feeds - Four Retrieval Signals for Property Discovery
Real-estate listing discovery has a nasty asymmetry: a result that is six hours old can be more damaging than a result that is merely less semantically similar.
A renter sees an unavailable apartment, calls an agent, and stops trusting the search. The retrieval architecture therefore has to balance ranking signals against freshness and response time, not chase vector similarity alone.
Short answer: use a two-stage retriever. Filter hard constraints and freshness first, blend lexical and vector candidates second, then rerank a small set with explicit listing signals and log the reason for every score.
I build RAG features in Python, so my first test is always a notebook-sized evaluation set before an index gets a production job. For an edtech team answering questions over PDFs, the same harness can hold listing records instead: address, price, bedrooms, availability timestamp, amenities, and the source feed revision. The domain changes; the discipline does not.
What Should a Retrieval Architecture Measure Before Ranking Signals?
Start with judgments, not an index. Make a few hundred queries that look like real searches:
- “two-bedroom near the light rail under 2,400”
- “pet-friendly condo with a balcony”
- “show units available next month”
Label each candidate for constraint satisfaction, factual freshness, and usefulness. Keep the labels separate. A listing can be relevant but stale, or fresh but outside the commute radius.
The primary metrics should expose that difference. Recall@50 tells you whether the candidate stage found viable homes. NDCG@10 captures ordering quality. Add a freshness-weighted precision metric that gives zero credit to a listing whose available_until is earlier than the query date.
Record p50 and p95 latency for each stage, because a reranker that adds 400 ms may be unacceptable on a mobile results page even when NDCG improves.
I once treated “no result” as a retrieval failure. That inflated recall while the system quietly returned homes that violated a budget cap. The fix was a negative test set: impossible combinations, expired listings, and deliberately ambiguous neighborhoods. Three words changed the dashboard: “constraint violations per query.”
Your mileage may vary. A market with hourly feed updates needs a tighter freshness window than a market where brokers publish weekly.
A Practical Two-Stage Design for Real-Estate Listing Discovery
Stage one is deterministic. Normalize currency, bedroom counts, coordinates, and timestamps at ingestion. Apply hard filters for price, property type, legal availability, and geographic bounds. Do this before embedding search; no model should be allowed to “semantic-match” its way around a maximum rent.
Stage two builds a broad candidate pool. A lexical retriever catches exact street names, school districts, and transit stops. A dense retriever catches paraphrases such as “walkable to trains” versus “near rail.” Merge the top 50 from each path, deduplicate by a stable listing identifier, and preserve the source revision in the candidate object.
In practice, I also keep the retrieval trace beside the result: query normalization, filter values, index snapshot, and the IDs that were discarded as duplicates. That extra record feels tedious during a launch week, but it turns a support ticket into a replayable test.
Without it, two engineers can stare at the same listing and disagree about whether the bug came from parsing, filtering, embedding drift, or a late feed update. The trace makes those branches visible and gives the eval harness a concrete fixture for the next release.
Then calculate a transparent score. One useful shape is:
from dataclasses import dataclass
from math import exp
@dataclass
class Candidate:
lexical: float
semantic: float
freshness_hours: float
constraint_ok: bool
source_quality: float
def rank_score(item: Candidate) -> float:
if not item.constraint_ok:
return float("-inf")
fresh = exp(-item.freshness_hours / 24.0)
return (
0.35 * item.lexical
+ 0.35 * item.semantic
+ 0.20 * fresh
+ 0.10 * item.source_quality
)
The coefficients are placeholders, not a universal recipe. Fit them against your judgments, then freeze a version and compare it with the previous version. Keeping the terms visible makes an agent’s citation easier to audit: it can say that a listing ranked highly because it matched the neighborhood, passed the budget filter, and was updated yesterday.
Four Signals That Prevent Plausible but Wrong Results
Semantic similarity is only one signal. For listings, four others routinely decide whether a result is useful.
- Freshness should be time-decayed, not binary. Use the feed’s published and observed timestamps, and cap the decay when a broker explicitly confirms availability. A listing updated ten minutes ago should beat an almost identical listing updated ten days ago, even if the older text has a slightly better embedding.
- Constraint satisfaction belongs outside the learned score. Price ceilings, move-in dates, bedrooms, and geography are predicates. Store the normalized value and the original text so a UI can explain a mismatch instead of silently dropping it.
- Source quality captures feed behavior, not brand reputation. Track revision frequency, field completeness, and duplicate rate per source. A feed that omits square footage should not outrank a complete record simply because its prose is lively.
- Intent fit handles what the query emphasizes. “Quiet top-floor studio” should give more weight to floor and noise attributes than to generic descriptions such as “sunny living space.” A lightweight classifier or hand-built facets can produce these weights before retrieval.
The catch is that more signals create more policy. If product managers cannot tell why a result moved, the ranking system becomes impossible to debug. Persist the component scores and a short reason code with every response.
How Do Lexical, Vector, and Rerank Paths Compare for Ranking Signals?
Elasticsearch-style lexical search is strong on exact entities and operational filters, and its inverted index is familiar to many teams. Its weakness is paraphrase recall unless you add analyzers or a second retriever.
PostgreSQL with a vector extension keeps listing data and embeddings near transactional tables, which simplifies joins; high-volume approximate search still requires careful index tuning and vacuum planning.
Vespa combines retrieval and ranking in one serving model, but its schema and ranking expressions introduce a steeper operational learning curve.
These are trade-offs, not a leaderboard. A small brokerage may prefer one relational deployment and accept a modest candidate set. A national marketplace may split ingestion, vector serving, and reranking so each can scale independently. Measure the same judgment set and latency budget for every option; otherwise “better retrieval” just means “the demo looked nicer.”
- Lexical index - Integration shape: Query API plus inverted fields. Good fit: Exact addresses, IDs, and fast filters. Main limitation: Misses paraphrases without extra analysis.
- Relational vector extension - Integration shape: SQL with vector operators. Good fit: Teams keeping listings and metadata together. Main limitation: Approximate-index tuning becomes important at scale.
- Dedicated ranking engine - Integration shape: Schema-defined retrieval and rank phases. Good fit: Large catalogs with complex rank expressions. Main limitation: Higher operational and schema learning cost.
Pick the boundary your team can operate. For the RAG answer layer, keep context selection separate from answer generation. Pass only the top, deduplicated listings plus their revision timestamps. The model should quote fields that exist in the record and say when availability is unknown.
The RAG paper’s original motivation is grounding generation in retrieved evidence, which is exactly why stale or weakly ranked candidates are a data-quality problem, not merely a search problem.
Shipping the Evaluation Loop
Version the corpus snapshot, embedding model, filter logic, and score coefficients together. On every change, run the same query set and compare recall, NDCG, freshness-weighted precision, constraint violations, and p95 latency. Keep a slice for new neighborhoods and a slice for long-tail queries; aggregate metrics can hide a system that only works downtown.
Add observability at stage boundaries. Log candidate counts after hard filters, lexical/vector overlap, reranker time, and the reason a result was excluded. Sample failed queries for human review. A 200-response sample with explanations is often more actionable than another synthetic benchmark.
The architecture is not suitable when your catalog is tiny and changes once a month. A single SQL query with explicit predicates may be clearer. Stick with a simple lexical index when users search exact MLS identifiers and latency dominates every other concern. Move to hybrid retrieval when paraphrase queries, messy descriptions, or multilingual data show measurable recall gaps.
Before copying the coefficients above, run the experiment on your own feed. That is the decision rule: choose the smallest architecture that meets the freshness, quality, and latency targets your users can actually feel.
Comments
No comments yet. Start the discussion.