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

Setting Up Your Development Environment

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

Step 1: Clone the Project Repository

Begin by cloning the project repository and installing the required dependencies:

git clone https://github.com/firebaseextended/codelab-dataconnect-web
cd codelab-dataconnect-web
cd ./app && npm i
npm run dev
  1. 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

Step 2: Open the Project in Visual Studio Code

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.

Step 3: Install Firebase Data Connect Visual Studio Extension

To use Data Connect features, install the Firebase Data Connect Visual Studio Extension.Or: Install it from the Visual Studio Code Marketplace or search for it within VS Code.

  1. Or: Install it from the Visual Studio Code Marketplace or search for it within VS Code.

b03ee38c9a81b648.png

Step 4: Create a Firebase Project

Go to the Firebase Console to create a new Firebase project if you don't already have one. Then, in the Firebase Data Connect VSCode extension:

  • Click the Sign in button.
  • Click Connect a Firebase Project and select the project you created in the Firebase Console.

4bb2fbf8f9fac29b.png

Step 5: Start Firebase emulators

In the Firebase Data Connect VSCode extension, click Start Emulators, and confirm that the emulators are running in the terminal.

6d3d95f4cb708db1.png

2. 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

Here's a quick overview of the app's folder and file structure:

dataconnect/

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

app/src/

Contains the application logic and interaction with Firebase Data Connect.

  • firebase.ts: Configuration to connect to a Firebase app on the console.
  • lib/dataconnect-sdk/: This folder 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.

3. Defining a schema for Movie Review

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.

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()").

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.

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.

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").

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
}

FavoriteMovie Table

The FavoriteMovie type is a join table that handles many-to-many relationships between users and their favorite movies or actors. Each table links a User to either 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!.

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!

4. Retrieving 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.

Inserting 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

  1. Check the Data Connect Execution terminal to confirm that the data was added successfully.

e0943d7704fb84ea.png

Implementing 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 (e.g., id, title, releaseYear). However, it does not sort the movies.

  1. Replace the ListMovies query with the one below 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
  }
}

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.

Integrating Queries in the Web App

In this part, 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 (schema.gql, queries.gql, mutations.gql) and connector.yaml. 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 .

  1. 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 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.

5. Displaying 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

Implementing 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
    }
  }
}
  1. 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 (e.g., "main" or "supporting").

In the Data Connect execution pane, you can test the query by inputting mock IDs, such as:

{"id": "550e8400-e29b-41d4-a716-446655440000"}

Click Run (local) for GetMovieById to retrieve the details about "Quantum Paradox" (the mock movie that the above ID relates to).

1b08961891e44da2.png

Integrating 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";
  1. 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.

6. Handling 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

Implementing 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 Takeaway:

  • 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.

Next, open queries.gql in dataconnect/movie-connector/.

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.

Integrating 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";
  1. 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 Auth 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.

7. Implementing User Interactions

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

b3d0ac1e181c9de9.png

Implementing 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.
  1. Next, 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.

Integrating Queries in the Web App

  1. In MovieService (app/src/lib/MovieService.tsx), uncomment the following imports:
import { addFavoritedMovie, deleteFavoritedMovie, getIfFavoritedMovie } from "@movie/dataconnect";
  1. 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.

Implementing User Reviews

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

Implementing Connectors

  1. 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.

Integrating Queries in the Web App

  1. In MovieService (app/src/lib/MovieService.tsx), uncomment the following imports:
import { addReview, deleteReview } from "@movie/dataconnect";
  1. 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.

8. 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

Implementing 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.

Integrating Queries in the Web App

  1. In MovieService (app/src/lib/MovieService.tsx), uncomment the following imports:
import { searchAll, SearchAllData } from "@movie/dataconnect";
  1. 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.

9. 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.

Adding a Web App in the Firebase Console

  1. Create a Web App in the Firebase Console and take note of your App ID

7030822793e4d75b.png

  1. Set up a web app in the Firebase Console by clicking on "Add App." You can safely ignore the SDK setup and configuration setup for now, but take note of the generated firebaseConfig object.
  2. Replace the firebaseConfig in app/src/lib/firebase.tsx:
const firebaseConfig = {
  apiKey: "API_KEY",
  authDomain: "PROJECT_ID.firebaseapp.com",
  projectId: "PROJECT_ID",
  storageBucket: "PROJECT_ID.appspot.com",
  messagingSenderId: "SENDER_ID",
  appId: "APP_ID"
};
  1. Build the Web App: In the app folder, use Vite to build the web app for hosting deployment:
cd app
npm run build

Set Up Firebase Authentication in the Console

  1. Set up Firebase Auth with Google Sign-In

62af2f225e790ef6.png

  1. (Optional) Allow domains for (Firebase Auth) [https://firebase.google.com/docs/auth/web/hosting] in your project console (e.g., http://127.0.0.1):

c255098f12549886.png

Deploy with 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"]
  1. Make sure that you have the Firebase CLI set up with your project
npm i -g firebase-tools
firebase login --reauth
firebase use --add
  1. In your terminal, run the following command to deploy:
firebase deploy --only dataconnect,hosting
  1. Run this command to compare your schema changes:
firebase dataconnect:sql:diff
  1. 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.

10. Optional: Vector Search with Firebase Data Connect

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.

4b5aca5a447d2feb.png

Update the Schema to Include Embeddings for a Field

  1. 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 Takeaway:

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

Activating Vertex AI

  1. Follow the prerequisites guide to set up Vertex AI APIs with 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 VSCode extension.

Populating the Database with Embeddings

  1. Open the dataconnect folder in VSCode and click Run(local) in optional_vector_embed.gql to populate your database with embeddings for the movies.

b858da780f6ec103.png

Add a Vector Search Query

  1. 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 the App

  1. In app/src/lib/MovieService.ts, uncomment the following imports:

Implement the Vector Search Function in 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.

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";

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

11. 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 FDC extension by mimicking the _insert.gql files, or add them through the Data Connect Execution pane.

Learn more