środa, 31 maja 2017

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.

Brak komentarzy:

Prześlij komentarz