惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Proofpoint News Feed
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Microsoft Security Blog
Microsoft Security Blog
云风的 BLOG
云风的 BLOG
MongoDB | Blog
MongoDB | Blog
Know Your Adversary
Know Your Adversary
K
Kaspersky official blog
人人都是产品经理
人人都是产品经理
IT之家
IT之家
Recorded Future
Recorded Future
Recent Announcements
Recent Announcements
D
DataBreaches.Net
C
CXSECURITY Database RSS Feed - CXSecurity.com
U
Unit 42
博客园 - 【当耐特】
I
Intezer
Cisco Talos Blog
Cisco Talos Blog
Spread Privacy
Spread Privacy
L
LINUX DO - 热门话题
H
Help Net Security
L
LangChain Blog
T
The Exploit Database - CXSecurity.com
P
Palo Alto Networks Blog
Scott Helme
Scott Helme
The GitHub Blog
The GitHub Blog
博客园_首页
P
Proofpoint News Feed
大猫的无限游戏
大猫的无限游戏
S
Secure Thoughts
The Register - Security
The Register - Security
Engineering at Meta
Engineering at Meta
T
Threatpost
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Blog — PlanetScale
Blog — PlanetScale
C
Cybersecurity and Infrastructure Security Agency CISA
T
Troy Hunt's Blog
罗磊的独立博客
腾讯CDC
Simon Willison's Weblog
Simon Willison's Weblog
Security Latest
Security Latest
Last Week in AI
Last Week in AI
P
Privacy & Cybersecurity Law Blog
Latest news
Latest news
The Cloudflare Blog
Jina AI
Jina AI
小众软件
小众软件
Cloudbric
Cloudbric
TaoSecurity Blog
TaoSecurity Blog
F
Full Disclosure
V
V2EX

Supabase Blog

AI Agents Know About Supabase. They Don't Always Use It Right. Custom OIDC Providers for Supabase Auth 100,000 GitHub stars Supabase docs over SSH Navigating Regional Network Blocks Supabase Joins the Stripe Projects Developer Preview Log Drains: Now available on Pro Supabase Storage: major performance, security, and reliability updates Supabase incident on February 12, 2026 Hydra joins Supabase X / Twitter OAuth 2.0 is now available for Supabase Auth BKND joins Supabase Supabase is now an official Claude connector Supabase PrivateLink is now available Introducing: Postgres Best Practices When to use Read Replicas vs. bigger compute Introducing TRAE SOLO integration with Supabase Supabase Security Retro: 2025 Sync Stripe Data to Your Supabase Database in One Click Building ChatGPT Apps with Supabase Edge Functions and mcp-use Own Your Observability: Supabase Metrics API Introducing iceberg-js: A JavaScript Client for Apache Iceberg Introducing Supabase for Platforms Adding Async Streaming to Postgres Foreign Data Wrappers Build "Sign in with Your App" using Supabase Auth Introducing Seven New Email Templates for Supabase Auth The new Supabase power for Kiro Introducing Supabase ETL Introducing Analytics Buckets Introducing Vector Buckets Snap, Inc. Launches Snap Cloud, Powered by Supabase Triplit joins Supabase Supabase Series E 1000 Y Combinator Founders Choose Supabase gm 👋 web3, welcome aboard to Sign in with Web3 (Solana, Ethereum) Announcing the Supabase Remote MCP Server Enterprise speed, enterprise standards with Bolt Cloud + Supabase PostgREST 13 Lovable Cloud + Supabase: The Default Platform for AI Builders Processing large jobs with Edge Functions, Cron, and Queues Defense in Depth for MCP Servers OrioleDB Patent: now freely available to the Postgres community Supabase Launch Week 15 Hackathon Winner Announcement The Vibe Coder's Guide to Supabase Environments Testing for Vibe Coders: From Zero to Production Confidence The Vibe Coding Master Checklist Vibe Coding: Best Practices for Prompting Supabase Auth: Build vs. Buy Top 10 Launches of Launch Week 15 Supabase Launch Week 15 Hackathon Storage: 10x Larger Uploads, 3x Cheaper Cached Egress, and 2x Egress Quota Persistent Storage and 97% Faster Cold Starts for Edge Functions Algolia Connector for Supabase New Observability Features in Supabase Improved Security Controls and A New Home for Security Introducing Branching 2.0 Stripe-To-Postgres Sync Engine as standalone Library Supabase Analytics Buckets with Iceberg Support Create a Supabase backend using Figma Make Introducing JWT Signing Keys Supabase UI: Platform Kit Build a Personalized AI Assistant with Postgres Announcing Multigres: Vitess for Postgres Building on open table formats Open Data Standards: Postgres, OTel, and Iceberg Simplifying back-end complexity with Supabase Data APIs PostgreSQL Event Triggers without superuser access Top 10 Launches of Launch Week 14 Supabase MCP Server Data API Routes to Nearest Read Replica Declarative Schemas for Simpler Database Management Realtime: Broadcast from Database Keeping Tabs on What's New in Supabase Studio Edge Functions: Deploy from the Dashboard + Deno 2.1 Automatic Embeddings in Postgres Introducing the Supabase UI Library Supabase Auth: Bring Your Own Clerk Postgres Language Server: Initial Release Migrating from Fauna to Supabase Migrating from the MongoDB Data API to Supabase Dedicated Poolers Postgres as a Graph Database: (Ab)using pgRouting AI Hackathon at Y Combinator Calendars in Postgres using Foreign Data Wrappers Supabase Launch Week 13 Hackathon Winners How to Hack the Base! Running Durable Workflows in Postgres using DBOS database.build v2: Bring-your-own-LLM Restore to a New Project Hack the Base! with Supabase Top 10 Launches of Launch Week 13 Supabase Queues High Performance Disk Supabase Cron Supabase CLI v2: Config as Code Supabase Edge Functions: Introducing Background Tasks, Ephemeral Storage, and WebSockets Supabase AI Assistant v2 OrioleDB Public Alpha Executing Dynamic JavaScript Code on Supabase with Edge Functions ClickHouse Partnership, improved Postgres Replication, and Disk Management
Build a content recommendation app with Flutter and OpenAI
Tyler Shukert · 2024-02-26 · via Supabase Blog

Build a content recommendation app with Flutter and OpenAI

Recommending relevant content to the user is essential to keep the user interested in the app. Although it is a common feature that we would like to have in our apps, building it is not straightforward. This changed as vector databases and Open AI emerged. Today, we can perform semantic searches that are highly aware of the context of the content with just a single query into our vector database. In this article, we will go over how you can create a Flutter movie-viewing app that recommends another movie based on what the user is viewing.

A quick disclaimer, this article provides an overview of what you can build with a vector database, so it will not go into every detail of the implementation. You can find the full code base of the app in this article here to find more details.

Why use a vector database for recommending content#

In machine learning, a process of converting a piece of content into a vector representation, called embeddings, is often used, because it allows us to analyze the semantic content mathematically. Assuming we have an engine that can create embeddings that are well aware of the context of the data, we can look at the distance between each embedding to see if the two content are similar or not. Open AI provides a well-trained model for converting text content into an embedding, so using it allows us to create a high-quality recommendation engine.

There are numerous choices for vector databases, but we will use Supabase as our vector database in this article, because we want to also store non-embedding data, and we want to be able to query them easily from our Flutter application.

We will be building a movie listing app. Think Netflix except the users will not be able to actually view the movie. The purpose of this app is to demonstrate how to surface related content to keep the users engaged.

  • Flutter - Used to create the interface of the app
  • Supabase - Used to store embeddings as well as other movie data in the database
  • Open AI API - Used to convert movie data into embeddings
  • TMDB API - A free API to get movie data

We first need to populate the database with some data about movies and its embeddings. For that, we will use the Supabase edge functions to call the TMDB API and the Open AI API to get the movie data and generate the embeddings. Once we have the data, we will store them in Supabase database, and query them from our Flutter application.

Step 1: Create the table#

We will have one table for this project, and it is the films table. films table will store some basic information about each movie like title or release data, as well as embedding of each movie’s overview so that we can perform vector similarity search on each other.


_20

-- Enable pgvector extension

_20

create extension vector

_20

with

_20

schema extensions;

_20

_20

-- Create table

_20

create table public.films (

_20

id integer primary key,

_20

title text,

_20

overview text,

_20

release_date date,

_20

backdrop_path text,

_20

embedding vector(1536)

_20

);

_20

_20

-- Enable row level security

_20

alter table public.films enable row level security;

_20

_20

-- Create policy to allow anyone to read the films table

_20

create policy "Fils are public." on public.films for select using (true);


Step 2: Get movie data#

Getting movie data is relatively straightforward. TMDB API provides an easy-to-use movies endpoint for querying information about movies while providing a wide range of filters to narrow down the query results.

We need a backend to securely call the API, and for that, we will use Supabase Edge Functions. Steps 2 through 4 will be constructing this edge function code, and the full code sample can be found here.

The following code will give us the top 20 most popular movies in a given year.


_32

const searchParams = new URLSearchParams()

_32

searchParams.set('sort_by', 'popularity.desc')

_32

searchParams.set('page', '1')

_32

searchParams.set('language', 'en-US')

_32

searchParams.set('primary_release_year', `${year}`)

_32

searchParams.set('include_adult', 'false')

_32

searchParams.set('include_video', 'false')

_32

searchParams.set('region', 'US')

_32

searchParams.set('watch_region', 'US')

_32

searchParams.set('with_original_language', 'en')

_32

_32

const tmdbResponse = await fetch(

_32

`https://api.themoviedb.org/3/discover/movie?${searchParams.toString()}`,

_32

{

_32

method: 'GET',

_32

headers: {

_32

'Content-Type': 'application/json',

_32

Authorization: `Bearer ${tmdbApiKey}`,

_32

},

_32

}

_32

)

_32

_32

const tmdbJson = await tmdbResponse.json()

_32

_32

const tmdbStatus = tmdbResponse.status

_32

if (!(200 <= tmdbStatus && tmdbStatus <= 299)) {

_32

return returnError({

_32

message: 'Error retrieving data from tmdb API',

_32

})

_32

}

_32

_32

const films = tmdbJson.results


Step 3: Generate embeddings#

We can take the movie data from the previous step and generate embedding for each of them. Here, we are calling the Open AI Embeddings API to convert the overview of each movie into embeddings. overview contains the summary of each movie, and is a good source to create embedding representing each of the movies.


_20

const response = await fetch('https://api.openai.com/v1/embeddings', {

_20

method: 'POST',

_20

headers: {

_20

'Content-Type': 'application/json',

_20

Authorization: `Bearer ${openAiApiKey}`,

_20

},

_20

body: JSON.stringify({

_20

input: film.overview,

_20

model: 'text-embedding-3-small',

_20

}),

_20

})

_20

_20

const responseData = await response.json()

_20

if (responseData.error) {

_20

return returnError({

_20

message: `Error obtaining Open API embedding: ${responseData.error.message}`,

_20

})

_20

}

_20

_20

const embedding = responseData.data[0].embedding


Step 4: Store the data in the Supabase database#

Once we have the movie data as well as embedding data, we are left with the task of storing them. We can call the upsert() function on the Supabase client to easily store the data.

Again, I omitted a lot of code here for simplicity, but you can find the full edge functions code of step 2 through step 4 here.


_20

// Code from Step 2

_20

// Get movie data and store them in `films` variable

_20

...

_20

_20

for(const film of films) {

_20

// Code from Step 3

_20

// Get the embedding and store it in `embeddings` variable

_20

_20

filmsWithEmbeddings.push({

_20

id: film.id,

_20

title: film.title,

_20

overview: film.overview,

_20

release_date: film.release_date,

_20

backdrop_path: film.backdrop_path,

_20

embedding,

_20

})

_20

}

_20

_20

// Store each movies as well as their embeddings into Supabase database

_20

const { error } = await supabase.from('films').upsert(filmsWithEmbeddings)


Step 5: Create a database function to query similar movies#

In order to perform a vector similarity search using Supabase, we need to create a database function. This database function will take an embedding and a film_id as its argument. The embedding argument will be the embedding to search through the database for similar movies, and the film_id will be used to filter out the same movie that is being queried.

Additionally, we will set an HSNW index on the embedding column to run the queries efficiently even with large data sets.


_14

-- Set index on embedding column

_14

create index on films using hnsw (embedding vector_cosine_ops);

_14

_14

-- Create function to find related films

_14

create or replace function get_related_film(embedding vector(1536), film_id integer)

_14

returns setof films

_14

language sql

_14

as $$

_14

select *

_14

from films

_14

where id != film_id

_14

order by films.embedding <=> get_related_film.embedding

_14

limit 6;

_14

$$ security invoker;


Step 6: Create the Flutter interface#

Now that we have the backend ready, all we need to do is create an interface to display and query the data from. Since the main focus of this article is to demonstrate similarity search using vectors, I will not go into all the details of the Flutter implementations, but you can find the full code base here.

Our app will have the following pages:

  • HomePage: entry point of the app, and displays a list of movies
  • DetailsPage: displays the details of a movie as well as its related movies


_10

lib/

_10

├── components/

_10

│ └── film_cell.dart # Component displaying a single movie.

_10

├── models/

_10

│ └── film.dart # A data model representing a single movie.

_10

├── pages/

_10

│ ├── details_page.dart # A page to display the details of a movie and other recommended movies.

_10

│ └── home_page.dart # A page to display a list of movies.

_10

└── main.dart


components/film_cell.dart is a shared component to display a tappable cell for the home and details page. models/film.dart contains the data model representing a single movie.

The two pages look like the following. The magic is happening at the bottom of the details page in the section labeled You might also like:. We are performing a vector similarity search to get a list of similar movies to the selected one using the database function we implemented earlier.

The following is the code for the home page. It’s a simple ListView with a standard select query from our films table. Nothing special going on here.


_48

import 'package:filmsearch/components/film_cell.dart';

_48

import 'package:filmsearch/main.dart';

_48

import 'package:filmsearch/models/film.dart';

_48

_48

import 'package:flutter/material.dart';

_48

_48

class HomePage extends StatefulWidget {

_48

const HomePage({super.key});

_48

_48

@override

_48

State<HomePage> createState() => _HomePageState();

_48

}

_48

_48

class _HomePageState extends State<HomePage> {

_48

final filmsFuture = supabase

_48

.from('films')

_48

.select<List<Map<String, dynamic>>>()

_48

.withConverter<List<Film>>((data) => data.map(Film.fromJson).toList());

_48

_48

@override

_48

Widget build(BuildContext context) {

_48

return Scaffold(

_48

appBar: AppBar(

_48

title: const Text('Films'),

_48

),

_48

body: FutureBuilder(

_48

future: filmsFuture,

_48

builder: (context, snapshot) {

_48

if (snapshot.hasError) {

_48

return Center(

_48

child: Text(snapshot.error.toString()),

_48

);

_48

}

_48

if (!snapshot.hasData) {

_48

return const Center(child: CircularProgressIndicator());

_48

}

_48

final films = snapshot.data!;

_48

return ListView.builder(

_48

itemBuilder: (context, index) {

_48

final film = films[index];

_48

return FilmCell(film: film);

_48

},

_48

itemCount: films.length,

_48

);

_48

}),

_48

);

_48

}

_48

}


In the details page, we are calling the get_related_film database function created in step 5 to get the top 6 most related movies and display them.


_106

import 'package:filmsearch/components/film_cell.dart';

_106

import 'package:filmsearch/main.dart';

_106

import 'package:filmsearch/models/film.dart';

_106

import 'package:flutter/material.dart';

_106

import 'package:intl/intl.dart';

_106

_106

class DetailsPage extends StatefulWidget {

_106

const DetailsPage({super.key, required this.film});

_106

_106

final Film film;

_106

_106

@override

_106

State<DetailsPage> createState() => _DetailsPageState();

_106

}

_106

_106

class _DetailsPageState extends State<DetailsPage> {

_106

late final Future<List<Film>> relatedFilmsFuture;

_106

_106

@override

_106

void initState() {

_106

super.initState();

_106

_106

// Create a future that calls the get_related_film function to query

_106

// related movies.

_106

relatedFilmsFuture = supabase.rpc('get_related_film', params: {

_106

'embedding': widget.film.embedding,

_106

'film_id': widget.film.id,

_106

}).withConverter<List<Film>>((data) =>

_106

List<Map<String, dynamic>>.from(data).map(Film.fromJson).toList());

_106

}

_106

_106

@override

_106

Widget build(BuildContext context) {

_106

return Scaffold(

_106

appBar: AppBar(

_106

title: Text(widget.film.title),

_106

),

_106

body: ListView(

_106

children: [

_106

Hero(

_106

tag: widget.film.imageUrl,

_106

child: Image.network(widget.film.imageUrl),

_106

),

_106

Padding(

_106

padding: const EdgeInsets.all(8.0),

_106

child: Column(

_106

crossAxisAlignment: CrossAxisAlignment.stretch,

_106

children: [

_106

Text(

_106

DateFormat.yMMMd().format(widget.film.releaseDate),

_106

style: const TextStyle(color: Colors.grey),

_106

),

_106

const SizedBox(height: 8),

_106

Text(

_106

widget.film.overview,

_106

style: const TextStyle(fontSize: 16),

_106

),

_106

const SizedBox(height: 24),

_106

const Text(

_106

'You might also like:',

_106

style: TextStyle(

_106

fontSize: 16,

_106

fontWeight: FontWeight.bold,

_106

),

_106

),

_106

],

_106

),

_106

),

_106

// Display the list of related movies

_106

FutureBuilder<List<Film>>(

_106

future: relatedFilmsFuture,

_106

builder: (context, snapshot) {

_106

if (snapshot.hasError) {

_106

return Center(

_106

child: Text(snapshot.error.toString()),

_106

);

_106

}

_106

if (!snapshot.hasData) {

_106

return const Center(child: CircularProgressIndicator());

_106

}

_106

final films = snapshot.data!;

_106

return Wrap(

_106

children: films

_106

.map((film) => InkWell(

_106

onTap: () {

_106

Navigator.of(context).push(MaterialPageRoute(

_106

builder: (context) =>

_106

DetailsPage(film: film)));

_106

},

_106

child: FractionallySizedBox(

_106

widthFactor: 0.5,

_106

child: FilmCell(

_106

film: film,

_106

isHeroEnabled: false,

_106

fontSize: 16,

_106

),

_106

),

_106

))

_106

.toList(),

_106

);

_106

}),

_106

],

_106

),

_106

);

_106

}

_106

}


And that is it. We now have a functioning similarity recommendation system powered by Open AI built into our Flutter app. The context used today was movies, but you can easily image that the same concept can be applied to other types of content as well.

In this article, we looked at how we could take a single movie, and recommend a list of movies that are similar to the selected movie. This works well, but we only have a single sample to get the similarity from. What if we want to recommend a list of movies to watch based on say the past 10 movies that a user watched? There are multiple ways you could go about solving problems like this, and I hope reading through this article got your intellectual curiosity going to solve problems like this.