使用 Flutter 和 OpenAI 构建内容推荐应用程序

向用户推荐相关内容对于保持用户对应用程序的兴趣至关重要。尽管这是我们希望在应用程序中拥有的常见功能,但构建它并不简单。随着矢量数据库和开放人工智能的出现,这种情况发生了变化。今天,我们只需对向量数据库进行一次查询,就可以执行高度了解内容上下文的语义搜索。在本文中,我们将介绍如何创建一个 Flutter 观影应用,该应用根据用户正在观看的内容推荐另一部电影。

作为快速免责声明,本文概述了您可以使用向量数据库构建的内容,因此不会深入探讨实现的每个细节。您可以在本文中找到该应用程序的完整代码库,以查找更多详细信息。

为什么要使用矢量数据库来推荐内容

在机器学习中,经常使用将一段内容转换为向量表示的过程,称为嵌入,因为它允许我们从数学上分析语义内容。假设我们有一个引擎,可以创建非常了解数据上下文的嵌入,我们可以查看每个嵌入之间的距离,看看这两个内容是否相似。Open AI 提供了一个训练有素的模型,用于将文本内容转换为嵌入,因此使用它可以让我们创建一个高质量的推荐引擎。

向量数据库有很多选择,但在本文中,我们将使用 Supabase 作为我们的向量数据库,因为我们也想存储非嵌入数据,并且我们希望能够从我们的 Flutter 应用程序中轻松查询它们。

我们将构建什么

我们将构建一个电影列表应用程序。 想想Netflix,除非用户无法实际观看电影。此应用程序的目的是演示如何显示相关内容以保持用户的参与度。

使用的工具/技术

  • Flutter - 用于创建应用的界面
  • Supabase- 用于在数据库中存储嵌入和其他电影数据
  • Open AI API - 用于将电影数据转换为嵌入
  • TMDB API - 获取电影数据的免费API

创建应用程序

我们首先需要用一些关于电影及其嵌入的数据来填充数据库。为此,我们将使用 Supabase 边缘函数调用 TMDB API 和 Open AI API 来获取电影数据并生成嵌入。一旦我们有了数据,我们就会把它们存储在 Supabase 数据库中,然后从我们的 Flutter 应用程序中查询它们。

步骤 1:创建表

我们将为这个项目准备一张桌子,那就是电影桌子。Films 表将存储有关每部电影的一些基本信息,例如标题或发行数据,以及嵌入每部电影的概述,以便我们可以对彼此进行向量相似性搜索。

sql 复制代码
-- Enable pgvector extension
create extension vector
with
  schema extensions;

-- Create table
create table public.films (
  id integer primary key,
  title text,
  overview text,
  release_date date,
  backdrop_path text,
  embedding vector(1536)
);

-- Enable row level security
alter table public.films enable row level security;

-- Create policy to allow anyone to read the films table
create policy "Fils are public." on public.films for select using (true);

第 2 步:获取动画数据

获取电影数据相对简单。TMDB API提供了一个易于使用的电影端点,用于查询有关电影的信息,同时提供了广泛的过滤器来缩小查询结果的范围。

我们需要一个后端来安全地调用 API,为此,我们将使用 Supabase Edge Functions。第 2 步到第 4 步将构造此边缘函数代码,完整的代码示例可在此处找到。

以下代码将为我们提供给定年份最受欢迎的 20 部电影。

csharp 复制代码
const searchParams = new URLSearchParams()
searchParams.set('sort_by', 'popularity.desc')
searchParams.set('page', '1')
searchParams.set('language', 'en-US')
searchParams.set('primary_release_year', `${year}`)
searchParams.set('include_adult', 'false')
searchParams.set('include_video', 'false')
searchParams.set('region', 'US')
searchParams.set('watch_region', 'US')
searchParams.set('with_original_language', 'en')

const tmdbResponse = await fetch(
  `https://api.themoviedb.org/3/discover/movie?${searchParams.toString()}`,
  {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${tmdbApiKey}`,
    },
  }
)

const tmdbJson = await tmdbResponse.json()

const tmdbStatus = tmdbResponse.status
if (!(200 <= tmdbStatus && tmdbStatus <= 299)) {
  return returnError({
    message: 'Error retrieving data from tmdb API',
  })
}

const films = tmdbJson.results

第 3 步:生成嵌入

我们可以从上一步中获取电影数据,并为每个数据生成嵌入。在这里,我们调用 Open AI Embeddings API 将每部电影的概述转换为嵌入。"概述"包含每个电影的摘要,是创建表示每个电影的嵌入的良好来源。

php 复制代码
const response = await fetch('https://api.openai.com/v1/embeddings', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${openAiApiKey}`,
  },
  body: JSON.stringify({
    input: film.overview,
    model: 'text-embedding-3-small',
  }),
})

const responseData = await response.json()
if (responseData.error) {
  return returnError({
    message: `Error obtaining Open API embedding: ${responseData.error.message}`,
  })
}

const embedding = responseData.data[0].embedding

第 4 步:将数据存储在 Supabase 数据库中

一旦我们有了电影数据以及嵌入数据,我们就剩下存储它们的任务了。我们可以在 Supabase 客户端上调用 upsert() 函数来轻松存储数据。

同样,为了简单起见,我在这里省略了很多代码,但您可以在此处找到步骤 2 到步骤 4 的完整边缘函数代码。

php 复制代码
// Code from Step 2
// Get movie data and store them in `films` variable
...

for(const film of films) {
        // Code from Step 3
  // Get the embedding and store it in `embeddings` variable

        filmsWithEmbeddings.push({
          id: film.id,
          title: film.title,
          overview: film.overview,
          release_date: film.release_date,
          backdrop_path: film.backdrop_path,
          embedding,
        })
}

// Store each movies as well as their embeddings into Supabase database
const { error } = await supabase.from('films').upsert(filmsWithEmbeddings)

步骤五:创建数据库函数查询相似影片

为了使用 Supabase 执行向量相似性搜索,我们需要创建一个数据库函数。此数据库函数将采用嵌入和film_id作为其参数。embedding 参数将是用于在数据库中搜索类似电影的嵌入,film_id将用于筛选出正在查询的同一部电影。

此外,我们将在嵌入列上设置 HSNW 索引,以便即使使用大型数据集也能有效地运行查询。

sql 复制代码
-- Set index on embedding column
create index on films using hnsw (embedding vector_cosine_ops);

-- Create function to find related films
create or replace function get_related_film(embedding vector(1536), film_id integer)
returns setof films
language sql
as $$
    select *
    from films
    where id != film_id
    order by films.embedding <=> get_related_film.embedding
    limit 6;
$$ security invoker;

第 6 步:创建 Flutter 接口

现在我们已经准备好了后端,我们需要做的就是创建一个界面来显示和查询数据。由于本文的主要重点是演示使用向量进行相似性搜索,因此我不会详细介绍 Flutter 实现的所有细节,但你可以在这里找到完整的代码库。

我们的应用程序将包含以下页面:

  • 主页:应用程序的入口点,并显示电影列表
  • DetailsPage:显示电影及其相关电影的详细信息
css 复制代码
lib/
├── components/
│   └── film_cell.dart          # Component displaying a single movie.
├── models/
│   └── film.dart               # A data model representing a single movie.
├── pages/
│   ├── details_page.dart       # A page to display the details of a movie and other recommended movies.
│   └── home_page.dart          # A page to display a list of movies.
└── main.dart

components/film_cell.dart 是一个共享组件,用于显示主页和详细信息页面的可点击单元格。models/film.dart 包含表示单个电影的数据模型。

这两个页面如下所示。神奇的事情发生在详细信息页面底部标有"您可能还喜欢:"的部分。我们正在执行矢量相似性搜索,以使用我们之前实现的数据库函数获取与所选电影相似的电影列表。

以下是主页的代码。这是一个简单的 ListView,带有来自我们 films 表的标准选择查询。这里没什么特别的。

scala 复制代码
import 'package:filmsearch/components/film_cell.dart';
import 'package:filmsearch/main.dart';
import 'package:filmsearch/models/film.dart';

import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final filmsFuture = supabase
      .from('films')
      .select<List<Map<String, dynamic>>>()
      .withConverter<List<Film>>((data) => data.map(Film.fromJson).toList());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Films'),
      ),
      body: FutureBuilder(
          future: filmsFuture,
          builder: (context, snapshot) {
            if (snapshot.hasError) {
              return Center(
                child: Text(snapshot.error.toString()),
              );
            }
            if (!snapshot.hasData) {
              return const Center(child: CircularProgressIndicator());
            }
            final films = snapshot.data!;
            return ListView.builder(
              itemBuilder: (context, index) {
                final film = films[index];
                return FilmCell(film: film);
              },
              itemCount: films.length,
            );
          }),
    );
  }
}

在详情页中,我们调用步骤5中创建的get_related_film数据库函数,获取最相关的6部电影并展示出来。

less 复制代码
import 'package:filmsearch/components/film_cell.dart';
import 'package:filmsearch/main.dart';
import 'package:filmsearch/models/film.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

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

  final Film film;

  @override
  State<DetailsPage> createState() => _DetailsPageState();
}

class _DetailsPageState extends State<DetailsPage> {
  late final Future<List<Film>> relatedFilmsFuture;

  @override
  void initState() {
    super.initState();

                // Create a future that calls the get_related_film function to query
                // related movies.
    relatedFilmsFuture = supabase.rpc('get_related_film', params: {
      'embedding': widget.film.embedding,
      'film_id': widget.film.id,
    }).withConverter<List<Film>>((data) =>
        List<Map<String, dynamic>>.from(data).map(Film.fromJson).toList());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.film.title),
      ),
      body: ListView(
        children: [
          Hero(
            tag: widget.film.imageUrl,
            child: Image.network(widget.film.imageUrl),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                Text(
                  DateFormat.yMMMd().format(widget.film.releaseDate),
                  style: const TextStyle(color: Colors.grey),
                ),
                const SizedBox(height: 8),
                Text(
                  widget.film.overview,
                  style: const TextStyle(fontSize: 16),
                ),
                const SizedBox(height: 24),
                const Text(
                  'You might also like:',
                  style: TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
          ),
                                        // Display the list of related movies
          FutureBuilder<List<Film>>(
              future: relatedFilmsFuture,
              builder: (context, snapshot) {
                if (snapshot.hasError) {
                  return Center(
                    child: Text(snapshot.error.toString()),
                  );
                }
                if (!snapshot.hasData) {
                  return const Center(child: CircularProgressIndicator());
                }
                final films = snapshot.data!;
                return Wrap(
                  children: films
                      .map((film) => InkWell(
                            onTap: () {
                              Navigator.of(context).push(MaterialPageRoute(
                                  builder: (context) =>
                                      DetailsPage(film: film)));
                            },
                            child: FractionallySizedBox(
                              widthFactor: 0.5,
                              child: FilmCell(
                                film: film,
                                isHeroEnabled: false,
                                fontSize: 16,
                              ),
                            ),
                          ))
                      .toList(),
                );
              }),
        ],
      ),
    );
  }
}

我们现在有一个功能正常的相似性推荐系统,该系统由 Open AI 提供支持,内置在我们的 Flutter 应用程序中。今天使用的上下文是电影,但你可以很容易地想象相同的概念也可以应用于其他类型的内容。

总结

在本文中,我们研究了如何拍摄一部电影,并推荐了与所选电影相似的电影列表。这很有效,但我们只有一个样本可以从中获得相似性。如果我们想根据用户过去观看的 10 部电影推荐要观看的电影列表,该怎么办?有多种方法可以解决这样的问题,我希望通读这篇文章能激发你的求知欲来解决这样的问题。

相关推荐
jcLee9520 小时前
Flutter/Dart:使用日志模块Logger Easier
flutter·log4j·dart·logger
tmacfrank21 小时前
Flutter 异步编程简述
flutter
tmacfrank21 小时前
Flutter 基础知识总结
flutter
叫我菜菜就好1 天前
【Flutter_Web】Flutter编译Web第三篇(网络请求篇):dio如何改造方法,变成web之后数据如何处理
前端·网络·flutter
AiFlutter1 天前
Flutter-底部分享弹窗(showModalBottomSheet)
java·前端·flutter
强哥之神1 天前
Nexa AI发布OmniAudio-2.6B:一款快速的音频语言模型,专为边缘部署设计
人工智能·深度学习·机器学习·语言模型·自然语言处理·音视频·openai
fanstuck2 天前
Prompt提示工程上手指南(七)Prompt编写实战-基于智能客服问答系统下的Prompt编写
人工智能·数据挖掘·openai
mortimer2 天前
实现一个用于cosoyVoice2的接口并兼容OpenAI TTS
openai·阿里巴巴
that's boy2 天前
突围边缘:OpenAI开源实时嵌入式API,AI触角延伸至微观世界
人工智能·gpt·chatgpt·开源·openai·midjourney
m0_748247802 天前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter