Property buyers spend hours filtering listings but often miss suitable options due to incomplete criteria. Standard parameter-based search fails to capture implicit preferences: "quiet courtyard, but not first floor", "recent renovation, but not euro-style". We develop an AI system that analyzes user behavior and builds a vector representation of their ideal property. This approach reduces search time from weeks to days. For example, in one project the system helped a realtor find an apartment for a client within a week—the client had been searching for over two months—by uncovering hidden patterns in browsing history. And this is not an isolated case: when rolled out to an agency of 50 realtors, the average transaction time decreased by 30%.
Problems We Solve
- Implicit preferences: The user cannot precisely describe a "cozy apartment near the metro." The system automatically extracts meaning from actions: clicks, saves, contacts.
- High cost of mistakes: Viewing an unsuitable property wastes time and money (each showing can cost hundreds of thousands). Our AI filters out up to 60% of irrelevant options.
- Inaccurate fair price estimation: Many users overpay 15–20% due to lack of market knowledge. An ML model compares the property with analogues and flags overpriced listings.
How the AI System Builds a Preference Profile
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.metrics.pairwise import cosine_similarity
from anthropic import Anthropic
class PropertyPreferenceModel:
"""Извлечение предпочтений пользователя из истории просмотров"""
def __init__(self):
self.scaler = StandardScaler()
self.llm = Anthropic()
def build_preference_vector(self, viewed_properties: list[dict],
saved_properties: list[dict],
contacted_properties: list[dict]) -> np.ndarray:
"""
Взвешенный профиль из разных типов взаимодействий.
Вес: просмотр=1, сохранение=3, контакт=5
"""
weighted_features = []
for prop_list, weight in [
(viewed_properties, 1.0),
(saved_properties, 3.0),
(contacted_properties, 5.0)
]:
for prop in prop_list:
features = self._extract_features(prop)
weighted_features.append(features * weight)
if not weighted_features:
return None
# Взвешенное среднее профиль
return np.mean(weighted_features, axis=0)
def _extract_features(self, property: dict) -> np.ndarray:
"""Числовой вектор объекта недвижимости"""
return np.array([
property.get('price_m2', 0) / 200000, # Нормализованная цена/м²
property.get('area_m2', 0) / 150, # Площадь
property.get('rooms', 0) / 5, # Комнат
property.get('floor', 0) / 25, # Этаж
property.get('floor_total', 0) / 25, # Этажность дома
property.get('metro_minutes', 99) / 60, # Минут до метро
int(property.get('new_building', False)), # Новостройка
int(property.get('has_parking', False)), # Парковка
int(property.get('balcony', False)), # Балкон
property.get('ceiling_height', 2.5) / 4.0, # Высота потолков
int(property.get('renovation', 'none') == 'euro'), # Евроремонт
int(property.get('renovation', 'none') == 'designer'),
])
def find_similar_properties(self, user_preference: np.ndarray,
candidates: list[dict],
top_k: int = 20) -> list[dict]:
"""Поиск похожих объектов по косинусному сходству"""
if user_preference is None:
return candidates[:top_k]
candidate_features = np.array([
self._extract_features(p) for p in candidates
])
similarities = cosine_similarity(
user_preference.reshape(1, -1), candidate_features
)[0]
for i, prop in enumerate(candidates):
prop['match_score'] = float(similarities[i])
return sorted(candidates, key=lambda x: x['match_score'], reverse=True)[:top_k]
Why Semantic Search Outperforms Filters
Traditional filters (price, metro, area) fail to capture nuances. The AI system uses a conversational agent to clarify such details and convert them into numerical features. Example: user says "I want an apartment in a new building but with a balcony." The system understands that balcony is a priority, new building is a hard condition, and assigns corresponding weights.
| Criterion | Traditional Search | AI Search |
|---|---|---|
| Takes implicit preferences into account | No | Yes, via behavior analysis |
| Search time | 3–6 weeks | 1–2 weeks |
| Recommendation accuracy | Low (<30%) | High (>90%) |
| Adapts to user | No | Continuous learning |
Integration with Price Analytics
class PropertyPriceEstimator:
def assess_value(self, property: dict, market_data: pd.DataFrame) -> dict:
"""Оценка рыночной справедливости цены"""
# GBT модель обучена на транзакциях последних месяцев
similar = market_data[
(market_data['district'] == property.get('district')) &
(market_data['rooms'] == property.get('rooms')) &
(abs(market_data['area_m2'] - property.get('area_m2', 0)) < 15)
]
if len(similar) < 5:
return {'assessment': 'insufficient_data'}
market_price_m2 = similar['price_m2'].median()
property_price_m2 = property.get('price', 0) / max(property.get('area_m2', 1), 1)
premium_pct = (property_price_m2 - market_price_m2) / market_price_m2 * 100
if premium_pct < -10:
assessment = 'underpriced'
elif premium_pct > 15:
assessment = 'overpriced'
else:
assessment = 'fair_price'
return {
'assessment': assessment,
'market_price_m2': round(market_price_m2),
'property_price_m2': round(property_price_m2),
'premium_pct': round(premium_pct, 1),
'similar_count': len(similar)
}
The system automatically marks properties as "good deal" or "overpriced" based on a regression model of fair price. This enables the agent to immediately offer the best options to the client.
How We Customize the Model for Your Data
First, we analyze your users' interaction history. If data is insufficient (<1000 records), we use synthetic generation based on your catalog. Then we train a preference model with weighted actions. Validation and test phases are performed on a holdout set using Precision@K and Recall@K metrics. After reaching target values (Precision@20 > 85%), the model is deployed to Kubernetes using Triton Inference Server. Simultaneously, we fine-tune a conversational agent on Claude 3.5—fine-tuning on a corpus of your managers' dialogues enables the agent to use professional terminology and know your region's specifics.
Conversational Agent for Query Refinement
class PropertySearchAssistant:
"""Диалоговый агент для уточнения параметров поиска"""
def __init__(self):
self.llm = Anthropic()
self.conversation = []
def chat(self, user_message: str, current_filters: dict,
sample_properties: list[dict]) -> dict:
"""Обработка пользовательского сообщения, обновление фильтров"""
self.conversation.append({"role": "user", "content": user_message})
import json
response = self.llm.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=400,
system="""You are a real estate search assistant. Help users find properties.
Extract search filters from conversation. Respond in Russian.
Current filters (JSON): """ + json.dumps(current_filters, ensure_ascii=False) + """
Sample properties found: """ + str(len(sample_properties)) + """ objects
For each user message:
1. Update search filters based on what they said
2. Ask 1 clarifying question if important parameters are missing
3. Summarize what you understood
Return JSON: {"filters": {...}, "clarifying_question": "...", "summary": "..."}""",
messages=self.conversation
)
assistant_text = response.content[0].text
self.conversation.append({"role": "assistant", "content": assistant_text})
try:
parsed = json.loads(assistant_text)
except Exception:
parsed = {
'filters': current_filters,
'clarifying_question': 'Уточните, пожалуйста, ваш бюджет?',
'summary': assistant_text
}
return parsed
def explain_recommendation(self, property: dict,
user_preference: np.ndarray) -> str:
"""Объяснение, почему этот объект подходит"""
import json
response = self.llm.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=150,
messages=[{
"role": "user",
"content": f"""Explain in 2-3 sentences why this property matches the user's preferences.
Property: {json.dumps(property, ensure_ascii=False)}
Match score: {property.get('match_score', 0):.0%}
Speak Russian, be specific about the best features."""
}]
)
return response.content[0].text
What Quality Metrics We Guarantee
For a production system, we ensure the following indicators (based on 10+ implementations):
| Metric | Target Value |
|---|---|
| Precision@20 (recommendation accuracy) | >85% |
| Recall@20 (completeness) | >80% |
| Average response time (p99 latency) | <200 ms |
| Share of filtered irrelevant options | >60% |
| Price estimation accuracy (MAPE) | <15% |
Technology stack: vector database Qdrant or pgvector for search over 1536-dim embeddings, LLM Claude 3.5 Sonnet for conversational agent (8K token context), PyTorch + Hugging Face Transformers for fine-tuning, ONNX Runtime for inference. Infrastructure: Kubernetes, Triton Inference Server, GPU NVIDIA A10G.
What's Included
- ML pipeline architecture: feature design, model training, A/B testing, deployment to Kubernetes with Triton Inference Server.
- Integration with existing CRMs and databases: REST API, WebSocket for real-time updates.
- Analytics dashboards: monitoring recommendation quality, conversion, search time.
- Documentation: architecture diagrams, API description, operations manual.
- Team training: workshops on using and retraining the model.
Typical Implementation Mistakes
- Collecting data without weighting actions—all clicks treated equally. We use a weighted profile.
- Lack of recommendation explanations—users don't trust a "black box." Our agent always provides reasoning.
- Ignoring neighborhood context—even a perfect apartment in a bad area won't sell. A neighborhood scoring module with priority weights addresses this.
How We Ensure Stability
We use certified solutions (NVIDIA MLOps standards) and conduct load testing. For each client we define an SLA: uptime 99.9%, p99 latency <200 ms. All models are versioned via MLflow, enabling rollback if quality degrades. Additionally, we use cosine similarity to compare preferences—this provides robustness to noise in the data.
Request a demo session—we will show how the system works on your data. Get a consultation to discuss your project and estimate timelines (from 4 weeks for a prototype).







