Skip to content

Add option to allow SDK create cache indexes automatically to improve query execution locally #4987

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Jul 26, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
beeb296
Add counter
cherylEnkidu May 8, 2023
d0ea51c
address feedback 1
cherylEnkidu May 10, 2023
4794dd7
add copyright
cherylEnkidu May 11, 2023
67471cf
fix concurrency bug
cherylEnkidu May 11, 2023
5d3d008
implement autoClientIndexing
cherylEnkidu May 17, 2023
6afe360
Add tests and fix bugs for BuildTargetIndex
cherylEnkidu May 23, 2023
28e9629
hide getter from public API
cherylEnkidu May 24, 2023
2502cb5
move the flag from IndexManager to QueryEngine
cherylEnkidu May 25, 2023
6d6eaec
Address feedback
cherylEnkidu Jun 25, 2023
544de86
move auto index flag to runtime
cherylEnkidu Jun 25, 2023
81b5c29
Support old way to enable persistent for PersistentCacheManager
cherylEnkidu Jun 26, 2023
00b4ea1
Polish Tests
cherylEnkidu Jun 26, 2023
0e71ee2
Add hide and copyright
cherylEnkidu Jun 27, 2023
fb9a561
clean up unused function
cherylEnkidu Jun 27, 2023
31095b0
Rename PersistentCacheManager to PersistentCacheIndexManager
cherylEnkidu Jun 28, 2023
5b56b0b
Remove unused QueryContext
cherylEnkidu Jul 11, 2023
87e3ac1
Address feedbacks other than adding tests and comments
cherylEnkidu Jul 12, 2023
32cfc69
Change the api to match the update
cherylEnkidu Jul 12, 2023
8c8ae8c
Add tests
cherylEnkidu Jul 13, 2023
39318da
Increase tests coverage
cherylEnkidu Jul 14, 2023
14a6a71
Add comments
cherylEnkidu Jul 17, 2023
39756f4
add configurable min documents to create indexes
cherylEnkidu Jul 19, 2023
3f7a970
Address Denver's feedback
cherylEnkidu Jul 20, 2023
48d649f
Address feedback
cherylEnkidu Jul 24, 2023
6c6698e
address more feedbacks
cherylEnkidu Jul 25, 2023
0336972
use the number getting from 100 ~ 1000 documents experiment
cherylEnkidu Jul 25, 2023
46cfa2f
Address feedbacks
cherylEnkidu Jul 26, 2023
fb12477
improve debug log
cherylEnkidu Jul 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@

package com.google.firebase.firestore;

import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testCollectionWithDocs;
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testFirestore;
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor;
import static com.google.firebase.firestore.testutil.TestUtil.assertDoesNotThrow;
import static com.google.firebase.firestore.testutil.TestUtil.map;
import static org.junit.Assert.assertEquals;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.gms.tasks.Task;
Expand Down Expand Up @@ -101,4 +106,59 @@ public void testBadIndexDoesNotCrashClient() {
+ " \"fieldOverrides\": []\n"
+ "}"));
}

@Test
public void testAutomaticIndexingSetSuccessfully() {
// Use persistent disk cache (default)
FirebaseFirestore db = testFirestore();
FirebaseFirestoreSettings settings =
new FirebaseFirestoreSettings.Builder(db.getFirestoreSettings())
.setLocalCacheSettings(PersistentCacheSettings.newBuilder().build())
.build();
db.setFirestoreSettings(settings);

CollectionReference collection =
testCollectionWithDocs(
map(
"a", map("match", true),
"b", map("match", false),
"c", map("match", false)));
QuerySnapshot results = waitFor(collection.whereEqualTo("match", true).get());
assertEquals(1, results.size());

assertDoesNotThrow(() -> db.getPersistentCacheIndexManager().enableIndexAutoCreation());

results = waitFor(collection.whereEqualTo("match", true).get());
assertEquals(1, results.size());

assertDoesNotThrow(() -> db.getPersistentCacheIndexManager().disableIndexAutoCreation());

results = waitFor(collection.whereEqualTo("match", true).get());
assertEquals(1, results.size());
}

@Test
public void testAutomaticIndexingSetSuccessfullyUseDefault() {
// Use persistent disk cache (default)
FirebaseFirestore db = testFirestore();

CollectionReference collection =
testCollectionWithDocs(
map(
"a", map("match", true),
"b", map("match", false),
"c", map("match", false)));
QuerySnapshot results = waitFor(collection.whereEqualTo("match", true).get());
assertEquals(1, results.size());

assertDoesNotThrow(() -> db.getPersistentCacheIndexManager().enableIndexAutoCreation());

results = waitFor(collection.whereEqualTo("match", true).get());
assertEquals(1, results.size());

assertDoesNotThrow(() -> db.getPersistentCacheIndexManager().disableIndexAutoCreation());

results = waitFor(collection.whereEqualTo("match", true).get());
assertEquals(1, results.size());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.TaskCompletionSource;
Expand Down Expand Up @@ -104,6 +105,8 @@ public interface InstanceRegistry {
private volatile FirestoreClient client;
private final GrpcMetadataProvider metadataProvider;

@Nullable private PersistentCacheIndexManager persistentCacheIndexManager;

@NonNull
private static FirebaseApp getDefaultFirebaseApp() {
FirebaseApp app = FirebaseApp.getInstance();
Expand Down Expand Up @@ -403,6 +406,28 @@ public Task<Void> setIndexConfiguration(@NonNull String json) {
return client.configureFieldIndexes(parsedIndexes);
}

/**
* Returns the PersistentCache Index Manager used by this {@code FirebaseFirestore} object.
*
* @return The {@code PersistentCacheIndexManager} instance or null if local persistent storage is
* not in use.
*/
// TODO(csi): Remove the `hide` and scope annotations.
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Nullable
public PersistentCacheIndexManager getPersistentCacheIndexManager() {
ensureClientConfigured();
if (persistentCacheIndexManager != null) {
return persistentCacheIndexManager;
}
if (settings.isPersistenceEnabled()
|| settings.getCacheSettings() instanceof PersistentCacheSettings) {
persistentCacheIndexManager = new PersistentCacheIndexManager(client);
}
return persistentCacheIndexManager;
}

/**
* Gets a {@code CollectionReference} instance that refers to the collection at the specified path
* within the database.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.firebase.firestore;

import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import com.google.firebase.firestore.core.FirestoreClient;

/**
* A {@code PersistentCacheIndexManager} which you can config persistent cache indexes used for
* local query execution.
*
* <p>To use, calling {@link FirebaseFirestore#getPersistentCacheIndexManager()} to get an instance.
*/
// TODO(csi): Remove the `hide` and scope annotations.
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class PersistentCacheIndexManager {
@NonNull private FirestoreClient client;

@RestrictTo(RestrictTo.Scope.LIBRARY)
public PersistentCacheIndexManager(FirestoreClient client) {
this.client = client;
}

/**
* Enables SDK to create persistent cache indexes automatically for local query execution when SDK
* believes cache indexes can help improves performance.
*
* <p>This feature is disabled by default.
*/
public void enableIndexAutoCreation() {
client.enableIndexAutoCreation();
}

/**
* Stops creating persistent cache indexes automatically for local query execution. The indexes
* which have been created by calling {@link #enableIndexAutoCreation()} still take effect.
*/
public void disableIndexAutoCreation() {
client.disableIndexAutoCreation();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,16 @@ public Task<Void> configureFieldIndexes(List<FieldIndex> fieldIndices) {
return asyncQueue.enqueue(() -> localStore.configureFieldIndexes(fieldIndices));
}

public void enableIndexAutoCreation() {
verifyNotTerminated();
asyncQueue.enqueueAndForget(() -> localStore.enableIndexAutoCreation());
}

public void disableIndexAutoCreation() {
verifyNotTerminated();
asyncQueue.enqueueAndForget(() -> localStore.disableIndexAutoCreation());
}

public void removeSnapshotsInSyncListener(EventListener<Void> listener) {
// Checks for shutdown but does not raise error, allowing remove after shutdown to be a no-op.
if (isTerminated()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ enum IndexType {
/** Removes the given field index and deletes all index values. */
void deleteFieldIndex(FieldIndex index);

/** Creates a full matched field index which serves the given target. */
void createTargetIndices(Target target);

/**
* Returns a list of field indexes that correspond to the specified collection group.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,19 +258,32 @@ void recalculateAndSaveOverlays(Set<DocumentKey> documentKeys) {
*
* @param query The query to match documents against.
* @param offset Read time and key to start scanning by (exclusive).
* @param context A optional tracker to keep a record of important details during database local
* query execution.
*/
ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingQuery(
Query query, IndexOffset offset) {
Query query, IndexOffset offset, @Nullable QueryContext context) {
ResourcePath path = query.getPath();
if (query.isDocumentQuery()) {
return getDocumentsMatchingDocumentQuery(path);
} else if (query.isCollectionGroupQuery()) {
return getDocumentsMatchingCollectionGroupQuery(query, offset);
return getDocumentsMatchingCollectionGroupQuery(query, offset, context);
} else {
return getDocumentsMatchingCollectionQuery(query, offset);
return getDocumentsMatchingCollectionQuery(query, offset, context);
}
}

/**
* Performs a query against the local view of all documents.
*
* @param query The query to match documents against.
* @param offset Read time and key to start scanning by (exclusive).
*/
ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingQuery(
Query query, IndexOffset offset) {
return getDocumentsMatchingQuery(query, offset, null);
}

/** Performs a simple document lookup for the given path. */
private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingDocumentQuery(
ResourcePath path) {
Expand All @@ -284,7 +297,7 @@ private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingDocumentQu
}

private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingCollectionGroupQuery(
Query query, IndexOffset offset) {
Query query, IndexOffset offset, @Nullable QueryContext context) {
hardAssert(
query.getPath().isEmpty(),
"Currently we only support collection group queries at the root.");
Expand All @@ -297,7 +310,7 @@ private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingCollection
for (ResourcePath parent : parents) {
Query collectionQuery = query.asCollectionQueryAtPath(parent.append(collectionId));
ImmutableSortedMap<DocumentKey, Document> collectionResults =
getDocumentsMatchingCollectionQuery(collectionQuery, offset);
getDocumentsMatchingCollectionQuery(collectionQuery, offset, context);
for (Map.Entry<DocumentKey, Document> docEntry : collectionResults) {
results = results.insert(docEntry.getKey(), docEntry.getValue());
}
Expand Down Expand Up @@ -362,11 +375,11 @@ private void populateOverlays(Map<DocumentKey, Overlay> overlays, Set<DocumentKe
}

private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingCollectionQuery(
Query query, IndexOffset offset) {
Query query, IndexOffset offset, @Nullable QueryContext context) {
Map<DocumentKey, Overlay> overlays =
documentOverlayCache.getOverlays(query.getPath(), offset.getLargestBatchId());
Map<DocumentKey, MutableDocument> remoteDocuments =
remoteDocumentCache.getDocumentsMatchingQuery(query, offset, overlays.keySet());
remoteDocumentCache.getDocumentsMatchingQuery(query, offset, overlays.keySet(), context);

// As documents might match the query because of their overlay we need to include documents
// for all overlays in the initial document set.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,14 @@ public void configureFieldIndexes(List<FieldIndex> newFieldIndexes) {
});
}

public void enableIndexAutoCreation() {
queryEngine.enableIndexAutoCreation();
}

public void disableIndexAutoCreation() {
queryEngine.disableIndexAutoCreation();
}

/** Mutable state for the transaction in allocateQuery. */
private static class AllocateQueryHolder {
TargetData cached;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ public void deleteFieldIndex(FieldIndex index) {
// Field indices are not supported with memory persistence.
}

@Override
public void createTargetIndices(Target target) {}

@Override
@Nullable
public List<DocumentKey> getDocumentsMatchingTarget(Target target) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static com.google.firebase.firestore.util.Assert.hardAssert;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.firebase.database.collection.ImmutableSortedMap;
import com.google.firebase.firestore.core.Query;
import com.google.firebase.firestore.model.Document;
Expand Down Expand Up @@ -97,7 +98,10 @@ public Map<DocumentKey, MutableDocument> getAll(

@Override
public Map<DocumentKey, MutableDocument> getDocumentsMatchingQuery(
Query query, IndexOffset offset, @Nonnull Set<DocumentKey> mutatedKeys) {
Query query,
IndexOffset offset,
@Nonnull Set<DocumentKey> mutatedKeys,
@Nullable QueryContext context) {
Map<DocumentKey, MutableDocument> result = new HashMap<>();

// Documents are ordered by key, so we can use a prefix scan to narrow down the documents
Expand Down Expand Up @@ -135,6 +139,12 @@ public Map<DocumentKey, MutableDocument> getDocumentsMatchingQuery(
return result;
}

@Override
public Map<DocumentKey, MutableDocument> getDocumentsMatchingQuery(
Query query, IndexOffset offset, @Nonnull Set<DocumentKey> mutatedKeys) {
return getDocumentsMatchingQuery(query, offset, mutatedKeys, null);
}

Iterable<Document> getDocuments() {
return new DocumentIterable();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.firebase.firestore.local;

/** A tracker to keep a record of important details during database local query execution. */
public class QueryContext {
public QueryContext() {}

/** Counts the number of documents passed through during local query execution. */
private int documentReadCount = 0;

public int getDocumentReadCount() {
return documentReadCount;
}

public void increaseDocumentCount() {
documentReadCount++;
}
}
Loading