1. Before You Begin
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
- Git
- Visual Studio Code
- Install Node.js using nvm-windows (Windows) or nvm (macOS/Linux)
- If you haven't already, create a Firebase project in the Firebase console
- (Optional) For vector search, upgrade your project to Blaze plan
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
- 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.
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.
- Or: Install it from the Visual Studio Code Marketplace or search for it within VS Code.
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.
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.
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 schemaconnector/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
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
- In VSCode, open
dataconnect/moviedata_insert.gql
. Ensure the emulators in the Firebase Data Connect extension are running. - You should see a Run (local) button at the top of the file. Click this to insert the mock movie data into your database.
- Check the Data Connect Execution terminal to confirm that the data was added successfully.
Implementing the Connector
- Open
dataconnect/movie-connector/queries.gql
. You'll find a basicListMovies
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.
- 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.
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.
- 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 .
- Replace the
handleGetTopMovies
andhandleGetLatestMovies
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.
Implementing Connectors
- Open
dataconnect/movie-connector/queries.gql
in your project. - 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
}
}
}
- 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 thewhere
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).
Integrating Queries in the Web App
- In
MovieService
(app/src/lib/MovieService.tsx
), uncomment the following imports:
import { getMovieById, GetMovieByIdData } from "@movie/dataconnect";
import { GetActorByIdData, getActorById } from "@movie/dataconnect";
- Replace the
handleGetMovieById
andhandleGetActorById
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.
Implementing Connectors
- Open
mutations.gql
indataconnect/movie-connector/
. - 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 usesauth.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
- In MovieService (
app/src/lib/MovieService.tsx
), uncomment the following imports:
import { upsertUser } from "@movie/dataconnect";
import { getCurrentUser, GetCurrentUserData } from "@movie/dataconnect";
- Replace the
handleAuthStateChange
andhandleGetCurrentUser
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 theupsertUser
mutation to create or update the user's information in the database.handleGetCurrentUser
: Fetches the current user's profile using thegetCurrentUser
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.
Implementing Connectors
- Open
mutations.gql
indataconnect/movie-connector/
. - 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"
: Usesauth.uid
, which is provided directly by Firebase Authentication, ensuring that only the authenticated user's data is accessed or modified.
- Next, open
queries.gql
indataconnect/movie-connector/
. - 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 thefavorite_movies
join table to see if a specific movie is marked as a favorite by the current user.
Integrating Queries in the Web App
- In
MovieService
(app/src/lib/MovieService.tsx
), uncomment the following imports:
import { addFavoritedMovie, deleteFavoritedMovie, getIfFavoritedMovie } from "@movie/dataconnect";
- Replace the
handleAddFavoritedMovie
,handleDeleteFavoritedMovie
, andhandleGetIfFavoritedMovie
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
andhandleDeleteFavoritedMovie
: Use the mutations to add or remove a movie from the user's favorites securely.handleGetIfFavoritedMovie
: Uses thegetIfFavoritedMovie
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
- 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
- In
MovieService
(app/src/lib/MovieService.tsx
), uncomment the following imports:
import { addReview, deleteReview } from "@movie/dataconnect";
- Replace the
handleAddReview
andhandleDeleteReview
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 theaddReview
mutation to add a review for the specified movie, securely linking it to the authenticated user.handleDeleteReview
: Uses thedeleteReview
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.
Implementing Connectors
- Open
queries.gql
indataconnect/movie-connector/
. - 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 likereleaseYear
,rating
, andgenre
.contains
operator: Searches for partial text matches within fields. In this query, it looks for matches withintitle
,description
,name
, orreviewText
.where
clause: Specifies the conditions for filtering data. Each section (movies, actors, reviews) uses awhere
clause to define the specific criteria for the search.
Integrating Queries in the Web App
- In
MovieService
(app/src/lib/MovieService.tsx
), uncomment the following imports:
import { searchAll, SearchAllData } from "@movie/dataconnect";
- 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 thesearchAll
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
- Create a Web App in the Firebase Console and take note of your App ID
- 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. - Replace the
firebaseConfig
inapp/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" };
- 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
- Set up Firebase Auth with Google Sign-In
- (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
):
- In the Auth settings, select your project and go to (Authorized Domains) [https://firebase.google.com/docs/auth/web/hosting]. Click "Add Domain" and include your local domain in the list.
Deploy with Firebase CLI
- 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"]
- Make sure that you have the Firebase CLI set up with your project
npm i -g firebase-tools
firebase login --reauth
firebase use --add
- In your terminal, run the following command to deploy:
firebase deploy --only dataconnect,hosting
- Run this command to compare your schema changes:
firebase dataconnect:sql:diff
- 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.
Update the Schema to Include Embeddings for a Field
- In
dataconnect/schema/schema.gql
, add thedescriptionEmbedding
field to theMovie
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
- 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.
- 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
- Open the
dataconnect
folder in VSCode and click Run(local) inoptional_vector_embed.gql
to populate your database with embeddings for the movies.
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 the App
- 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 thesearchMovieDescriptionUsingL2similarity
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.
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.