Firestore with GeoFlutterFire – Is there a more efficient way for nearest users + liked/disliked filter?

huangapple go评论62阅读模式
英文:

Firestore with GeoFlutterFire - Is there a more efficient way for nearest users + liked/disliked filter?

问题

翻译

上下文

我的应用程序使用类似 Tinder 的功能:

  1. 获取最近的用户(geohash 过滤 + 其他一些过滤器)
  2. 用户可以喜欢/不喜欢用户
  3. 在后续启动时,用户需要看到新用户,而不是已经喜欢/不喜欢的用户

目前的结构如下:
Firestore with GeoFlutterFire – Is there a more efficient way for nearest users + liked/disliked filter?

这是我目前的代码:

UserRepository:

// 使用 GeoFlutterFire2 进行 Geohashing
Stream<List<DocumentSnapshot>> 获取半径内的所有用户(
    {required LocationData currentLocation, required double radius}) {
  String field = 'position';

  GeoFirePoint centerPoint = _geo.point(
      latitude: currentLocation.latitude ?? 0.0,
      longitude: currentLocation.longitude ?? 0.0);

  return _geo
      .collection(collectionRef: _buildUserQuery())
      .withinAsSingleStreamSubscription(
          center: centerPoint, radius: radius, field: field);
}

// 在 geohash 查询之前进行预过滤的查询
Query<Map<String, dynamic>> _buildUserQuery() {
  // 根据 1. 位置(上面)、2. 是否活跃,3. 游戏列表,4. 用户类型进行过滤
  var query = _firestoreDB
      .collection(userCollection)
      .where("isActive", isEqualTo: true)
      .where("gameList", arrayContainsAny: ["Example1","Example2"])
      .where(Filter.or(Filter("userType", isEqualTo: currentUser.lookingFor[0]),
          Filter("userType", isEqualTo: currentUser.lookingFor[1])));

  return query;
}

// UserRelations = 喜欢/不喜欢的其他用户列表
Future<List<String>> 获取用户关系ID(String userID) async {
  List<String> result = [];

  await _firestoreDB
      .collection(userCollection)
      .doc(userID)
      .collection("user_relations")
      .get()
      .then((querySnapshot) {
    for (var doc in querySnapshot.docs) {
      final otherUserID = doc.data()["otherUserID"];
      result.add(otherUserID);
    }
  });

  return result;
}

UserViewModel:

Future<void> _获取半径内的所有用户() async {
  currentLocationValue = await _userRepo.getCurrentUserLocation();
  await _userRepo.getCurrentUser(_authRepo.user!.uid);

  if (currentLocationValue != null) {
    // 用于以后的过滤
    final userRelationIDs =
        await _userRepo.getUserRelationIDs(_userRepo.currentUser!.uuid);

    var subcription = _userRepo
        .getAllUsersInRadius(
            currentLocation: currentLocationValue!, radius: 2000)
        .listen((documentList) {
      // 首先,过滤掉已经被当前用户喜欢/不喜欢的所有用户
      var filteredUserList = documentList.where((docSnapshot) {
        if (!docSnapshot.exists || docSnapshot.data() == null) return false;
        String uuid = docSnapshot.get("uuid");
        if (!userRelationIDs.contains(uuid)) return true;
        return false;
      });
      // 现在将所有文档转换为用户对象并发布列表
      allUsersList = filteredUserList
          .map((docSnapshot) => _createUserFromSnapshot(docSnapshot))
          .toList();
      _allUsersStreamcontroller.add(allUsersList);
    });

    subscriptions.add(subcription);
  }
}

我的问题

我认为以下几点存在问题:

  • 感觉先加载所有用户,然后可能丢弃大部分用户非常低效 -> 浪费了每月的读取次数
  • 由于使用了地理查询(GeoFlutterFire2),我在应用其他过滤器时受到很大限制(无法使用范围或排序,因为这已经用于地理哈希) -> 因此在查询方面选项不多

因此我想知道:是否有更高效的方法?也许通过以不同的方式结构化数据?

英文:

Context

My app uses a similar functionality like Tinder:

  1. Get nearest users (geohash filter + some others)
  2. User can like/dislike users
  3. On subsequent launches, user needs to see NEW users, without those he or she already liked/disliked

Current structure looks like this:
Firestore with GeoFlutterFire – Is there a more efficient way for nearest users + liked/disliked filter?

This is my code so far:

UserRepository:

  // Geohashing with GeoFlutterFire2
  Stream<List<DocumentSnapshot>> getAllUsersInRadius(
      {required LocationData currentLocation, required double radius}) {
    String field = 'position';

    GeoFirePoint centerPoint = _geo.point(
        latitude: currentLocation.latitude ?? 0.0,
        longitude: currentLocation.longitude ?? 0.0);

    return _geo
        .collection(collectionRef: _buildUserQuery())
        .withinAsSingleStreamSubscription(
            center: centerPoint, radius: radius, field: field);
  }

  // The query to pre-filter BEFORE the geohash query
  Query<Map<String, dynamic>> _buildUserQuery() {
    // Filter by 1. location (above), 2. isActive, 3. game list, 4. user type
    var query = _firestoreDB
        .collection(userCollection)
        .where("isActive", isEqualTo: true)
        .where("gameList", arrayContainsAny: ["Example1","Example2"])
        .where(Filter.or(Filter("userType", isEqualTo: currentUser.lookingFor[0]),
            Filter("userType", isEqualTo: currentUser.lookingFor[1])));

    return query;
  }

  // UserRelations = list of liked/disliked other users
  Future<List<String>> getUserRelationIDs(String userID) async {
    List<String> result = [];

    await _firestoreDB
        .collection(userCollection)
        .doc(userID)
        .collection("user_relations")
        .get()
        .then((querySnapshot) {
      for (var doc in querySnapshot.docs) {
        final otherUserID = doc.data()["otherUserID"];
        result.add(otherUserID);
      }
    });

    return result;
  }

UserViewModel:

  Future<void> _getAllUsersInRadius() async {
    currentLocationValue = await _userRepo.getCurrentUserLocation();
    await _userRepo.getCurrentUser(_authRepo.user!.uid);

    if (currentLocationValue != null) {
      // Needed for later filtering
      final userRelationIDs =
          await _userRepo.getUserRelationIDs(_userRepo.currentUser!.uuid);

      var subcription = _userRepo
          .getAllUsersInRadius(
              currentLocation: currentLocationValue!, radius: 2000)
          .listen((documentList) {
        // First, filter out all users that were already liked/disliked by currentUser
        var filteredUserList = documentList.where((docSnapshot) {
          if (!docSnapshot.exists || docSnapshot.data() == null) return false;
          String uuid = docSnapshot.get("uuid");
          if (!userRelationIDs.contains(uuid)) return true;
          return false;
        });
        // Now turn all documents into user objects and publish the list
        allUsersList = filteredUserList
            .map((docSnapshot) => _createUserFromSnapshot(docSnapshot))
            .toList();
        _allUsersStreamcontroller.add(allUsersList);
      });

      subscriptions.add(subcription);
    }
  }

My Problem

I see the following points as problematic:

  • It feels very inefficient to load all the users first and then possibly discard most of them -> wasted reads/month
  • Due to using a geo-query (GeoFlutterFire2), I am very limited in the other filters I can apply (no ranges or sorting, since that is already used for the geohash) -> so I don't have a lot of options on the query side

So what I am wondering is: is there a more performant/efficient way to go about this?

Maybe by structuring the data differently?

答案1

得分: 1

我可能有一个在有限情况下的解决方案,它假设:

  • 您的用户只能设置其位置一次
  • 您正在使用地理哈希来编码用户的位置
  • 进行搜索的半径是固定的,比如3公里对应地理哈希5

这个想法是在查询中“包括”用户而不是“排除”他们,Firestore 无法很好地实现:

  1. 当用户设置其位置时,为其分配一个唯一的数字,每个地理哈希5个唯一的数字,从第一个用户开始为1,每个新用户递增

  2. 每个地理哈希5个的用户总数也存储在中央文档中

  3. 当您的应用程序选择要显示的用户列表时,它将
    a. 选择要使用的地理哈希5 'currentgeohash5',即用户的地理哈希5

    b. 从该地理哈希5的用户总数中选择要显示的用户的数字[no1, ..., no10],但仅限于那些既没有被喜欢也没有被不喜欢的用户

    c. 查询新用户,使用以下条件:

    .where("geohash5", "==", currentgeohash5)
    .where("no", "in", [no1, ..., no10])

    d. 也在本地存储任何喜欢和不喜欢的信息,这样您就不必在步骤3.b中从Firestore加载它们

注意:使用这种方式使用地理哈希时,您还需要考虑用户接近其地理哈希边缘的情况,此时您还希望包括周围的一些地理哈希。但算法的工作方式完全相同。

英文:

I may have a solution in a limited case, it assumes:

  • that you users can only set their location once
  • you are using geohashes to encode the user's positions
  • that the radius on which a search is made is static. Say 3km corresponding to geohash5

The idea is to "include" users in your query rather then "excluding" them, which Firestore cannot do well:

  1. When a user sets its location, it is assigned a unique number per geohash5, a number starting at 1 for the first user and incremented for each new user

  2. The total number of users per geohash5 is also stored in a central document

  3. When your app picks a list of users to show, it will

    a. Select the geohash5 currentgeohash5 to use, the one of the user

    b. Pick the numbers [no1, ..., no10] of the users to show between 1 and the max for this geoash5 (step 2 above), but only among those that have been neither liked, nor disliked

    c. Query new users with

    .where("geohash5", "==", currentgeohash5)
    .where("no", "in", [no1, ..., no10])
    

    d. Also store locally any likes and dislikes so you dont have to load them from Firestore at step 3.b

Note: when using geohashes this way you also need to account for the frequent case of your user being close to the edge of its geohash, in which case you also want to include some of the ones around. But the algorithm works just the same way.

huangapple
  • 本文由 发表于 2023年4月17日 02:44:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/76029705.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定