Recommendation Logic
AI-powered matching algorithms and recommendation system
Recommendation Logic
The Esto platform uses advanced AI algorithms to intelligently match properties with potential clients. Our recommendation system combines multiple approaches to deliver highly accurate and personalized matches.
🧠 AI Architecture Overview
Multi-Layer Matching System
Our recommendation engine operates on multiple layers:
- Vector Similarity Search - High-performance similarity matching using embeddings
- Business Rules Engine - Domain-specific logic for real estate matching
- Collaborative Filtering - Learning from historical match patterns
- Content-Based Filtering - Property feature analysis
- Hybrid Scoring - Combined scoring algorithm
🔍 Matching Process
Step 1: Data Preprocessing
Before matching, the system processes and normalizes data:
// Property embedding generation
fn generate_property_embedding(property: &Property) -> Vec<f32> {
let features = vec![
normalize_price(property.price),
normalize_surface(property.surface),
encode_property_type(&property.property_type),
encode_location(&property.wilaya, &property.baladiya),
normalize_bedrooms(property.bedrooms),
normalize_bathrooms(property.bathrooms)
];
// Convert to embedding vector
embed_features(&features)
}Step 2: Vector Similarity Search
The system uses Qdrant vector database for high-performance similarity search:
// Fetch contact vectors from Qdrant
let contact_vectors = qdrant_client
.fetch_all_contact_vectors()
.await?;
// Compute similarity scores
let matches = scorer.score_contacts(
property_embedding,
&contact_vectors,
max_matches,
min_similarity_threshold
);Step 3: Business Rules Application
Domain-specific rules are applied to refine matches:
fn apply_business_rules(contact: &Contact, property: &Property) -> f32 {
let mut score = 1.0;
// Budget compatibility
if !is_budget_compatible(&contact, &property) {
score *= 0.0;
}
// Location preference
if contact.wilaya != property.wilaya {
score *= 0.7;
}
// Property type preference
if contact.prefered_property_type != property.property_type {
score *= 0.8;
}
// Surface area compatibility
if !is_surface_compatible(&contact, &property) {
score *= 0.6;
}
score
}🎯 Scoring Algorithms
1. Budget Compatibility Score
fn calculate_budget_score(contact: &Contact, property: &Property) -> f32 {
let property_price = property.price;
let mut best_score = 0.0;
for (min_budget, max_budget) in contact.min_budgets.iter()
.zip(contact.max_budgets.iter()) {
let score = if property_price >= *min_budget && property_price <= *max_budget {
1.0
} else if property_price < *min_budget {
// Below budget - partial score
(property_price as f32 / *min_budget as f32).min(0.8)
} else {
// Above budget - exponential decay
let ratio = *max_budget as f32 / property_price as f32;
ratio.powf(2.0)
};
best_score = best_score.max(score);
}
best_score
}2. Location Compatibility Score
fn calculate_location_score(contact: &Contact, property: &Property) -> f32 {
// Exact wilaya match
if contact.wilaya == property.wilaya {
if contact.baladiya.is_some() &&
contact.baladiya.as_ref().unwrap() == &property.baladiya {
return 1.0; // Perfect location match
}
return 0.9; // Wilaya match, baladiya different
}
// Nearby wilayas (predefined proximity matrix)
if is_nearby_wilaya(&contact.wilaya, &property.wilaya) {
return 0.7;
}
0.0 // No location compatibility
}3. Property Type Compatibility
fn calculate_property_type_score(contact: &Contact, property: &Property) -> f32 {
match (&contact.prefered_property_type, &property.property_type) {
(pref, prop) if pref == prop => 1.0,
("apartment", "studio") | ("studio", "apartment") => 0.8,
("villa", "apartment") | ("apartment", "villa") => 0.6,
_ => 0.3
}
}4. Surface Area Compatibility
fn calculate_surface_score(contact: &Contact, property: &Property) -> f32 {
let property_surface = property.surface;
let mut best_score = 0.0;
for (min_surface, max_surface) in contact.min_surface.iter()
.zip(contact.max_surface.iter()) {
let score = if property_surface >= *min_surface && property_surface <= *max_surface {
1.0
} else {
// Calculate distance from preferred range
let preferred_center = (*min_surface + *max_surface) / 2.0;
let distance = (property_surface - preferred_center).abs();
let range = *max_surface - *min_surface;
// Exponential decay based on distance
(-distance / range).exp()
};
best_score = best_score.max(score);
}
best_score
}5. Features Compatibility
fn calculate_features_score(contact: &Contact, property: &Property) -> f32 {
let contact_options = &contact.options;
let property_features = extract_property_features(property);
if contact_options.is_empty() {
return 0.8; // No specific requirements
}
let matching_features = contact_options.iter()
.filter(|option| property_features.contains(option))
.count();
let total_required = contact_options.len();
if total_required == 0 {
0.8
} else {
(matching_features as f32 / total_required as f32)
}
}🔄 Hybrid Scoring Algorithm
The final score combines all individual scores with weighted importance:
fn calculate_final_score(contact: &Contact, property: &Property) -> f32 {
let budget_score = calculate_budget_score(contact, property);
let location_score = calculate_location_score(contact, property);
let type_score = calculate_property_type_score(contact, property);
let surface_score = calculate_surface_score(contact, property);
let features_score = calculate_features_score(contact, property);
// Weighted combination
let final_score =
budget_score * 0.35 + // Most important
location_score * 0.25 + // Very important
type_score * 0.20 + // Important
surface_score * 0.15 + // Moderately important
features_score * 0.05; // Nice to have
// Apply business rules
final_score * apply_business_rules(contact, property)
}📊 Performance Optimization
Vector Database Optimization
// Efficient vector search with Qdrant
async fn optimized_vector_search(
property_embedding: &[f32],
qdrant_client: &QdrantClient
) -> Result<Vec<ContactMatch>> {
// Use approximate nearest neighbor search
let search_params = SearchParams {
hnsw_config: Some(HnswConfigDiff {
m: Some(16),
ef_construct: Some(100),
full_scan_threshold: Some(10000),
max_indexing_threads: Some(4),
on_disk: Some(false),
payload_m: Some(16),
ef: Some(50),
}),
..Default::default()
};
qdrant_client.search(
"contacts",
property_embedding,
search_params,
limit: 100
).await
}Caching Strategy
// Redis caching for frequently accessed data
async fn get_cached_matches(property_id: &str) -> Option<Vec<ContactMatch>> {
let cache_key = format!("matches:{}", property_id);
redis_client.get(&cache_key).await.ok()
}
async fn cache_matches(property_id: &str, matches: &[ContactMatch]) {
let cache_key = format!("matches:{}", property_id);
let ttl = 3600; // 1 hour cache
redis_client.set_ex(&cache_key, matches, ttl).await;
}🎯 Match Quality Metrics
Precision and Recall
fn calculate_match_quality(matches: &[ContactMatch]) -> MatchQuality {
let high_quality_matches = matches.iter()
.filter(|m| m.score >= 0.8)
.count();
let total_matches = matches.len();
MatchQuality {
precision: high_quality_matches as f32 / total_matches as f32,
recall: calculate_recall(matches),
average_score: matches.iter().map(|m| m.score).sum::<f32>() / total_matches as f32
}
}Explanation Generation
fn generate_explanation(contact: &Contact, property: &Property, score: f32) -> String {
let mut reasons = Vec::new();
if score >= 0.9 {
reasons.push("Perfect match".to_string());
} else if score >= 0.8 {
reasons.push("Excellent match".to_string());
} else if score >= 0.7 {
reasons.push("Good match".to_string());
}
// Add specific reasons
if is_budget_compatible(contact, property) {
reasons.push("Budget requirements met".to_string());
}
if contact.wilaya == property.wilaya {
reasons.push("Location preference matched".to_string());
}
if contact.prefered_property_type == property.property_type {
reasons.push("Property type preference matched".to_string());
}
format!("{}: {}", score, reasons.join(", "))
}🔧 Configuration Parameters
Matching Configuration
#[derive(Debug, Clone)]
pub struct MatcherConfig {
pub max_matches: usize, // Maximum matches per property
pub min_similarity_threshold: f32, // Minimum score threshold
pub budget_weight: f32, // Budget importance weight
pub location_weight: f32, // Location importance weight
pub type_weight: f32, // Property type importance weight
pub surface_weight: f32, // Surface area importance weight
pub features_weight: f32, // Features importance weight
pub cache_ttl: u64, // Cache time-to-live
pub batch_size: usize, // Batch processing size
}Default Configuration
impl Default for MatcherConfig {
fn default() -> Self {
Self {
max_matches: 50,
min_similarity_threshold: 0.6,
budget_weight: 0.35,
location_weight: 0.25,
type_weight: 0.20,
surface_weight: 0.15,
features_weight: 0.05,
cache_ttl: 3600,
batch_size: 1000,
}
}
}📈 Performance Benchmarks
Matching Performance
| Metric | Value | Description |
|---|---|---|
| Average Response Time | 150ms | Time to generate recommendations |
| Throughput | 1000 req/s | Requests per second |
| Accuracy | 94% | Precision of top matches |
| Scalability | 10K+ contacts | Maximum contacts supported |
Memory Usage
| Component | Memory Usage | Description |
|---|---|---|
| Vector Database | 2GB | Contact embeddings |
| Application Cache | 500MB | Frequently accessed data |
| Business Logic | 100MB | Matching algorithms |
🔄 Continuous Learning
Feedback Loop
The system continuously learns from user interactions:
async fn update_match_quality(
property_id: &str,
contact_id: &str,
user_feedback: UserFeedback
) -> Result<()> {
// Update match score based on user feedback
let feedback_score = match user_feedback {
UserFeedback::Positive => 1.0,
UserFeedback::Neutral => 0.5,
UserFeedback::Negative => 0.0,
};
// Update historical data
update_match_history(property_id, contact_id, feedback_score).await?;
// Retrain model if needed
if should_retrain_model().await? {
retrain_matching_model().await?;
}
Ok(())
}📚 Next Steps
- API Reference - Complete API documentation
- Data Models - Entity schemas and relationships
- Integration Guide - Implementation examples
- Testing - Testing strategies