Подтвердить что ты не робот

Firestore: Как получить случайные документы в коллекции

Крайне важно, чтобы мое приложение было в состоянии выбрать несколько случайных событий из коллекции в firebase.

Так как в Firebase (что я знаю) нет встроенной функции, чтобы достичь запроса, который делает именно это, моя первая мысль заключалась в том, чтобы использовать курсоры запросов для выбора случайного начального и конечного индекса при условии, что у меня есть номер документов в коллекции.

Этот подход будет работать, но только ограниченным образом, поскольку каждый документ будет обслуживаться последовательно с его соседними документами каждый раз; однако, если бы я смог выбрать документ по его индексу в своей родительской коллекции, я мог бы получить случайный запрос документа, но проблема в том, что я не могу найти документацию, описывающую, как вы можете это сделать, или даже если вы можете это сделать.

Здесь, что я хотел бы сделать, рассмотрим следующую схему firestore:

root/
  posts/
     docA
     docB
     docC
     docD

Затем в моем клиенте (я в среде Swift) я хотел бы написать запрос, который может это сделать:

db.collection("posts")[0, 1, 3] // would return: docA, docB, docD

В любом случае, я могу что-то сделать в соответствии с этим? Или, есть ли другой способ выбрать случайные документы аналогичным образом?

Пожалуйста, помогите.

4b9b3361

Ответ 1

Используя случайно сгенерированные индексы и простые запросы, вы можете случайным образом выбирать документы из собрания или группы сбора в Cloud Firestore.

Этот ответ разбит на 4 раздела с различными параметрами в каждом разделе:

  1. Как генерировать случайные индексы
  2. Как запросить случайные индексы
  3. Выбор нескольких случайных документов
  4. Пересев на постоянную случайность

Как генерировать случайные индексы

Основой этого ответа является создание индексированного поля, которое при упорядочении по возрастанию или убыванию приводит к произвольному упорядочению всего документа. Есть разные способы сделать это, поэтому давайте посмотрим на 2, начиная с самого легкодоступного.

Версия с автоматическим идентификатором

Если вы используете случайно сгенерированные автоматические идентификаторы, предоставленные в наших клиентских библиотеках, вы можете использовать эту же систему для случайного выбора документа. В этом случае случайным образом упорядоченный индекс является идентификатором документа.

Далее в нашем разделе запросов случайное значение, которое вы генерируете, - это новый авто-идентификатор (iOS, Android, Web), а поле, которое вы запрашиваете, - __name__, а упомянутое ниже "низкое значение" - пустая строка. На сегодняшний день это самый простой метод для генерации случайного индекса, который работает независимо от языка и платформы.

По умолчанию имя документа (__name__) индексируется только по возрастанию, и вы также не можете переименовать существующий документ за исключением удаления и повторного создания. Если вам нужен какой-либо из них, вы все равно можете использовать этот метод и просто сохранить автоматический идентификатор как фактическое поле с именем random, а не перегружать имя документа для этой цели.

Случайная целочисленная версия

Когда вы пишете документ, сначала сгенерируйте случайное целое число в ограниченном диапазоне и установите его как поле с именем random. В зависимости от ожидаемого количества документов вы можете использовать другой ограниченный диапазон, чтобы сэкономить место или снизить риск коллизий (что снижает эффективность этого метода).

Вы должны подумать, какие языки вам нужны, так как будут другие соображения. В то время как Swift прост, JavaScript, в частности, может иметь ошибку:

  • 32-разрядное целое число: отлично подходит для небольших (~ 10K вряд ли будет коллизия) наборов данных
  • 64-разрядное целое число: большие наборы данных (примечание: JavaScript изначально не поддерживается, пока)

Это создаст индекс с вашими документами, отсортированными случайным образом. Позже в нашем разделе запросов случайное значение, которое вы сгенерируете, будет другим из этих значений, а упомянутое позже "низкое значение" будет -1.

Как запросить случайные индексы

Теперь, когда у вас есть случайный индекс, вы захотите запросить его. Ниже мы рассмотрим несколько простых вариантов выбора 1 случайного документа, а также варианты выбора более 1.

Для всех этих опций вы захотите сгенерировать новое случайное значение в той же форме, что и индексированные значения, которые вы создали при написании документа, обозначенные переменной random ниже. Мы будем использовать это значение, чтобы найти случайное место в индексе.

Оберните вокруг

Теперь, когда у вас есть случайное значение, вы можете запросить один документ:

let postsRef = db.collection("posts")
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: random)
                   .order(by: "random")
                   .limit(to: 1)

Убедитесь, что это вернул документ. Если это не так, запросите снова, но используйте "низкое значение" для вашего случайного индекса. Например, если вы сделали Случайные целые числа, то lowValue будет 0:

let postsRef = db.collection("posts")
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: lowValue)
                   .order(by: "random")
                   .limit(to: 1)

Пока у вас есть один документ, вы гарантированно вернете хотя бы 1 документ.

Двунаправленный

Метод циклического преобразования прост в реализации и позволяет оптимизировать хранилище с включенным только восходящим индексом. Одним из недостатков является возможность несправедливого экранирования ценностей. Например, если первые 3 документа (A, B, C) из 10K имеют случайные значения индекса A: 409496, B: 436496, C: 818992, то A и C имеют шанс выбора чуть менее 1/10K, тогда как B эффективно защищен близостью A и только примерно 1/160K шанс.

Вместо того, чтобы запрашивать в одном направлении и оборачиваться, если значение не найдено, вы можете вместо этого случайным образом выбирать между >= и <=, что уменьшает вероятность несправедливо экранированных значений вдвое за счет удвоения хранилища индекса.

Если одно направление не дает результатов, переключитесь на другое направление:

queryRef = postsRef.whereField("random", isLessThanOrEqualTo: random)
                   .order(by: "random", descending: true)
                   .limit(to: 1)

queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: random)
                   .order(by: "random")
                   .limit(to: 1)

Выбор нескольких случайных документов

Часто вам нужно выбрать более 1 случайного документа за раз. Есть 2 различных способа отрегулировать вышеупомянутые методы в зависимости от того, какие компромиссы вы хотите.

Промыть & Повторите

Этот метод прост. Просто повторите процесс, в том числе каждый раз выбирая новое случайное целое число.

Этот метод даст вам случайные последовательности документов, не беспокоясь о том, чтобы неоднократно видеть одни и те же шаблоны.

Компромисс заключается в том, что он будет медленнее, чем в следующем методе, поскольку для каждого документа требуется отдельное путешествие в оба конца.

Продолжайте в том же духе

При таком подходе просто увеличьте количество в лимите до желаемых документов. Это немного сложнее, так как вы можете вернуть 0..limit документы в вызове. Затем вам нужно будет получить недостающие документы таким же образом, но с ограничением, уменьшенным только до разницы. Если вы знаете, что в общей сложности документов больше, чем запрашиваемое число, вы можете оптимизировать их, игнорируя крайний случай, когда никогда не получите достаточно документов при втором вызове (но не при первом).

Компромисс с этим решением в повторяющихся последовательностях. Хотя документы упорядочены случайным образом, если вы когда-нибудь столкнетесь с перекрывающимися диапазонами, вы увидите тот же шаблон, что и раньше. Существуют способы смягчения этой проблемы, которые обсуждаются в следующем разделе о повторном заполнении.

Этот подход быстрее, чем "Полоскание & Повторите эти действия, так как вы будете запрашивать все документы в лучшем случае - один звонок или в худшем - 2 звонка.

Пересев на постоянную случайность

Хотя этот метод дает вам документы случайным образом, если набор документов является статическим, вероятность того, что каждый документ будет возвращен, также будет статической. Это проблема, поскольку некоторые значения могут иметь неоправданно низкие или высокие вероятности, основанные на начальных случайных значениях, которые они получили. Во многих случаях это хорошо, но в некоторых случаях вы можете увеличить долгосрочную случайность, чтобы иметь более равномерный шанс вернуть любой 1 документ.

Обратите внимание, что вставленные документы будут в итоге переплетаться, постепенно изменяя вероятности, так же как и удаление документов. Если скорость вставки/удаления слишком мала, учитывая количество документов, существует несколько стратегий для решения этой проблемы.

Multi-Random

Вместо того, чтобы беспокоиться о повторном заполнении, вы всегда можете создать несколько случайных индексов для каждого документа, а затем случайным образом выбирать один из этих индексов каждый раз. Например, поле random должно быть картой с подполями с 1 по 3:

{'random': {'1': 32456, '2':3904515723, '3': 766958445}}

Теперь вы будете запрашивать случайные, случайные, случайные, случайные значения 1, создавая больший разброс случайности. По сути, это заменяет увеличенное хранилище, чтобы сохранить увеличенное количество вычислений (записи документа) от необходимости повторного заполнения.

Перезапрошено пишет

Каждый раз, когда вы обновляете документ, заново генерируйте случайные значения из поля random. Это переместит документ в случайном индексе.

Перечитал на читает

Если сгенерированные случайные значения распределены неравномерно (они случайны, так что это ожидаемо), то один и тот же документ может быть выбран в соответствующее количество времени. Этому легко противостоять, обновляя случайно выбранный документ новыми случайными значениями после того, как он прочитан.

Поскольку запись обходится дороже и может работать в горячих точках, вы можете выбрать обновление только для чтения подмножества времени (например, if random(0,100) === 0) update;).

Ответ 2

Публикуйте это, чтобы помочь любому, кто столкнется с этой проблемой в будущем.

Если вы используете автоидентификаторы, вы можете сгенерировать новый автоидентификатор и запросить ближайший автоидентификатор, как указано в ответе Дана МакГрата.

Недавно я создал API для случайных цитат, и мне нужно было получать случайные цитаты из коллекции пожарного магазина.
Вот как я решил эту проблему:

var db = admin.firestore();
var quotes = db.collection("quotes");

var key = quotes.doc().id;

quotes.where(admin.firestore.FieldPath.documentId(), '>=', key).limit(1).get()
.then(snapshot => {
    if(snapshot.size > 0) {
        snapshot.forEach(doc => {
            console.log(doc.id, '=>', doc.data());
        });
    }
    else {
        var quote = quotes.where(admin.firestore.FieldPath.documentId(), '<', key).limit(1).get()
        .then(snapshot => {
            snapshot.forEach(doc => {
                console.log(doc.id, '=>', doc.data());
            });
        })
        .catch(err => {
            console.log('Error getting documents', err);
        });
    }
})
.catch(err => {
    console.log('Error getting documents', err);
});

Ключ к запросу:

.where(admin.firestore.FieldPath.documentId(), '>', key)

И повторный вызов с отмененной операцией, если документы не найдены.

Надеюсь, это поможет!
Если вам интересно, вы можете найти эту конкретную часть моего API на GitHub

Ответ 3

Просто сделал эту работу в Angular 7 + RxJS, так что поделитесь здесь с людьми, которые хотят пример.

Я использовал ответ @Dan McGrath и выбрал следующие параметры: случайная целочисленная версия + полоскание и повторение для нескольких чисел. Я также использовал материал, описанный в этой статье: RxJS, где оператор If-Else? делать операторы if/else на уровне потока (просто если кому-то из вас нужен учебник по этому вопросу).

Также обратите внимание, что я использовал angularfire2 для легкой интеграции Firebase в Angular.

Вот код:

import { Component, OnInit } from '@angular/core';
import { Observable, merge, pipe } from 'rxjs';
import { map, switchMap, filter, take } from 'rxjs/operators';
import { AngularFirestore, QuerySnapshot } from '@angular/fire/firestore';

@Component({
  selector: 'pp-random',
  templateUrl: './random.component.html',
  styleUrls: ['./random.component.scss']
})
export class RandomComponent implements OnInit {

  constructor(
    public afs: AngularFirestore,
  ) { }

  ngOnInit() {
  }

  public buttonClicked(): void {
    this.getRandom().pipe(take(1)).subscribe();
  }

  public getRandom(): Observable<any[]> {
    const randomNumber = this.getRandomNumber();
    const request$ = this.afs.collection('your-collection', ref => ref.where('random', '>=', randomNumber).orderBy('random').limit(1)).get();
    const retryRequest$ = this.afs.collection('your-collection', ref => ref.where('random', '<=', randomNumber).orderBy('random', 'desc').limit(1)).get();

    const docMap = pipe(
      map((docs: QuerySnapshot<any>) => {
        return docs.docs.map(e => {
          return {
            id: e.id,
            ...e.data()
          } as any;
        });
      })
    );

    const random$ = request$.pipe(docMap).pipe(filter(x => x !== undefined && x[0] !== undefined));

    const retry$ = request$.pipe(docMap).pipe(
      filter(x => x === undefined || x[0] === undefined),
      switchMap(() => retryRequest$),
      docMap
    );

    return merge(random$, retry$);
  }

  public getRandomNumber(): number {
    const min = Math.ceil(Number.MIN_VALUE);
    const max = Math.ceil(Number.MAX_VALUE);
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }
}

Ответ 4

У меня есть один способ получить случайный список документов в Firebase Firestore, это действительно легко. Когда я загружаю данные в Firestore, я создаю имя поля "position" со случайным значением от 1 до 1 миллиона. Когда я получаю данные из Fire store, я устанавливаю Порядок по полю "Положение" и обновляю для него значение, многие пользовательские данные и данные загрузки всегда обновляются, и это будет случайное значение.

Ответ 5

Для тех, кто использует Angular + Firestore, опираясь на методы @Dan McGrath, вот фрагмент кода.

Ниже фрагмент кода возвращает 1 документ.

  getDocumentRandomlyParent(): Observable<any> {
    return this.getDocumentRandomlyChild()
      .pipe(
        expand((document: any) => document === null ? this.getDocumentRandomlyChild() : EMPTY),
      );
  }

  getDocumentRandomlyChild(): Observable<any> {
      const random = this.afs.createId();
      return this.afs
        .collection('my_collection', ref =>
          ref
            .where('random_identifier', '>', random)
            .limit(1))
        .valueChanges()
        .pipe(
          map((documentArray: any[]) => {
            if (documentArray && documentArray.length) {
              return documentArray[0];
            } else {
              return null;
            }
          }),
        );
  }

1).expand() является операцией rxjs для рекурсии, чтобы гарантировать, что мы определенно получим документ из случайного выбора.

2) Чтобы рекурсия работала должным образом, нам нужно иметь 2 отдельные функции.

3) Мы используем EMPTY для завершения оператора .expand().

import { Observable, EMPTY } from 'rxjs';