Search code examples
iosswiftfirebasegoogle-cloud-firestore

Unable to retrieve documents from Firestore when adding .whereField


I have added the Gemini ChatBot extension to a Firebase project.

Sample: https://medium.com/google-cloud/build-a-chatbot-on-ios-with-the-firebase-gemini-api-extension-in-swift-swiftui-a-step-by-step-64657205ca80

I have added the profile!.uid to the document via send message function so that I can only retrieve the users ChatDocuments.

func sendMessage(_ message: String) {
    guard let profileId = profile?.uid else {
        print("Profile ID not found")
        return
    }
    
    let data: [String: Any] = [
        "prompt": message,
        "userID": profileId,
    ]
    
    db.collection(collectionPath).addDocument(data: data) { error in
        if let error = error {
            print("Error adding document: \(error)")
        } else {
            print("Document added for userID: \(profileId)")
        }
    }
}

At this point, the app successfully adds a document to the FireStore and the extension adds a response.

The response is the show in the UI using the following function.

func fetchMessages() {
    print("Fetch messages called for userID: \(profile!.uid)")
    db.collection(collectionPath)
        .whereField("userID", isEqualTo: profile!.uid)
        .order(by: "createTime", descending: false)
        .addSnapshotListener { [weak self] querySnapshot, error in
            guard let self else { return }
            guard let documents = querySnapshot?.documents else {
                print("No documents found")
                return
            }
            
            self.messages = documents.compactMap { queryDocumentSnapshot -> [ChatMessage]? in
                do {
                    let document = try queryDocumentSnapshot.data(as: ChatDocument.self)
                    
                    let prompt = ChatMessage(text: document.prompt, isUser: true, state: .COMPLETED)
                    let response = ChatMessage(text: document.response ?? "", isUser: false, state: document.status.chatState)
                    print("RESPONSE FROM DOCUMENTS")
                    print("fetched \(response)")
                    
                    return [prompt, response]
                } catch {
                    print(error.localizedDescription)
                    return nil
                }
            }.flatMap { $0 }
        }
}

However when adding the following line, the documents are not fetched.

.whereField("userID", isEqualTo: profile!.uid)

Chat Document

struct ChatDocument: Codable {
    let createTime: Timestamp
    let prompt: String
    let response: String?
    let status: Status
    let userID: String

    struct Status: Codable {
        let startTime: Timestamp?
        let completeTime: Timestamp?
        let updateTime: Timestamp
        let state: String
        let error: String?
        
        var chatState: ChatState {
            return ChatState(rawValue: state) ?? .PROCESSING
        }
    }
}

struct ChatMessage: Hashable {
    
    @ServerTimestamp var createdTime: Timestamp?
    @DocumentID var uid: String?
    
    private(set) var id: UUID = .init()
    var text: String?
    var isUser: Bool
    var state: ChatState = .PROCESSING
    
    
    var message: String {
        switch state {
        case .COMPLETED:
            return text ?? ""
        case .ERROR:
            return "Something went wrong. Please try again."
        case .PROCESSING:
            return "..."
        }
    }
    
}

enum ChatState: String, Codable {
    case COMPLETED
    case ERROR
    case PROCESSING
}

FireStore:

enter image description here

Interestingly the following removes all documents for the profile.uid and uses the same .whereField("userID", isEqualTo: profile!.uid)

func removeAllMessages() async {
    
    do {
        let querySnapshot = try await db.collection(collectionPath)
            .whereField("userID", isEqualTo: profile!.uid)
            .getDocuments()
        
        for document in querySnapshot.documents {
            do {
                try await db.collection(collectionPath).document(document.documentID).delete()
                self.messages.removeAll()
                print("Document with ID \(document.documentID) successfully removed!")
            } catch {
                print("Error removing document with ID \(document.documentID): \(error)")
            }
        }
    } catch {
        print("Error querying documents for profileId \(profile!.uid): \(error)")
    }
}

Solution

  • Most likely you're missing composite index that is required for this combination:

    .whereField("userID", isEqualTo: profile!.uid)
    .order(by: "createTime", descending: false)
    

    The simplest solution is to catch-and-log any error that is thrown by the code, as that'll include a URL for the Firestore console to create the link - with all the fields already filled in.

    To learn more about Firestore's index logic, read the documentation on index types, on composite indexes, and on creating the required indexes.