← Kursa Dön
📄 Text · 40 min

Geo Queries — Konum Tabanlı Arama

Giriş — "Yakınımdaki Restoranlar"

Telefonunuzu açıyorsunuz, "yakınımdaki restoranlar" yazıyorsunuz ve saniyeler içinde en yakından en uzağa sıralanmış bir liste görüyorsunuz. Haritada bir alan çiziyorsunuz, sadece o alandaki sonuçlar gösteriliyor. Belirli bir bölgenin ısı haritasını çıkarıyorsunuz. Bunların hepsi geo query ve geo aggregation'ların gücüdür.

Elasticsearch, coğrafi verilerle çalışmak için kapsamlı bir araç seti sunar. Bir noktaya olan mesafe, bir dikdörtgen alan içinde arama, polygon sınırları içinde filtreleme, coğrafi grid bazlı gruplama — hepsini Query DSL ile yapabilirsiniz. Bu ders, geo_point mapping'den geo_distance query/sort/aggregation'a, geo_bounding_box'tan geo_shape polygon aramalarına, geohash/geotile/geohex grid aggregation'larına kadar tüm coğrafi yetenekleri derinlemesine inceleyecek.


1. Geo Point Mapping — Konum Verisi Saklama

1.1 Temel Mapping

PUT /restaurants
{
  "mappings": {
    "properties": {
      "name": { "type": "text" },
      "cuisine": { "type": "keyword" },
      "rating": { "type": "float" },
      "location": { "type": "geo_point" }
    }
  }
}

1.2 Doküman İndeksleme — Farklı Formatlar

// Format 1: Object (en okunabilir) — ÖNERİLEN
POST /restaurants/_doc/1
{
  "name": "Kebapçı Mehmet",
  "cuisine": "türk",
  "rating": 4.5,
  "location": {
    "lat": 41.0082,
    "lon": 28.9784
  }
}

// Format 2: String "lat,lon"
POST /restaurants/_doc/2
{
  "name": "Pizza Roma",
  "cuisine": "italyan",
  "rating": 4.2,
  "location": "41.0122,28.9760"
}

// Format 3: Array [lon, lat] — GeoJSON sırası!
POST /restaurants/_doc/3
{
  "name": "Sushi Master",
  "cuisine": "japon",
  "rating": 4.8,
  "location": [28.9690, 41.0105]
}

// Format 4: Geohash
POST /restaurants/_doc/4
{
  "name": "Waffle House",
  "cuisine": "amerikan",
  "rating": 3.9,
  "location": "sxk9g5"
}

// Format 5: WKT (Well-Known Text)
POST /restaurants/_doc/5
{
  "name": "Taco Bell",
  "cuisine": "meksika",
  "rating": 3.5,
  "location": "POINT (28.9784 41.0082)"
}

⚠️ Dikkat: Array formatı [longitude, latitude] sırasını kullanır — GeoJSON standardı! String formatı ise "latitude,longitude" sırasını kullanır. Bu tutarsızlık en yaygın hata kaynağıdır.

Format Özeti:
──────────────────────────────────
Object:  { "lat": 41.00, "lon": 28.97 }   ← lat/lon açık, karışmaz
String:  "41.00,28.97"                      ← lat,lon sırası
Array:   [28.97, 41.00]                     ← LON,LAT sırası! ⚠️
WKT:     "POINT (28.97 41.00)"             ← LON LAT (boşlukla) ⚠️
──────────────────────────────────

1.3 geo_point Mapping Parametreleri

PUT /locations
{
  "mappings": {
    "properties": {
      "position": {
        "type": "geo_point",
        "ignore_malformed": true,
        "null_value": { "lat": 0, "lon": 0 }
      }
    }
  }
}
  • `ignore_malformed`: Geçersiz koordinatlar hata vermez, doküman indexlenir (o alan atlanır)

  • `null_value`: null değer yerine kullanılacak varsayılan konum


2. Distance Units — Mesafe Birimleri

Elasticsearch'te mesafe belirtirken çeşitli birimler kullanılabilir:

┌──────────────┬────────────────┬──────────────────────────┐
│ Birim        │ Kısaltma       │ Açıklama                 │
├──────────────┼────────────────┼──────────────────────────┤
│ Mile         │ mi             │ 1 mil = 1.609 km         │
│ Yard         │ yd             │ 1 yard = 0.914 m         │
│ Feet         │ ft             │ 1 feet = 0.305 m         │
│ Inch         │ in             │ 1 inç = 0.0254 m         │
│ Kilometer    │ km             │ 1000 metre               │
│ Meter        │ m              │ Varsayılan birim         │
│ Centimeter   │ cm             │ 0.01 metre               │
│ Millimeter   │ mm             │ 0.001 metre              │
│ Nautical Mile│ nmi            │ 1.852 km (deniz mili)    │
└──────────────┴────────────────┴──────────────────────────┘
// Kullanım örnekleri
"distance": "5km"
"distance": "3mi"
"distance": "500m"
"distance": "0.5nmi"

3. geo_distance Query — Belirli Mesafe İçinde Arama

3.1 Temel Kullanım

Bir noktaya belirli mesafe içindeki dokümanları filtreler:

GET /restaurants/_search
{
  "query": {
    "bool": {
      "must": {
        "match": { "cuisine": "türk" }
      },
      "filter": {
        "geo_distance": {
          "distance": "2km",
          "location": {
            "lat": 41.0082,
            "lon": 28.9784
          }
        }
      }
    }
  }
}

3.2 distance_type Parametresi

GET /restaurants/_search
{
  "query": {
    "geo_distance": {
      "distance": "5km",
      "location": { "lat": 41.0082, "lon": 28.9784 },
      "distance_type": "arc"
    }
  }
}

distance_type seçenekleri:

  • `arc` (varsayılan): Gerçek küre yüzeyi mesafesi (Haversine formülü). Doğru sonuç verir ama biraz daha yavaştır

  • `plane`: Düzlem mesafesi. Küçük mesafelerde (~5km) iyi yaklaşım, büyük mesafelerde hata artar. Daha hızlıdır

arc vs plane doğruluğu:
Mesafe < 1 km:     arc ≈ plane (fark yok)
Mesafe 1-10 km:    plane %0.1 sapma
Mesafe 10-100 km:  plane %1 sapma
Mesafe > 100 km:   plane %5+ sapma → arc kullanın!

3.3 Validation Mode

"geo_distance": {
  "distance": "5km",
  "location": { "lat": 41.0082, "lon": 28.9784 },
  "validation_method": "STRICT"
}
  • `STRICT`: Geçersiz koordinatlar hata verir (varsayılan)

  • `IGNORE_MALFORMED`: Geçersiz koordinatları sessizce yok sayar

  • `COERCE`: Koordinatları normalize eder (360° sarmalama gibi)


4. geo_distance Sort — Mesafeye Göre Sıralama

GET /restaurants/_search
{
  "query": {
    "bool": {
      "filter": {
        "geo_distance": {
          "distance": "10km",
          "location": { "lat": 41.0082, "lon": 28.9784 }
        }
      }
    }
  },
  "sort": [
    {
      "_geo_distance": {
        "location": { "lat": 41.0082, "lon": 28.9784 },
        "order": "asc",
        "unit": "km",
        "mode": "min",
        "distance_type": "arc"
      }
    }
  ]
}

Sort yanıtı — mesafe bilgisi sort values'da döner:

{
  "hits": {
    "hits": [
      {
        "_source": { "name": "Kebapçı Mehmet", "location": { "lat": 41.0082, "lon": 28.9784 } },
        "sort": [0.0]   // 0 km uzaklıkta
      },
      {
        "_source": { "name": "Pizza Roma", "location": { "lat": 41.0122, "lon": 28.9760 } },
        "sort": [0.478]  // 478 metre uzaklıkta
      },
      {
        "_source": { "name": "Sushi Master", "location": { "lat": 41.0105, "lon": 28.9690 } },
        "sort": [0.812]  // 812 metre uzaklıkta
      }
    ]
  }
}

mode parametresi (doküman birden fazla location'a sahipse):

  • min: En yakın noktaya göre sırala

  • max: En uzak noktaya göre sırala

  • avg: Ortalama mesafeye göre sırala

  • median: Ortanca mesafeye göre sırala

// Birden fazla lokasyona sahip doküman
POST /businesses/_doc/1
{
  "name": "Starbucks",
  "locations": [
    { "lat": 41.0082, "lon": 28.9784 },
    { "lat": 41.0200, "lon": 28.9500 },
    { "lat": 40.9900, "lon": 29.0100 }
  ]
}

// En yakın şubeye göre sırala
"sort": [{
  "_geo_distance": {
    "locations": { "lat": 41.0082, "lon": 28.9784 },
    "order": "asc",
    "mode": "min"
  }
}]

5. geo_bounding_box — Dikdörtgen Alanda Arama

Haritada görünen alanı temsil eden dikdörtgen (bounding box) içindeki dokümanları filtreler:

GET /restaurants/_search
{
  "query": {
    "bool": {
      "filter": {
        "geo_bounding_box": {
          "location": {
            "top_left": {
              "lat": 41.05,
              "lon": 28.90
            },
            "bottom_right": {
              "lat": 40.95,
              "lon": 29.05
            }
          }
        }
      }
    }
  }
}

Alternatif format — Vertices:

"geo_bounding_box": {
  "location": {
    "top": 41.05,
    "left": 28.90,
    "bottom": 40.95,
    "right": 29.05
  }
}

WKT envelope formatı:

"geo_bounding_box": {
  "location": {
    "wkt": "BBOX (28.90, 29.05, 41.05, 40.95)"
  }
}

💡 İpucu: geo_bounding_box, geo_distance'dan çok daha hızlıdır çünkü daire yerine dikdörtgen hesaplaması yapar (basit aralık karşılaştırması). Harita görünümünde kullanıyorsanız, önce bounding box ile filtreleyin, sonra mesafeye göre sıralayın.

// Hız optimizasyonu — bounding box + distance sort
GET /restaurants/_search
{
  "query": {
    "bool": {
      "filter": {
        "geo_bounding_box": {
          "location": {
            "top_left": { "lat": 41.05, "lon": 28.90 },
            "bottom_right": { "lat": 40.95, "lon": 29.05 }
          }
        }
      }
    }
  },
  "sort": [{
    "_geo_distance": {
      "location": { "lat": 41.0082, "lon": 28.9784 },
      "order": "asc",
      "unit": "km"
    }
  }]
}

6. geo_shape Query — Polygon ve Şekil Araması

6.1 geo_shape Mapping

PUT /delivery-zones
{
  "mappings": {
    "properties": {
      "zone_name": { "type": "keyword" },
      "boundary": { "type": "geo_shape" }
    }
  }
}

6.2 Şekil Tipleri ile İndeksleme

// Point
POST /delivery-zones/_doc/1
{
  "zone_name": "merkez-ofis",
  "boundary": {
    "type": "point",
    "coordinates": [28.9784, 41.0082]
  }
}

// Polygon
POST /delivery-zones/_doc/2
{
  "zone_name": "istanbul-avrupa-merkez",
  "boundary": {
    "type": "polygon",
    "coordinates": [[
      [28.90, 41.05],
      [29.05, 41.05],
      [29.05, 40.95],
      [28.90, 40.95],
      [28.90, 41.05]
    ]]
  }
}

// Circle (Elasticsearch'e özel, standart GeoJSON değil)
POST /delivery-zones/_doc/3
{
  "zone_name": "kadikoy-bolge",
  "boundary": {
    "type": "circle",
    "coordinates": [29.0250, 40.9907],
    "radius": "3km"
  }
}

// MultiPolygon
POST /delivery-zones/_doc/4
{
  "zone_name": "istanbul-tum-bolgeler",
  "boundary": {
    "type": "multipolygon",
    "coordinates": [
      [[[28.90, 41.05], [29.05, 41.05], [29.05, 40.95], [28.90, 40.95], [28.90, 41.05]]],
      [[[29.00, 40.99], [29.10, 40.99], [29.10, 40.94], [29.00, 40.94], [29.00, 40.99]]]
    ]
  }
}

6.3 Spatial Relations — Mekânsal İlişkiler

geo_shape query'de relation parametresi, doküman şeklinin sorgu şekliyle nasıl ilişkili olması gerektiğini belirler:

GET /delivery-zones/_search
{
  "query": {
    "geo_shape": {
      "boundary": {
        "shape": {
          "type": "polygon",
          "coordinates": [[
            [28.95, 41.02],
            [29.00, 41.02],
            [29.00, 40.98],
            [28.95, 40.98],
            [28.95, 41.02]
          ]]
        },
        "relation": "intersects"
      }
    }
  }
}

Spatial relations:

┌──────────────┬────────────────────────────────────────────────────┐
│ Relation     │ Açıklama                                          │
├──────────────┼────────────────────────────────────────────────────┤
│ INTERSECTS   │ Doküman şekli, sorgu şekliyle KESİŞİYOR mu?       │
│              │ (herhangi bir ortak alan var mı?) — Varsayılan     │
├──────────────┼────────────────────────────────────────────────────┤
│ WITHIN       │ Doküman şekli, sorgu şeklinin İÇİNDE mi?          │
│              │ (tamamen kapsamalı)                                │
├──────────────┼────────────────────────────────────────────────────┤
│ CONTAINS     │ Doküman şekli, sorgu şeklini KAPSIYOR mu?         │
│              │ (sorgu tamamen dokümanın içinde)                   │
├──────────────┼────────────────────────────────────────────────────┤
│ DISJOINT     │ Doküman şekli, sorgu şekliyle KESİŞMİYOR mu?     │
│              │ (hiçbir ortak alan yok)                            │
└──────────────┴────────────────────────────────────────────────────┘

Görselleştirme:

  ┌───────────────┐
  │  Query Shape  │
  │    ┌──────┐   │
  │    │ Doc A│   │   Doc A → WITHIN ✅, INTERSECTS ✅
  │    └──────┘   │
  │         ┌─────┼───┐
  │         │Doc B│   │  Doc B → INTERSECTS ✅, WITHIN ❌
  └─────────┼─────┘   │
            └─────────┘
  ┌──────┐
  │Doc C │                Doc C → DISJOINT ✅, INTERSECTS ❌
  └──────┘

6.4 Indexed Shape — Kayıtlı Şekille Arama

Daha önce index'lenmiş bir şekli referans alarak arama:

// Önceden kaydedilmiş bir bölge ile arama
GET /restaurants/_search
{
  "query": {
    "geo_shape": {
      "location": {
        "indexed_shape": {
          "index": "delivery-zones",
          "id": "2",
          "path": "boundary"
        },
        "relation": "within"
      }
    }
  }
}

Bu, delivery-zones index'indeki ID 2'nin boundary alanını alır ve bu polygon içindeki restoranları bulur.


7. Geo Aggregations

7.1 geo_distance Aggregation

Mesafe halkalarına göre gruplama:

GET /restaurants/_search
{
  "size": 0,
  "aggs": {
    "distance_rings": {
      "geo_distance": {
        "field": "location",
        "origin": { "lat": 41.0082, "lon": 28.9784 },
        "unit": "km",
        "ranges": [
          { "to": 1, "key": "0-1km" },
          { "from": 1, "to": 3, "key": "1-3km" },
          { "from": 3, "to": 5, "key": "3-5km" },
          { "from": 5, "to": 10, "key": "5-10km" },
          { "from": 10, "key": "10km+" }
        ]
      },
      "aggs": {
        "avg_rating": { "avg": { "field": "rating" } }
      }
    }
  }
}

Yanıt:

{
  "aggregations": {
    "distance_rings": {
      "buckets": [
        { "key": "0-1km", "doc_count": 15, "avg_rating": { "value": 4.2 } },
        { "key": "1-3km", "doc_count": 42, "avg_rating": { "value": 4.0 } },
        { "key": "3-5km", "doc_count": 78, "avg_rating": { "value": 3.8 } },
        { "key": "5-10km", "doc_count": 120, "avg_rating": { "value": 3.7 } },
        { "key": "10km+", "doc_count": 350, "avg_rating": { "value": 3.5 } }
      ]
    }
  }
}

7.2 Geohash Grid Aggregation

Geohash hücrelerine göre gruplama — ısı haritası oluşturmak için ideal:

GET /restaurants/_search
{
  "size": 0,
  "aggs": {
    "grid": {
      "geohash_grid": {
        "field": "location",
        "precision": 5,
        "size": 100
      },
      "aggs": {
        "cell_centroid": {
          "geo_centroid": { "field": "location" }
        },
        "avg_rating": {
          "avg": { "field": "rating" }
        }
      }
    }
  }
}

precision seçimi:

┌───────────┬────────────────────┬─────────────────────────┐
│ Precision │ Hücre Boyutu       │ Kullanım                │
├───────────┼────────────────────┼─────────────────────────┤
│ 1         │ ~5000 × 5000 km    │ Kıta seviyesi           │
│ 2         │ ~1250 × 625 km     │ Ülke seviyesi           │
│ 3         │ ~156 × 156 km      │ Büyük bölge             │
│ 4         │ ~39 × 19.5 km      │ Şehir seviyesi          │
│ 5         │ ~5 × 5 km          │ İlçe seviyesi ✅         │
│ 6         │ ~1.2 × 0.6 km      │ Mahalle seviyesi        │
│ 7         │ ~153 × 153 m       │ Sokak seviyesi          │
│ 8         │ ~38 × 19 m         │ Bina seviyesi           │
│ 9         │ ~5 × 5 m           │ Oda seviyesi            │
│ 12        │ ~4 × 2 cm          │ Santimetre hassasiyeti  │
└───────────┴────────────────────┴─────────────────────────┘

7.3 Geotile Grid Aggregation

Geotile, web harita servislerinin (Google Maps, OpenStreetMap) kullandığı tile koordinat sistemini kullanır:

GET /restaurants/_search
{
  "size": 0,
  "aggs": {
    "tile_grid": {
      "geotile_grid": {
        "field": "location",
        "precision": 12,
        "size": 1000
      },
      "aggs": {
        "count_per_tile": { "value_count": { "field": "location" } },
        "centroid": { "geo_centroid": { "field": "location" } }
      }
    }
  }
}

Geotile vs Geohash:

geohash:  Base32 encoded (sxk9g5)    → Düzensiz dikdörtgenler
geotile:  z/x/y format (12/2212/1420) → Web harita tile'ları ile uyumlu
geohex:   H3 hexagonal grid           → Eşit alanlı altıgenler ✅

7.4 Geohex Grid Aggregation (Elasticsearch 8.1+)

Uber'in H3 hexagonal grid sistemini kullanır. Altıgen hücreler eşit alana sahiptir (geohash'in aksine):

GET /restaurants/_search
{
  "size": 0,
  "aggs": {
    "hex_grid": {
      "geohex_grid": {
        "field": "location",
        "precision": 6,
        "size": 100
      },
      "aggs": {
        "centroid": { "geo_centroid": { "field": "location" } }
      }
    }
  }
}

Geohex precision seviyeleri:

Precision 0:  ~4.3M km²     → Kıta
Precision 3:  ~12,000 km²   → Büyük şehir
Precision 5:  ~250 km²      → İlçe
Precision 7:  ~5 km²        → Mahalle
Precision 9:  ~0.1 km²      → Blok
Precision 11: ~0.002 km²    → Bina

7.5 geo_centroid Aggregation

Doküman grubunun ağırlık merkezini hesaplar:

GET /restaurants/_search
{
  "size": 0,
  "aggs": {
    "by_cuisine": {
      "terms": { "field": "cuisine" },
      "aggs": {
        "center": {
          "geo_centroid": { "field": "location" }
        }
      }
    }
  }
}

// Yanıt:
{
  "aggregations": {
    "by_cuisine": {
      "buckets": [
        {
          "key": "türk",
          "doc_count": 150,
          "center": {
            "location": { "lat": 41.005, "lon": 28.985 },
            "count": 150
          }
        },
        {
          "key": "italyan",
          "doc_count": 45,
          "center": {
            "location": { "lat": 41.012, "lon": 28.970 },
            "count": 45
          }
        }
      ]
    }
  }
}

7.6 geo_bounds Aggregation

Tüm dokümanları kapsayan en küçük dikdörtgeni hesaplar:

GET /restaurants/_search
{
  "size": 0,
  "aggs": {
    "viewport": {
      "geo_bounds": {
        "field": "location",
        "wrap_longitude": true
      }
    }
  }
}

// Yanıt:
{
  "aggregations": {
    "viewport": {
      "bounds": {
        "top_left": { "lat": 41.0500, "lon": 28.9000 },
        "bottom_right": { "lat": 40.9500, "lon": 29.1000 }
      }
    }
  }
}

Bu, haritanın zoom seviyesini otomatik ayarlamak için kullanılır — tüm sonuçları kapsayacak viewport'u hesaplar.


8. Java ile Geo Arama

8.1 Geo Distance Query + Sort

public class GeoSearchService {
    private final ElasticsearchClient client;

    public GeoSearchService(ElasticsearchClient client) {
        this.client = client;
    }

    /**
     * Belirli bir noktaya yakın restoranları bul
     */
    public SearchResponse<Restaurant> findNearby(
            double lat, double lon, String distance,
            String cuisineFilter, int size) throws IOException {

        return client.search(s -> s
            .index("restaurants")
            .query(q -> q
                .bool(b -> {
                    // Geo distance filter
                    b.filter(f -> f
                        .geoDistance(gd -> gd
                            .field("location")
                            .location(l -> l.latlon(ll -> ll.lat(lat).lon(lon)))
                            .distance(distance)
                        )
                    );

                    // Opsiyonel cuisine filter
                    if (cuisineFilter != null) {
                        b.filter(f -> f
                            .term(t -> t
                                .field("cuisine")
                                .value(cuisineFilter)
                            )
                        );
                    }

                    return b;
                })
            )
            .sort(so -> so
                .geoDistance(gd -> gd
                    .field("location")
                    .location(l -> l.latlon(ll -> ll.lat(lat).lon(lon)))
                    .order(SortOrder.Asc)
                    .unit(DistanceUnit.Kilometers)
                )
            )
            .size(size),
            Restaurant.class
        );
    }

    /**
     * Sonuçları mesafe bilgisiyle birlikte yazdır
     */
    public void printResults(SearchResponse<Restaurant> response) {
        for (Hit<Restaurant> hit : response.hits().hits()) {
            Restaurant r = hit.source();
            double distanceKm = hit.sort().get(0).doubleValue();
            System.out.printf("%-25s | %-10s | ⭐%.1f | 📍%.2f km%n",
                r.getName(), r.getCuisine(), r.getRating(), distanceKm);
        }
    }
}

8.2 Geo Bounding Box Query

public SearchResponse<Restaurant> findInBoundingBox(
        double topLat, double leftLon,
        double bottomLat, double rightLon) throws IOException {

    return client.search(s -> s
        .index("restaurants")
        .query(q -> q
            .geoBoundingBox(gbb -> gbb
                .field("location")
                .boundingBox(bb -> bb
                    .tlbr(tlbr -> tlbr
                        .topLeft(tl -> tl.latlon(ll -> ll.lat(topLat).lon(leftLon)))
                        .bottomRight(br -> br.latlon(ll -> ll.lat(bottomLat).lon(rightLon)))
                    )
                )
            )
        )
        .size(100),
        Restaurant.class
    );
}

8.3 Geo Aggregation

public void geoDistanceAggregation(double lat, double lon) throws IOException {
    SearchResponse<Void> response = client.search(s -> s
        .index("restaurants")
        .size(0)
        .aggregations("distance_rings", a -> a
            .geoDistance(gd -> gd
                .field("location")
                .origin(o -> o.latlon(ll -> ll.lat(lat).lon(lon)))
                .unit(DistanceUnit.Kilometers)
                .ranges(
                    r -> r.to(1.0).key("0-1km"),
                    r -> r.from(1.0).to(5.0).key("1-5km"),
                    r -> r.from(5.0).to(10.0).key("5-10km"),
                    r -> r.from(10.0).key("10km+")
                )
            )
            .aggregations("avg_rating", sub -> sub
                .avg(avg -> avg.field("rating"))
            )
        ),
        Void.class
    );

    // Sonuçları yazdır
    var buckets = response.aggregations()
        .get("distance_rings").geoDistance().buckets().array();

    for (var bucket : buckets) {
        double avgRating = bucket.aggregations()
            .get("avg_rating").avg().value();
        System.out.printf("%-10s: %d restaurants, avg rating: %.1f%n",
            bucket.key(), bucket.docCount(), avgRating);
    }
}

8.4 Tam Uygulama — Restoran Arama Servisi

public class RestaurantSearchService {
    private final ElasticsearchClient client;

    public RestaurantSearchService(ElasticsearchClient client) {
        this.client = client;
    }

    /**
     * Harita görünümü: bounding box + mesafe sıralaması + aggregation
     */
    public MapSearchResult searchForMap(
            double centerLat, double centerLon,
            double topLat, double leftLon,
            double bottomLat, double rightLon,
            String query, int size) throws IOException {

        SearchResponse<Restaurant> response = client.search(s -> s
            .index("restaurants")
            .query(q -> q
                .bool(b -> {
                    // Bounding box filter (hızlı)
                    b.filter(f -> f
                        .geoBoundingBox(gbb -> gbb
                            .field("location")
                            .boundingBox(bb -> bb
                                .tlbr(tlbr -> tlbr
                                    .topLeft(tl -> tl.latlon(ll ->
                                        ll.lat(topLat).lon(leftLon)))
                                    .bottomRight(br -> br.latlon(ll ->
                                        ll.lat(bottomLat).lon(rightLon)))
                                )
                            )
                        )
                    );

                    // Text arama (opsiyonel)
                    if (query != null && !query.isBlank()) {
                        b.must(m -> m
                            .multiMatch(mm -> mm
                                .query(query)
                                .fields("name^3", "cuisine^2", "description")
                            )
                        );
                    }

                    return b;
                })
            )
            // Mesafeye göre sırala
            .sort(so -> so
                .geoDistance(gd -> gd
                    .field("location")
                    .location(l -> l.latlon(ll ->
                        ll.lat(centerLat).lon(centerLon)))
                    .order(SortOrder.Asc)
                    .unit(DistanceUnit.Kilometers)
                )
            )
            // Aggregation: Cuisine dağılımı
            .aggregations("cuisines", a -> a
                .terms(t -> t.field("cuisine").size(20))
            )
            // Aggregation: Viewport bounds
            .aggregations("bounds", a -> a
                .geoBounds(gb -> gb.field("location"))
            )
            .size(size),
            Restaurant.class
        );

        return new MapSearchResult(response);
    }
}

9. Yaygın Hatalar

Hata 1: Array'de lat/lon sırasını karıştırmak

// ❌ YANLIŞ — [lat, lon]
{ "location": [41.0082, 28.9784] }

// ✅ DOĞRU — [lon, lat] (GeoJSON standardı)
{ "location": [28.9784, 41.0082] }

// ✅ En güvenli — object formatı
{ "location": { "lat": 41.0082, "lon": 28.9784 } }

Hata 2: Polygon'u kapatmamak

// ❌ YANLIŞ — İlk ve son nokta farklı
"coordinates": [[
  [28.90, 41.05], [29.05, 41.05], [29.05, 40.95], [28.90, 40.95]
]]

// ✅ DOĞRU — İlk ve son nokta aynı olmalı (polygon kapalı olmalı)
"coordinates": [[
  [28.90, 41.05], [29.05, 41.05], [29.05, 40.95], [28.90, 40.95], [28.90, 41.05]
]]

Hata 3: geo_distance'ı filter yerine query'de kullanmak

// ❌ Gereksiz scoring maliyeti
"query": {
  "geo_distance": { "distance": "5km", "location": {...} }
}

// ✅ filter context'te kullan — scoring hesaplamaz, daha hızlı
"query": {
  "bool": {
    "filter": {
      "geo_distance": { "distance": "5km", "location": {...} }
    }
  }
}

10. Performans İpuçları

✅ YAP:
  • Geo query'leri filter context'te kullan (scoring overhead'i yok)
  • geo_bounding_box kullan harita görünümlerinde (geo_distance'tan hızlı)
  • Geohex grid tercih et (eşit alanlı hücreler, daha adil dağılım)
  • geo_centroid ile aggregation sonuçlarını haritada göster
  • precision'ı ihtiyaca göre ayarla (gereksiz yüksek precision yavaşlatır)

❌ YAPMA:
  • Çok yüksek precision geohash grid kullanma (precision 8+ dikkatli!)
  • geo_distance'ı query context'te kullanma (filter yeterli)
  • Array formatı kullanma (lat/lon karışıklığı riski) — object kullan
  • distance_type: arc'ı küçük mesafelerde kullanma (plane yeterli, daha hızlı)

11. Özet

  • geo_point yeryüzünde bir noktayı, geo_shape polygon/circle gibi karmaşık şekilleri saklar. Object formatı {lat, lon} en güvenli seçimdir

  • geo_distance query belirli mesafe içindeki dokümanları, geo_bounding_box dikdörtgen alandaki dokümanları filtreler. Bounding box geo_distance'tan çok daha hızlıdır

  • geo_shape query ile polygon, circle ve diğer şekillerle spatial arama yapılır. relation parametresi (INTERSECTS, WITHIN, CONTAINS, DISJOINT) mekânsal ilişkiyi belirler

  • _geo_distance sort mesafeye göre sıralama yapar ve sort values'da mesafe bilgisi döner — "yakınımdaki" özelliği için temeldir

  • geo_distance aggregation mesafe halkalarına göre gruplar, geohash/geotile/geohex grid coğrafi hücrelere göre gruplar. Geohex (H3) eşit alanlı altıgenler sunar ve ısı haritaları için idealdir

  • geo_centroid bir doküman grubunun coğrafi merkezini, geo_bounds tüm sonuçları kapsayan en küçük dikdörtgeni hesaplar — harita viewport hesaplaması için kullanılır

  • Performans için geo query'leri her zaman filter context'te kullanın ve mümkünse bounding box ile ön filtreleme yapın