Liczba kolekcji w Cloud Firestore

Odpowiedzi:

190

Aktualizacja (kwiecień 2019 r.) - FieldValue.increment (zobacz rozwiązanie dotyczące dużej kolekcji)


Jak w przypadku wielu pytań, odpowiedź brzmi - to zależy .

Należy być bardzo ostrożnym podczas obsługi dużych ilości danych w interfejsie użytkownika. Oprócz spowolnienia interfejsu, Firestore pobiera również opłatę w wysokości 0,60 USD za milion wykonanych odczytów .


Mała kolekcja (mniej niż 100 dokumentów)

Używaj ostrożnie - wrażenia użytkownika Frontend mogą się nie udać

Obsługa tego z przodu powinna wystarczyć, o ile nie wykonujesz zbyt dużej logiki z tą zwróconą tablicą.

db.collection('...').get().then(snap => {
   size = snap.size // will return the collection size
});

Średni zbiór (od 100 do 1000 dokumentów)

Używaj ostrożnie - wywołania odczytu Firestore mogą dużo kosztować

Poradzenie sobie z tym z przodu nie jest wykonalne, ponieważ ma zbyt duży potencjał spowolnienia systemu użytkowników. Powinniśmy obsłużyć tę stronę serwera logiki i zwracać tylko rozmiar.

Wadą tej metody jest to, że nadal wywołujesz odczyty Firestore (równe rozmiarowi twojej kolekcji), co na dłuższą metę może Cię kosztować więcej niż oczekiwano.

Funkcja chmury:

...
db.collection('...').get().then(snap => {
    res.status(200).send({length: snap.size});
});

Front End:

yourHttpClient.post(yourCloudFunctionUrl).toPromise().then(snap => {
     size = snap.length // will return the collection size
})

Duża kolekcja (ponad 1000 dokumentów)

Najbardziej skalowalne rozwiązanie


FieldValue.increment ()

Od kwietnia 2019 roku Firestore umożliwia teraz zwiększanie liczników całkowicie atomowo i bez wcześniejszego odczytu danych . Zapewnia to prawidłowe wartości liczników nawet podczas jednoczesnej aktualizacji z wielu źródeł (wcześniej rozwiązanych za pomocą transakcji), jednocześnie zmniejszając liczbę odczytów bazy danych, które wykonujemy.


Słuchając usuniętych lub utworzonych dokumentów, możemy dodawać lub usuwać z pola licznika, które znajduje się w bazie danych.

Zapoznaj się z dokumentacją firestore - Distributed Counters lub zajrzyj do Data Aggregation autorstwa Jeffa Delaneya. Jego przewodniki są naprawdę fantastyczne dla każdego, kto używa AngularFire, ale jego lekcje powinny zostać przeniesione również na inne ramy.

Funkcja chmury:

export const documentWriteListener = 
    functions.firestore.document('collection/{documentUid}')
    .onWrite((change, context) => {

    if (!change.before.exists) {
        // New document Created : add one to count

        db.doc(docRef).update({numberOfDocs: FieldValue.increment(1)});

    } else if (change.before.exists && change.after.exists) {
        // Updating existing document : Do nothing

    } else if (!change.after.exists) {
        // Deleting document : subtract one from count

        db.doc(docRef).update({numberOfDocs: FieldValue.increment(-1)});

    }

return;
});

Teraz w interfejsie możesz po prostu zapytać to pole numberOfDocs, aby uzyskać rozmiar kolekcji.

Matthew Mullin
źródło
17
Świetne rozwiązanie dla dużych kolekcji! Chciałbym tylko dodać, że implementatorzy powinni opakować odczyt i zapis w firestore.runTransaction { ... }bloku. Rozwiązuje to problemy ze współbieżnością podczas uzyskiwania dostępu numberOfDocs.
efemoney
2
Te metody wykorzystują przeliczanie liczby rekordów. Jeśli używasz licznika i zwiększasz licznik za pomocą transakcji, czy nie przyniesie to tego samego wyniku bez dodatkowych kosztów i potrzeby funkcji chmury?
user3836415
10
Rozwiązanie dla dużych kolekcji nie jest idempotentne i nie działa w żadnej skali. Gwarantujemy, że wyzwalacze dokumentów Firestore uruchomią się co najmniej raz, ale mogą działać wiele razy. Gdy tak się stanie, nawet utrzymywanie aktualizacji w ramach transakcji może działać więcej niż jeden raz, co da fałszywy numer. Kiedy próbowałem tego, napotkałem problemy z mniej niż tuzinem kreacji dokumentów naraz.
Tym Pollack
2
Cześć @TymPollack. Zauważyłem niespójne zachowanie przy użyciu wyzwalaczy chmurowych. Jest jakaś szansa, że ​​mógłbyś połączyć mnie z artykułem lub forum, aby wyjaśnić zachowanie, którego doświadczyłeś?
Matthew Mullin
2
@cmprogram, gdy używasz db.collection ('...'), czytasz całą kolekcję i dane ... więc jeśli nie potrzebujesz danych, masz rację - możesz łatwo poprosić o listę identyfikatory kolekcji (nie dane dokumentów kolekcji) i liczy się jako jeden odczyt.
atereshkov
25

Najprostszym sposobem jest odczytanie rozmiaru „querySnapshot”.

db.collection("cities").get().then(function(querySnapshot) {      
    console.log(querySnapshot.size); 
});

Możesz również odczytać długość tablicy docs w „querySnapshot”.

querySnapshot.docs.length;

Lub jeśli „querySnapshot” jest puste, odczytując pustą wartość, która zwróci wartość logiczną.

querySnapshot.empty;
Ompel
źródło
73
Należy pamiętać, że każdy dokument kosztuje jedno przeczytanie. Więc jeśli policzysz w ten sposób 100 pozycji w kolekcji, zostaniesz obciążony opłatą za 100 odczytów!
Georg,
Prawidłowo, ale nie ma innego sposobu podsumowania liczby dokumentów w kolekcji. A jeśli już pobrałeś kolekcję, odczytanie tablicy „docs” nie będzie wymagało więcej pobierania, więc nie „kosztuje” więcej odczytów.
Ompel,
5
To czyta wszystkie dokumenty w pamięci! Powodzenia w przypadku dużych zbiorów danych ...
Dan Dascalescu
85
to naprawdę niewiarygodne, że Firebase Firestore nie ma db.collection.count(). Myśląc o upuszczeniu ich tylko z tego powodu
Blue Bot
8
Zwłaszcza w przypadku dużych zbiorów nie jest sprawiedliwe pobieranie opłat tak, jakbyśmy faktycznie pobrali i wykorzystali wszystkie dokumenty. Liczenie na stół (zbiór) to taka podstawowa cecha. Biorąc pod uwagę ich model cenowy i fakt, że Firestore został uruchomiony w 2017 roku, jest po prostu niewiarygodne, że Google nie zapewnia alternatywnego sposobu na uzyskanie rozmiaru kolekcji. Dopóki tego nie wdrożą, powinni przynajmniej unikać pobierania opłat za to.
nibbana
23

O ile wiem, nie ma na to wbudowanego rozwiązania i jest to obecnie możliwe tylko w węźle sdk. Jeśli masz

db.collection('someCollection')

możesz użyć

.select([fields])

aby zdefiniować, które pole chcesz zaznaczyć. Jeśli wykonasz puste polecenie select (), otrzymasz po prostu tablicę odwołań do dokumentów.

przykład:

db.collection('someCollection').select().get().then( (snapshot) => console.log(snapshot.docs.length) );

To rozwiązanie jest tylko optymalizacją dla najgorszego przypadku pobierania wszystkich dokumentów i nie skaluje się na duże zbiory!

Spójrz też na to:
jak uzyskać liczbę dokumentów w kolekcji za pomocą Cloud Firestore

jbb
źródło
Z mojego doświadczenia select(['_id'])wynika , że jest szybszy niżselect()
JAnton
13

Uważaj, licząc dokumenty w przypadku dużych zbiorów . Jest to trochę skomplikowane z bazą danych Firestore, jeśli chcesz mieć wstępnie obliczony licznik dla każdej kolekcji.

Taki kod nie działa w tym przypadku:

export const customerCounterListener = 
    functions.firestore.document('customers/{customerId}')
    .onWrite((change, context) => {

    // on create
    if (!change.before.exists && change.after.exists) {
        return firestore
                 .collection('metadatas')
                 .doc('customers')
                 .get()
                 .then(docSnap =>
                     docSnap.ref.set({
                         count: docSnap.data().count + 1
                     }))
    // on delete
    } else if (change.before.exists && !change.after.exists) {
        return firestore
                 .collection('metadatas')
                 .doc('customers')
                 .get()
                 .then(docSnap =>
                     docSnap.ref.set({
                         count: docSnap.data().count - 1
                     }))
    }

    return null;
});

Powodem jest to, że każdy wyzwalacz Cloud Firestore musi być idempotentny, jak mówi dokumentacja Firestore: https://firebase.google.com/docs/functions/firestore-events#limitations_and_guarantees

Rozwiązanie

Tak więc, aby zapobiec wielokrotnemu wykonywaniu kodu, musisz zarządzać zdarzeniami i transakcjami. Oto mój sposób obsługi dużych liczników kolekcji:

const executeOnce = (change, context, task) => {
    const eventRef = firestore.collection('events').doc(context.eventId);

    return firestore.runTransaction(t =>
        t
         .get(eventRef)
         .then(docSnap => (docSnap.exists ? null : task(t)))
         .then(() => t.set(eventRef, { processed: true }))
    );
};

const documentCounter = collectionName => (change, context) =>
    executeOnce(change, context, t => {
        // on create
        if (!change.before.exists && change.after.exists) {
            return t
                    .get(firestore.collection('metadatas')
                    .doc(collectionName))
                    .then(docSnap =>
                        t.set(docSnap.ref, {
                            count: ((docSnap.data() && docSnap.data().count) || 0) + 1
                        }));
        // on delete
        } else if (change.before.exists && !change.after.exists) {
            return t
                     .get(firestore.collection('metadatas')
                     .doc(collectionName))
                     .then(docSnap =>
                        t.set(docSnap.ref, {
                            count: docSnap.data().count - 1
                        }));
        }

        return null;
    });

Zastosuj tutaj:

/**
 * Count documents in articles collection.
 */
exports.articlesCounter = functions.firestore
    .document('articles/{id}')
    .onWrite(documentCounter('articles'));

/**
 * Count documents in customers collection.
 */
exports.customersCounter = functions.firestore
    .document('customers/{id}')
    .onWrite(documentCounter('customers'));

Jak widać, kluczem do zapobiegania wielokrotnemu wykonywaniu jest właściwość o nazwie eventId w obiekcie kontekstu. Jeśli funkcja była obsługiwana wiele razy dla tego samego zdarzenia, identyfikator zdarzenia będzie taki sam we wszystkich przypadkach. Niestety, musisz mieć kolekcję „wydarzeń” w swojej bazie danych.

Ferran Verdés
źródło
2
Mówią to tak, jakby to zachowanie zostało naprawione w wersji 1.0. Funkcje Amazon AWS mają ten sam problem. Coś tak prostego, jak liczenie pól, staje się skomplikowane i kosztowne.
MarcG
Spróbuję tego teraz, ponieważ wydaje się to lepszym rozwiązaniem. Czy kiedykolwiek wracasz i czyścisz swoją kolekcję wydarzeń? Myślałem o dodaniu pola daty i wyczyszczeniu starszego niż jeden dzień lub o czymś tylko po to, aby zestaw danych był mały (prawdopodobnie 1 mil + zdarzenia dziennie). Chyba że w FS jest łatwy sposób na zrobienie tego ... Używałem FS dopiero od kilku miesięcy.
Tym Pollack,
1
Czy możemy zweryfikować, że context.eventIdzawsze będzie to samo w przypadku wielu wywołań tego samego wyzwalacza? W moich testach wydaje się być spójny, ale nie mogę znaleźć żadnej „oficjalnej” dokumentacji, która to potwierdza.
Mike McLin
2
Po pewnym czasie odkryłem, że chociaż to rozwiązanie działa z dokładnie jednym zapisem, co jest świetne, jeśli zbyt wiele wyzwalaczy uruchamia się z wielu dokumentów zapisywanych jednocześnie i próbując zaktualizować ten sam dokument zliczania, możesz uzyskać błędy rywalizacji z Firestore. Czy spotkałeś się z nimi i jak sobie z tym poradziłeś? (Błąd: 10 PRZERWANY: Za dużo sporu o te dokumenty. Spróbuj ponownie.)
Tym Pollack,
1
@TymPollack spójrz na rozproszone liczniki, zapisy dokumentów są ograniczone do około jednej aktualizacji na sekundę
Jamie
8

W 2020 r. Nadal nie jest dostępny w pakiecie Firebase SDK, ale jest dostępny w rozszerzeniach Firebase (Beta), jednak konfiguracja i użytkowanie jest dość skomplikowane ...

Rozsądne podejście

Pomocnicy ... (tworzenie / usuwanie wydaje się zbędne, ale jest tańsze niż w przypadku aktualizacji)

export const onCreateCounter = () => async (
  change,
  context
) => {
  const collectionPath = change.ref.parent.path;
  const statsDoc = db.doc("counters/" + collectionPath);
  const countDoc = {};
  countDoc["count"] = admin.firestore.FieldValue.increment(1);
  await statsDoc.set(countDoc, { merge: true });
};

export const onDeleteCounter = () => async (
  change,
  context
) => {
  const collectionPath = change.ref.parent.path;
  const statsDoc = db.doc("counters/" + collectionPath);
  const countDoc = {};
  countDoc["count"] = admin.firestore.FieldValue.increment(-1);
  await statsDoc.set(countDoc, { merge: true });
};

export interface CounterPath {
  watch: string;
  name: string;
}

Wyeksportowane hooki Firestore


export const Counters: CounterPath[] = [
  {
    name: "count_buildings",
    watch: "buildings/{id2}"
  },
  {
    name: "count_buildings_subcollections",
    watch: "buildings/{id2}/{id3}/{id4}"
  }
];


Counters.forEach(item => {
  exports[item.name + '_create'] = functions.firestore
    .document(item.watch)
    .onCreate(onCreateCounter());

  exports[item.name + '_delete'] = functions.firestore
    .document(item.watch)
    .onDelete(onDeleteCounter());
});

W akcji

Śledzona będzie główna kolekcja budynku i wszystkie kolekcje podrzędne .

wprowadź opis obrazu tutaj

Tutaj pod /counters/ścieżką główną

wprowadź opis obrazu tutaj

Teraz liczniki kolekcji będą aktualizowane automatycznie i ostatecznie! Jeśli potrzebujesz licznika, po prostu użyj ścieżki kolekcji i poprzedz ją prefiksem counters.

const collectionPath = 'buildings/138faicnjasjoa89/buildingContacts';
const collectionCount = await db
  .doc('counters/' + collectionPath)
  .get()
  .then(snap => snap.get('count'));
Ben Winding
źródło
Czy to nie podlega temu samemu ograniczeniu „1 aktualizacja dokumentu na sekundę”?
Ayyappa
Tak, ale ostatecznie jest spójne, co oznacza, że ​​liczba kolekcji ostatecznie dopasuje się do rzeczywistej liczby kolekcji, jest to najłatwiejsze rozwiązanie do wdrożenia iw wielu przypadkach dopuszczalne jest krótkie opóźnienie w liczeniu.
Ben Winding
7

Zgadzam się z @Matthew, wykonanie takiego zapytania będzie sporo kosztować .

[PORADY DLA PROGRAMISTÓW PRZED ROZPOCZĘCIEM PROJEKTÓW]

Ponieważ przewidzieliśmy taką sytuację na początku, możemy faktycznie stworzyć kolekcję, a mianowicie liczniki z dokumentem do przechowywania wszystkich liczników w polu z typem number.

Na przykład:

Dla każdej operacji CRUD w kolekcji zaktualizuj dokument licznika:

  1. Podczas tworzenia nowej kolekcji / podkolekcji: (+1 w liczniku) [1 operacja zapisu]
  2. Kiedy usuwasz kolekcję / podkolekcję: (-1 w liczniku) [1 operacja zapisu]
  3. Podczas aktualizowania istniejącej kolekcji / kolekcji podrzędnej nie rób nic na dokumencie licznika: (0)
  4. Kiedy czytasz istniejącą kolekcję / podkolekcję, nie rób nic na dokumencie licznika: (0)

Następnym razem, gdy chcesz uzyskać numer kolekcji, wystarczy wysłać zapytanie / wskazać pole dokumentu. [1 operacja odczytu]

Ponadto możesz przechowywać nazwę kolekcji w tablicy, ale będzie to trudne, stan tablicy w firebase jest pokazany poniżej:

// we send this
['a', 'b', 'c', 'd', 'e']
// Firebase stores this
{0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e'}

// since the keys are numeric and sequential,
// if we query the data, we get this
['a', 'b', 'c', 'd', 'e']

// however, if we then delete a, b, and d,
// they are no longer mostly sequential, so
// we do not get back an array
{2: 'c', 4: 'e'}

Tak więc, jeśli nie zamierzasz usuwać kolekcji, możesz faktycznie użyć tablicy do przechowywania listy nazw kolekcji, zamiast sprawdzać całą kolekcję za każdym razem.

Mam nadzieję, że to pomoże!

Angus Tay
źródło
Może dla małej kolekcji. Pamiętaj jednak, że limit rozmiaru dokumentów w Firestore to ~ 1 MB, co oznacza, że ​​jeśli identyfikatory dokumentów w kolekcji są generowane automatycznie (20 bajtów), będziesz w stanie przechowywać tylko ~ 52 425 z nich przed dokumentem zawierającym tablicę jest za duży. Sądzę, że jako obejście problemu można by tworzyć nowy dokument co 50 000 elementów, ale wtedy utrzymywanie tych tablic byłoby całkowicie niemożliwe do zarządzania. Ponadto, wraz ze wzrostem rozmiaru dokumentu, jego odczytanie i aktualizacja zajmie więcej czasu, co ostatecznie spowoduje, że wszelkie inne operacje na nim nie będą rywalizować.
Tym Pollack,
5

Nie, obecnie nie ma wbudowanej obsługi zapytań agregujących. Jest jednak kilka rzeczy, które możesz zrobić.

Pierwsza jest udokumentowana tutaj . Możesz używać transakcji lub funkcji chmury do przechowywania informacji zbiorczych:

Ten przykład pokazuje, jak używać funkcji do śledzenia liczby ocen w podkolekcji, a także średniej oceny.

exports.aggregateRatings = firestore
  .document('restaurants/{restId}/ratings/{ratingId}')
  .onWrite(event => {
    // Get value of the newly added rating
    var ratingVal = event.data.get('rating');

    // Get a reference to the restaurant
    var restRef = db.collection('restaurants').document(event.params.restId);

    // Update aggregations in a transaction
    return db.transaction(transaction => {
      return transaction.get(restRef).then(restDoc => {
        // Compute new number of ratings
        var newNumRatings = restDoc.data('numRatings') + 1;

        // Compute new average rating
        var oldRatingTotal = restDoc.data('avgRating') * restDoc.data('numRatings');
        var newAvgRating = (oldRatingTotal + ratingVal) / newNumRatings;

        // Update restaurant info
        return transaction.update(restRef, {
          avgRating: newAvgRating,
          numRatings: newNumRatings
        });
      });
    });
});

Rozwiązanie, o którym wspomniał jbb, jest również przydatne, jeśli chcesz rzadko zliczać dokumenty. Upewnij się, że używasz select()instrukcji, aby uniknąć pobierania całego każdego dokumentu (to dużo przepustowości, gdy potrzebujesz tylko liczby). select()jest obecnie dostępne tylko w pakietach SDK serwera, więc rozwiązanie nie będzie działać w aplikacji mobilnej.

Sam Stern
źródło
1
To rozwiązanie nie jest idempotentne, więc wszelkie wyzwalacze, które uruchamiają się więcej niż jeden raz, zrzucą liczbę ocen i średnią.
Tym Pollack,
4

Nie ma dostępnej opcji bezpośredniej. Nie możesz tego zrobić db.collection("CollectionName").count(). Poniżej znajdują się dwa sposoby, za pomocą których można znaleźć liczbę dokumentów w kolekcji.

1: - Zbierz wszystkie dokumenty w kolekcji, a następnie sprawdź ich rozmiar. (Nie jest to najlepsze rozwiązanie)

db.collection("CollectionName").get().subscribe(doc=>{
console.log(doc.size)
})

Używając powyższego kodu, odczytany dokument będzie równy rozmiarowi dokumentów w kolekcji, dlatego należy unikać stosowania powyższego rozwiązania.

2: - Utwórz osobny dokument w swojej kolekcji, który będzie przechowywać liczbę dokumentów w kolekcji. (Najlepsze rozwiązanie)

db.collection("CollectionName").doc("counts")get().subscribe(doc=>{
console.log(doc.count)
})

Powyżej stworzyliśmy dokument z licznikami nazw, aby przechowywać wszystkie informacje o licznikach.Możesz zaktualizować dokument liczenia w następujący sposób: -

  • Utwórz wyzwalacze Firestore dla liczby dokumentów
  • Zwiększ właściwość count dokumentu counts podczas tworzenia nowego dokumentu.
  • Zmniejsz właściwość count dokumentów zliczania po usunięciu dokumentu.

cena wrt (odczyt dokumentu = 1) i szybkie pobieranie danych powyższe rozwiązanie jest dobre.

Nipun Madan
źródło
3

Zwiększ licznik za pomocą admin.firestore.FieldValue.increment :

exports.onInstanceCreate = functions.firestore.document('projects/{projectId}/instances/{instanceId}')
  .onCreate((snap, context) =>
    db.collection('projects').doc(context.params.projectId).update({
      instanceCount: admin.firestore.FieldValue.increment(1),
    })
  );

exports.onInstanceDelete = functions.firestore.document('projects/{projectId}/instances/{instanceId}')
  .onDelete((snap, context) =>
    db.collection('projects').doc(context.params.projectId).update({
      instanceCount: admin.firestore.FieldValue.increment(-1),
    })
  );

W tym przykładzie zwiększamy instanceCountpole w projekcie za każdym razem, gdy dokument jest dodawany do instanceskolekcji podrzędnej. Jeśli pole jeszcze nie istnieje, zostanie utworzone i zwiększone do 1.

Przyrost jest wewnętrznie transakcyjny, ale jeśli chcesz zwiększać częściej niż co 1 sekundę , powinieneś używać licznika rozproszonego .

Często lepiej jest wdrożyć onCreatei onDeletezamiast onWritewzywać onWriteaktualizacje, co oznacza, że ​​wydajesz więcej pieniędzy na niepotrzebne wywołania funkcji (jeśli aktualizujesz dokumenty w swojej kolekcji).

Dominic
źródło
2

Sposób obejścia:

napisz licznik w dokumencie Firebase, który zwiększasz w ramach transakcji za każdym razem, gdy tworzysz nowy wpis

Liczbę zapisujesz w polu nowego wpisu (np. Pozycja: 4).

Następnie tworzysz indeks na tym polu (pozycja DESC).

Możesz ustawić pomiń + limit za pomocą zapytania. Gdzie ("pozycja", "<" x) .OrderBy ("pozycja", DESC)

Mam nadzieję że to pomoże!

Kathan Shah
źródło
1

Stworzyłem uniwersalną funkcję, wykorzystując wszystkie te pomysły do ​​obsługi wszystkich przeciwnych sytuacji (z wyjątkiem zapytań).

Jedynym wyjątkiem byłoby, gdyby tak wiele zapisów na sekundę spowalniało. Przykładem może być polubienie postu zyskującego popularność. Na przykład jest to przesada w przypadku wpisu na blogu i będzie Cię kosztować więcej. Proponuję w takim przypadku stworzyć osobną funkcję za pomocą shardów: https://firebase.google.com/docs/firestore/solutions/counters

// trigger collections
exports.myFunction = functions.firestore
    .document('{colId}/{docId}')
    .onWrite(async (change: any, context: any) => {
        return runCounter(change, context);
    });

// trigger sub-collections
exports.mySubFunction = functions.firestore
    .document('{colId}/{docId}/{subColId}/{subDocId}')
    .onWrite(async (change: any, context: any) => {
        return runCounter(change, context);
    });

// add change the count
const runCounter = async function (change: any, context: any) {

    const col = context.params.colId;

    const eventsDoc = '_events';
    const countersDoc = '_counters';

    // ignore helper collections
    if (col.startsWith('_')) {
        return null;
    }
    // simplify event types
    const createDoc = change.after.exists && !change.before.exists;
    const updateDoc = change.before.exists && change.after.exists;

    if (updateDoc) {
        return null;
    }
    // check for sub collection
    const isSubCol = context.params.subDocId;

    const parentDoc = `${countersDoc}/${context.params.colId}`;
    const countDoc = isSubCol
        ? `${parentDoc}/${context.params.docId}/${context.params.subColId}`
        : `${parentDoc}`;

    // collection references
    const countRef = db.doc(countDoc);
    const countSnap = await countRef.get();

    // increment size if doc exists
    if (countSnap.exists) {
        // createDoc or deleteDoc
        const n = createDoc ? 1 : -1;
        const i = admin.firestore.FieldValue.increment(n);

        // create event for accurate increment
        const eventRef = db.doc(`${eventsDoc}/${context.eventId}`);

        return db.runTransaction(async (t: any): Promise<any> => {
            const eventSnap = await t.get(eventRef);
            // do nothing if event exists
            if (eventSnap.exists) {
                return null;
            }
            // add event and update size
            await t.update(countRef, { count: i });
            return t.set(eventRef, {
                completed: admin.firestore.FieldValue.serverTimestamp()
            });
        }).catch((e: any) => {
            console.log(e);
        });
        // otherwise count all docs in the collection and add size
    } else {
        const colRef = db.collection(change.after.ref.parent.path);
        return db.runTransaction(async (t: any): Promise<any> => {
            // update size
            const colSnap = await t.get(colRef);
            return t.set(countRef, { count: colSnap.size });
        }).catch((e: any) => {
            console.log(e);
        });;
    }
}

Obsługuje zdarzenia, przyrosty i transakcje. Piękno w tym polega na tym, że jeśli nie jesteś pewien dokładności dokumentu (prawdopodobnie będąc jeszcze w wersji beta), możesz usunąć licznik, aby automatycznie dodawał je przy następnym wyzwalaczu. Tak, to kosztuje, więc nie usuwaj go inaczej.

Ten sam rodzaj rzeczy, aby obliczyć:

const collectionPath = 'buildings/138faicnjasjoa89/buildingContacts';
const colSnap = await db.doc('_counters/' + collectionPath).get();
const count = colSnap.get('count');

Możesz również utworzyć zadanie cron (funkcja zaplanowana), aby usunąć stare zdarzenia, aby zaoszczędzić pieniądze na przechowywaniu bazy danych. Potrzebujesz przynajmniej planu blaze i może być więcej konfiguracji. Na przykład można go uruchamiać w każdą niedzielę o 23:00. https://firebase.google.com/docs/functions/schedule-functions

To nie jest testowane , ale powinno działać z kilkoma poprawkami:

exports.scheduledFunctionCrontab = functions.pubsub.schedule('5 11 * * *')
    .timeZone('America/New_York')
    .onRun(async (context) => {

        // get yesterday
        const yesterday = new Date();
        yesterday.setDate(yesterday.getDate() - 1);

        const eventFilter = db.collection('_events').where('completed', '<=', yesterday);
        const eventFilterSnap = await eventFilter.get();
        eventFilterSnap.forEach(async (doc: any) => {
            await doc.ref.delete();
        });
        return null;
    });

I wreszcie, nie zapomnij zabezpieczyć kolekcji w firestore.rules :

match /_counters/{document} {
  allow read;
  allow write: if false;
}
match /_events/{document} {
  allow read, write: if false;
}

Aktualizacja: zapytania

Dodając do mojej drugiej odpowiedzi, jeśli chcesz również zautomatyzować liczbę zapytań, możesz użyć tego zmodyfikowanego kodu w swojej funkcji chmury:

    if (col === 'posts') {

        // counter reference - user doc ref
        const userRef = after ? after.userDoc : before.userDoc;
        // query reference
        const postsQuery = db.collection('posts').where('userDoc', "==", userRef);
        // add the count - postsCount on userDoc
        await addCount(change, context, postsQuery, userRef, 'postsCount');

    }
    return delEvents();

Który automatycznie zaktualizuje postsCount w userDocument. W ten sposób możesz łatwo dodać kolejny do wielu obliczeń. To tylko daje pomysły na zautomatyzowanie rzeczy. Podałem ci również inny sposób usuwania wydarzeń. Musisz przeczytać każdą datę, aby ją usunąć, więc tak naprawdę nie zaoszczędzi ci to późniejszego usunięcia, po prostu spowalnia funkcję.

/**
 * Adds a counter to a doc
 * @param change - change ref
 * @param context - context ref
 * @param queryRef - the query ref to count
 * @param countRef - the counter document ref
 * @param countName - the name of the counter on the counter document
 */
const addCount = async function (change: any, context: any, 
  queryRef: any, countRef: any, countName: string) {

    // events collection
    const eventsDoc = '_events';

    // simplify event type
    const createDoc = change.after.exists && !change.before.exists;

    // doc references
    const countSnap = await countRef.get();

    // increment size if field exists
    if (countSnap.get(countName)) {
        // createDoc or deleteDoc
        const n = createDoc ? 1 : -1;
        const i = admin.firestore.FieldValue.increment(n);

        // create event for accurate increment
        const eventRef = db.doc(`${eventsDoc}/${context.eventId}`);

        return db.runTransaction(async (t: any): Promise<any> => {
            const eventSnap = await t.get(eventRef);
            // do nothing if event exists
            if (eventSnap.exists) {
                return null;
            }
            // add event and update size
            await t.set(countRef, { [countName]: i }, { merge: true });
            return t.set(eventRef, {
                completed: admin.firestore.FieldValue.serverTimestamp()
            });
        }).catch((e: any) => {
            console.log(e);
        });
        // otherwise count all docs in the collection and add size
    } else {
        return db.runTransaction(async (t: any): Promise<any> => {
            // update size
            const colSnap = await t.get(queryRef);
            return t.set(countRef, { [countName]: colSnap.size }, { merge: true });
        }).catch((e: any) => {
            console.log(e);
        });;
    }
}
/**
 * Deletes events over a day old
 */
const delEvents = async function () {

    // get yesterday
    const yesterday = new Date();
    yesterday.setDate(yesterday.getDate() - 1);

    const eventFilter = db.collection('_events').where('completed', '<=', yesterday);
    const eventFilterSnap = await eventFilter.get();
    eventFilterSnap.forEach(async (doc: any) => {
        await doc.ref.delete();
    });
    return null;
}

Powinienem również ostrzec, że funkcje uniwersalne będą działać w każdym okresie wywołania onWrite. Tańsze może być uruchamianie tej funkcji tylko w instancjach onCreate i onDelete określonych kolekcji. Podobnie jak w przypadku bazy danych noSQL, z którego korzystamy, powtarzanie kodu i danych pozwala zaoszczędzić pieniądze.

Jonathan
źródło
napisz artykuł o tym na nośniku, aby mieć do niego łatwy dostęp.
ahmadalibaloch
0

Zajęło mi trochę czasu, zanim to działało w oparciu o niektóre z powyższych odpowiedzi, więc pomyślałem, że podzielę się nim z innymi. Mam nadzieję, że to przydatne.

'use strict';

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();

exports.countDocumentsChange = functions.firestore.document('library/{categoryId}/documents/{documentId}').onWrite((change, context) => {

    const categoryId = context.params.categoryId;
    const categoryRef = db.collection('library').doc(categoryId)
    let FieldValue = require('firebase-admin').firestore.FieldValue;

    if (!change.before.exists) {

        // new document created : add one to count
        categoryRef.update({numberOfDocs: FieldValue.increment(1)});
        console.log("%s numberOfDocs incremented by 1", categoryId);

    } else if (change.before.exists && change.after.exists) {

        // updating existing document : Do nothing

    } else if (!change.after.exists) {

        // deleting document : subtract one from count
        categoryRef.update({numberOfDocs: FieldValue.increment(-1)});
        console.log("%s numberOfDocs decremented by 1", categoryId);

    }

    return 0;
});
Rob Phillips
źródło
0

Wiele próbowałem z różnymi podejściami. I na koniec doskonalę jedną z metod. Najpierw musisz utworzyć osobną kolekcję i zapisać tam wszystkie wydarzenia. Po drugie, musisz stworzyć nową lambdę, która będzie wyzwalana przez czas. Ta lambda zlicza zdarzenia w kolekcji zdarzeń i czyści dokumenty zdarzeń. Szczegóły kodu w artykule. https://medium.com/@ihor.malaniuk/how-to-count-documents-in-google-cloud-firestore-b0e65863aeca

Ihor Malaniuk
źródło
Proszę podać odpowiednie szczegóły i kod w samej odpowiedzi , wskazywanie ludzi na posty na blogu nie jest tak naprawdę celem StackOverflow.
DBS
0

To zapytanie zwróci liczbę dokumentów.

this.db.collection(doc).get().subscribe((data) => {
      count = data.docs.length;
    });

console.log(count)
Aravin
źródło
1
Nie jest to dobre rozwiązanie, ponieważ za każdym razem pobierasz wszystkie dokumenty z kolekcji. Będzie to dużo kosztować. Lepszym podejściem jest ustawienie licznika za każdym razem, gdy dodawany jest nowy dokument do tej kolekcji, aby można było po prostu pobrać jeden dokument zamiast kilku tysięcy.
Corentin Houdayer
0

Wykorzystuje liczenie do tworzenia numerycznego unikalnego identyfikatora. W moim użyciu nigdy nie będę dekrementował , nawet jeśli documentidentyfikator jest potrzebny, zostanie usunięty.

Na collectionstworzenie, które wymaga unikalnej wartości liczbowej

  1. Wyznacz kolekcję appDatajednym dokumentem seto .docidentyfikatorzeonly
  2. Ustaw uniqueNumericIDAmountna 0 wfirebase firestore console
  3. Użyj doc.data().uniqueNumericIDAmount + 1jako unikalnego identyfikatora numerycznego
  4. Zaktualizuj appDatakolekcję za uniqueNumericIDAmountpomocąfirebase.firestore.FieldValue.increment(1)
firebase
    .firestore()
    .collection("appData")
    .doc("only")
    .get()
    .then(doc => {
        var foo = doc.data();
        foo.id = doc.id;

        // your collection that needs a unique ID
        firebase
            .firestore()
            .collection("uniqueNumericIDs")
            .doc(user.uid)// user id in my case
            .set({// I use this in login, so this document doesn't
                  // exist yet, otherwise use update instead of set
                phone: this.state.phone,// whatever else you need
                uniqueNumericID: foo.uniqueNumericIDAmount + 1
            })
            .then(() => {

                // upon success of new ID, increment uniqueNumericIDAmount
                firebase
                    .firestore()
                    .collection("appData")
                    .doc("only")
                    .update({
                        uniqueNumericIDAmount: firebase.firestore.FieldValue.increment(
                            1
                        )
                    })
                    .catch(err => {
                        console.log(err);
                    });
            })
            .catch(err => {
                console.log(err);
            });
    });
Nick Carducci
źródło
-1
firebaseFirestore.collection("...").addSnapshotListener(new EventListener<QuerySnapshot>() {
        @Override
        public void onEvent(QuerySnapshot documentSnapshots, FirebaseFirestoreException e) {

            int Counter = documentSnapshots.size();

        }
    });
Sampath Patro
źródło
1
Ta odpowiedź mogłaby użyć więcej kontekstu, jak w przykładzie kodu.
ShellNinja