Dogevents – wyszukiwanie wydarzeń wg lokalizacji

Jedną z głównych funkcjonalności w projekcie Dogevents jest wyszukiwanie wydarzeń wg lokalizacji. MongoDB natywnie udostępnia taką funkcjonalność, należy tylko posiadać dane geolokalizacyjne w odpowiednim formacie, przygotować odpowiedni indeks i wywołać odpowiednie zapytanie.

Format danych

MongoDB wspiera wiele typów GeoJSON takich jak punkt, linia, wielokąt. W moim przypadku ten pierwszy ma znaczenie gdyż miejsce wydarzenia to nic innego jak wskazanie na koordynaty (współrzędne) w postaci szerokości i długości geograficznej, np. [54.4760932,18.5446327]. 
W bazie muszą one zostać zapisane w postaci:
  • tablicy: [54.4760932,18.5446327]
  • dokumentu, np.: { lat: 54.4760932, lng: 18.5446327 }
U mnie właśnie ten wymóg okazał się barierą i musiałem dokonać zmiany w modelu. Dane na temat miejsca pobrane z Facebook graphApi zwracane są w postaci:

"place": {
"name": "Psia Kość - szkolenie psów w Skoczowie",
"location": {
"city": "Skoczów",
"country": "Poland",
"latitude": 49.80972,
"longitude": 18.79726,
"street": "Wiejska 2",
"zip": "43-430"
},
"id": "519318824878019"
}

i przy próbie założenia wymaganego indeksu otrzymywałem błąd związany z brakiem możliwości rozpoznania danych lokalizacyjnych.

Transformacja danych

Aby rozwiązać powyższy problem zmieniłem mój model domenowy dodając nową klasę zgodną z formatem MongoDB i zmodyfikowałem strukturę location: 

public class Coordinates
{
public double lng { get; set; }
public double lat { get; set; }
}

public class Location
{
public string City { get; set; }
public string Country { get; set; }

//For MongoDB geospatial search
public Coordinates Coordinates { get; set; }

public float Latitude { get; set; }
public float Longitude { get; set; }
public string Street { get; set; }
public string Zip { get; set; }

public Location()
{
this.Coordinates = new Coordinates();
}
}

Aby dane zostały wprowadzone do nowej właściwości Coordinates dodałem metodę, która jest wołana w momencie deserializacji:

[OnDeserialized]
public void OnDeserialized(StreamingContext context)
{
if (this.Place?.Location != null)
{
this.Place.Location.Coordinates.lat = this.Place.Location.Latitude;
this.Place.Location.Coordinates.lng = this.Place.Location.Longitude;
}
}

Jest to trochę mało eleganckie wyjście i powoduje duplikowanie danych. Ale chciałem na szybko uzyskać działający efekt. Na pewno jest to miejsce do optymalizacji, transformacji modelu. Końcowy dokument przyjął następującą formę:

"Place" : {
"Name" : "Ośrodek Wypoczynkowy Omega",
"Location" : {
"City" : "Przywidz",
"Country" : "Poland",
"Coordinates" : {
"lng" : 18.326669692993164,
"lat" : 54.193641662597656
},
"Latitude" : 54.193641662597656,
"Longitude" : 18.326669692993164,
"Street" : "Wczasowa 11",
"Zip" : "83-047"
}
},

Geospatial indeks

Kolejnym krokiem było utworzenie indeksów, który umożliwią tworzenie zapytań wykorzystujących dane lokalizacyjne. W swojej funkcjonalności będę wykorzystywał funkcję $near która wymaga tylko jednego indeksu typu 2d.

db.Events.createIndex({ "Place.Location.Coordinates":"2d" })

Przykładowe zapytanie

Tak przygotowana baza danych umożliwia rozpoczęcie wyszukiwania wydarzeń wg lokalizacji. Możliwości jest naprawdę wiele, mój przykład użycia jest jednym z prostszych! Poniższy przykład pokazuję zapytanie zwracające wydarzenia w odległości 50km od wskazanego miejsca:

db.Events.find({
"Place.Location.Coordinates": {
$near: {
$geometry: {
type: "Point",
coordinates: [18.3737986, 54.5792772]
},
$maxDistance: 50 * 1000
}
}
}).pretty()

Koordynaty muszą zostać podane w postaci [długość_geograficzna (lng), szerokość_geograficzna (lat)]. Możemy określić maksymalny dystans (w metrach) jak również minimalny poprzez wskazane $minDistance.

Podsumowując. W bardzo prosty sposób możemy dodać do aplikacji funkcjonalność, która w połączeniu z bieżącą lokalizacją użytkownika umożliwi dostarczanie interesujących wydarzeń w w jego okolicy. Nie spodziewałem się tak szybkiego rozwiązania tego problemu. Jedynym z jakim się spotkałem to w dużej mierze brak danych lokalizacyjnych lub wpisanie samego miasta jako lokalizacji wydarzenia. Także wynik końcowy jest w dużej mierze zależny od osoby tworzącej wydarzenie.