realm / realm-swift

Realm is a mobile database: a replacement for Core Data & SQLite
https://realm.io
Apache License 2.0
16.32k stars 2.15k forks source link

Sort observable collection in-memory based on a computed property #7670

Open jadar opened 2 years ago

jadar commented 2 years ago

Problem

I need to sort a Realm collection by geo-spatial distance based on a location property on a Realm Object. I'd also like this collection to receive update notification for insertions, deletions, and updates.

Solution

Obviously, this can't happen in the database engine unless the engine supported some kind of complex sort descriptor, or unless it was built to support geo-spacial operations in the first place. I know that geo-spatial features have been on the roadmap for quite some time, but it seems that other efforts have taken precedence.

I think the best way forward to support my use-case would be to add a special Results type that wraps a RealmCollection, performs the sorting operation in-memory based on a comparator, and forwards updates for the newly sorted indices like a normal RealmCollection. This allows users to opt-in to lower performance when a specific feature is required.

The use of the API would look something like this:

class InMemorySortedResults<Element: RealmCollectionValue> { ... }
let collection = realm.objects(Location.self)
let sorted = InMemorySortedResults(collection, { (lhs, rhs)
    lhs.coordinate.distance(to: currentLocation) < rhs.coordinate.distance(to: currentLocation)
})
let notificationToken = sorted.observe( ... )

How important is this improvement for you?

Would be a major improvement

leemaguire commented 2 years ago

Hi @jadar,

Depending on how you are storing the location data, you could make use of our Type Projections feature for your use case. Here's a blog post showcasing it .

If you could show us your Realm model and your usage we may be able to help you further.

jadar commented 2 years ago

@leemaguire

I recently switched to storing locations by using type projection to story a CLLocationCoordinate2D as an embedded Coordinate object.

class Boundary: Object {
  @Persisted
  var centerCoordinate: CLLocationCoordinate2D?

  @Persisted
  var geometry: String? // geojson
  ...
}

class Location: Object {
  @Persisted
  var boundary: Boundary
  ...
}

I'd like a sorted collection of Location objects sorted by distance to the current location from location.boundary.centerCoordinate.

pavel-ship-it commented 2 years ago

We have plans on implementation of Geospatial Indexing but it's not yet any close.

At the moment you may take a look at RealmGeoQueries It's not recent but you may have an idea how to achieve this.

jadar commented 2 years ago

We have plans on implementation of Geospatial Indexing but it's not yet any close.

At the moment you may take a look at RealmGeoQueries It's not recent but you may have an idea how to achieve this.

I have looked at RealmGeoQueries but it doesn't include support for live collections or change notifications, which is what I am looking for (see my "Problem" above.) It only gives you a sorted array.

I realize Geospatial indexing might be far away on the road map. That is why I've suggested the half-way work around by allowing you to have a collection type that sorts based on a computed property while also maintaining all the state necessary to continue vending change notifications.

pavel-ship-it commented 2 years ago

@jadar you can sort using SortComparator and you can use computed property there something like this. Sure you want to add some math

class Point: Object {
    @Persisted var x: Float
    @Persisted var y: Float

    var referencePt: (Float, Float) = (0, 0)
    var distance: Float { get { sqrt((x - referencePt.0)*(x - referencePt.0) + (y - referencePt.1)*(y - referencePt.1)) } }
}

extension Point: Comparable {
    static func <(lhs: Point, rhs: Point) -> Bool {
        lhs.distance < rhs.distance
    }
}

        let points = realm.objects(Point.self)
        points.forEach { pt in
            pt.locale = (2, 0)
        }
        print(points.sorted())
jadar commented 2 years ago

@pavel-ship-it

Thanks for your suggestion. In your example, is points.sorted() going to be of the Results type? Also, is there a way I can sort on the collection on the distance keypath directly, rather than the comparable implementation? I'm running into exceptions both when I try to sort based on "self" and on "distance".

leemaguire commented 2 years ago

Hi @jadar, until we support geospatial queries / sorting points.sorted() will not return Results<Point> but instead a Swift Array. Also because distance is a computed property it will not work with our sorting mechanism.