How to Build a Personalized Real Estate Feed: Location, History & Smart Fallbacks

Nov 4, 2025

How to Build a Personalized Real Estate Feed: Location, History & Smart Fallbacks

Learn how to build a personalized real estate feed using location, user history, and smart fallbacks for accurate, privacy-first property recommendations.

Author

Abhishek Krishnan Rathaur
Abhishek Krishnan RathaurSoftware Engineer - II

Hey folks! Recently, I have been working on an exciting real estate application where sellers can easily list their properties, and buyers can discover homes tailored just for them. One of the key challenges we tackled was personalizing the userโ€™s feedโ€Šโ€”โ€Šensuring that buyers see the most relevant listings based on their location, past searches, and preferences, while also keeping the experience fresh with smart fallbacks.

In this post, I will walk you through our approach to building a dynamic, user-centric feed that balances personalization with discovery. Whether youโ€™re a developer, product manager, or just curious about recommendation systems, this breakdown will give you practical insights!

Approach to Building a Dynamic Real Estate Feed

The Challenge: Making Property Discovery Smarter

In todayโ€™s hyper-competitive real estate market, relevance isnโ€™t just importantโ€Šโ€”โ€Šitโ€™s the difference between keeping users engaged or losing them forever. With countless property platforms available, buyers expect instant access to listings that perfectly match their needs, while sellers demand that their properties are seen by the right audiences. A generic, one-size-fits-all approach leads to frustrated users who waste hours scrolling through irrelevant options, while valuable properties get buried in the noise. This challenge is compounded by the fact that 78% of buyers abandon a platform after just three irrelevant recommendations (2023 PropTech Survey).


Why Personalization Matters

Imagine two users searching for properties in Mumbai:


  1. First-time buyer Priya wants a 2BHK apartment under โ‚น1.5Cr near schools.
  2. Investor Rohan seeks commercial spaces in Bandra with high rental yields

A generic feed showing random listings would frustrate both. Too broad, and users scroll endlessly. Too restrictive, and they miss hidden gems.

The Core Problem

Most platforms fail at:

1. Cold-start personalization (What to show new users?)

2. Over-reliance on manual filters (Forcing constant tweaking)

3 . Static recommendations (Ignoring evolving preferences)


Our Solution

We built an adaptive recommendation engine that:

1. Learns from explicit preferences (onboarding choices)

2. Adapts to implicit behavior (searches, clicks, dwell time)

3. Works seamlessly for logged-in and anonymous users

4. Respects privacy while maximizing relevance

Technical Foundations

Tech Stack

Backend: NestJS 

Database: PostgreSQL 

SearchPrisma: Raw SQL (Haversine formula)

Geolocation: Browser GPS + ipinfo.io API (fallback)

Example Scenario

When Priya (our first-time buyer) signs up:


  1. Onboarding: Selects โ€œFamily homeโ€, budget range, school proximity
  2. First search: Filters for โ€œ2BHK, near international schoolsโ€

System response:

  • Prioritizes 2BHKs in her budget
  • Boosts listings near top-rated schools
  • Gradually learns she prefers gated communities

Meanwhile, anonymous users get location-aware defaults based on:


1. Browser GPS (if permitted) โ†’ 500m precision

2. IP geolocation โ†’ Neighborhood-level

3. City-level trending listings โ†’ Fallback


1. Personalized Feeds Need Dataโ€Šโ€”โ€ŠBut Where Do We Get It?

To customize a userโ€™s feed, we need search metadataโ€Šโ€”โ€Šlocation, property type, budget range, and preferences. But this data comes from different sources depending on whether the user is logged in or not.


For Authenticated User: Leveraging Past Preferences


For authenticated users, our personalization system combines explicit onboarding preferences with implicit behavioral learning to deliver hyper-relevant property recommendations. During onboarding, users provide explicit baseline preferencesโ€Šโ€”โ€Šincluding locations (like hometown or work city), property type, budget ranges, and property categoryโ€Šโ€”โ€Šwhich we store as their primary preference profile. Beyond these initial inputs, our system automatically captures and analyzes every search interaction, converting filters like location, budget ranges, or property type selections into evolving implicit preferences.


Location history (array of {lat, long} coordinates)

Property category (residential/commercial)

Property type (apartment, villa, office, etc.)

Budget range (budget_min, budget_max)


This helps us prioritize listings that match their past behavior.


For anonymous users, we prioritize immediacy while respecting privacy. If location permissions are granted, we use real-time browser GPS coordinates for precise matching. When denied, we fall back to IP-based geolocation (via services like ipinfo.io) to approximate the userโ€™s city/region, supplemented by popular local listings. This ensures even first-time visitors receive contextually relevant options without friction, while logged-in users benefit from a feed that grows smarter with every interaction.

async function fetchLocationFromIP(ip) {
 const response = await fetch(
   `https://ipinfo.io/${ip}?token=${process.env.IPINFO_TOKEN}`
 );
 const { loc } = await response.json();
 const [lat, long] = loc.split(',');
 return { lat, long };
}

This gives us an approximate location to start with.


2. Building the Search Metadata

Hereโ€™s the logic we use to construct the feed criteria:

async function buildSearchMetadata(user ,ip ,currentLocation) {
 // Case 1: User is authenticated & has a search history
 const searchParams = {
   property_category: undefined,
   property_type: undefined,
   budget_min: undefined,
   budget_max: undefined,
   listing_type: undefined,
   locations: undefined,
   area_type: undefined,
   area_min: undefined,
   area_max: undefined,
   car_parking_capacity: undefined,
 };

 if (user?.searchPreference) {
   searchParams.locations = user.searchPreference.location;
   searchParams.property_category = user.searchPreference.propertyCategory || undefined;
   searchParams.property_type = user.searchPreference.propertyType || undefined;
   searchParams.budget_min = user.searchPreference.budget_min || undefined;
   searchParams.budget_max = user.searchPreference.budget_max || undefined;
   searchParams.listing_type = user.searchPreference.listing_type || undefined;
   searchParams.area_type = user.searchPreference.area_type || undefined;
   searchParams.car_parking_capacity = user.searchPreference.car_parking_capacity || undefined;
   searchParams.area_min = user.searchPreference.area_min || undefined;
   searchParams.area_max = user.searchPreference.area_max || undefined;
 }
 // case 2: For anonymous user location provided
 if(currentLocation){
   const {lat, long} = currentLocation;
   searchParams.locations = [{ lat, long }];
 }

 // Case 3: Fallback for ip-based location if location denied
 if (!searchParams.locations?.length) {
   const { lat, long } = await fetchLocationFromIP(ip);
   searchParams.locations = [{ lat, long }];
 }

 return searchParams;
}

1.Privacy-firstโ€Šโ€”โ€ŠWe donโ€™t store precise location for anonymous users.

2.Progressive personalizationโ€Šโ€”โ€ŠThe feed improves as users engage more.

3.Smart defaultsโ€Šโ€”โ€ŠIf no budget is set, we show mid-range properties.


3. Location-Based Filtering: Fast & Accurate

We use a two-phase geo-filter approach:


A. Phase 1: Bounding Box Filter (Fast Approximate Filtering)

Before calculating exact distances, the query first applies a bounding box to quickly eliminate distant listings:

loc.latitude BETWEEN ${latitude - latDiff} AND ${latitude + latDiff}
AND
loc.longitude BETWEEN ${longitude - lonDiff} AND ${longitude + lonDiff}

B. Phase 2: Precise Distance Calculation (Haversine Formula)

After the bounding box filter, the query applies an exact distance check:

${earthRadius} * acos(
 cos(radians(${latitude})) *
 cos(radians(loc.latitude)) *
 cos(radians(loc.longitude) - radians(${longitude})) +
 sin(radians(${latitude})) *
 sin(radians(loc.latitude))
) <= ${proximityRadius}

4. Dynamic Relevance Scoring

The query ranks listings based on how well they match search criteria:

const {property_type, listing_type, area_min, area_max, budget_min , budget_max, car_parking_capacity} = searchParams;
const scoreConditions: Prisma.Sql[] = [];

A. Property Type Matching

if (propertyType?.length > 0 && !propertyType.includes(PropertyType.ALL)) {
   scoreConditions.push(
     Prisma.sql`CASE WHEN l.property_type IN (${Prisma.join(
       propertyType.map((type) => Prisma.raw(`'${type}'::property_type`)),
     )}) THEN 1 ELSE 0 END`,
   );
 }

B. Listing Type Matching

if (listingType) {
   scoreConditions.push(
     Prisma.sql`CASE WHEN l.listing_type = ${Prisma.raw(`'${listingType}'::listing_type`)} THEN 1 ELSE 0 END`,
   );
 }

C. Area Matching

if (area_min && area_max) {
 scoreConditions.push(
   Prisma.sql`CASE WHEN (
     l.area_total BETWEEN ${area_min} AND ${area_max})
   THEN 1 ELSE 0 END`
 );
}

D. Car Parking Capacity Matching

if (car_parking_capacity) {
 scoreConditions.push(
   Prisma.sql`CASE WHEN (
     l.car_parking_capacity >= ${car_parking_capacity}
   ) THEN 1 ELSE 0 END` 
 );
}

E. Budget Matching

CASE WHEN (l.price BETWEEN ${budgetMin} AND ${budgetMax}) THEN 1 ELSE 0 END

Final Score Calculation

Final score calculation is based on four to five key matching criteria, with additional optional factors that can be incorporated for more granular personalization.

(property_type_match + listing_type_match + area_match + budget_match + car_parking) AS match_score

5. Smart Sorting & Prioritization

Listings are sorted using a three-tier ranking system:

match_score DESC (Best matches first)
l.priority DESC (Boosted listings)
la.impression_count DESC (Popular listings)

This ensures:

1. Relevance (Userโ€™s search preferences)

2. Business Needs (Manually prioritized listings)

3. Engagement (Trending properties)


Final Summary

Thank you for joining this exploration of how we built a smarter property recommendation system! By combining user preferences, location data, and search history, we created feeds that adapt to each personโ€™s needsโ€Šโ€”โ€Šwhether theyโ€™re a first-time visitor or a returning user. For logged-in users, the system learns from past behavior, while guests still get relevant results using approximate locations. This balance ensures everyone finds what theyโ€™re looking for quickly.


To make searches lightning-fast, we optimized our database with smart indexing on key filters like price, property category, and property type. By pre-filtering results and caching popular listings, we cut query times by over 80%. These tweaks ensure smooth performance, even with millions of properties in our system.

CREATE INDEX idx_listing_filters ON listings (property_type, listing_type, price, property_category,area_total); 
CREATE INDEX idx_geo_search ON locations (latitude, longitude);

Looking ahead, AI will take personalization even further. Imagine the system automatically suggesting filters based on your search queries or predicting preferences you havenโ€™t even stated yet. Weโ€™re excited to explore these innovationsโ€Šโ€”โ€Šand weโ€™d love to hear your ideas too! Thanks for reading, and happy house hunting!

Subscribe to Our Newsletter

More from the engineering frontline.

Dive deep into our research and insights on design, development, and the impact of various trends to businesses.
Insight
Building Local LLMs Using Dart FFI And llama.cpp: Beyond Wrapper Packages
Sep 11, 2026

Building Local LLMs Using Dart FFI And llama.cpp: Beyond Wrapper Packages

Build local LLMs in Flutter with Dart FFI and llama.cpp, and see how native bridges, GGUF models, memory management, and token streaming enable private, on-device AI.

Insight
My Flutter App Froze With Three Photos on Screen. Here's What I Was Doing Wrong
Sep 11, 2026

My Flutter App Froze With Three Photos on Screen. Here's What I Was Doing Wrong

This blog explains how rethinking Flutterโ€™s image-processing architecture fixed severe performance issues and improved rendering efficiency.

Insight
Building a Production-Ready Canva-like Editor with Konva.js, React 19 and Next.js 15
Sep 10, 2026

Building a Production-Ready Canva-like Editor with Konva.js, React 19 and Next.js 15

This blog explains how to build a production-ready canvas editor with Konva.js, React, and Next.js, covering architecture, performance, and key engineering decisions.

Insight
Building PCI DSS-Ready AI Finance Products: Chatbot Architecture, Payment Security, and Production Challenges
Sep 9, 2026

Building PCI DSS-Ready AI Finance Products: Chatbot Architecture, Payment Security, and Production Challenges

A practical guide to building PCI DSS-compliant AI finance products, covering chatbot architecture, payment security, and governance for enterprise leaders.

Insight
What a PHP-to-NestJS Banking Migration Taught Us About Architecture, Security, and Trust
Sep 8, 2026

What a PHP-to-NestJS Banking Migration Taught Us About Architecture, Security, and Trust

This blog explores the architecture, security, performance, and documentation lessons from migrating a legacy PHP/Laravel banking platform to NestJS.

Insight
Can You Take an AI-Built MVP to Production? The Security, Scaling, IP, and Open-Source Risks Startups Need to Know
Sep 8, 2026

Can You Take an AI-Built MVP to Production? The Security, Scaling, IP, and Open-Source Risks Startups Need to Know

A practical guide to taking an AI-built MVP to production by addressing security, scalability, code ownership, licensing, and technical due diligence.

Insight
Building Production-Grade Video Thumbnail Scrubbing in the Browser: HLS, Frame Extraction, Caching, and Performance Trade-offs
Sep 7, 2026

Building Production-Grade Video Thumbnail Scrubbing in the Browser: HLS, Frame Extraction, Caching, and Performance Trade-offs

This blog explains how to build responsive video thumbnail scrubbing in the browser for local files and HLS streams, covering frame extraction, caching, and performance trade-offs.

The Right Conversation Can

Save You Six Months.

Book a call