Build with Firebase Data Connect

1. Before you begin

FriendlyMovies app

In this codelab, you'll integrate Firebase Data Connect with a Cloud SQL database to build a movie review web app. The completed app showcases how Firebase Data Connect simplifies the process of building SQL-powered applications. It includes these features:

  • Authentication: Implement custom authentication for your app's queries and mutations, ensuring only authorized users can interact with your data.
  • GraphQL schema: Create and manage your data structures using a flexible GraphQL schema tailored to the needs of a movie review web app.
  • SQL queries and mutations: Retrieve, update, and manage data in Cloud SQL using queries and mutations powered by GraphQL.
  • Advanced search with partial string match: Use filters and search options to find movies based on fields like title, description, or tags.
  • (Optional) Vector search integration: Add content search functionality using Firebase Data Connect's vector search to provide a rich user experience based on input and preferences.

Prerequisites

You'll need a basic understanding of JavaScript.

What you'll learn

  • Set up Firebase Data Connect with local emulators.
  • Design a data schema using Data Connect and GraphQL.
  • Write and test various queries and mutations for a movie review app.
  • Learn how Firebase Data Connect generates and uses the SDK in the app.
  • Deploy your schema and manage the database efficiently.

What you'll need

2. Set up your development environment

This stage of the codelab will guide you through setting up the environment to start building your movie review app using Firebase Data Connect.

  1. Clone the project repository and install the required dependencies:
    git clone https://github.com/firebaseextended/codelab-dataconnect-web
    cd codelab-dataconnect-web
    cd ./app && npm i
    npm run dev
    
  2. After running these commands, open http://localhost:5173 in your browser to see the web app running locally. This serves as your front end for building the movie review app and interacting with its features.93f6648a2532c606.png
  3. Open the cloned codelab-dataconnect-web folder using Visual Studio Code. This is where you'll define your schema, write queries, and test the functionality of the app.
  4. To use Data Connect features, install the Firebase Data Connect Visual Studio Extension.
    Alternatively, you can install the extension from the Visual Studio Code Marketplace or search for it within VS Code.b03ee38c9a81b648.png
  5. Open or create a new Firebase project in the Firebase console.
  6. Connect your Firebase project to the Firebase Data Connect VSCode extension. In the extension, do the following:
    1. Click the Sign in button.
    2. Click Connect a Firebase Project and select your Firebase project.
    4bb2fbf8f9fac29b.png
  7. Start the Firebase emulators using the Firebase Data Connect VS Code extension:
    Click Start Emulators, and then confirm that the emulators are running in the terminal.6d3d95f4cb708db1.png

3. Review the starter codebase

In this section, you'll explore key areas of the app's starter codebase. While the app is missing some functionality, it's helpful to understand the overall structure.

Folder and file structure

The following sub-sections provide an overview of the app's folder and file structure.

The dataconnect/ directory

Contains Firebase Data Connect configurations, connectors (which define queries and mutations), and schema files.

  • schema/schema.gql: Defines the GraphQL schema
  • connector/queries.gql: Queries needed in your app
  • connector/mutations.gql: Mutations needed in your app
  • connector/connector.yaml: Configuration file for SDK generation

The app/src/ directory

Contains the application logic and interaction with Firebase Data Connect.

  • firebase.ts: Configuration to connect to a Firebase App in your Firebase project.
  • lib/dataconnect-sdk/: Contains the generated SDK. You can edit the location of SDK generation in connector/connector.yaml file and SDKs will be automatically generated any time you define a query or mutation.

4. Define a schema for movie reviews

In this section, you'll define the structure and relationships between the key entities in the movie application in a schema. Entities such as Movie, User, Actor, and Review are mapped to database tables, with relationships established using Firebase Data Connect and GraphQL schema directives. Once it's in place, your app will be ready to handle everything from searching for top-rated movies and filtering by genre to letting users leave reviews, mark favorites, explore similar movies, or find recommended movies based on text input through vector search.

Core entities and relationships

The Movie type holds key details like title, genre, and tags, which the app uses for searches and movie profiles. The User type tracks user interactions, like reviews and favorites. Reviews connect users to movies, letting the app show user-generated ratings and feedback.

Relationships between movies, actors, and users make the app more dynamic. The MovieActor join table helps display cast details and actor filmographies. The FavoriteMovie type allows users to favorite movies, so the app can show a personalized favorites list and highlight popular picks.

Set up the Movie table

The Movie type defines the main structure for a movie entity, including fields like title, genre, releaseYear, and rating.

Copy and paste the code snippet into your dataconnect/schema/schema.gql file:

type Movie
  @table {
  id: UUID! @default(expr: "uuidV4()")
  title: String!
  imageUrl: String!
  releaseYear: Int
  genre: String
  rating: Float
  description: String
  tags: [String]
}

Key Takeaways:

  • id: A unique UUID for each movie, generated using @default(expr: "uuidV4()").

Set up the MovieMetadata table

The MovieMetadata type establishes a one-to-one relationship with the Movie type. It includes additional data such as the movie's director.

Copy and paste the code snippet into your dataconnect/schema/schema.gql file:

type MovieMetadata
  @table {
  # @ref creates a field in the current table (MovieMetadata)
  # It is a reference that holds the primary key of the referenced type
  # In this case, @ref(fields: "movieId", references: "id") is implied
  movie: Movie! @ref
  # movieId: UUID <- this is created by the above @ref
  director: String
}

Key Takeaways:

  • Movie! @ref: References the Movie type, establishing a foreign key relationship.

Set up the Actor table

Copy and paste the code snippet into your dataconnect/schema/schema.gql file:

type Actor @table {
  id: UUID!
  imageUrl: String!
  name: String! @col(name: "name", dataType: "varchar(30)")
}

The Actor type represents an actor in the movie database, where each actor can be part of multiple movies, forming a many-to-many relationship.

Set up the MovieActor table

Copy and paste the code snippet into your dataconnect/schema/schema.gql file:

type MovieActor @table(key: ["movie", "actor"]) {
  # @ref creates a field in the current table (MovieActor) that holds the primary key of the referenced type
  # In this case, @ref(fields: "id") is implied
  movie: Movie!
  # movieId: UUID! <- this is created by the implied @ref, see: implicit.gql

  actor: Actor!
  # actorId: UUID! <- this is created by the implied  @ref, see: implicit.gql

  role: String! # "main" or "supporting"
}

Key Takeaways:

  • movie: References the Movie type, implicitly generates a foreign key movieId: UUID!.
  • actor: References the Actor type, implicitly generates a foreign key actorId: UUID!.
  • role: Defines the actor's role in the movie (e.g., "main" or "supporting").

Set up the User table

The User type defines a user entity who interacts with movies by leaving reviews or favoriting movies.

Copy and paste the code snippet into your dataconnect/schema/schema.gql file:

type User
  @table {
  id: String! @col(name: "auth_uid")
  username: String! @col(dataType: "varchar(50)")
  # The following are generated from the @ref in the Review table
  # reviews_on_user
  # movies_via_Review
}

Set up the FavoriteMovie table

The FavoriteMovie type is a join table that handles many-to-many relationships between users and their favorite movies. Each table links a User to a Movie.

Copy and paste the code snippet into your dataconnect/schema/schema.gql file:

type FavoriteMovie
  @table(name: "FavoriteMovies", singular: "favorite_movie", plural: "favorite_movies", key: ["user", "movie"]) {
  # @ref is implicit
  user: User!
  movie: Movie!
}

Key Takeaways:

  • movie: References the Movie type, implicitly generates a foreign key movieId: UUID!.
  • user: References the user type, implicitly generates a foreign key userId: UUID!.

Set up the Review table

The Review type represents the review entity and links the User and Movie types in a many-to-many relationship (one user can leave many reviews, and each movie can have many reviews).

Copy and paste the code snippet into your dataconnect/schema/schema.gql file:

type Review @table(name: "Reviews", key: ["movie", "user"]) {
  id: UUID! @default(expr: "uuidV4()")
  user: User!
  movie: Movie!
  rating: Int
  reviewText: String
  reviewDate: Date! @default(expr: "request.time")
}

Key Takeaways:

  • user: References the user who left the review.
  • movie: References the movie being reviewed.
  • reviewDate: Automatically set to the time when the review is created using @default(expr: "request.time").

Auto-generated fields and defaults

The schema uses expressions like @default(expr: "uuidV4()") to automatically generate unique IDs and timestamps. For instance, the id field in the Movie and Review types is automatically populated with a UUID when a new record is created.

Now that the schema is defined, your movie app has a solid foundation for its data structure and relationships!

5. Retrieve top and latest movies

FriendlyMovies app

In this section, you will insert mock movie data into the local emulators, then implement the connectors (queries) and the TypeScript code to call these connectors in the web application. By the end, your app will be able to dynamically fetch and display the top-rated and latest movies directly from the database.

Insert mock movie, actor, and review data

  1. In VSCode, open dataconnect/moviedata_insert.gql. Ensure the emulators in the Firebase Data Connect extension are running.
  2. You should see a Run (local) button at the top of the file. Click this to insert the mock movie data into your database.
    e424f75e63bf2e10.png
  3. Check the Data Connect Execution terminal to confirm that the data was added successfully.
    e0943d7704fb84ea.png

Implement the connector

  1. Open dataconnect/movie-connector/queries.gql. You'll find a basic ListMovies query in the comments:
    query ListMovies @auth(level: PUBLIC) {
      movies {
        id
        title
        imageUrl
        releaseYear
        genre
        rating
        tags
        description
      }
    }
    
    This query fetches all movies and their details (for example, id, title, releaseYear). However, it does not sort the movies.
  2. Replace the existing ListMovies query with the following query to add sorting and limit options:
    # List subset of fields for movies
    query ListMovies($orderByRating: OrderDirection, $orderByReleaseYear: OrderDirection, $limit: Int) @auth(level: PUBLIC) {
      movies(
        orderBy: [
          { rating: $orderByRating },
          { releaseYear: $orderByReleaseYear }
        ]
        limit: $limit
      ) {
        id
        title
        imageUrl
        releaseYear
        genre
        rating
        tags
        description
      }
    }
    
  3. Click the Run (local) button to execute the query against your local database. You can also input the query variables in the configuration pane before running.
    c4d947115bb11b16.png

Key Takeaways:

  • movies(): GraphQL query field for fetching movie data from the database.
  • orderByRating: Parameter to sort movies by rating (ascending/descending).
  • orderByReleaseYear: Parameter to sort movies by release year (ascending/descending).
  • limit: Restricts the number of movies returned.

Integrate queries in the web app

In this part of the codelab, you'll use the queries defined in the previous section in your web app. The Firebase Data Connect emulators generate SDKs based on the information in the .gql files (specifically, schema.gql, queries.gql, mutations.gql) and the connector.yaml file. These SDKs can be directly called in your application.

  1. In MovieService (app/src/lib/MovieService.tsx), uncomment the import statement at the top:
    import { listMovies, ListMoviesData, OrderDirection } from "@movie/dataconnect";
    
    The function listMovies, the response type ListMoviesData, and the enum OrderDirection are all SDKs generated by the Firebase Data Connect emulators based on the schema and the queries you've previously defined .
  2. Replace the handleGetTopMovies and handleGetLatestMovies functions with the following code:
    // Fetch top-rated movies
    export const handleGetTopMovies = async (
      limit: number
    ): Promise<ListMoviesData["movies"] | null> => {
      try {
        const response = await listMovies({
          orderByRating: OrderDirection.DESC,
          limit,
        });
        return response.data.movies;
      } catch (error) {
        console.error("Error fetching top movies:", error);
        return null;
      }
    };
    
    // Fetch latest movies
    export const handleGetLatestMovies = async (
      limit: number
    ): Promise<ListMoviesData["movies"] | null> => {
      try {
        const response = await listMovies({
          orderByReleaseYear: OrderDirection.DESC,
          limit,
        });
        return response.data.movies;
      } catch (error) {
        console.error("Error fetching latest movies:", error);
        return null;
      }
    };
    

Key Takeaways:

  • listMovies: An auto-generated function that calls the listMovies query to retrieve a list of movies. It includes options for sorting by rating or release year and limiting the number of results.
  • ListMoviesData: The result type used to display the top 10 and latest movies on the app's homepage.

See it in action

Reload your web app to see the query in action. The homepage now dynamically displays the list of movies, fetching data directly from your local database. You'll see the top-rated and latest movies appear seamlessly, reflecting the data you've just set up.

6. Display movie and actor details

In this section, you'll implement the functionality to retrieve detailed information for a movie or an actor using their unique IDs. This involves not only fetching data from their respective tables but also joining related tables to display comprehensive details, such as movie reviews and actor filmographies.

ac7fefa7ff779231.png

Implement connectors

  1. Open dataconnect/movie-connector/queries.gql in your project.
  2. Add the following queries to retrieve movie and actor details:
    # Get movie by id
    query GetMovieById($id: UUID!) @auth(level: PUBLIC) {
    movie(id: $id) {
        id
        title
        imageUrl
        releaseYear
        genre
        rating
        description
        tags
        metadata: movieMetadatas_on_movie {
          director
        }
        mainActors: actors_via_MovieActor(where: { role: { eq: "main" } }) {
          id
          name
          imageUrl
        }
        supportingActors: actors_via_MovieActor(
          where: { role: { eq: "supporting" } }
        ) {
          id
          name
          imageUrl
        }
        reviews: reviews_on_movie {
          id
          reviewText
          reviewDate
          rating
          user {
            id
            username
          }
        }
      }
    }
    
    # Get actor by id
    query GetActorById($id: UUID!) @auth(level: PUBLIC) {
      actor(id: $id) {
        id
        name
        imageUrl
        mainActors: movies_via_MovieActor(where: { role: { eq: "main" } }) {
          id
          title
          genre
          tags
          imageUrl
        }
        supportingActors: movies_via_MovieActor(
          where: { role: { eq: "supporting" } }
        ) {
          id
          title
          genre
          tags
          imageUrl
        }
      }
    }
    
  3. Save your changes and review the queries.

Key Takeaways:

  • movie()/actor(): GraphQL query fields for fetching a single movie or actor from the Movies or Actors table.
  • _on_: This allows direct access to fields from an associated type that has a foreign key relationship. For example, reviews_on_movie fetches all reviews related to a specific movie.
  • _via_: Used to navigate many-to-many relationships through a join table. For instance, actors_via_MovieActor accesses the Actor type through the MovieActor join table, and the where condition filters actors based on their role (for example, "main" or "supporting").

Test the query by inputting mock data

  1. In the Data Connect execution pane, you can test the query by inputting mock IDs, such as:
    {"id": "550e8400-e29b-41d4-a716-446655440000"}
    
  2. Click Run (local) for GetMovieById to retrieve the details about "Quantum Paradox" (the mock movie that the above ID relates to).

1b08961891e44da2.png

Integrate queries in the web app

  1. In MovieService (app/src/lib/MovieService.tsx), uncomment the following imports:
    import { getMovieById, GetMovieByIdData } from "@movie/dataconnect";
    import { GetActorByIdData, getActorById } from "@movie/dataconnect";
    
  2. Replace the handleGetMovieById and handleGetActorById functions with the following code:
    // Fetch movie details by ID
    export const handleGetMovieById = async (
      movieId: string
    ) => {
      try {
        const response = await getMovieById({ id: movieId });
        if (response.data.movie) {
          return response.data.movie;
        }
        return null;
      } catch (error) {
        console.error("Error fetching movie:", error);
        return null;
      }
    };
    
    // Calling generated SDK for GetActorById
    export const handleGetActorById = async (
      actorId: string
    ): Promise<GetActorByIdData["actor"] | null> => {
      try {
        const response = await getActorById({ id: actorId });
        if (response.data.actor) {
          return response.data.actor;
        }
        return null;
      } catch (error) {
        console.error("Error fetching actor:", error);
        return null;
      }
    };
    

Key Takeaways:

  • getMovieById / getActorById: These are auto-generated functions that call the queries you defined, retrieving detailed information for a specific movie or actor.
  • GetMovieByIdData / GetActorByIdData: These are the result types, used to display movie and actor details in the app.

See it in action

Now, go to your web app's homepage. Click on a movie, and you'll be able to view all its details, including actors and reviews—information pulled from related tables. Similarly, clicking on an actor will display the movies they were part of.

7. Handle user authentication

In this section, you'll implement user sign-in and sign-out functionality using Firebase Authentication. You'll also use Firebase Authentication data to directly retrieve or upsert user data in Firebase DataConnect, ensuring secure user management within your app.

9890838045d5a00e.png

Implement connectors

  1. Open mutations.gql in dataconnect/movie-connector/.
  2. Add the following mutation to create or update the current authenticated user:
    # Create or update the current authenticated user
    mutation UpsertUser($username: String!) @auth(level: USER) {
      user_upsert(
        data: {
          id_expr: "auth.uid"
          username: $username
        }
      )
    }
    

Key Takeaways:

  • id_expr: "auth.uid": This uses auth.uid, which is provided directly by Firebase Authentication, not by the user or the app, adding an extra layer of security by ensuring the user ID is handled securely and automatically.

Fetch the current user

  1. Open queries.gql in dataconnect/movie-connector/.
  2. Add the following query to fetch the current user:
    # Get user by ID
    query GetCurrentUser @auth(level: USER) {
      user(key: { id_expr: "auth.uid" }) {
        id
        username
        reviews: reviews_on_user {
          id
          rating
          reviewDate
          reviewText
          movie {
            id
            title
          }
        }
        favoriteMovies: favorite_movies_on_user {
          movie {
            id
            title
            genre
            imageUrl
            releaseYear
            rating
            description
            tags
            metadata: movieMetadatas_on_movie {
              director
            }
          }
        }
      }
    }
    

Key Takeaways:

  • auth.uid: This is retrieved directly from Firebase Authentication, ensuring secure access to user-specific data.
  • _on_ fields: These fields represent the join tables:
    • reviews_on_user: Fetches all reviews related to the user, including the movie's id and title.
    • favorite_movies_on_user: Retrieves all movies marked as favorites by the user, including detailed information like genre, releaseYear, rating, and metadata.

Integrate queries in the web app

  1. In MovieService (app/src/lib/MovieService.tsx), uncomment the following imports:
    import { upsertUser } from "@movie/dataconnect";
    import { getCurrentUser, GetCurrentUserData } from "@movie/dataconnect";
    
  2. Replace the handleAuthStateChange and handleGetCurrentUser functions with the following code:
    // Handle user authentication state changes and upsert user
    export const handleAuthStateChange = (
      auth: any,
      setUser: (user: User | null) => void
    ) => {
      return onAuthStateChanged(auth, async (user) => {
        if (user) {
          setUser(user);
          const username = user.email?.split("@")[0] || "anon";
          await upsertUser({ username });
        } else {
          setUser(null);
        }
      });
    };
    
    // Fetch current user profile
    export const handleGetCurrentUser = async (): Promise<
      GetCurrentUserData["user"] | null
    > => {
      try {
        const response = await getCurrentUser();
        return response.data.user;
      } catch (error) {
        console.error("Error fetching user profile:", error);
        return null;
      }
    };
    

Key Takeaways:

  • handleAuthStateChange: This function listens for authentication state changes. When a user signs in, it sets the user's data and calls the upsertUser mutation to create or update the user's information in the database.
  • handleGetCurrentUser: Fetches the current user's profile using the getCurrentUser query, which retrieves the user's reviews and favorite movies.

See it in action

Now, click on the "Sign in with Google" button in the navbar. You can sign in using the Firebase Authentication emulator. After signing in, click on "My Profile". It will be empty for now, but you've set up the foundation for user-specific data handling in your app.

8. Implement user interactions

In this section of the codelab, you'll implement user interactions in the movie review app, specifically letting users manage their favorite movies and leave or delete reviews.

b3d0ac1e181c9de9.png

Let a user favorite a movie

In this section, you'll set up the database to let users favorite a movie.

Implement connectors

  1. Open mutations.gql in dataconnect/movie-connector/.
  2. Add the following mutations to handle favoriting movies:
    # Add a movie to the user's favorites list
    mutation AddFavoritedMovie($movieId: UUID!) @auth(level: USER) {
      favorite_movie_upsert(data: { userId_expr: "auth.uid", movieId: $movieId })
    }
    
    # Remove a movie from the user's favorites list
    mutation DeleteFavoritedMovie($movieId: UUID!) @auth(level: USER) {
      favorite_movie_delete(key: { userId_expr: "auth.uid", movieId: $movieId })
    }
    
    

Key Takeaways:

  • userId_expr: "auth.uid": Uses auth.uid, which is provided directly by Firebase Authentication, ensuring that only the authenticated user's data is accessed or modified.

Check if a movie is favorited

  1. Open queries.gql in dataconnect/movie-connector/.
  2. Add the following query to check if a movie is favorited:
    query GetIfFavoritedMovie($movieId: UUID!) @auth(level: USER) {
      favorite_movie(key: { userId_expr: "auth.uid", movieId: $movieId }) {
        movieId
      }
    }
    

Key Takeaways:

  • auth.uid: Ensures secure access to user-specific data using Firebase Authentication.
  • favorite_movie: Checks the favorite_movies join table to see if a specific movie is marked as a favorite by the current user.

Integrate queries in the web app

  1. In MovieService (app/src/lib/MovieService.tsx), uncomment the following imports:
    import { addFavoritedMovie, deleteFavoritedMovie, getIfFavoritedMovie } from "@movie/dataconnect";
    
  2. Replace the handleAddFavoritedMovie, handleDeleteFavoritedMovie, and handleGetIfFavoritedMovie functions with the following code:
    // Add a movie to user's favorites
    export const handleAddFavoritedMovie = async (
      movieId: string
    ): Promise<void> => {
      try {
        await addFavoritedMovie({ movieId });
      } catch (error) {
        console.error("Error adding movie to favorites:", error);
        throw error;
      }
    };
    
    // Remove a movie from user's favorites
    export const handleDeleteFavoritedMovie = async (
      movieId: string
    ): Promise<void> => {
      try {
        await deleteFavoritedMovie({ movieId });
      } catch (error) {
        console.error("Error removing movie from favorites:", error);
        throw error;
      }
    };
    
    // Check if the movie is favorited by the user
    export const handleGetIfFavoritedMovie = async (
      movieId: string
    ): Promise<boolean> => {
      try {
        const response = await getIfFavoritedMovie({ movieId });
        return !!response.data.favorite_movie;
      } catch (error) {
        console.error("Error checking if movie is favorited:", error);
        return false;
      }
    };
    

Key Takeaways:

  • handleAddFavoritedMovie and handleDeleteFavoritedMovie: Use the mutations to add or remove a movie from the user's favorites securely.
  • handleGetIfFavoritedMovie: Uses the getIfFavoritedMovie query to check if a movie is marked as a favorite by the user.

See it in action

Now, you can favorite or unfavorite movies by clicking the heart icon on the movie cards and the movie details page. Additionally, you can view your favorite movies on your profile page.

Let users leave or delete reviews

Next, you'll implement the section for managing user reviews in the app.

Implement connectors

In mutations.gql (dataconnect/movie-connector/mutations.gql): Add the following mutations:

# Add a review for a movie
mutation AddReview($movieId: UUID!, $rating: Int!, $reviewText: String!)
@auth(level: USER) {
  review_insert(
    data: {
      userId_expr: "auth.uid"
      movieId: $movieId
      rating: $rating
      reviewText: $reviewText
      reviewDate_date: { today: true }
    }
  )
}

# Delete a user's review for a movie
mutation DeleteReview($movieId: UUID!) @auth(level: USER) {
  review_delete(key: { userId_expr: "auth.uid", movieId: $movieId })
}

Key Takeaways:

  • userId_expr: "auth.uid": Ensures that reviews are associated with the authenticated user.
  • reviewDate_date: { today: true }: Automatically generates the current date for the review using DataConnect, eliminating the need for manual input.

Integrate queries in the web app

  1. In MovieService (app/src/lib/MovieService.tsx), uncomment the following imports:
    import { addReview, deleteReview } from "@movie/dataconnect";
    
  2. Replace the handleAddReview and handleDeleteReview functions with the following code:
    // Add a review to a movie
    export const handleAddReview = async (
      movieId: string,
      rating: number,
      reviewText: string
    ): Promise<void> => {
      try {
        await addReview({ movieId, rating, reviewText });
      } catch (error) {
        console.error("Error adding review:", error);
        throw error;
      }
    };
    
    // Delete a review from a movie
    export const handleDeleteReview = async (movieId: string): Promise<void> => {
      try {
        await deleteReview({ movieId });
      } catch (error) {
        console.error("Error deleting review:", error);
        throw error;
      }
    };
    

Key Takeaways:

  • handleAddReview: Calls the addReview mutation to add a review for the specified movie, securely linking it to the authenticated user.
  • handleDeleteReview: Uses the deleteReview mutation to remove a review for a movie by the authenticated user.

See it in action

Users can now leave reviews for movies on the movie details page. They can also view and delete their reviews on their profile page, giving them full control over their interactions with the app.

9. Advanced filters and partial text matching

In this section, you'll implement advanced search capabilities, allowing users to search movies based on a range of ratings and release years, filter by genres and tags, perform partial text matching in titles or descriptions, and even combine multiple filters for more precise results.

ece70ee0ab964e28.png

Implement connectors

  1. Open queries.gql in dataconnect/movie-connector/.
  2. Add the following query to support various search capabilities:
    # Search for movies, actors, and reviews
    query SearchAll(
      $input: String
      $minYear: Int!
      $maxYear: Int!
      $minRating: Float!
      $maxRating: Float!
      $genre: String!
    ) @auth(level: PUBLIC) {
      moviesMatchingTitle: movies(
        where: {
          _and: [
            { releaseYear: { ge: $minYear } }
            { releaseYear: { le: $maxYear } }
            { rating: { ge: $minRating } }
            { rating: { le: $maxRating } }
            { genre: { contains: $genre } }
            { title: { contains: $input } }
          ]
        }
      ) {
        id
        title
        genre
        rating
        imageUrl
      }
      moviesMatchingDescription: movies(
        where: {
          _and: [
            { releaseYear: { ge: $minYear } }
            { releaseYear: { le: $maxYear } }
            { rating: { ge: $minRating } }
            { rating: { le: $maxRating } }
            { genre: { contains: $genre } }
            { description: { contains: $input } }
          ]
        }
      ) {
        id
        title
        genre
        rating
        imageUrl
      }
      actorsMatchingName: actors(where: { name: { contains: $input } }) {
        id
        name
        imageUrl
      }
      reviewsMatchingText: reviews(where: { reviewText: { contains: $input } }) {
        id
        rating
        reviewText
        reviewDate
        movie {
          id
          title
        }
        user {
          id
          username
        }
      }
    }
    

Key Takeaways:

  • _and operator: Combines multiple conditions in a single query, allowing the search to be filtered by several fields like releaseYear, rating, and genre.
  • contains operator: Searches for partial text matches within fields. In this query, it looks for matches within title, description, name, or reviewText.
  • where clause: Specifies the conditions for filtering data. Each section (movies, actors, reviews) uses a where clause to define the specific criteria for the search.

Integrate queries in the web app

  1. In MovieService (app/src/lib/MovieService.tsx), uncomment the following imports:
    import { searchAll, SearchAllData } from "@movie/dataconnect";
    
  2. Replace the handleSearchAll function with the following code:
    // Function to perform the search using the query and filters
    export const handleSearchAll = async (
      searchQuery: string,
      minYear: number,
      maxYear: number,
      minRating: number,
      maxRating: number,
      genre: string
    ): Promise<SearchAllData | null> => {
      try {
        const response = await searchAll({
          input: searchQuery,
          minYear,
          maxYear,
          minRating,
          maxRating,
          genre,
        });
    
        return response.data;
      } catch (error) {
        console.error("Error performing search:", error);
        return null;
      }
    };
    

Key Takeaways:

  • handleSearchAll: This function uses the searchAll query to perform a search based on the user's input, filtering results by parameters like year, rating, genre, and partial text matches.

See it in action

Head over to the "Advanced Search" page from the navbar in the web app. You can now search for movies, actors, and reviews using various filters and inputs, getting detailed and tailored search results.

10. Optional: Deploy to Cloud (billing required)

Now that you've worked through the local development iteration, it's time to deploy your schema, data, and queries to the server. This can be done using the Firebase Data Connect VS Code extension or the Firebase CLI.

Upgrade your Firebase pricing plan

To integrate Firebase Data Connect with Cloud SQL for PostgreSQL, your Firebase project needs to be on the pay-as-you go (Blaze) pricing plan, which means it's linked to a Cloud Billing account.

  • A Cloud Billing account requires a payment method, like a credit card.
  • If you're new to Firebase and Google Cloud, check if you're eligible for a $300 credit and a Free Trial Cloud Billing account.
  • If you're doing this codelab as part of an event, ask your organizer if there are any Cloud credits available.

To upgrade your project to the Blaze plan, follow these steps:

  1. In the Firebase console, select to upgrade your plan.
  2. Select the Blaze plan. Follow the on-screen instructions to link a Cloud Billing account to your project.
    If you needed to create a Cloud Billing account as part of this upgrade, you might need to navigate back to the upgrade flow in the Firebase console to complete the upgrade.

Connect your web app to your Firebase project

  1. Register your web app in your Firebase project using the Firebase console:
    1. Open your project, and then click Add App.
    2. Ignore the SDK setup and configuration setup for now, but make sure to copy the generated firebaseConfig object.
    7030822793e4d75b.png
  2. Replace the existing firebaseConfig in app/src/lib/firebase.tsx with the configuration you just copied from the Firebase console.
    const firebaseConfig = {
      apiKey: "API_KEY",
      authDomain: "PROJECT_ID.firebaseapp.com",
      projectId: "PROJECT_ID",
      storageBucket: "PROJECT_ID.firebasestorage.app",
      messagingSenderId: "SENDER_ID",
      appId: "APP_ID"
    };
    
  3. Build the web app: Back in VS Code, in the app folder, use Vite to build the web app for hosting deployment:
    cd app
    npm run build
    

Set up Firebase Authentication in your Firebase project

  1. Set up Firebase Authentication with Google Sign-In.62af2f225e790ef6.png
  2. (Optional) Allow domains for Firebase Authentication using the Firebase console (for example, http://127.0.0.1).
    1. In the Authentication settings, go to Authorized Domains.
    2. Click "Add Domain" and include your local domain in the list.

c255098f12549886.png

Deploy with the Firebase CLI

  1. In dataconnect/dataconnect.yaml, ensure that your instance ID, database, and service ID match your project:
    specVersion: "v1alpha"
    serviceId: "your-service-id"
    location: "us-central1"
    schema:
      source: "./schema"
      datasource:
        postgresql:
          database: "your-database-id"
          cloudSql:
            instanceId: "your-instance-id"
    connectorDirs: ["./movie-connector"]
    
  2. Make sure that you have the Firebase CLI set up with your project:
    npm i -g firebase-tools
    firebase login --reauth
    firebase use --add
    
  3. In your terminal, run the following command to deploy:
    firebase deploy --only dataconnect,hosting
    
  4. Run this command to compare your schema changes:
    firebase dataconnect:sql:diff
    
  5. If the changes are acceptable, apply them with:
    firebase dataconnect:sql:migrate
    

Your Cloud SQL for PostgreSQL instance will be updated with the final deployed schema and data. You can monitor the status in the Firebase console.

You should now be able to see your app live at your-project.web.app/. Additionally, you can click Run (Production) in the Firebase Data Connect panel, just as you did with the local emulators, to add data to the production environment.

11. Optional: Vector search with Firebase Data Connect (billing required)

In this section, you'll enable vector search in your movie review app using Firebase Data Connect. This feature allows for content-based searches, such as finding movies with similar descriptions using vector embeddings.

This step requires that you completed the last step of this codelab to deploy to Google Cloud.

4b5aca5a447d2feb.png

Update the schema to include embeddings for a field

In dataconnect/schema/schema.gql, add the descriptionEmbedding field to the Movie table:

type Movie
  # The below parameter values are generated by default with @table, and can be edited manually.
  @table {
  # implicitly calls @col to generates a column name. ex: @col(name: "movie_id")
  id: UUID! @default(expr: "uuidV4()")
  title: String!
  imageUrl: String!
  releaseYear: Int
  genre: String
  rating: Float
  description: String
  tags: [String]
  descriptionEmbedding: Vector @col(size:768) # Enables vector search
}

Key Takeaways:

  • descriptionEmbedding: Vector @col(size:768): This field stores the semantic embeddings of movie descriptions, enabling vector-based content search in your app.

Activate Vertex AI

  1. Follow the prerequisites guide to set up Vertex AI APIs from Google Cloud. This step is essential to support the embedding generation and vector search functionality.
  2. Re-deploy your schema to activate pgvector and vector search by clicking on "Deploy to Production" using the Firebase Data Connect VS Code extension.

Populate the database with embeddings

  1. Open the dataconnect folder in VS Code.
  2. Click Run(local) in optional_vector_embed.gql to populate your database with embeddings for the movies.

b858da780f6ec103.png

Add a vector search query

In dataconnect/movie-connector/queries.gql, add the following query to perform vector searches:

# Search movie descriptions using L2 similarity with Vertex AI
query SearchMovieDescriptionUsingL2Similarity($query: String!)
@auth(level: PUBLIC) {
  movies_descriptionEmbedding_similarity(
    compare_embed: { model: "textembedding-gecko@003", text: $query }
    method: L2
    within: 2
    limit: 5
  ) {
    id
    title
    description
    tags
    rating
    imageUrl
  }
}

Key Takeaways:

  • compare_embed: Specifies the embedding model (textembedding-gecko@003) and the input text ($query) for comparison.
  • method: Specifies the similarity method (L2), which represents the Euclidean distance.
  • within: Limits the search to movies with an L2 distance of 2 or less, focusing on close content matches.
  • limit: Restricts the number of results returned to 5.

Implement the vector search function in your app

Now that the schema and query are set up, integrate the vector search into your app's service layer. This step allows you to call the search query from your web app.

  1. In app/src/lib/ MovieService.ts, uncomment the following imports from the SDKs, this will work like any other query.
    import {
      searchMovieDescriptionUsingL2similarity,
      SearchMovieDescriptionUsingL2similarityData,
    } from "@movie/dataconnect";
    
  2. Add the following function to integrate vector-based search into the app:
    // Perform vector-based search for movies based on description
    export const searchMoviesByDescription = async (
      query: string
    ): Promise<
      | SearchMovieDescriptionUsingL2similarityData["movies_descriptionEmbedding_similarity"]
      | null
    > => {
      try {
        const response = await searchMovieDescriptionUsingL2similarity({ query });
        return response.data.movies_descriptionEmbedding_similarity;
      } catch (error) {
        console.error("Error fetching movie descriptions:", error);
        return null;
      }
    };
    

Key Takeaways:

  • searchMoviesByDescription: This function calls the searchMovieDescriptionUsingL2similarity query, passing the input text to perform a vector-based content search.

See it in action

Navigate to the "Vector Search" section in the navbar and type in phrases like "romantic and modern". You'll see a list of movies that match the content you're searching for, or, go into the movie details page of any movie, and check out the similar movies section at the bottom of the page.

7b71f1c75633c1be.png

12. Conclusion

Congratulations, you should be able to use the web app! If you want to play with your own movie data, don't worry, insert your own data using the Firebase Data Connect extension by mimicking the _insert.gql files, or add them through the Data Connect execution pane in VS Code.

Learn more