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:

  1. Vector Similarity Search - High-performance similarity matching using embeddings
  2. Business Rules Engine - Domain-specific logic for real estate matching
  3. Collaborative Filtering - Learning from historical match patterns
  4. Content-Based Filtering - Property feature analysis
  5. 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)
}

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

MetricValueDescription
Average Response Time150msTime to generate recommendations
Throughput1000 req/sRequests per second
Accuracy94%Precision of top matches
Scalability10K+ contactsMaximum contacts supported

Memory Usage

ComponentMemory UsageDescription
Vector Database2GBContact embeddings
Application Cache500MBFrequently accessed data
Business Logic100MBMatching 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