.Net Core Üzerinde ElasticSearch ile İstenen Bir Mesafe İçindeki Lokasyonların Filitrelenmesi
Selamlar,
Bu makalede girilen bir konuma göre, belirlenen bir mesafe içinde kalan DB’deki tüm lokasyonların filitrelenmesini, ElasticSearch ve .Net Core kullanarak inceleyeceğiz. ElasticSearch hakkında detaylı bilgiye burdaki makaleden erişebilirsiniz.
Örneğin 1000 tane firmanın 10 tane lokasyonu olan bir DB kaydı olsun. Mobil bir uygulamada, bulunulan konumun 20km çevresindeki lokasyonu olan firmaların sıralanması istendiğinde, bunun matematiksel işlemler yapılarak bulunması büyük bir vakit ve kaynak kaybını neden olacaktır. Bunun için ElasticSearch ve GeoDistance() fliter’ının kullanılması, bize sonucun saniyeler içinde dönmesini sağlıyacaktır.
Öncelikle “Cm_Customer” modelimiz aşağıda görüldüğü gibi oluşturulur:
Cm_Customer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; [Table("Cm_Customer", Schema = "dbo")] public class Cm_Customer { public Cm_Customer() { CmCustomerLocations = new HashSet<Cm_CustomerLocations>(); } [Key] public int Id { get; set; } public string Name { get; set; } public bool? IsDeleted { get; set; } public virtual ICollection<Cm_CustomerLocations> CmCustomerLocations { get; set; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; [Table("Cm_CustomerLocations", Schema = "dbo")] public class Cm_CustomerLocations { public Cm_CustomerLocations() { } [Key] public int Id { get; set; } public string Name { get; set; } public int CustomerId { get; set; } public string Longitude { get; set; } public string Latitude { get; set; } public bool IsDeleted { get; set; } public virtual Cm_Customer Customer { get; set; } } |
Örnek amaçlı CustomerLocations Kayıt Kümesi:
Uygulamada .Net Core EntityFrameWork kullanılmıştır:
LocationContext: Entity’de kullanılan DBContext aşağıdaki gibidir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using System; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; public class LocationContext : DbContext { public DbSet<Cm_CustomerLocations> CustomerLocations { get; set; } public DbSet<Cm_Customer> Customers { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); optionsBuilder.UseSqlServer("Server=tcp:10.211.55.9,1433;Initial Catalog=Customer;User ID=sa;Password=********;"); } } |
Şimdi sıra geldi ElasticSearch üzerinde Lokasyon için gerekli Index’in oluşturulmasına. Bu makalede ElasticSearch local makinada çalıştırılacak ve http://localhost:9200 adresinden erişilecektir.
Öncelikle .Net Core Console application aşağıdaki gibi oluşturulur.
1 |
dotnet new console -o ElasticSearch |
MacOs ortamında, eğer önceden ElasticSearch yüklenmiş ise Bash’de, “ElasticSearch” komutu yazılarak ayağa kaldırılır. Ayrıca monitor tool’u olan Kibana‘da yine yüklenmiş ise, bash ortamında “Kibana” komutu yazılarak ayağa kaldırılır.
ElasticSearch için Browser’a “http://localhost:9200” yazıldığında, aşağıdaki gibi bir ekran ile karşılaşılır. “You Konw, for Search” :)
Kibana için Browser’a “http://localhost:5601” yazıldığında, aşağıdaki gibi bir ekran ile karşılaşılır:
1-)ElasticSearch ConnectionSettings:
- Connection string yolu : “http://localhost:9200” tanımlanmıştır.
- Default olarak “customerlocation” indexi kullanılacaktır.
- ElasticSearch Index’inde tutlacak DataModel “CustomerModel“‘dir.
- Oluşturulacak index adı “customerlocation“‘dır.
- ElasticClient, ilgili connection ile oluşturulur.
1 2 3 4 5 6 7 |
private static readonly ConnectionSettings connSettings = new ConnectionSettings(new Uri("http://localhost:9200/")) .DefaultIndex("customerlocation") .DefaultMappingFor<CustomerModel>(m => m .IndexName("customerlocation") private static readonly ElasticClient elasticClient = new ElasticClient(connSettings); |
CustomerModel: ElasticSearch üzerinde tutulacak index’in modeli, aşağıdaki gibidir. Customer tablosundan “CustomerName”, ve geri kalan lokasyon bilgileri de “CustomerLocations” tablosundan alınır. Bir çeşit Sql View’ın, ElasticSearch üzerindeki iz düşümüdür :)
Not: “public GeoLocation Location { get; set; } “: Lokasyon tipi Nest kütüphanesinden “GeoLocation” olarak alınmaktadır. Bu tip, ElasticSearch tarafından Indexlenirken kordinatlar ile çalışma anlamında büyük önem arz etmektedir. GeoLocation tipine bağlı propertyler ve methodlar aşağıda görüldüğü gibidir.
GeoLocation(Model): Nest kütüphanesinde tanımlı olan, GeoLocation sınıfının, property ve methodları aşağıdaki gibidir.
CustomerModel:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using Nest; public class CustomerModel { public int CustomerId { get; set; } public string CustomerName { get; set; } public int LocationId { get; set; } public string LocationName { get; set; } public GeoLocation Location { get; set; } } public class Message { public GeoLocation Location { get; set; } } |
2-) ElasticSearch Üstünde Index Oluşturma: Database’e kayıtlı, tüm Customer ve Lokasyon kayıtları çekilip ElasticSearch’e “customerlocation” adında bir index altına atılmaktadır.
- “using (LocationContext dbContext = new LocationContext())” : LocationContext ile Database’e bağlanılır.
- “customerLocationList” : Belirlenen koşula göre databaseden, “List<CustomerModel>” çekilir.
- “Location = new GeoLocation(Convert.ToDouble(s.Latitude), Convert.ToDouble(s.Longitude))” : GeoLocation tipine, Db’den gelen “Latitude” ve “Longitude” koordinatları atılır.
- “var defaultIndex = “customerlocation”” : Default index adı olarak, “customerlocation” atanır.
- “if (client.IndexExists(defaultIndex).Exists) { client.DeleteIndex(defaultIndex); }” : Eğer önceden bu isimde bir index var ise silinir.
- “client.CreateIndex(defaultIndex, c => c” : ElasticSearch üzerinde ilgili index oluşturulur.
- “.Mappings(m => m .Map<CustomerModel>(mm => mm .AutoMap() )” : Ilgili index, “CustomerModel”‘e göre şablonlanır.
- “.Aliases(a => a.Alias(“location_alias”))” : ElasticSearch’de alias mutlaka kullanılmalıdır. Böylece ilgili index silindiği zaman, indexlenen önceki kayıtlar silinmez.
- “var bulkIndexer = new BulkDescriptor();” : Bu makalede Database’den çekilen kayıtlar Toplu(Bulk) olarak indexlenmiştir.
- “foreach (var document in customerLocationList)”: Db’den çekilen herbir kayıt tek tek gezilmiştir.
- “bulkIndexer.Index<CustomerModel>(i => i .Document(document) .Id(document.LocationId) .Index(“customerlocation”))” : Her bir CustomerModel kayıdı, yani ElasticSearch’de karşılığı document, BulkDescriptor’a tek tek atılır.
- “elasticClient.Bulk(bulkIndexer)” : ElasticSearch’deki “customerlocation” indexine, tüm documentler toplu olarak atılırlar.
Indexing():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
public static void Indexing() { using (LocationContext dbContext = new LocationContext()) { var customerLocationList = dbContext.CustomerLocations.Where(s => !s.IsDeleted) .Include(s => s.Customer) .Select(s => new CustomerModel { CustomerId = s.CustomerId, CustomerName = s.Customer.Name, LocationId = s.Id, LocationName = s.Name, Location = new GeoLocation(Convert.ToDouble(s.Latitude), Convert.ToDouble(s.Longitude)) }).ToList(); var defaultIndex = "customerlocation"; var client = new ElasticClient(); if (client.IndexExists(defaultIndex).Exists) { client.DeleteIndex(defaultIndex); } if (!elasticClient.IndexExists("location_alias").Exists) { client.CreateIndex(defaultIndex, c => c .Mappings(m => m .Map<CustomerModel>(mm => mm .AutoMap() ) ).Aliases(a => a.Alias("location_alias")) ); } // Insert Data Classic // for (int i = 0; i < customerLocationList.Count; i++) // { // var item = customerLocationList[i]; // elasticClient.Index<CustomerModel>(item, idx => idx.Index("customerlocation").Id(item.LocationId)); // } // Bulk Insert var bulkIndexer = new BulkDescriptor(); foreach (var document in customerLocationList) { bulkIndexer.Index<CustomerModel>(i => i .Document(document) .Id(document.LocationId) .Index("customerlocation")); } elasticClient.Bulk(bulkIndexer); } } |
Monitoring: ElasticSearch’de, oluşturulan Index’in monitor edilebilmesi için bu makalede 2 tool kullanılmıştır. 1.si herkesin bildiği :5601 “Kibana” :) Diğeri, pek bilinmeyen Chrome’da Extension olarak bile karşımıza çıkan, “ElasticSearch-Head“. İlgili “customerlocation” index’in oluştuktan sonra, içeri atılan documentlerin Kibana ve ElasticSearch-Head’deki görünümü aşağıdaki gibidir:
Kibana :
ElasticSearch-Head:
3-) ElasticSearch’de Belli Bir Mesafeye Göre Lokasyonları Filitreleme:
Search():
- “elasticClient.Search<CustomerModel>” : Yapılacak arama işleminden “CustomerModel” tipinde bir result’ın dönmesi beklenmektedir.
- “GeoDistance()” : Lokasyona göre filitreleme işleminin yapılmasını sağlamaktadır.
- “.Field(f => f.Location)” : Hangi alana göre GeoLocation Filter yapılacağı belirlenir.
- “.Distance(“250km”).Location(41, 28)” Bulunulan konum, dummy olarak (41,28) olarak verilmiş ve çevresinde 250 km bir çember içinde kalan tüm kayıtlı lokasyonlar çekilmiştir.
- “.DistanceType(GeoDistanceType.Plane)” : Kuş bakışı’na göre, ilgili mesafeye bakılmıştır.
- “foreach (var customer in geoResult.Documents) {” Tüm bulunan lokasyonlar, ekrana basılır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public static void Search() { var geoResult = elasticClient.Search<CustomerModel>(s => s.From(0).Size(10000).Query(query => query.Bool(b => b.Filter(filter => filter .GeoDistance(geo => geo .Field(f => f.Location) .Distance("250km").Location(41, 28) .DistanceType(GeoDistanceType.Plane) )) ))); foreach (var customer in geoResult.Documents) { Console.WriteLine(customer.CustomerName + ":" + customer.LocationName + " = " + GetDistanceFromLatLonInKm(41,28,customer.Location.Latitude,customer.Location.Longitude).ToString()+"km"); } Console.ReadLine(); } |
Not: ElasticSearch Result’da Distance, yani mesafe geri dönülmemektedir. Kısaca bu örnekte olduğu gibi “250km” içinde bulunulan lokasyonların, gerçekte bulunulan konuma olan mesafesi için, ayrıca bir hesaplamanın yapılması gerekmektedir. Aşağıda, “Haversine Formulünün” fonksiyona döndürülmüş hali gözükmektedir. Yukarıda her bir lokasyon ekrana basılırken, ilgili “GetDistanceFromLatLonInKm()” çağırılmış ve bulunulan noktaya olan uzaklık yaklaşık olarak hesaplanarak ekrana basılmıştır.
GetDistanceFromLatLonInKm():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public static double GetDistanceFromLatLonInKm(double lat1, double lon1, double lat2, double lon2) { var R = 6371; // Radius of the earth in km var dLat = deg2rad(lat2 - lat1); // deg2rad below var dLon = deg2rad(lon2 - lon1); var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + Math.Cos(deg2rad(lat1)) * Math.Cos(deg2rad(lat2)) * Math.Sin(dLon / 2) * Math.Sin(dLon / 2) ; var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); var d = R * c; // Distance in km return d; } public static double deg2rad(double deg) { return deg * (Math.PI / 180); } |
Geldik bir makalenin daha sonuna. Bu makalede, lokasyon ile ilgili bir filitreleme ihtiyacına karşılık olarak ElasticSearch kullanılmıştır. Bu esnada tüm data, ElasticSearch’de GeoLocation tipinde bulk olarak bir index altına atılmış ve sorgulama esnasında “GeoDistance()” filter’ı ile birlikte istenen bir mesafe içindeki documentler, custom bir model şeklinde liste olarak alınmıştır. ElasticSearch geri dönüş tipinde, her bir lokasyonun bulunulan konuma olan mesafesi ayrıca geri dönülmemektedir. Bu durumda, result listden dönen her bir kayıt için bulunulan konuma uzaklığı “Haversine Formulü” kullanılarak hesaplanmıştır.
Yeni bir makalede görüşmek üzere hoşçakalın.
Source Code: https://github.com/borakasmer/ElasticSearchGeolocation.git
Source:
Çok faydalı.
Teşekkürler
Her bir paylaşımınız için ayrı ayrı çok teşekkür ederim. Bir çok konuda sizin sayenizde bilmediğim teknolojiler ile tanışma fırsatı buldum, buda bende ayrı bir heyecan oluşturuyor ve deneyimleme konusunda öncü oluyor. Paylaşımlarınız için tekrar tekrar teşekkür ederim.
Çok teşekkür ederim Nurullah …
hocam selamlar,
uzun zamandır elastic search inceliyorum. Fakat aklıma takılan bir nokta var. Turizm uygulamaları geliştiriyrum ve aramalar tamamen değişken kriterler üzerine kurulu. klasik e ticaret sistemleri gibi full text search mantığı ile çalışmıyor. lokasyon, kişi sayısı, tarih vs gibi kriterler her aramada değişebiliyor ve her aramada farklı sonuçlar geliyor doğal olarak. Böyle bir sistemde elasticsearch ne kadar uygun olur?
Selam Yakup,
Arama sırasında kayıt sayın milyon mertebesinde ise, mutlaka elastik search gibi bir yapı kurman gerekir. Ya da MongoDB gibi NoSql bir DB ile ilerleyebilirsin. Elasticsearch kullanmada durumunda, değişen datanın anlık olarak ElasticSearch’e indexlenmesi gerekir. Bu durumda da var olan sistemden bağımsız, Microservisler ile Queueler async okunarak, ilgili ElasticSearch’ü Indexleyebilirsin.
İyi çalışmalar.
Hocam Merhaba,
Tam aradığım konuydu bulup uygulayabilince çok işime yaradı öncellikle teşekkürler.
Ben web uygulaması için uyguladım, NEST in 7.x versiyonunda client.IndexExists(defaultIndex).Exists yerine elasticClient.Indices.Exists(defaultIndex).Exists gibi ufak değişiklikler olmuş, 7.x kullananlar takılırsa iletmek istedim.
İndexletirken desc alanında bir alan ve productnumber adında bir alan daha indexletiyorum, filter için .Distance a ek olarak aranan kelimeleride nasıl sorguya dahil edebilirim. Seçilen noktanın 250km çapında aranan kelimeye karşılık gelen marketlerin lokasyonu gibi. Bu konuda minik bir kod örneği paylaşabilirseniz sevinirim.
İlerki aşamada yazılan kelimeye benzer sonuçlarında gelmesi ve suggest kısımlarına geçmeyi planlıyorum umarım onlarıda yapabilirim :)
Çok teşekkürler.