AI-powered real estate search engine using a multi-phase deterministic pipeline. Returns only properties matching all criteria — accurate results, not just similar ones.
- FastAPI + Uvicorn (async web server)
- PostgreSQL 16 + PostGIS 3.4 (geospatial queries)
- Azure OpenAI GPT-5.1 (query parsing, vision analysis, geocoding)
- asyncpg (async PostgreSQL driver)
- Docker Compose (deployment)
cp .env.example .env
# Fill in your Azure OpenAI credentials in .envdocker-compose up --build- App:
http://localhost:8888 - PostgreSQL:
localhost:5433 - Schema auto-initializes from
schema/postgresql.sql
pip install -r requirements.txt
uvicorn src.main:app --reload --port 8888| Variable | Default | Description |
|---|---|---|
AZURE_OPENAI_API_KEY |
(required) | Azure OpenAI API key |
AZURE_OPENAI_ENDPOINT |
(required) | Azure cognitive services endpoint |
AZURE_OPENAI_DEPLOYMENT |
gpt-5.1 |
Deployment name |
AZURE_OPENAI_API_VERSION |
2025-04-01-preview |
API version |
POSTGRES_HOST |
localhost |
PostgreSQL host |
POSTGRES_PORT |
5432 |
PostgreSQL port |
POSTGRES_USER |
admin |
PostgreSQL user |
POSTGRES_PASSWORD |
admin123 |
PostgreSQL password |
POSTGRES_DB |
real_estate |
Database name |
LOG_LEVEL |
INFO |
Logging level |
Natural language property search with optional map bounds and structured filters.
Request:
{
"query": "Family home near good schools",
"bounds": {
"north": 47.7,
"south": 47.5,
"east": -122.2,
"west": -122.4
},
"filters": {
"price_min": 500000,
"price_max": 550000,
"beds_min": 1,
"baths_min": 2,
"property_types": ["SINGLE_FAMILY", "CONDO"],
"financing": ["cash", "fha"],
"sqft_min": 255,
"sqft_max": 555,
"year_from": 1990,
"year_to": 2010
},
"debug": true
}query— natural language search (required). LLM extracts features, proximity, location, and any hard filters not specified infilters.bounds— optional. Map viewport (north/south/east/west as floats).filters— optional. Explicit hard filters that override any matching field the LLM might extract fromquery. Per-field override:price_min/price_max(USD)beds_min,baths_min(min only)property_types[]— array ofSINGLE_FAMILY/CONDO/TOWNHOUSE/MANUFACTURED/MULTI_FAMILY(case-insensitive). OR semantics.financing[]— e.g.,["cash", "fha", "va_loan"]. OR semantics. Sourced from Zillow'slistingTerms.sqft_min/sqft_maxyear_from/year_to(year_built bounds)
debug— whentrue, the response includesparsed_query,stats,filter_stepsfor inspection.
Response:
{
"query": "...",
"zillowProperties": ["guid1", "guid2"]
}Health check with property count.
Upload Zillow JSON file to start background image analysis.
Response:
{
"job_id": "uuid",
"status": "processing",
"progress": "0/27 properties"
}Poll background job status.
Ingest processed data from src/processed/data.json into PostgreSQL and rebuild feature registry.
Zillow JSON file
│
▼
POST /process
│ Sends each property photo to GPT-5.1 Vision API
│ Extracts: RoomType + Features per photo
│ Runs in background (5 concurrent image analyses)
│
▼
src/processed/data.json (enriched with RoomType & Features)
│
▼
POST /saveprocesseddata
│ Converts Zillow data → PostgreSQL schema
│ Upserts properties, rooms, room_instances, schools
│ Rebuilds in-memory feature registry
│
▼
PostgreSQL (ready for search)
User Query: "3 bedrooms, wood flooring, no pool, near good schools"
(+ optional map bounds: north/south/east/west)
│
▼
Phase 1 — Query Parser (LLM)
│ Decomposes natural language → structured criteria
│ Maps synonyms to exact DB feature names
│ "wood flooring" → "hardwood floors"
│ "hearth" → "fireplace"
│
▼
Phase 2 — Hard Filters + Map Bounds (PostgreSQL indexed queries)
│ WHERE bedroom_count = 3
│ AND has_pool = FALSE
│ AND geom && ST_MakeEnvelope(...) ← if bounds provided
│ Fast elimination using B-tree + GIST spatial indexes
│
▼
Phase 3 — Proximity Filters (PostGIS spatial queries)
│ "near good schools" → schools with rating >= 7 within 5 miles
│ Fast path: fuzzy match school names in property_schools table
│ Fallback: LLM geocoding + ST_DWithin
│
▼
Phase 4 — Feature Matching
│ Matches features in room_instances
│ Uses synonym expansion (reconstructed_queries)
│ Falls back to property description text search
│ Negated features use set subtraction
│
▼
Results: Property GUIDs matching ALL criteria
| Type | Examples |
|---|---|
| Room Count | 2 bedrooms, at least 3 bathrooms |
| Features | hardwood floors, granite countertops, fireplace |
| Negation | no pool, without stone tile |
| Room-Specific Features | kitchen with white cabinets, bedroom with carpet |
| Price | under $500k, between $300k and $600k |
| Area | at least 2000 sqft |
| Location | in Seattle, in Washington state |
| Proximity | near Oak Park Elementary, near good schools |
| Property Type | single family, condo, townhouse |
| Property Attributes | with pool, waterfront, built after 2000, single story |
| Rent | rent under $2000/mo |
| Map Bounds | (sent via bounds field — filters to a lat/lng rectangle, e.g. for map viewport) |
All criteria are combined with AND logic — properties must match every condition.
The bounds field on /search filters properties to a rectangular region using PostGIS. Designed for map-based UX (Google Maps / Leaflet / Mapbox) where you want results constrained to the current viewport.
{
"query": "3 bedroom",
"bounds": {
"north": 47.7,
"south": 47.5,
"east": -122.2,
"west": -122.4
}
}- Applied in Phase 2 alongside hard filters — single SQL query, uses the GIST index on
geom - Combines with
locationcriteria (e.g., query"in Seattle"+ bounds) via AND - Invalid bounds (non-numeric) are gracefully ignored
- Omitting
boundspreserves the original search behavior (backwards compatible)
properties— Main property data (address, price, area, room counts, geom, attributes)rooms— Room types per property with countsroom_instances— Individual room features extracted from photos (featuresarray +features_text)property_schools— Nearby schools with ratings and distance
- Room counts, price, area, location — B-tree indexes for fast hard filtering
geom— GIST index for PostGIS spatial queriesfeatures— GIN array index for feature lookupsfeatures_text— GIN trigram index for fuzzy text matchingschool_name— GIN trigram index for fuzzy school matching
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── .env / .env.example
├── schema/
│ └── postgresql.sql # DB schema with PostGIS + indexes
├── config/
│ └── settings.py # Pydantic settings (env vars)
├── src/
│ ├── main.py # FastAPI app entry point
│ ├── llm_client.py # Azure OpenAI client factory
│ ├── data/
│ │ ├── database.py # asyncpg connection pool
│ │ ├── feature_registry.py # In-memory feature/room type registry
│ │ └── ingest.py # Data ingestion (mockup.json)
│ ├── models/
│ │ └── search.py # Pydantic search criteria models
│ ├── search/
│ │ ├── query_parser.py # LLM query → structured criteria
│ │ ├── filter_engine.py # Hard filters (SQL WHERE)
│ │ ├── geo_search.py # Proximity / geospatial filters
│ │ └── orchestrator.py # 4-phase search pipeline
│ ├── img_analyzer/
│ │ ├── router.py # /process, /job, /saveprocesseddata
│ │ ├── analyzer.py # Vision API image analysis
│ │ ├── db_ingest.py # Zillow data → PostgreSQL
│ │ ├── job_manager.py # Background job tracking
│ │ ├── models.py # Pydantic data models
│ │ └── prompt/
│ │ ├── prompt.txt # Vision API system prompt
│ │ └── feature.txt # Known features list
│ └── processed/ # Generated processed data
├── data.json # Sample property data
└── mockup.json # Test data
- Deterministic Search — Uses PostgreSQL indexed queries + PostGIS, not vector search. Every result matches all criteria.
- Feature Registry — Built at startup from DB. LLM maps user input to exact feature names, eliminating fuzzy matching errors.
- Denormalized Room Counts — Stored in
propertiestable for fast filtering without JOINs. - Two-Tier Geospatial — Fast path checks school table; fallback uses LLM geocoding + PostGIS.
- Background Image Processing —
/processreturns job ID immediately, client polls for completion. - Upsert Pattern — Data ingestion merges by GUID, allowing incremental updates.