---
title: Building an Agentic NLQ System for Real Estate Search
description: "SIE embeddings and Qdrant retrieval behind a GPT-4 router: cross-encoder reranking, hard filters, and five agent tools for natural language real estate search."
canonical_url: https://superlinked.com/blog/real-estate-nlq-agent
last_updated: 2026-04-14
---

Real estate search is straight-up broken. I mean we all know the drill right, It's just too much dropdowns, rigid filters, and somehow we still end up scrolling through properties that miss the mark completely. Want a "family-friendly home near good schools"? Good luck translating that into checkboxes and price sliders. I mean imagine there is a system which can just translate these queries to something meaningful reference and give us better results.

Taking into that account, I created an agentic Natural Language Querying (NLQ) system that actually understands what people want. Just tell it "Find me a 3-bedroom home in Toronto under $600,000 with 2 bedrooms and 3 bathrooms" and it figures out the rest. I mean this is one of the coolest thing right..

Here's how I combined the Superlinked Inference Engine with AI agents to create a real estate assistant that actually gets it.

![Complete Flow Diagram](/cdn/65dce6831bf9f730421e2915/blog/real_estate_agentic_nlq/flow_diagram.png)

## The Problem with Current Real Estate Search

Every real estate platform treats search like you're querying a database. It's all structured filters and rigid categories. Need a family home at a place? Here's your homework:

- Pick a price range from a dropdown
- Select bedrooms and bathrooms
- Choose your city from a list
- Cross your fingers that "good schools" somehow correlates with your selected neighborhood

This approach fails because real estate decisions are nuanced as hell. It's super hard to translate the relevant requirements to the dropdowns and rigid filters. When someone says "affordable family home," they could mean totally different things based on their income, family size, and what they consider "affordable." I mean we need something where we can just put out the queries and get the relevant results. That's where NLQ comes into play.

## What's Natural Language Querying All About?

NLQ flips the script. Instead of forcing users to translate their thoughts into database queries, it lets them just... talk normally.

**Old way**: `Price: $400K-$600K, Bedrooms: 3+, Location: Toronto`

**New way**: `"Find me a 3-bedroom home in Toronto under $600,000"`

The system handles the translation from human language to structured queries while actually understanding context and intent.

## What it took to made it..

Well there are couple of the different components that I glued together which provides NLQ capabilities, semantic search, and contextual reasoning.

- **SIE (Superlinked Inference Engine)**: the open-source inference server at the core of this pipeline. SIE serves the embedding model over HTTP, so every property description and user query gets encoded through the same endpoint. It handles the `encode` and `score` calls that power both retrieval and reranking.
- **Qdrant**: the vector database that stores the property embeddings produced by SIE and handles approximate nearest neighbour search at query time.
- **LLM**: Well for this project, I used OpenAI GPT-4 which powers both the intent classification and generative outputs such as recommendations, insight narratives, and query reformulations.

You can use any LLM or any embedding model supported by SIE based on the requirements.

![NLQ_Only](/cdn/65dce6831bf9f730421e2915/blog/real_estate_agentic_nlq/NLQ_only.png)

## Data Requirements

To demonstrate how the NLQ system and agentic flow work in practice, I used a dataset that reflects what you'd typically find in real-world property listings. This includes property-level attributes like price, number of bedrooms and bathrooms, and address, along with geographic fields such as city and province.

To support more context-aware reasoning, the data also includes demographic details like population size and median household income, useful for queries that imply lifestyle or investment considerations.

## How I Built This Thing

### Step 1: Start SIE and install dependencies

```bash
# Start the SIE inference server (CPU)
docker run -p 8080:8080 ghcr.io/superlinked/sie-server:latest-cpu-default
```

```bash
pip install sie-sdk qdrant-client openai pandas
```

### Step 2: Embed properties and index them in Qdrant

SIE encodes every property description into a dense vector. We then upsert those vectors into Qdrant so they can be searched at query time.

```python
import pandas as pd
from sie_sdk import SIEClient
from sie_sdk.types import Item
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct

EMBED_MODEL = "sentence-transformers/all-mpnet-base-v2"
RERANK_MODEL = "BAAI/bge-reranker-v2-m3"
COLLECTION = "real_estate"
VECTOR_DIM = 768

sie = SIEClient("http://localhost:8080")
qdrant = QdrantClient(":memory:")

# Load the property dataset
df = pd.read_csv("canadian_real_estate.csv")

# Build a text description for each listing that captures all searchable attributes
df["description"] = (
    df["address"] + ", " + df["city"] + ", " + df["province"]
    + ". " + df["number_beds"].astype(str) + " bed, "
    + df["number_baths"].astype(str) + " bath. "
    + "$" + df["price"].astype(str) + ". "
    + "Population: " + df["population"].astype(str) + ". "
    + "Median income: $" + df["median_family_income"].astype(str) + "."
)

# Encode all descriptions with SIE
items = [Item(text=desc) for desc in df["description"].tolist()]
embeddings = sie.encode(EMBED_MODEL, items)

# Create Qdrant collection and upsert
qdrant.create_collection(
    collection_name=COLLECTION,
    vectors_config=VectorParams(size=VECTOR_DIM, distance=Distance.COSINE),
)

points = [
    PointStruct(
        id=int(row["id"]),
        vector=embeddings[i]["dense"].tolist(),
        payload={
            "address": row["address"],
            "city": row["city"],
            "province": row["province"],
            "price": int(row["price"]),
            "number_beds": int(row["number_beds"]),
            "number_baths": int(row["number_baths"]),
            "population": int(row["population"]),
            "median_family_income": int(row["median_family_income"]),
            "description": row["description"],
        },
    )
    for i, (_, row) in enumerate(df.iterrows())
]

qdrant.upsert(collection_name=COLLECTION, points=points)
print(f"Indexed {len(points)} properties")
```

### Step 3: Build the search function

At query time, we encode the user's natural language query with SIE, search Qdrant for the nearest vectors, and optionally rerank the results with a cross-encoder, all through the same SIE server.

```python
from qdrant_client.models import Filter, FieldCondition, Range

def search_properties(
    query: str,
    top_k: int = 10,
    rerank: bool = True,
    price_max: int = None,
    min_beds: int = None,
) -> pd.DataFrame:
    # Encode the query
    query_embedding = sie.encode(EMBED_MODEL, Item(text=query))["dense"].tolist()

    # Build optional hard filters
    must = []
    if price_max:
        must.append(FieldCondition(key="price", range=Range(lte=price_max)))
    if min_beds:
        must.append(FieldCondition(key="number_beds", range=Range(gte=min_beds)))
    query_filter = Filter(must=must) if must else None

    # Vector search
    hits = qdrant.search(
        collection_name=COLLECTION,
        query_vector=query_embedding,
        query_filter=query_filter,
        limit=top_k * 3 if rerank else top_k,
    )

    if not hits:
        return pd.DataFrame()

    results = pd.DataFrame([h.payload | {"relevance_score": h.score} for h in hits])

    # Rerank with SIE cross-encoder
    if rerank and len(results) > 0:
        scores = sie.score(
            RERANK_MODEL,
            Item(text=query),
            [Item(text=desc) for desc in results["description"].tolist()],
        )
        results["rerank_score"] = [s["score"] for s in scores["scores"]]
        results = results.sort_values("rerank_score", ascending=False).head(top_k)

    return results.reset_index(drop=True)
```

Notice price gets 100x importance when passed as a hard filter; budget is usually the make-or-break factor. No point showing someone a $2M mansion when they said "affordable." Crazy laugh here...

### Why Agents + NLQ for Real Estate Search?

Before we get into the tools and how they simplify everything, let's take a quick look at why pairing agents with Natural Language Querying (NLQ) leads to such a powerful search experience.

1. **NLQ helps machines speak our language.**

   When someone says, "affordable family homes under $250,000," the agent parses that into a structured `search_properties()` call with the right price cap and ranking intent.

2. **But on its own, search hits a ceiling.**

   Sure, it's great at filtering by the criteria as I mention, but it tends to fall short when:

   - The results are too few or too literal, without understanding the intent behind the query.
   - You're looking for personalised suggestions that go beyond the filters. I mean just think when you have let's say 10 different properties which are completing your criteria but you still need some kind of personalised recommendations.
   - You want to compare multiple results in context.

3. **That's where agents step in.**

   Agents bring reasoning to the table. They can:

   - Recognise when a search is too narrow and offer smarter alternatives.
   - Read between the lines and pick up on what the user might actually want. So instead of just throwing the results, we can utilize these agents to make more of those findings.
   - Handle more complex workflows, like comparing options or building narratives from raw data.

So when you blend SIE-powered search (which encodes and ranks) with agents (which think and act), you get something far more intelligent: a system that doesn't just understand your request but also knows how to respond meaningfully. That's the sweet spot.

### Step 4: The Agentic Layer - Where the Magic Happens

Ok, Here's where it starts to get fun. Rather than building a one-size-fits-all (and honestly, kind of dumb) search tool, I designed an agentic system made up of five specialised tools, each one kicks in based on what the user is actually trying to do.

![Agentic_Sys](/cdn/65dce6831bf9f730421e2915/blog/real_estate_agentic_nlq/Agentic_only.png)

```python
from abc import ABC, abstractmethod
from typing import Any
from openai import OpenAI

openai_client = OpenAI()

class Tool(ABC):
    @abstractmethod
    def name(self) -> str: pass

    @abstractmethod
    def description(self) -> str: pass

    @abstractmethod
    def use(self, query: str, openai_client: Any) -> str: pass

class RealEstateAgent:
    def __init__(self):
        self.tools = [
            PropertyRetrievalTool(),      # Basic "show me homes" requests
            PropertyRecommendationTool(), # "What's good for me?" queries
            QueryRefinementTool(),        # When searches come up empty
            NarrativeInsightTool(),       # Market analysis stuff
            MultiStepQueryTool()          # "Compare Toronto vs Vancouver"
        ]
```

Each tool has a specific job. Let me break them down all of them one step at a time..

### PropertyRetrievalTool

This one's is used for the straightforward lookups. So if we type a query something like, _"3-bedroom homes in Oshawa under $600K."_ This tool encodes the query with SIE, searches Qdrant, reranks with a cross-encoder, and returns the listings. It's quick, clean, and no fuss.

If nothing shows up, it gracefully passes the action to `QueryRefinementTool` which takes care of the situation. More on this a bit later..

```python
def format_property_display(df: pd.DataFrame, query: str, detailed: bool = False) -> str:
    lines = []
    for _, row in df.iterrows():
        line = (
            f"{row['city']} | {row['address']} | "
            f"${row['price']:,} | {int(row['number_beds'])}bed "
            f"{int(row['number_baths'])}bath | "
            f"score: {row.get('rerank_score', row.get('relevance_score', 0)):.3f}"
        )
        lines.append(line)
    return "\n".join(lines)

class PropertyRetrievalTool(Tool):
    def name(self) -> str:
        return "PropertyRetrievalTool"

    def description(self) -> str:
        return "Retrieves properties based on natural language queries using SIE embeddings and reranking"

    def use(self, query: str, openai_client: Any) -> str:
        try:
            results = search_properties(query, top_k=10, rerank=True)
            if results.empty:
                return QueryRefinementTool().use(query, openai_client)
            return f"Search Results for '{query}':\n\n{format_property_display(results, query, detailed=True)}"
        except Exception as e:
            return f"Error in property retrieval: {str(e)}"
```

### PropertyRecommendationTool

This tool isn't just matching filters, it's more like a reasoning. When someone enters a query, it tries to understand their _intent_ and _preferences_. For example, it might see that "affordable housing for a growing family" suggests a budget, space needs, and a preference for family-friendly areas.

It:

- Uses GPT-4 to extract a lightweight user profile from the query.
- Fetches and reranks results with SIE.
- Then it generates top 3 recommendations _with pros and cons_, using both the data and inferred preferences.

It's like having a real estate advisor who reads between the lines.

```python
class PropertyRecommendationTool(Tool):
    def name(self) -> str:
        return "PropertyRecommendationTool"

    def description(self) -> str:
        return "Provides personalized property recommendations based on inferred user preferences"

    def use(self, query: str, openai_client: Any) -> str:
        profile_prompt = f"""
        Extract user preferences from this query, such as budget, family size, commute preferences, or lifestyle (e.g., urban, family-friendly).
        Query: "{query}"
        Return a JSON object with inferred preferences (e.g., {{"budget": 800000, "family_size": 4}}). If none, return {{}}.
        """
        try:
            profile_response = openai_client.chat.completions.create(
                model="gpt-4",
                messages=[{"role": "user", "content": profile_prompt}],
                temperature=0.3,
                max_tokens=100,
            )
            user_profile = eval(profile_response.choices[0].message.content.strip()) or {}
        except Exception:
            user_profile = {}

        results = search_properties(query, top_k=15, rerank=True)
        if results.empty:
            return "No properties found to recommend."

        properties_summary = results.head(10)[
            ["address", "city", "province", "price", "number_beds", "number_baths",
             "population", "median_family_income", "rerank_score"]
        ].to_dict("records")

        prompt = f"""
        You are a real estate advisor. Provide recommendations based on:
        Query: "{query}"
        Inferred Profile: {user_profile}
        Properties (ranked by SIE reranker): {properties_summary}

        Provide:
        1. Top 3 recommendations with reasons (include address) - respect the reranker ranking
        2. Pros and cons for each
        3. Additional considerations (e.g., neighborhood, lifestyle fit)
        """
        try:
            response = openai_client.chat.completions.create(
                model="gpt-4",
                messages=[{"role": "user", "content": prompt}],
                temperature=0.7,
                max_tokens=800,
            )
            recommendations = response.choices[0].message.content.strip()
            formatted = format_property_display(results.head(5), query, detailed=True)
            return f"Recommendations for '{query}':\n\n{recommendations}\n\nProperty Listings (SIE Ranked):\n{formatted}"
        except Exception as e:
            return f"Error generating recommendations: {str(e)}"
```

### QueryRefinementTool

Sometimes people are too specific and get little (or nothing) back. That's where this tool steps in. If a query returns too few results, it uses GPT-4 to suggest a smarter alternative, maybe relaxing price, broadening the location, or tweaking filters, then reruns it through SIE search.

```python
class QueryRefinementTool(Tool):
    def name(self) -> str:
        return "QueryRefinementTool"

    def description(self) -> str:
        return "Refines queries when results are sparse or irrelevant"

    def use(self, query: str, openai_client: Any) -> str:
        results = search_properties(query, top_k=10, rerank=True)

        if len(results) >= 3:
            return f"Search Results for '{query}':\n\n{format_property_display(results, query, detailed=True)}"

        refine_prompt = f"""
        The query "{query}" returned {len(results)} results. Suggest an alternative query to get more relevant results.
        Consider relaxing filters (e.g., price, location) or expanding scope (e.g., nearby cities).
        Return a single alternative query as a string.
        """
        try:
            response = openai_client.chat.completions.create(
                model="gpt-4",
                messages=[{"role": "user", "content": refine_prompt}],
                temperature=0.3,
                max_tokens=50,
            )
            new_query = response.choices[0].message.content.strip()

            new_results = search_properties(new_query, top_k=10, rerank=True)
            if new_results.empty:
                return f"No properties found for '{query}'. Suggested query '{new_query}' also returned no results."

            return (
                f"Original Query: '{query}' returned {len(results)} results.\n"
                f"Suggested Query: '{new_query}'\n"
                f"Results for '{new_query}' (SIE Ranked):\n"
                f"{format_property_display(new_results, new_query, detailed=True)}"
            )
        except Exception as e:
            return f"Error refining query: {str(e)}"
```

### NarrativeInsightTool

This tool's for the data-savvy users, the investors, the analysts, the curious minds. It takes a query, retrieves and reranks results with SIE, computes market stats, and then wraps all that data into a readable narrative: market trends, investment potential, and key takeaways based on the context.

You get both the numbers and the story behind them. So instead of just seeing the relevant results in the tabular format, this tool gives the results a more readable format.

```python
class NarrativeInsightTool(Tool):
    def name(self) -> str:
        return "NarrativeInsightTool"

    def description(self) -> str:
        return "Provides narrative insights for complex queries (e.g., investment potential)"

    def use(self, query: str, openai_client: Any) -> str:
        results = search_properties(query, top_k=10, rerank=True)
        if results.empty:
            return "No properties found for analysis."

        stats = {
            "avg_price": int(results["price"].mean()),
            "median_price": int(results["price"].median()),
            "price_range": (int(results["price"].min()), int(results["price"].max())),
            "avg_beds": round(results["number_beds"].mean(), 1),
            "avg_baths": round(results["number_baths"].mean(), 1),
            "cities": results["city"].value_counts().head(3).to_dict(),
            "avg_income": int(results["median_family_income"].mean()),
        }

        prompt = f"""
        Provide a narrative analysis for the query: "{query}"
        Data: {stats}
        Properties (SIE reranker ranked): {results.head(5)[["address", "city", "price", "number_beds", "number_baths", "rerank_score"]].to_dict("records")}

        Include:
        1. Market overview
        2. Investment potential (if relevant)
        3. Key considerations (e.g., location, price trends)
        """
        try:
            response = openai_client.chat.completions.create(
                model="gpt-4",
                messages=[{"role": "user", "content": prompt}],
                temperature=0.7,
                max_tokens=600,
            )
            insights = response.choices[0].message.content.strip()
            formatted = format_property_display(results, query, detailed=True)
            return f"Insights for '{query}':\n\n{insights}\n\nProperty Listings (SIE Ranked):\n{formatted}"
        except Exception as e:
            return f"Error generating insights: {str(e)}"
```

### MultiStepQueryTool

This one's built for the power users, the folks asking things like, _"Compare properties in Toronto vs. family homes in Ontario under $300K."_ That's not a simple query; it's actually two or more requests bundled into one.

Instead of throwing its hands up, this tool does it:

- It just breaks down the question into manageable parts, something like more doable.
- Now it runs those sub-queries independently through SIE search.
- Then comes the comparison where it just compares them side by side.

It's what makes the whole system feel flexible, like it _understands_ layered intent instead of forcing users to rephrase things over and over.

```python
import re

class MultiStepQueryTool(Tool):
    def name(self) -> str:
        return "MultiStepQueryTool"

    def description(self) -> str:
        return "Handles queries requiring multiple SIE search calls (e.g., comparisons)"

    def use(self, query: str, openai_client: Any) -> str:
        compare_pattern = re.compile(r"\b(compare|versus|vs\.?)\b", re.IGNORECASE)
        if not compare_pattern.search(query):
            return "Query does not require comparison."

        prompt = f"""
        Split the query into sub-queries for comparison.
        Query: "{query}"
        Return a JSON list of sub-queries (e.g., ["3 bedroom homes in Toronto", "3 bedroom homes in Kitchener"]).
        """
        try:
            response = openai_client.chat.completions.create(
                model="gpt-4",
                messages=[{"role": "user", "content": prompt}],
                temperature=0.3,
                max_tokens=100,
            )
            sub_queries = eval(response.choices[0].message.content.strip())
            if not isinstance(sub_queries, list) or len(sub_queries) < 2:
                return "Unable to split query for comparison."

            results_dict = {}
            for sub_query in sub_queries[:2]:
                results_dict[sub_query] = search_properties(sub_query, top_k=5, rerank=True)

            data_str = ""
            for sub_query, df_sub in results_dict.items():
                data_str += f"\n{sub_query} (SIE ranked):\n"
                for _, row in df_sub.head(3).iterrows():
                    data_str += (
                        f"  - Address: {row['address']}, City: {row['city']}, "
                        f"Price: ${row['price']:,.0f}, Beds: {row['number_beds']}, "
                        f"Baths: {row['number_baths']}, Score: {row['rerank_score']:.3f}\n"
                    )

            compare_prompt = f"""
            Compare properties from these sub-queries (each ranked by SIE reranker):
            {list(results_dict.keys())}
            Data:
            {data_str}

            Provide:
            1. Side-by-side comparison (price, beds, baths, rerank score)
            2. Key differences (e.g., location, value, market positioning)
            3. Recommendations for different buyer types
            """
            response = openai_client.chat.completions.create(
                model="gpt-4",
                messages=[{"role": "user", "content": compare_prompt}],
                temperature=0.7,
                max_tokens=800,
            )
            comparison = response.choices[0].message.content.strip()

            output = [f"Comparison for '{query}':\n\n{comparison}"]
            for sub_query, df_sub in results_dict.items():
                if not df_sub.empty:
                    output.append(
                        f"\nResults for '{sub_query}' (SIE Ranked):\n"
                        f"{format_property_display(df_sub, sub_query, detailed=True)}"
                    )
            return "\n".join(output)
        except Exception as e:
            return f"Error processing comparison: {str(e)}"
```

### Step 5: Intent Classification - Figuring Out What the User Really Wants

At this point, the system needs to make a smart decision on which tool we should invoke? _Which tool is the best fit for this query?_

To do that, it relies on GPT-4 to act like a traffic director. I mean more like a router which takes the query and classifies the intent behind it. Based on the outcome, it routes the query to the right tool:

```python
class RealEstateAgent:
    def __init__(self):
        self.tools = {
            "retrieval": PropertyRetrievalTool(),
            "recommendation": PropertyRecommendationTool(),
            "refinement": QueryRefinementTool(),
            "insight": NarrativeInsightTool(),
            "multistep": MultiStepQueryTool(),
        }

    def classify_query(self, query: str) -> str:
        valid_categories = set(self.tools.keys())
        prompt = f"""
Classify the query into one category based on its intent, returning only the category name ('retrieval' if unsure):
- retrieval: Search for properties by criteria (e.g., location, price, bedrooms).
- recommendation: Personalized property suggestions for user needs.
- refinement: Strict criteria likely yielding few results, needing adjustment.
- insight: Analysis, investment advice, or market trends.
- multistep: Comparison or multiple criteria across locations.
Query: "{query}"
        """
        try:
            response = openai_client.chat.completions.create(
                model="gpt-4",
                messages=[{"role": "user", "content": prompt}],
                temperature=0.3,
                max_tokens=20,
            )
            classification = response.choices[0].message.content.strip().lower()
            return classification if classification in valid_categories else "retrieval"
        except Exception:
            return "retrieval"

    def run(self, query: str) -> str:
        category = self.classify_query(query)
        print(f"Query classified as: {category}")
        return self.tools[category].use(query, openai_client)
```

To understand how the system operates in practice, let's walk through a few real-world queries and how they are handled end-to-end.

### Example 1: Basic Search

Consider the query: "3 bedroom homes in Oshawa under \$600,000." The system classifies this as a retrieval task. SIE encodes the query, Qdrant retrieves the nearest listings, and the SIE reranker puts the most relevant ones at the top.

```markdown
Processing query: 3 bedroom homes in Oshawa under $600,000
Query classified as: retrieval
Search Results for '3 bedroom homes in Oshawa under $600,000':

Oshawa | #70 -53 TAUNTON RD E | $520,000 | 3bed 3bath | score: 0.418
Oshawa | 145 BANTING AVE | $499,900 | 6bed 5bath | score: 0.417
```

### Example 2: Personalised Recommendations

In this case, the query is: _"I live in Toronto, show me the most expensive option that would be good for me."_ The system classifies this under the recommendation category. It infers that the user is located in Toronto and likely has a preference for high-end listings. Now if this thing just went straight to a keyword search, the intent would get lost, that's where the agentic tooling helps.

So now the query passes through the router, then the router directs that to `RecommendationTool`, and then that tool extracts additional implicit preferences (e.g., lifestyle, city familiarity, price range), fetches and reranks properties using SIE, and then generates recommendations with a short rationale, including pros and cons for each option.

THIS IS TOO MUCH HANDY MANNN

```markdown
Processing query: I want to know which will be a good option for me if I live in Toronto, most expensive one
Query classified as: recommendation
Recommendations for 'I want to know which will be a good option for me if I live in Toronto, most expensive one':

1. Top 3 Recommendations:

   1.1. Property: #LPH19 -2095 LAKE SHORE BLVD W
   Reasons: This property is the most expensive on the list, priced at $4,336,900...
```

### Example 3: Query Refinement

The query "3 bedroom homes in Oshawa under $300,000" is classified as a retrieval request, but the result set is empty due to overly strict constraints. Instead of returning nothing, the system triggers a refinement process.

It uses GPT-4 to suggest a more realistic variant like "3 bedroom homes in Oshawa under $400,000", reruns it through SIE search, and displays both the original and refined results. This helps the user quickly iterate without needing to rephrase their search.

```markdown
Processing query: 3 bedroom homes in Oshawa under $300,000
Query classified as: retrieval

Original Query: '3 bedroom homes in Oshawa under $300,000' returned 0 results.
Suggested Query: '3 bedroom homes in Durham Region under $400,000'
Results for '3 bedroom homes in Durham Region under $400,000' (SIE Ranked):
Calgary | 84 Whitehaven Road NE | $329,000 | 3bed 2bath | score: 0.793
St. John's | 28 Chapman Crescent | $199,900 | 3bed 1bath | score: 0.790
...
```

### Example 4: Market Insight Generation

When a user asks: "What's a good investment in Toronto?" the system identifies this as an insight-oriented query. It retrieves and reranks results with SIE, computes market stats, then produces a narrative summary covering average prices, market trends, top-performing neighbourhoods, and potential investment signals.

```markdown
Processing query: What's a good investment in Toronto?
Query classified as: insight
Insights for 'What's a good investment in Toronto?':

Market Overview:
The Toronto property market is characterized by an average property price of $577,158...
```

### Example 5: Comparative Search

In the query: "Compare 3 bedroom homes in Toronto and Kitchener," the system recognizes the need for multi-step reasoning. It decomposes the query into two independent sub-queries, runs each through SIE search and reranking, and then formats the results into a side-by-side comparison.

```markdown
Processing query: Compare 3 bedroom homes in Toronto and Kitchener
Query classified as: multistep
Comparison for 'Compare 3 bedroom homes in Toronto and Kitchener':

1. Side-by-side comparison:
   - 3 bedroom homes in Toronto: Prices: $929,000, $848,000, $1,149,999
   - 3 bedroom homes in Kitchener: Prices: $589,000, $649,900, $624,900
...
```

Here is the concluded look of how the whole system looks under the hood..

![complete_system](/cdn/65dce6831bf9f730421e2915/blog/real_estate_agentic_nlq/NLQ_Agentic.png)

Well before ending this one, there are couple of points I want your attention. So doing this end to end, we took care of a few core architectural choices that shaped how this system really works.

The most important one here is that I intentionally gave more weight to the Price by passing it as a hard filter to Qdrant, so it acts as a strict constraint, while the SIE embedding and reranker handle the semantic ranking within those boundaries.

The SIE reranker is what gives the results their final ordering. It scores every query-document pair through a cross-encoder rather than just cosine distance, which is why results feel like they actually understand what the user meant rather than just matching keywords.

For production deployment, autoscaling, GPU routing, and Kubernetes, the [SIE repo](https://github.com/superlinked/sie) includes Helm charts and Terraform modules for GKE and EKS. The full guide is at [sie.dev/docs/deployment](https://sie.dev/docs/deployment/).

---
## Contributors

- [Vipul Maheshwari, author](https://www.linkedin.com/in/vipulmaheshwarii/)
- [Filip Makraduli, reviewer](https://www.linkedin.com/in/filipmakraduli/)
