Scala'da Gayrimenkul Platformu Mimarisi
Modern emlak platformları Zillow, İdealist, Immobiliare.it e sağa hamle milyonlarca reklamı yönetin, ajanslardan, MLS'lerden milyarlarca aylık arama ve gerçek zamanlı veri akışı (Çoklu Listeleme Hizmeti) ve özel sahipler. Sürdürebilecek bir sistem tasarlayın bu yük, yığının her katmanında kesin mimari kararlar gerektirir: multimedya boru hattından sisteme veri modellemeden jeouzaysal araştırmaya kadar gerçek zamanlı bildirimler.
Bu makalede geniş ölçekte bir PropTech platformunun eksiksiz mimarisini oluşturacağız. her bileşenin kod örnekleriyle analiz edilmesi TypeScript e Python, üretimde kanıtlanmış teknolojiler ve tasarım modelleri arasındaki karşılaştırmalar.
Ne Öğreneceksiniz
- Etki alanı ayrıştırma özelliğine sahip emlak platformları için mikro hizmet mimarisi
- Eksiksiz veri modeli: mülkler, listeler, aracılar, işlemler, medya
- Elasticsearch, PostGIS ve H3 ile gelişmiş coğrafi arama
- Heterojen kaynaklardan (MLS/RESO Web API, kazıma, XML beslemesi) reklam alma hattı
- Medya hattı: görüntü işleme, kat planları, 3D sanal turlar
- Gerçek zamanlı işlevsellik: bildirimler, mesajlaşma, fiyat uyarıları
- Geniş ölçekte performans stratejileri: önbelleğe alma, CDN, parçalama, CQRS
- Uyumluluk: GDPR, adil konut, bölgesel düzenlemeler
Gayrimenkul Platformlarının Görünümü
Mimariyi tasarlamadan önce rekabet ortamını ve Teknik seçimlere rehberlik eden iş modelleri. Her platformun benzersiz bir karışımı vardır Altyapı kararlarını doğrudan etkileyen işlevsellik.
| platformu | Pazar | Aktif Reklamlar | Ayırt Edici Özellik | Bilinen Yığın |
|---|---|---|---|---|
| Zillow | Amerika | ~135M mülk | Zestimate (ML derecesi) | Java, Kafka, Elasticsearch |
| İdealist | AB (ES, IT, PT) | ~1,8 milyon reklam | İnteraktif harita araması | Java, Solr, PostgreSQL |
| Immobiliare.it | İtalya | ~1,2 milyon reklam | Otomatik değerlendirme, ısı haritası | PHP/Go, Elasticsearch |
| sağa hamle | UK | ~1 milyon reklam | Arama çiz (alan çizimi) | .NET, SQL Sunucusu, Azure |
| Kızılyüzgeç | Amerika | ~100M mülk | Entegre acenteler, 3D turlar | Java, Tepki, Kafka |
Para Kazanma Modelleri ve Teknik Etki
İş modeli mimariyi doğrudan etkiler. Bir platform sponsorlu listelemelerle freemium reklam sunma motoruna ihtiyaç var, A/B gösterimleri test etme ve izleme. Bir model kurşun bazlı yönlendirme gerektirir acentelerle akıllı iletişim ve entegre CRM. Bir model işlemsel (iBuying), ML değerlendirme hattını ve yönetimini içerir finansal ve gelişmiş mevzuat uyumluluğu.
- Sponsorlu Listeler: YG için bir reklam sıralama motoru, teklif sistemi ve analiz gerektirir
- Ajans aboneliği: katman yönetimi, yinelenen faturalandırma, temsilci kontrol paneli
- Potansiyel müşteri yaratma: müşteri adayı puanlama, akıllı yönlendirme, ilişkilendirme takibi
- İşlemsel (iBuying): ML değerlendirme modelleri, teklif yönetimi, yasal süreç
- Ajanslar için SaaS: çoklu kiracılı, beyaz etiketli, CRM entegrasyonları için API
Üst Düzey Mimari
Merdiven emlak platformu bir mimariyi benimsiyor Etki alanı odaklı mikro hizmetler, her hizmetin kendi veritabanına sahip olduğu ve eşzamansız olaylar aracılığıyla iletişim kurduğu yer. Bu yaklaşım Daha büyük yük altında bileşenleri (genellikle arama ve API'ler) bağımsız olarak ölçeklendirmenize olanak tanır Daha az talep edilen hizmetleri etkilemeden kamuya açık).
Yol Gösterici İlke: Boğucu İncir Deseni
Çoğu emlak platformu monolit olarak başlar. Mikro hizmetlere geçiş aracılığıyla kademeli olarak gerçekleşir. Strangler İncir Deseni: önce hizmetler çıkarılır yüksek yükte (araştırma, medya) ve ardından kademeli olarak geri kalanlarda, monolitin işleyişini sürdürür tüm geçiş boyunca.
Mikro Hizmetlere Ayrışma
| Hizmet | Sorumluluk | Veritabanları | Desenler |
|---|---|---|---|
| Listeleme Hizmeti | CRUD reklamları, doğrulama, yayınlama iş akışı | PostgreSQL + PostGIS | CQRS, Olay Kaynak Kullanımı |
| Arama Hizmeti | Tam metin araması, filtreler, jeouzaysal, yönlü | Elasticsearch/Açık Arama | Modeli Oku (CQRS) |
| Kullanıcı Hizmeti | Kimlik doğrulama, profiller, tercihler, kaydedildi | PostgreSQL | OAuth 2.0 / OIDC |
| Temsilci Hizmeti | Temsilci profilleri, ajanslar, derecelendirmeler, uygunluk | PostgreSQL | Alan Adı Hizmeti |
| Medya Hizmeti | Yükleme, işleme, optimizasyon, CDN | S3 + DynamoDB (meta veriler) | Asenkron boru hattı |
| Mesajlaşma Hizmeti | Temsilci-kullanıcı sohbeti, bilgi talepleri, bildirimler | MongoDB / Cassandra | WebSocket + Olay Odaklı |
| Bildirim Hizmeti | E-posta, push, SMS, fiyat uyarıları, yeni reklamlar | Redis + PostgreSQL | Genişletme, Şablon Motoru |
| Analitik Hizmeti | Temsilciler için ziyaretleri, ısı haritalarını ve raporları izleme | ClickHouse / BigQuery | Etkinlik Akışı |
| Besleme Hizmeti | MLS'den, XML yayınlarından, harici API'lerden içe aktarma | Aşama Veritabanı + Kuyruğu | ETL Boru Hattı |
| Değerleme Hizmeti | ML fiyat tahmini, karşılaştırılabilir öğeler, pazar eğilimleri | Özellik Mağazası + Model Kaydı | Makine Öğrenimi Ardışık Düzeni |
Mimari Şeması
+------------------+
| API Gateway |
| (Kong / Envoy) |
+--------+---------+
|
+--------------------+--------------------+
| | |
+--------v------+ +--------v------+ +--------v------+
| Listing | | Search | | User |
| Service | | Service | | Service |
| (PostgreSQL) | | (Elasticsearch)| | (PostgreSQL) |
+--------+------+ +---------------+ +--------+------+
| |
| Events (Kafka / NATS) |
+--------------------+-------------------+
| | |
+--------v------+ +--------v------+ +--------v------+
| Media | | Messaging | | Notification |
| Service | | Service | | Service |
| (S3 + CDN) | | (MongoDB) | | (Redis) |
+---------------+ +---------------+ +---------------+
|
+--------v------+ +---------------+
| Ingestion | | Valuation |
| Service | | Service |
| (ETL Pipeline)| | (ML Models) |
+---------------+ +---------------+
Veri Modeli: Platformun Kalbi
Bir emlak platformunun veri modeli şaşırtıcı derecede karmaşıktır. Tek bir mülk fiziğin katları olabilir reklamlar zamanla yönetilmek farklı ajanlar, yukarı çık işlemler ve yüzlerce üretin multimedya varlıkları. Bu ilişkileri doğru ve kritik bir şekilde tasarlayın performans ve veri tutarlılığı.
Ana Varlık Şeması
// Entità base: Proprietà fisica
interface Property {
readonly id: string;
readonly externalId?: string; // ID da MLS/fonte esterna
readonly source: DataSource;
readonly propertyType: PropertyType;
readonly address: Address;
readonly location: GeoPoint; // lat/lng per ricerca spaziale
readonly h3Index: string; // H3 cell index (risoluzione 9)
readonly characteristics: PropertyCharacteristics;
readonly amenities: ReadonlyArray<Amenity>;
readonly energyRating?: EnergyRating;
readonly cadastralRef?: string; // Riferimento catastale (IT)
readonly createdAt: Date;
readonly updatedAt: Date;
}
type PropertyType =
| 'apartment' | 'house' | 'villa' | 'penthouse'
| 'studio' | 'loft' | 'commercial' | 'land'
| 'garage' | 'office' | 'warehouse';
interface Address {
readonly street: string;
readonly streetNumber: string;
readonly city: string;
readonly province: string;
readonly region: string;
readonly postalCode: string;
readonly country: string;
readonly formattedAddress: string;
readonly neighborhood?: string;
}
interface GeoPoint {
readonly lat: number;
readonly lng: number;
}
interface PropertyCharacteristics {
readonly sqMeters: number;
readonly rooms: number;
readonly bedrooms: number;
readonly bathrooms: number;
readonly floor?: number;
readonly totalFloors?: number;
readonly hasElevator?: boolean;
readonly hasBalcony?: boolean;
readonly hasTerrace?: boolean;
readonly hasGarden?: boolean;
readonly gardenSqMeters?: number;
readonly yearBuilt?: number;
readonly condition: PropertyCondition;
readonly heatingType?: HeatingType;
readonly orientation?: ReadonlyArray<Orientation>;
}
// Annuncio: un'istanza di vendita/affitto
interface Listing {
readonly id: string;
readonly propertyId: string;
readonly agentId: string;
readonly agencyId?: string;
readonly listingType: 'sale' | 'rent' | 'auction';
readonly status: ListingStatus;
readonly price: Money;
readonly pricePerSqMeter: Money;
readonly condominiumFees?: Money;
readonly description: LocalizedText;
readonly media: ReadonlyArray<MediaAsset>;
readonly virtualTourUrl?: string;
readonly availableFrom?: Date;
readonly publishedAt?: Date;
readonly expiresAt?: Date;
readonly viewCount: number;
readonly favoriteCount: number;
readonly contactCount: number;
}
interface Money {
readonly amount: number;
readonly currency: CurrencyCode;
}
type CurrencyCode = 'EUR' | 'USD' | 'GBP' | 'CHF';
interface LocalizedText {
readonly [locale: string]: string; // { it: "...", en: "..." }
}
type ListingStatus =
| 'draft' | 'pending_review' | 'active'
| 'under_offer' | 'sold' | 'rented'
| 'expired' | 'withdrawn';
Bu modelin bazı önemli yönleri:
- Mülk/Giriş ayrımı: fiziksel bir özellik bağımsız olarak mevcuttur reklamlardan. Aynı daire önce satışa çıkarılabilir, sonra geri çekilip kiralanabilir, aynı varlığa bağlı farklı listeler oluşturmak.
-
Değişmezlik: tüm arayüzlerin kullanımı
readonlyönlemek için tesadüfi mutasyonlar. Durum geçişleri yeni nesneler üretir. - H3 Endeksi: Jeo-uzamsal aramaları etkinleştirmek için ekleme sırasında önceden hesaplanmıştır altıgen toplama yoluyla verimli.
-
Çoklu para birimi ve çoklu dil: aracılığıyla yerel destek
MoneyeLocalizedTextuluslararası pazarlar için.
Reklam Besleme Ardışık Düzeni
Bir emlak platformunun atan kalbi ve veri alma hattı. reklamlar heterojen kaynaklardan geliyorlar: beslemeler RESO Web API'si (ABD/Uluslararası standart), Tescilli XML yayınları, ajans API'leri, manuel yüklemeler ve iş ortağı portalı kazıma. Her kaynak kendine ait bir formatı, güncelleme hızı ve veri kalite seviyesi vardır.
RESO Web API: Endüstri Standardı
Il RESO (Gayrimenkul Standartları Organizasyonu) Web API'si ve modern standart MLS veri alışverişi için, JSON yüküyle REST/OData'ya dayalı. Eski RETS'in yerini alır (artık kullanımdan kaldırıldı). Veri Sözlüğü 2.x, alanlar için standart adları tanımlar (ListingId, StandardStatus, ListPrice, LivingArea) sistemler arasında birlikte çalışabilirliğin sağlanması.
ETL Boru Hattı Mimarisi
import { EventEmitter } from 'events';
// Interfaccia generica per sorgenti dati
interface ListingSource {
readonly sourceId: string;
readonly sourceType: 'reso_api' | 'xml_feed' | 'manual' | 'scraper';
fetch(since: Date): Promise<ReadonlyArray<RawListing>>;
}
// Dati grezzi da qualsiasi fonte
interface RawListing {
readonly externalId: string;
readonly source: string;
readonly rawData: Record<string, unknown>;
readonly fetchedAt: Date;
}
// Pipeline di ingestione con step chiari
class ListingIngestionPipeline {
private readonly events = new EventEmitter();
constructor(
private readonly sources: ReadonlyArray<ListingSource>,
private readonly normalizer: ListingNormalizer,
private readonly validator: ListingValidator,
private readonly deduplicator: ListingDeduplicator,
private readonly enricher: ListingEnricher,
private readonly repository: ListingRepository,
private readonly searchIndex: SearchIndexer,
) {}
async ingest(source: ListingSource): Promise<IngestionResult> {
const startTime = Date.now();
const result: IngestionResult = {
sourceId: source.sourceId,
processed: 0,
created: 0,
updated: 0,
skipped: 0,
errors: [],
};
// Step 1: Fetch dati grezzi
const rawListings = await source.fetch(
await this.getLastSyncTime(source.sourceId)
);
for (const raw of rawListings) {
try {
// Step 2: Normalizzazione (formato sorgente -> formato interno)
const normalized = this.normalizer.normalize(raw);
// Step 3: Validazione (campi obbligatori, range, formato)
const validation = this.validator.validate(normalized);
if (!validation.isValid) {
result.skipped++;
result.errors.push({
externalId: raw.externalId,
errors: validation.errors,
});
continue;
}
// Step 4: Deduplicazione (match per indirizzo, coordinate, ID esterno)
const existingId = await this.deduplicator.findDuplicate(normalized);
// Step 5: Arricchimento (geocoding, H3 index, neighborhood)
const enriched = await this.enricher.enrich(normalized);
// Step 6: Persistenza
if (existingId) {
await this.repository.update(existingId, enriched);
result.updated++;
} else {
await this.repository.create(enriched);
result.created++;
}
// Step 7: Indicizzazione per la ricerca (asincrona)
this.events.emit('listing:upserted', enriched);
result.processed++;
} catch (error) {
result.errors.push({
externalId: raw.externalId,
errors: [String(error)],
});
}
}
// Aggiorna timestamp ultima sync
await this.updateLastSyncTime(source.sourceId, new Date());
this.events.emit('ingestion:completed', {
...result,
durationMs: Date.now() - startTime,
});
return result;
}
private async getLastSyncTime(sourceId: string): Promise<Date> {
// Recupera da DB il timestamp dell'ultima sincronizzazione
return new Date(Date.now() - 24 * 60 * 60 * 1000); // fallback: 24h fa
}
private async updateLastSyncTime(sourceId: string, time: Date): Promise<void> {
// Persiste il timestamp per la prossima esecuzione
}
}
interface IngestionResult {
readonly sourceId: string;
processed: number;
created: number;
updated: number;
skipped: number;
errors: Array<{ externalId: string; errors: string[] }>;
}
Normalleştirme: RETURN'den Dahili Formata
Normalleşme sürecin en kritik adımıdır. Her veri kaynağının bir formatı vardır farklıdır ve normalizasyon, heterojen alanları tek tip bir modele eşlemelidir. İşte bir RESO formatı için normalleştirici örneği:
class ResoListingNormalizer implements ListingNormalizer {
normalize(raw: RawListing): NormalizedListing {
const data = raw.rawData as ResoPropertyData;
return {
externalId: String(data.ListingId),
source: raw.source,
propertyType: this.mapPropertyType(data.PropertyType),
listingType: this.mapListingType(data.TransactionType),
status: this.mapStatus(data.StandardStatus),
price: {
amount: data.ListPrice,
currency: data.CurrencyCode ?? 'USD',
},
address: {
street: data.StreetName,
streetNumber: data.StreetNumber,
city: data.City,
province: data.StateOrProvince,
postalCode: data.PostalCode,
country: data.Country ?? 'US',
formattedAddress: this.buildFormattedAddress(data),
},
location: data.Latitude && data.Longitude
? { lat: data.Latitude, lng: data.Longitude }
: undefined,
characteristics: {
sqMeters: this.sqFeetToSqMeters(data.LivingArea),
rooms: data.RoomsTotal ?? 0,
bedrooms: data.BedroomsTotal ?? 0,
bathrooms: data.BathroomsTotalInteger ?? 0,
yearBuilt: data.YearBuilt,
},
description: {
en: data.PublicRemarks ?? '',
},
photos: (data.Media ?? []).map((m: ResoMedia) => ({
url: m.MediaURL,
order: m.Order,
caption: m.ShortDescription,
})),
rawData: raw.rawData,
fetchedAt: raw.fetchedAt,
};
}
private mapPropertyType(resoType: string): PropertyType {
const mapping: Record<string, PropertyType> = {
'Residential': 'house',
'Condominium': 'apartment',
'Townhouse': 'house',
'Land': 'land',
'Commercial': 'commercial',
};
return mapping[resoType] ?? 'apartment';
}
private mapStatus(resoStatus: string): ListingStatus {
const mapping: Record<string, ListingStatus> = {
'Active': 'active',
'Pending': 'under_offer',
'Closed': 'sold',
'Withdrawn': 'withdrawn',
'Expired': 'expired',
};
return mapping[resoStatus] ?? 'draft';
}
private sqFeetToSqMeters(sqFeet?: number): number {
return sqFeet ? Math.round(sqFeet * 0.092903 * 100) / 100 : 0;
}
private mapListingType(txType: string): 'sale' | 'rent' {
return txType === 'Lease' ? 'rent' : 'sale';
}
private buildFormattedAddress(data: ResoPropertyData): string {
return [data.StreetNumber, data.StreetName, data.City, data.StateOrProvince]
.filter(Boolean)
.join(', ');
}
}
Arama Mimarisi: Elasticsearch ve Geospatial
Herhangi bir emlak platformunun en kritik arama ve işlevselliği. Kullanıcılar bunu yapar Anında sonuçlar, birleştirilebilir filtreler, alaka düzeyine/fiyat/mesafeye göre sıralama bekliyoruz ve gerçek zamanlı güncellemeyle harita arama. Elasticsearch (veya çatalı Açık Arama) ve desteği sayesinde sektördeki baskın tercih tam metin, coğrafi ve yönlü aramalar için yerel.
Reklamlar için Elasticsearch Dizini
{
"mappings": {
"properties": {
"listingId": { "type": "keyword" },
"propertyType": { "type": "keyword" },
"listingType": { "type": "keyword" },
"status": { "type": "keyword" },
"price": { "type": "long" },
"pricePerSqm": { "type": "float" },
"currency": { "type": "keyword" },
"location": { "type": "geo_point" },
"geoShape": { "type": "geo_shape" },
"h3Index": { "type": "keyword" },
"h3Res7": { "type": "keyword" },
"city": { "type": "keyword" },
"neighborhood": { "type": "keyword" },
"province": { "type": "keyword" },
"postalCode": { "type": "keyword" },
"sqMeters": { "type": "integer" },
"rooms": { "type": "integer" },
"bedrooms": { "type": "integer" },
"bathrooms": { "type": "integer" },
"floor": { "type": "integer" },
"yearBuilt": { "type": "integer" },
"hasElevator": { "type": "boolean" },
"hasBalcony": { "type": "boolean" },
"hasGarden": { "type": "boolean" },
"energyRating": { "type": "keyword" },
"amenities": { "type": "keyword" },
"description": { "type": "text", "analyzer": "multilingual_analyzer" },
"title": { "type": "text", "analyzer": "multilingual_analyzer",
"fields": { "keyword": { "type": "keyword" } } },
"agentId": { "type": "keyword" },
"agencyId": { "type": "keyword" },
"publishedAt": { "type": "date" },
"updatedAt": { "type": "date" },
"viewCount": { "type": "integer" },
"photoCount": { "type": "integer" },
"hasVirtualTour": { "type": "boolean" }
}
},
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1,
"analysis": {
"analyzer": {
"multilingual_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "asciifolding", "stop_it", "stop_en"]
}
},
"filter": {
"stop_it": { "type": "stop", "stopwords": "_italian_" },
"stop_en": { "type": "stop", "stopwords": "_english_" }
}
}
}
}
Jeo-uzaysal Filtrelere sahip Arama API'si
Arama sorgusu Boolean filtrelerini, sayısal aralıkları, tam metin aramasını ve kısıtlamaları birleştirir jeouzaysal. Üç coğrafi arama modunu destekliyoruz: geo_distance (bir noktanın yarıçapı), geo_bounding_box (haritadaki dikdörtgen) e geo_shape (Kullanıcı tarafından çizilmiş çokgen, Rightmove'un "Bir arama çiz" özelliği gibi).
interface PropertySearchParams {
readonly query?: string;
readonly listingType: 'sale' | 'rent';
readonly propertyTypes?: ReadonlyArray<PropertyType>;
readonly priceMin?: number;
readonly priceMax?: number;
readonly sqMetersMin?: number;
readonly sqMetersMax?: number;
readonly bedroomsMin?: number;
readonly bathroomsMin?: number;
readonly amenities?: ReadonlyArray<string>;
readonly geoFilter?: GeoFilter;
readonly sortBy?: 'relevance' | 'price_asc' | 'price_desc' | 'newest' | 'distance';
readonly page?: number;
readonly pageSize?: number;
}
type GeoFilter =
| { type: 'radius'; center: GeoPoint; radiusKm: number }
| { type: 'bbox'; topLeft: GeoPoint; bottomRight: GeoPoint }
| { type: 'polygon'; points: ReadonlyArray<GeoPoint> };
class PropertySearchService {
constructor(private readonly esClient: ElasticsearchClient) {}
async search(params: PropertySearchParams): Promise<SearchResult> {
const must: any[] = [];
const filter: any[] = [];
// Filtro obbligatorio: tipo annuncio e stato attivo
filter.push({ term: { listingType: params.listingType } });
filter.push({ term: { status: 'active' } });
// Full-text search (titolo + descrizione)
if (params.query) {
must.push({
multi_match: {
query: params.query,
fields: ['title^3', 'description', 'city^2', 'neighborhood^2'],
type: 'best_fields',
fuzziness: 'AUTO',
},
});
}
// Filtri range prezzo
if (params.priceMin || params.priceMax) {
filter.push({
range: {
price: {
...(params.priceMin ? { gte: params.priceMin } : {}),
...(params.priceMax ? { lte: params.priceMax } : {}),
},
},
});
}
// Filtri superficie
if (params.sqMetersMin || params.sqMetersMax) {
filter.push({
range: {
sqMeters: {
...(params.sqMetersMin ? { gte: params.sqMetersMin } : {}),
...(params.sqMetersMax ? { lte: params.sqMetersMax } : {}),
},
},
});
}
// Tipo proprietà
if (params.propertyTypes?.length) {
filter.push({ terms: { propertyType: params.propertyTypes } });
}
// Camere minime
if (params.bedroomsMin) {
filter.push({ range: { bedrooms: { gte: params.bedroomsMin } } });
}
// Amenities (AND logic)
if (params.amenities?.length) {
for (const amenity of params.amenities) {
filter.push({ term: { amenities: amenity } });
}
}
// Filtro geospaziale
if (params.geoFilter) {
filter.push(this.buildGeoFilter(params.geoFilter));
}
const page = params.page ?? 0;
const pageSize = params.pageSize ?? 20;
const response = await this.esClient.search({
index: 'listings',
body: {
query: {
bool: {
must: must.length ? must : [{ match_all: {} }],
filter,
},
},
sort: this.buildSort(params.sortBy, params.geoFilter),
from: page * pageSize,
size: pageSize,
aggs: this.buildAggregations(),
},
});
return this.mapResponse(response, page, pageSize);
}
private buildGeoFilter(geo: GeoFilter): Record<string, unknown> {
switch (geo.type) {
case 'radius':
return {
geo_distance: {
distance: `${geo.radiusKm}km`,
location: { lat: geo.center.lat, lon: geo.center.lng },
},
};
case 'bbox':
return {
geo_bounding_box: {
location: {
top_left: { lat: geo.topLeft.lat, lon: geo.topLeft.lng },
bottom_right: { lat: geo.bottomRight.lat, lon: geo.bottomRight.lng },
},
},
};
case 'polygon':
return {
geo_shape: {
geoShape: {
shape: {
type: 'polygon',
coordinates: [
geo.points.map(p => [p.lng, p.lat]),
],
},
relation: 'within',
},
},
};
}
}
private buildAggregations(): Record<string, unknown> {
return {
price_stats: { stats: { field: 'price' } },
property_types: { terms: { field: 'propertyType', size: 15 } },
bedrooms: { terms: { field: 'bedrooms', size: 10 } },
price_ranges: {
range: {
field: 'price',
ranges: [
{ key: 'under_100k', to: 100000 },
{ key: '100k_200k', from: 100000, to: 200000 },
{ key: '200k_350k', from: 200000, to: 350000 },
{ key: '350k_500k', from: 350000, to: 500000 },
{ key: 'over_500k', from: 500000 },
],
},
},
neighborhoods: { terms: { field: 'neighborhood', size: 30 } },
energy_ratings: { terms: { field: 'energyRating', size: 10 } },
};
}
private buildSort(
sortBy?: string,
geoFilter?: GeoFilter
): Array<Record<string, unknown>> {
switch (sortBy) {
case 'price_asc':
return [{ price: 'asc' }];
case 'price_desc':
return [{ price: 'desc' }];
case 'newest':
return [{ publishedAt: 'desc' }];
case 'distance':
if (geoFilter?.type === 'radius') {
return [{
_geo_distance: {
location: { lat: geoFilter.center.lat, lon: geoFilter.center.lng },
order: 'asc',
unit: 'km',
},
}];
}
return [{ _score: 'desc' }];
default:
return [{ _score: 'desc' }, { publishedAt: 'desc' }];
}
}
private mapResponse(
response: any,
page: number,
pageSize: number
): SearchResult {
return {
listings: response.hits.hits.map((hit: any) => ({
...hit._source,
score: hit._score,
distance: hit.sort?.[0],
})),
total: response.hits.total.value,
page,
pageSize,
facets: {
priceStats: response.aggregations?.price_stats,
propertyTypes: response.aggregations?.property_types?.buckets,
bedrooms: response.aggregations?.bedrooms?.buckets,
priceRanges: response.aggregations?.price_ranges?.buckets,
neighborhoods: response.aggregations?.neighborhoods?.buckets,
energyRatings: response.aggregations?.energy_ratings?.buckets,
},
};
}
}
Jeo-uzaysal İndeksleme: PostGIS, H3 ve S2
Elasticsearch'e ek olarak, birincil kalıcılık katmanı jeouzaysal yetenekler gerektirir Kesin mesafelerin hesaplanması, bölgelere göre toplama ve pazar analizi. Bu alana üç teknoloji hakimdir.
| Teknoloji | Tip | Birincil Kullanım | Avantajları | Sınırlar |
|---|---|---|---|---|
| PostGIS | PostgreSQL uzantısı | SQL uzamsal sorguları, karmaşık geometriler | Standart OGC, olgun, ilişkisel verilerle birleştirme | Dikey ölçeklenebilirlik, yerel kümeleme yok |
| H3 (Über) | Hiyerarşik altıgen ızgara | Alana, yakınlığa ve analize göre toplama | Tek tip, hiyerarşik, O(1) araması | Kenarlarda beşgenler, sınır yaklaşımı |
| S2 Geometri (Google) | Hiyerarşik küresel hücreler | Bölge kapsamı, aralık sorgusu, parçalama | BigQuery/Spanner'da kullanılan tam kapsam | Düzgün olmayan dörtgen hücreler, karmaşık API |
PostGIS ile Jeo-uzamsal Sorgulamalar
-- Ricerca per raggio: proprietà entro 5km da un punto
SELECT
l.id,
l.title,
l.price,
p.property_type,
ST_Distance(
p.location::geography,
ST_MakePoint(12.4964, 41.9028)::geography
) AS distance_meters
FROM listings l
JOIN properties p ON l.property_id = p.id
WHERE l.status = 'active'
AND l.listing_type = 'sale'
AND ST_DWithin(
p.location::geography,
ST_MakePoint(12.4964, 41.9028)::geography, -- Roma centro
5000 -- 5km in metri
)
AND l.price BETWEEN 150000 AND 400000
ORDER BY distance_meters ASC
LIMIT 50;
-- Ricerca per bounding box (viewport della mappa)
SELECT l.id, l.title, l.price,
ST_X(p.location) AS lng, ST_Y(p.location) AS lat
FROM listings l
JOIN properties p ON l.property_id = p.id
WHERE l.status = 'active'
AND p.location && ST_MakeEnvelope(
12.45, 41.87, -- bottom-left (lng, lat)
12.55, 41.93, -- top-right (lng, lat)
4326 -- SRID WGS84
)
ORDER BY l.published_at DESC
LIMIT 200;
-- Clustering per H3: conteggio annunci per cella esagonale
SELECT
h3_lat_lng_to_cell(p.location, 7) AS h3_cell,
COUNT(*) AS listing_count,
AVG(l.price) AS avg_price,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY l.price) AS median_price
FROM listings l
JOIN properties p ON l.property_id = p.id
WHERE l.status = 'active'
AND l.listing_type = 'sale'
AND p.city = 'Milano'
GROUP BY h3_cell
HAVING COUNT(*) >= 3
ORDER BY avg_price DESC;
Neden PropTech için H3?
H3 temel bir sorunu çözüyor: verimli mekansal toplama. Hesaplamak yerine
Haritadaki gerçek zamanlı reklam kümesinde her mülkün H3 endeksini önceden hesaplar
yerleştirildiğinde. Çözünürlük 7 (~5,16 km²) şehir manzaraları için idealdir, çözünürlük 9
(~0,105 km²) mahalle düzeyi için. Toplu sorgular basitleştirildi
GROUP BY h3_index, büyüklük sıraları geometrik işlemlerden daha hızlı
gerçek zamanlı olarak.
Medya Boru Hattı: Görseller, Kat Planları ve Sanal Turlar
Görseller, kullanıcının bir reklamı tıklama kararında en etkili faktördür. Ölçekli bir platform, her biri birden fazla formatta ve her gün milyonlarca görüntüyü işler. kararlar. Ortalama boru hattı şöyle olmalıdır: asenkron, dayanıklı ve için optimize edilmiş teslimat hızı CDN aracılığıyla.
Medya İşleme Akışı
- Yüklemeler: temsilci orijinal fotoğrafları önceden imzalanmış URL aracılığıyla S3'e yükler
- Doğrulama: kontrol formatı, boyutu, içeriği NSFW (ML)
- İşleme: varyant oluşturma (küçük resim 300 piksel, orta 800 piksel, büyük 1600 piksel, WebP/AVIF)
- Optimizasyon: akıllı sıkıştırma, hassas EXIF meta verileri kaldırma, filigran
- Makine öğrenimi zenginleştirmesi: oda sınıflandırması (mutfak, banyo, oturma odası), kalite puanı, kat planı araştırması
- CDN Dağıtımı: Global olarak <50 ms teslimat için CDN uç düğümlerine itin
- İndeksleme: arama dizininde meta veri güncellemesi
interface ImageVariant {
readonly suffix: string;
readonly width: number;
readonly quality: number;
readonly format: 'webp' | 'avif' | 'jpeg';
}
const IMAGE_VARIANTS: ReadonlyArray<ImageVariant> = [
{ suffix: 'thumb', width: 300, quality: 75, format: 'webp' },
{ suffix: 'medium', width: 800, quality: 80, format: 'webp' },
{ suffix: 'large', width: 1600, quality: 85, format: 'webp' },
{ suffix: 'avif-medium', width: 800, quality: 70, format: 'avif' },
{ suffix: 'original', width: 0, quality: 90, format: 'jpeg' },
] as const;
class ImageProcessingService {
constructor(
private readonly storage: ObjectStorage,
private readonly sharp: SharpInstance,
private readonly classifier: RoomClassifier,
private readonly queue: MessageQueue,
) {}
async processUploadedImage(event: ImageUploadEvent): Promise<ProcessedImage> {
const { listingId, imageKey, uploadedBy } = event;
// 1. Scarica l'originale
const originalBuffer = await this.storage.download(imageKey);
// 2. Valida (dimensione, formato, contenuto)
const metadata = await this.sharp(originalBuffer).metadata();
if (!metadata.width || metadata.width < 600) {
throw new ImageValidationError('Risoluzione minima: 600px di larghezza');
}
// 3. Genera tutte le varianti
const variants = await Promise.all(
IMAGE_VARIANTS.map(variant =>
this.generateVariant(originalBuffer, imageKey, variant)
)
);
// 4. Classifica la stanza (ML asincrono)
const classification = await this.classifier.classify(originalBuffer);
// 5. Rimuovi EXIF sensibili, mantieni orientamento
const sanitizedMetadata = this.sanitizeExif(metadata);
const processedImage: ProcessedImage = {
listingId,
originalKey: imageKey,
variants: variants.map(v => ({
key: v.key,
url: this.storage.getPublicUrl(v.key),
width: v.width,
height: v.height,
format: v.format,
sizeBytes: v.sizeBytes,
})),
classification: {
roomType: classification.label,
confidence: classification.score,
},
metadata: sanitizedMetadata,
processedAt: new Date(),
};
// 6. Invalida cache CDN per l'annuncio
await this.queue.publish('cdn:invalidate', {
patterns: [`/listings/${listingId}/images/*`],
});
return processedImage;
}
private async generateVariant(
buffer: Buffer,
originalKey: string,
variant: ImageVariant
): Promise<GeneratedVariant> {
let pipeline = this.sharp(buffer);
if (variant.width > 0) {
pipeline = pipeline.resize(variant.width, undefined, {
fit: 'inside',
withoutEnlargement: true,
});
}
const outputBuffer = await pipeline
.toFormat(variant.format, { quality: variant.quality })
.toBuffer({ resolveWithObject: true });
const variantKey = originalKey.replace(
/\.[^.]+$/, `-${variant.suffix}.${variant.format}`
);
await this.storage.upload(variantKey, outputBuffer.data, {
contentType: `image/${variant.format}`,
cacheControl: 'public, max-age=31536000, immutable',
});
return {
key: variantKey,
width: outputBuffer.info.width,
height: outputBuffer.info.height,
format: variant.format,
sizeBytes: outputBuffer.info.size,
};
}
private sanitizeExif(metadata: any): Record<string, unknown> {
// Mantieni solo dati non sensibili
return {
width: metadata.width,
height: metadata.height,
orientation: metadata.orientation,
format: metadata.format,
};
}
}
Gerçek Zamanlı işlevsellik
Modern emlak platformları bunun ötesine geçen gerçek zamanlı yetenekler gerektirir basit sayfa güncellemesi. Kullanıcılar bunu bekliyor anlık bildirimler ilgi alanına giren bir mülk yayınlandığında, mesajlaşma anlık acentelerle ve fiyat uyarısı ne zaman bir mülk kaydedilen değişikliklere uğrar.
Uyarı ve Bildirim Sistemi
interface SavedSearch {
readonly userId: string;
readonly searchParams: PropertySearchParams;
readonly notifyVia: ReadonlyArray<'push' | 'email' | 'sms'>;
readonly frequency: 'instant' | 'daily' | 'weekly';
readonly createdAt: Date;
readonly lastNotifiedAt?: Date;
}
interface PriceAlert {
readonly userId: string;
readonly listingId: string;
readonly thresholdType: 'any_change' | 'decrease' | 'below_amount';
readonly thresholdAmount?: number;
readonly notifyVia: ReadonlyArray<'push' | 'email'>;
}
class PriceAlertService {
constructor(
private readonly alertRepo: PriceAlertRepository,
private readonly listingRepo: ListingRepository,
private readonly notifier: NotificationService,
private readonly searchService: PropertySearchService,
) {}
// Invocato dall'event bus quando un listing cambia prezzo
async onPriceChanged(event: ListingPriceChangedEvent): Promise<void> {
const { listingId, oldPrice, newPrice } = event;
// Trova tutti gli alert per questo listing
const alerts = await this.alertRepo.findByListingId(listingId);
const notifications = alerts
.filter(alert => this.shouldNotify(alert, oldPrice, newPrice))
.map(alert => ({
userId: alert.userId,
channels: alert.notifyVia,
template: 'price_change',
data: {
listingId,
oldPrice: oldPrice.amount,
newPrice: newPrice.amount,
changePercent: ((newPrice.amount - oldPrice.amount) / oldPrice.amount * 100).toFixed(1),
currency: newPrice.currency,
direction: newPrice.amount < oldPrice.amount ? 'decrease' : 'increase',
},
}));
await Promise.all(
notifications.map(n => this.notifier.send(n))
);
}
// Invocato su schedule per "nuovi annunci nelle ricerche salvate"
async processNewListingAlerts(): Promise<void> {
const searches = await this.getSearchesToProcess();
for (const savedSearch of searches) {
const results = await this.searchService.search({
...savedSearch.searchParams,
publishedAfter: savedSearch.lastNotifiedAt,
pageSize: 10,
});
if (results.total > 0) {
await this.notifier.send({
userId: savedSearch.userId,
channels: savedSearch.notifyVia,
template: 'new_listings',
data: {
count: results.total,
topListings: results.listings.slice(0, 3),
searchUrl: this.buildSearchUrl(savedSearch.searchParams),
},
});
await this.alertRepo.updateLastNotified(savedSearch.userId, new Date());
}
}
}
private shouldNotify(
alert: PriceAlert,
oldPrice: Money,
newPrice: Money
): boolean {
switch (alert.thresholdType) {
case 'any_change':
return oldPrice.amount !== newPrice.amount;
case 'decrease':
return newPrice.amount < oldPrice.amount;
case 'below_amount':
return newPrice.amount <= (alert.thresholdAmount ?? 0);
default:
return false;
}
}
private async getSearchesToProcess(): Promise<ReadonlyArray<SavedSearch>> {
// Recupera le ricerche salvate che necessitano di notifica
// basandosi sulla frequenza configurata
return this.alertRepo.findSearchesReadyForNotification();
}
private buildSearchUrl(params: PropertySearchParams): string {
const qs = new URLSearchParams();
if (params.listingType) qs.set('type', params.listingType);
if (params.priceMin) qs.set('price_min', String(params.priceMin));
if (params.priceMax) qs.set('price_max', String(params.priceMax));
return `/search?${qs.toString()}`;
}
}
API Tasarımı: RESTful Uç Noktalar
Platformun genel API'si, yoldaki sürüm oluşturma ve sayfalandırma ile REST ilkelerini takip eder
imleç tabanlı sonuçlar ve başlık aracılığıyla çoklu dil desteği Accept-Language.
Etki alanına göre düzenlenen ana uç noktalar şunlardır.
| Yöntem | Uç noktalar | Tanım | Yetki |
|---|---|---|---|
GET |
/api/v1/listings |
Filtreli arama reklamları | Halk |
GET |
/api/v1/listings/{id} |
Tek reklam ayrıntısı | Halk |
POST |
/api/v1/listings |
Yeni reklam oluştur | Ajan |
PATCH |
/api/v1/listings/{id} |
İlanı güncelle (fiyat, durum, fotoğraf) | Temsilci (sahip) |
POST |
/api/v1/search |
Coğrafi filtrelerle gelişmiş arama | Halk |
POST |
/api/v1/search/map |
Harita görünümü başına küme (H3 toplama) | Halk |
GET |
/api/v1/search/suggest |
Konumları ve mahalleleri otomatik tamamlama | Halk |
POST |
/api/v1/users/{id}/favorites |
Reklamı favorilere kaydet | Kullanıcı |
POST |
/api/v1/users/{id}/saved-searches |
Aramayı uyarı için kaydet | Kullanıcı |
POST |
/api/v1/messages |
Bir temsilciye mesaj gönder | Kullanıcı |
GET |
/api/v1/agents/{id} |
Derecelendirmeli temsilci profili | Halk |
POST |
/api/v1/media/upload-url |
Yükleme için önceden imzalanmış URL oluştur | Ajan |
GET |
/api/v1/valuations/estimate |
Piyasa fiyatı tahmini | Halk |
İmleç Tabanlı Sayfalandırma
Büyük veri kümeleri için ofset tabanlı sayfalandırma (sayfa/sayfaboyutu) bozulur çabuk. Benimsiyoruz imleç tabanlı sayfalandırma yüksek trafikli API'ler için, sayfanın derinliğine bakılmaksızın sabit performansı garanti eder.
{
"data": [
{
"id": "lst_abc123",
"title": "Trilocale luminoso zona Brera",
"price": { "amount": 385000, "currency": "EUR" },
"propertyType": "apartment",
"bedrooms": 2,
"sqMeters": 95,
"location": { "lat": 45.4726, "lng": 9.1860 },
"thumbnail": "https://cdn.platform.com/lst_abc123/thumb.webp"
}
],
"pagination": {
"cursor": "eyJzIjoiMjAyNi0wMy0wOFQxMDozMDowMFoiLCJpIjoibHN0X2FiYzEyMyJ9",
"hasMore": true,
"totalEstimate": 1247
},
"facets": {
"propertyTypes": [
{ "key": "apartment", "count": 832 },
{ "key": "house", "count": 215 }
],
"priceStats": { "min": 85000, "max": 2500000, "avg": 342000 }
}
}
Scala Performansı
Milyonlarca ilana sahip bir emlak platformunun önemli trafik artışlarını yönetmesi gerekir: Pazar akşamları ve sezon değişim dönemlerinde aramalar %300-400 oranında artıyor. stratejiler performansın mimarinin her seviyesinde çalışması gerekir.
Çok Düzeyli Önbelleğe Alma
| Seviye | Teknoloji | TTL | Kullanmak | Tipik İsabet Oranı |
|---|---|---|---|---|
| L1 - Kenar/CDN | CloudFront / Hızlı | 5-60 dakika | Resimler, CSS/JS, statik sayfalar | %90-95 |
| L2 - API Ağ Geçidi | Vernik / Kong önbelleği | 1-5 dakika | API yanıtlarını AL (liste ayrıntısı, arama sonuçları) | %60-75 |
| L3 - Uygulama | Redis Kümesi | 30s-15 dk | Oturum, özellik sayıları, coğrafi kodlama, H3 toplamaları | %80-90 |
| L4 - Sorgular | Elasticsearch kopyaları | Neredeyse gerçek zamanlı | Aramalar için yanıtları okuyun, öneriler için ayrı dizin | Yok (ölçeklendirmeyi oku) |
| L5 - Veritabanları | PostgreSQL + pgbouncer | Yok | Bağlantı havuzu oluşturma, rapor başına kopya okuma | Yok |
Bölgeye Göre Veritabanı Parçalama
Coğrafi olarak dağıtılmış, bölgeye ve stratejiye göre parçalanmış milyonlarca mülkle emlak platformları için daha doğal. Her parça bir makro bölgenin verilerini içerir. yatay olarak ölçeklendirmenize ve aynı zamanda düzenlemelere uymanıza olanak tanır veri yerleşimi (GDPR).
- Parça Anahtarı: coğrafi bölge (ör.
country:region=IT:lombardia) - Yönlendirme: ağ geçidi API'si sorgunun konumuna göre parçayı belirler
- Parçalar arası sorgular: çoklu bölge aramaları için dağılma-toplanma (nadiren)
- Yeniden dengeleme: bir bölge eşiği aştığında bölünür (örneğin, Milan özel bir parça haline gelir)
Okuma/Yazma Ayırma için CQRS Modeli
Desen CQRS (Komut Sorgusu Sorumluluk Ayrımı) ve özellikle Okuma/yazma oranının genellikle 100:1 olduğu emlak platformları için etkilidir. kutsal yazılar (reklamların oluşturulması, fiyat güncellemeleri) Listeleme Hizmetinden geçer gerçeğin kaynağı olarak PostgreSQL ile. okumalar (araştırma, listeleme detayı) faydalı oldu Elasticsearch ve Redis'ten, olaylar yoluyla eşzamansız olarak güncellenir. Bu ayrıştırma iki yolu bağımsız olarak optimize etmenize olanak tanır.
Çoklu Dil ve Çoklu Para Birimi
Uluslararası pazarlarda faaliyet gösteren platformlar (İspanya, İtalya ve Portekiz’deki İdealista gibi) birden çok dildeki içeriği ve farklı para birimlerinde fiyatlandırmayı yönetmeleri gerekiyor. Strateji tabana göre değişir içeriğin türüne göre değişir.
İçerik Türü Stratejisi
- Statik kullanıcı arayüzü: ön uçtan yüklenen çeviri dosyaları (i18n), tek dil paket başına (Angular i18n, React i18next)
-
Reklam açıklamaları: alan
LocalizedTextDB'de, çeviri "Otomatik olarak çevrildi" işaretiyle DeepL/Google Translate aracılığıyla otomatik - Adresler: tercüme edilmez (yerel dilde kalırlar), ancak şehir varyasyonları var (Milano/Milano, Roma/Roma)
- Fiyatlar: orijinal para biriminde saklanır, görüntülenmek üzere anında dönüştürülür API tarafından güncellenen kurlarla (ECB, Açık Döviz Kurları)
-
SEO: Yerelleştirilmiş URL'ler (
/it/vendita/milanovs/en/sale/milan) etiketlihreflangyinelenen içeriği önlemek için
Uyumluluk: GDPR ve Adil Konut
Gayrimenkul platformları son derece hassas verileri işler: konut tercihleri, yetenekler mali, araştırma geçmişi. Uyumluluk bir seçenek değil, mimari bir gerekliliktir Bu da sistem tasarımını her düzeyde etkiler.
GDPR: Mimariye Etkisi
-
Veri minimizasyonu: yalnızca kesinlikle gerekli olan verileri toplayın.
Sahaya hizmet etmeyin
viewCounthalka açık yanıtta konumu takip etmeyin Açık rıza olmadan kullanıcının GPS'i. -
Silme hakkı: uygulamak
DELETE /api/v1/users/{id}/data30 gün içinde profilleri, kayıtlı aramaları, mesajları siler ve günlükleri anonimleştirir. -
Veri taşınabilirliği:
GET /api/v1/users/{id}/exportoluşturmak tüm kullanıcı verilerini içeren bir JSON/CSV arşivi. - Onay yönetimi: her izleyici (GA4, ısı haritası, yeniden pazarlama) yalnızca etkinleştirildi açık rızanın ardından, kategoriye göre ayrıntı düzeyiyle.
- Veri yerleşimi: Bölgeye göre parçalama, gereksinime uyumu kolaylaştırır AB kullanıcı verilerinin AB veri merkezlerinde kalması.
Adil Barınma ve Ayrımcılık
ABD'de Adil Konut Yasası satış ve kiralamada ayrımcılığı yasaklar ırk, renk, din, cinsiyet, engellilik, ailevi durum veya kökene dayalı gayrimenkul ulusal. Teknik çıkarımlar:
- Arama filtreleri: hiçbir zaman proxy görevi görebilecek filtreler sunmayın. korunan özellikler (örneğin "mahallenin demografik bileşimi")
- Reklam hedefleme: Sponsorlu reklam hedefleme kullanılmamalıdır korunan demografik kriterler (Facebook bunun için 2022'de 5 milyon dolar ödedi)
- ML Modelleri: Değerleme modellerinde önyargıya yönelik düzenli denetimler e tavsiye. Mahallelerdeki mülklere sistematik olarak düşük değer biçen bir model azınlık çoğunluğu ve sadece adaletsiz ve yasa dışı değil.
- Reklam açıklamaları: ayrımcı dil için otomatik filtre açıklamalarda (örneğin "çocuksuz çiftler için ideal" Adil Konut Yasasını ihlal eder)
Uyarı: Derecelendirme Modellerinde Önyargı
Gayrimenkul değerleme makine öğrenimi modelleri ("Zestimate" gibi) tarihsel önyargıları güçlendirebilir. Eğitim verileri piyasada onlarca yıldır süren ayrımcılığı yansıtıyorsa (kırmızı çizgi), model bu eşitsizlikleri yeniden üretecektir. Uygulamak önemlidir adalet denetimler demografik ve sürekli izlemeye dayalı eşitlik ölçümleriyle Bölgelere göre tahminler.
Sonuçlar ve Referans Mimarisi
Ölçekli bir emlak platformu oluşturmak ve bir alıştırma yapmak sistem tasarımı yazılım mühendisliğinin neredeyse her alanına dokunuyor: karmaşık alan modellemeden multimedya boru hatlarını yönetmekten düzenlemelere uymaya kadar jeo-uzamsal araştırmalara kadar katı.
Bu analizden ortaya çıkan temel ilkeler şunlardır:
- Etki alanı ayrıştırması: Listeleme, Arama, Medya'yı açıkça ayırın, Özel veri tabanlarına sahip bağımsız hizmetlerde Kullanıcı ve Bildirim
- Temel bir model olarak CQRS: 100:1 okuma/yazma oranıyla, optimizasyon değil, ayrı okuma (Elasticsearch) ve yazma (PostgreSQL) yolları erken ve mimari bir gereklilik
- Birinci sınıf vatandaş olarak jeouzaysal: Toplama için H3, için PostGIS hassaslık, tam yığın arama için Elasticsearch geo_point
- Asenkron boru hatları: reklam alımı, medya işleme ve bildirimler kullanıcı deneyimini olumsuz etkilemeden ani artışların üstesinden gelmek için olay odaklı olmaları gerekir
- Tasarıma uygunluk: GDPR ve adil konut eklenecek özellikler değildir daha sonra, ancak ilk günden itibaren seçimleri yönlendiren mimari kısıtlamalar
Serinin bir sonraki makalesinde ayrıntılı olarak inceleyeceğiz. değerlendirme modelleri Makine Öğrenimine dayalı gayrimenkulZillow, Redfin ve Avrupa platformları piyasa fiyatlarını Otomatik Değerleme Modelleri (AVM) ile tahmin ediyor Bu yeteneğin ne gibi teknik ve etik zorluklara yol açtığı.
Kaynaklar ve Analizler
- DÖNÜŞ Web API'si: reso.org - MLS veri alışverişi standardı
- H3: h3geo.org - Uber'in altıgen coğrafi indeksleme sistemi
- Elasticsearch Jeo-uzaysal: elastic.co/docs - Coğrafi sorgu belgeleri
- PostGIS: postgis.net - PostgreSQL için coğrafi uzantı
- PropTech için GDPR: Profil oluşturma ve otomatik karar almaya ilişkin EDPB yönergeleri
- Adil Konut Yasası: hud.gov - ABD ayrımcılıkla mücadele yasası







