如何向 React 应用添加 AI 驱动的搜索
使用 Meilisearch 的 AI 驱动搜索构建 React 电影搜索和推荐应用。

在本指南中,我们将引导您构建一个 AI 驱动的电影推荐应用。与传统的关键词搜索不同,AI 驱动的搜索使用机器学习根据查询的上下文和含义返回结果。
您将使用 Meilisearch 和 OpenAI 的嵌入模型构建一个搜索和推荐系统。该应用将提供“边输入边搜索”的体验,结合精确的关键词匹配和更深层次的语义搜索上下文,帮助用户找到相关的电影,即使他们的查询与电影标题或描述不完全匹配。
此外,该应用还将提供 AI 驱动的推荐功能,根据用户的选择建议相似电影,以增强用户体验。
无论您是 Meilisearch 的新手,还是希望扩展您的搜索技能,本教程都将指导您构建一个尖端的电影搜索和推荐系统。让我们开始吧!
先决条件
在开始之前,请确保您已具备:
- Node.js 和 npm(包含在 Node.js 中)
- 一个正在运行的 v1.10 Meilisearch 项目——一个用于开箱即用地创建相关搜索体验的搜索引擎
- OpenAI 的 API 密钥,用于使用其嵌入模型(至少为 Tier 2 密钥以获得最佳性能)
1. 设置 Meilisearch
在本指南中,我们将使用Meilisearch Cloud,因为它是快速启动 Meilisearch 最简单的方法。您可以免费试用 14 天,无需信用卡。这也是在生产环境中运行 Meilisearch 的推荐方式。
如果您喜欢在自己的机器上运行,没问题——Meilisearch 是开源的,所以您可以本地安装它。
1.1. 创建新索引
创建一个名为 movies
的索引,并将此 movies.json 添加到其中。如有必要,请遵循入门指南。
电影数据集中的每个文档代表一部电影,并具有以下结构:
id
:每部电影的唯一标识符title
:电影的标题overview
:电影情节的简要摘要genres
:电影所属的类型数组poster
:电影海报图片的 URLrelease_date
:电影的发布日期,以 Unix 时间戳表示
1.2. 激活 AI 驱动的搜索
在 Meilisearch Cloud 控制面板中:
- 在您的项目设置中找到“实验功能”部分
- 勾选“AI 驱动的搜索”框
或者,通过 API 使用实验功能路由激活它。
1.3. 配置嵌入器
为了利用 AI 驱动搜索的力量,我们需要为我们的索引配置一个嵌入器。
当我们配置嵌入器时,我们是在告诉 Meilisearch 如何将我们的文本数据转换为嵌入——文本的数值表示,它捕捉了文本的语义含义。这允许进行语义相似性比较,使我们的搜索能够理解上下文和含义,而不仅仅是简单的关键词匹配。
本教程我们将使用 OpenAI 的模型,但 Meilisearch 兼容多种嵌入器。您可以在我们的兼容性列表中探索其他选项。不知道如何选择模型?我们为您提供了帮助,请阅读我们关于选择适合语义搜索的最佳模型的博客文章。
配置嵌入器索引设置
- 在 Cloud UI 中
- 或通过 API
curl -X PATCH 'https://ms-*****.sfo.meilisearch.io/indexes/movies/settings' -H 'Content-Type: application/json' -H 'Authorization: Bearer YOUR_MEILISEARCH_API_KEY' --data-binary '{ "embedders": { "text": { "source": "openAi", "apiKey": "YOUR_OPENAI_API_KEY", "model": "text-embedding-3-small", "documentTemplate": "A movie titled '{{doc.title}}' that released in {{ doc.release_date }}. The movie genres are: {{doc.genres}}. The storyline is about: {{doc.overview|truncatewords: 100}}" } } }'
text
是我们给嵌入器取的名称- 将
https://ms-*****.sfo.meilisearch.io
替换为您的项目 URL - 将
YOUR_MEILISEARCH_API_KEY
和YOUR_OPENAI_API_KEY
替换为您的实际密钥 model
字段指定要使用的 OpenAI 模型documentTemplate
字段自定义发送给嵌入器的数据
提示:创建简短、相关的文档模板,以获得更好的搜索结果和最佳性能。
2. 创建一个 React 应用
现在我们的 Meilisearch 后端已经配置完毕,接下来让我们使用 React 设置 AI 驱动搜索应用的前端。
2.1. 设置项目
我们将使用 Vite 模板创建一个具有基本结构的新 React 项目,为我们快速开发做好准备。
npm create vite@latest movie-search-app -- --template react cd movie-search-app npm install
2.2. 安装 Meilisearch 客户端
接下来,我们需要安装 Meilisearch JavaScript 客户端 来与我们的 Meilisearch 后端进行交互。
npm install meilisearch
2.3. 添加 Tailwind CSS
为了样式化,我们将使用 Tailwind CSS。为了简单起见,我们将使用 Tailwind CSS Play CDN,而不是将其作为依赖项安装。将以下脚本标签添加到您的 index.html 文件的 <head>
中:
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>AI-Powered movie search</title> <script src="https://cdn.tailwindcss.com"></script> </head> <body> <div id="root"></div> <script type="module" src="/src/main.jsx"></script> </body> </html>
我们还更新了 <title>
标签以反映我们应用程序的目的。
2.4. 验证设置
为确保一切设置正确,请启动开发服务器
npm run dev
您应该会看到一个 URL(通常是 https://:5173
),您可以在浏览器中查看您的应用程序。如果您看到 Vite + React 欢迎页面,那么您就准备就绪了!
完成这些步骤后,我们已经准备好一个 React 项目,用于构建我们的 AI 驱动的电影搜索界面。在接下来的部分中,我们将开始使用 Meilisearch 实现搜索功能。
3. 构建 AI 驱动的搜索体验
混合搜索结合了传统的关键词搜索和 AI 驱动的语义搜索。关键词搜索非常适合精确匹配,而语义搜索则能理解上下文。通过同时使用这两种方法,我们可以兼得两者之长——精确的结果和上下文相关的匹配。
3.1. 创建 MovieSearchService.jsx 文件
我们有一个 Meilisearch 实例正在运行,为了与之交互,我们在 src
目录中创建了一个 MovieSearchService.jsx
文件。此服务充当我们 Meilisearch 后端的客户端接口,为我们的电影数据库提供基本的搜索相关功能。
首先,我们需要将 Meilisearch 凭据添加到 .env
文件中。您可以在 Meilisearch Cloud 项目的“设置”页面找到数据库 URL(您的主机)和默认搜索 API 密钥。
VITE_MEILISEARCH_HOST=https://ms-************.sfo.meilisearch.io VITE_MEILISEARCH_API_KEY='yourSearchAPIKey'
请注意,Vite 项目中的变量必须以 VITE_
为前缀,才能在应用程序代码中访问。
现在,让我们创建 Meilisearch 客户端以连接到 Meilisearch 实例
// src/MovieSearchService.jsx import { MeiliSearch } from 'meilisearch'; const client = new MeiliSearch({ host: import.meta.env.VITE_MEILISEARCH_HOST || 'https://:7700', apiKey: import.meta.env.VITE_MEILISEARCH_API_KEY || 'yourSearchAPIKey', }); // We target the 'movies' index in our Meilisearch instance. const index = client.index('movies');
接下来,我们来创建一个执行混合搜索的函数
// src/MovieSearchService.jsx // ... existing search client configuration const hybridSearch = async (query) => { const searchResult = await index.search(query, { hybrid: { semanticRatio: 0.5, embedder: 'text', }, }); return searchResult; }; export { hybridSearch }
当在搜索查询中添加 hybrid
参数时,Meilisearch 会返回语义和全文匹配的混合结果。
semanticRatio
决定了关键词搜索和语义搜索之间的平衡,其中 1
表示完全语义搜索,0
表示完全关键词搜索。比例为 0.5
意味着结果将受到两种方法同等影响。调整此比例可以微调搜索行为,以最适合您的数据和用户需求。
embedder
指定已配置的嵌入器。在这里,我们使用在步骤 1.3 中配置的 text
嵌入器。
3.2. 创建搜索 UI 组件
首先,让我们为组件创建一个专用目录 src/components
,以保持项目随着增长而保持整洁和易于管理。
3.2.1. 搜索输入
现在,我们可以创建搜索输入组件。这将是用户与我们的 AI 驱动搜索交互的主要界面。在 src/components
目录中创建一个新文件 SearchInput.jsx
:
// src/components/SearchInput.jsx import React from 'react'; const SearchInput = ({ query, setQuery }) => { return ( <div className="relative"> <input type="text" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search for movies..." className="px-6 py-4 w-full my-2 border border-gray-300 rounded-md pr-10" /> {/* Clear button appears when there's any text in the input (query is truthy) */} {query && ( // Clicking the clear button sets the query to an empty string <button onClick={() => setQuery('')} className="absolute right-6 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700" > ✕ </button> )} </div> ); }; export default SearchInput
SearchInput
组件接受两个属性:query
和 setQuery
。输入字段的值由 query
属性控制。当用户在输入框中键入时,它会触发 onChange
事件,该事件会调用 setQuery
并传入新值。
当输入框中有任何文本时(当 query
为真时),会显示一个清除按钮 (❌)。点击此按钮会将查询设置为空字符串,从而有效地清除输入。
我们将控制父组件 App.jsx
中 query
和 setQuery
属性的状态和行为。
3.2.2. 结果卡片
现在我们有了一个搜索栏,我们需要一个组件来显示搜索结果。让我们创建一个 ResultCard
组件来展示搜索返回的每一部电影。
在 src/components
目录中创建一个新文件 ResultCard.jsx
:
// src/components/ResultCard.jsx const ResultCard = ({ url, title, overview }) => { return ( <div className='flex w-full sm:w-1/2 md:w-1/3 lg:w-1/4 p-3'> <div className='flex-1 rounded overflow-hidden shadow-lg'> <img className='w-full h-48 object-cover' src={url} alt={title} /> <div className='px-6 py-3'> <div className='font-bold text-xl mb-2 text-gray-800'> {title} </div> <div className='font-bold text-sm mb-1 text-gray-600 truncate'> {overview} </div> </div> </div> </div> ) } export default ResultCard
此组件接收 url
、title
和 overview
作为属性。该组件首先使用 url
属性显示电影海报,然后是 title
和截断的 overview
,提供每部电影的紧凑预览。
3.3. 在主 App 组件中集成搜索和 UI
让我们更新 App.jsx
组件,将所有内容连接起来,处理搜索逻辑并渲染 UI。
// src/App.jsx // Import necessary dependencies and components import { useState, useEffect } from 'react' import './App.css' import { hybridSearch } from './MovieSearchService'; import SearchInput from './components/SearchInput'; import ResultCard from './components/ResultCard' function App() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { async function performSearch() { setIsLoading(true); setError(null); try { const response = await hybridSearch(query); setResults(response.hits); } catch (err) { setError('An error occurred while searching. Please try again.'); console.error('Search error:', err); } finally { setIsLoading(false); } } performSearch(); }, [query]); return ( <div className='container w-10/12 mx-auto'> <SearchInput query={query} setQuery={setQuery} /> {isLoading && <p>Loading...</p>} {error && <p className="text-red-500">{error}</p>} <div className='flex flex-wrap'> {results.map((result) => ( <ResultCard url={result.poster} title={result.title} overview={result.overview} key={result.id} /> ))} </div> </div> ); } export default App
我们使用几个状态变量:
query
:存储当前的搜索查询results
:保存搜索结果isLoading
:指示搜索是否正在进行中error
:存储任何错误消息
组件的核心是一个 useEffect
钩子,它在查询更改时触发 performSearch
函数。此函数管理搜索过程,包括设置加载状态、调用 hybridSearch
函数、更新结果以及处理任何错误。
在渲染方法中,我们使用 SearchInput
组件构建 UI,它位于顶部,然后是加载和错误消息(如果适用)。搜索结果以 ResultCard
组件网格的形式显示,通过遍历 results
数组进行映射。
4. 构建电影推荐系统
现在我们已经实现了搜索逻辑,让我们用推荐系统来增强我们的应用程序。Meilisearch 通过其 /similar
路由提供了一个AI 驱动的相似性搜索功能。此功能允许我们检索与目标文档相似的多个文档,这对于创建电影推荐非常有用。
让我们将此功能添加到我们的 MovieSearchService.jsx
中
// src/MovieSearchService.jsx // ... existing search client configuration and hybridSearch function const searchSimilarMovies = async (id, limit = 3, embedder = 'text') => { const similarDocuments = await index.searchSimilarDocuments({id, limit, embedder }); return similarDocuments; }; export { hybridSearch, searchSimilarMovies }
索引方法searchSimilarDocuments 接收目标电影的 id
和 embedder
名称作为参数。它还可以与其他搜索参数一起使用,例如 limit
来控制推荐的数量。
4.1. 创建一个显示推荐的模态框
让我们创建一个模态框来显示电影详情和推荐。模态框允许我们显示更多信息,而无需离开搜索结果,这通过保持上下文来改善用户体验。
//src/components/MovieModal.jsx import React, { useEffect, useRef } from 'react'; import ResultCard from './ResultCard'; const MovieModal = ({ movie, similarMovies, onClose }) => { const modalRef = useRef(null); useEffect(() => { const handleEscape = (e) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', handleEscape); modalRef.current?.focus(); return () => document.removeEventListener('keydown', handleEscape); }, [onClose]); return ( <div className="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center p-4 z-50" role="dialog" aria-modal="true" aria-labelledby="modal-title"> <div ref={modalRef} className="bg-white rounded-lg p-6 max-w-4xl w-full max-h-[95vh] overflow-y-auto" tabIndex="-1"> <h2 id="modal-title" className="text-2xl font-bold mb-4">{movie.title}</h2> <div className="flex mb-4"> <div className="mr-4"> <img className='w-48 object-cover' src={movie.poster} alt={movie.title} /> </div> <div className="flex-1"> <p>{movie.overview}</p> </div> </div> <h3 className="text-xl font-semibold mb-4">Similar movies</h3> <div className='flex flex-wrap justify-between'> {similarMovies.map((similarMovie, index) => ( <ResultCard key={index} url={similarMovie.poster} title={similarMovie.title} /> ))} </div> <button onClick={onClose} className="absolute top-2 right-2 w-10 h-10 flex items-center justify-center text-gray-500 hover:text-gray-700 bg-gray-200 rounded-full" // Added background and increased size aria-label="Close modal" > <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> </svg> </button> </div> </div> ); } export default MovieModal;
此组件接受 3 个属性:
movie
:一个包含所选电影详细信息的对象。此属性用于显示模态框的主要内容。similarMovies
:一个电影对象数组,表示与主要电影相似的电影。我们重用ResultCard
组件来展示每部推荐电影。onClose
:一个函数,当模态框应该关闭时被调用。此函数在点击关闭按钮或按下“Escape”键时触发。
useRef
和 useEffect
钩子用于管理焦点和键盘交互,这对于可访问性至关重要。aria-*
属性进一步增强了模态框对屏幕阅读器的可访问性。
4.2. 实现模态功能
让我们更新主 App.jsx
组件,这样我们就可以在点击电影时调用相似电影函数并打开模态框。
首先,让我们导入之前创建的模态框和 searchSimilarMovies 函数
// src/App.jsx // ... existing imports import MovieModal from './components/MovieModal'; import { hybridSearch, searchSimilarMovies } from './MovieSearchService';
使用 useState
为 selectedMovie
添加状态
// src/App.jsx // ... existing state ... const [selectedMovie, setSelectedMovie] = useState(null);
这将创建一个状态变量来存储当前选定的电影,最初设置为 null,以及一个用于更新它的函数。
接下来,让我们创建 2 个函数:
handleMovieClick
用于更新selectedMovie
状态为被点击的电影,使模态框能够显示所选电影的详细信息closeModal
用于将selectedMovie
状态重置为null
const handleMovieClick = (movie) => { setSelectedMovie(movie); }; const closeModal = () => { setSelectedMovie(null); };
现在,我们可以更新 ResultCard
组件,使其在点击时触发 handleMovieClick
函数,并将 MovieModal
组件添加到 JSX 中,当电影被选中时有条件地渲染它。
// src/ ResultCard.jsx const ResultCard = ({ url, title, overview, onClick }) => { return ( <div className='flex w-full sm:w-1/2 md:w-1/3 lg:w-1/4 p-3' onClick={onClick}> <div className='flex-1 rounded overflow-hidden shadow-lg'> <img className='w-full h-48 object-cover' src={url} alt={title} /> <div className='px-6 py-3'> <div className='font-bold text-xl mb-2 text-gray-800'> {title} </div> <div className='font-bold text-sm mb-1 text-gray-600 truncate'> {overview} </div> </div> </div> </div> ) } export default ResultCard
// src/App.jsx // ... in the return statement <div className='flex flex-wrap'> {results.map((result) => ( <ResultCard url={result.poster} title={result.title} overview={result.overview} key={result.id} onClick={() => handleMovieClick(result)} /> ))} </div> {selectedMovie && ( <MovieModal movie={selectedMovie} onClose={closeModal} /> )} </div>
让我们创建一个新的状态变量 similarMovies
(最初为空数组)及其设置器函数 setSimilarMovies
,用于存储和更新与所选电影相似的电影列表。
const [similarMovies, setSimilarMovies] = useState([]);
现在,我们需要更新 handleMovieClick
函数,使其也获取相似电影,并使用结果更新 similarMovies
状态,我们将其传递给模态框。
const handleMovieClick = async (movie) => { setSelectedMovie(movie); try { const similar = await searchSimilarMovies(movie.id); setSimilarMovies(similar.hits); } catch (err) { // error handling for the API call. console.error('Error fetching similar movies:', err); // Avoid broken content by setting `similarMovies` to an empty array setSimilarMovies([]); } }; // ... existing code ... <MovieModal movie={selectedMovie} similarMovies={similarMovies} onClose={closeModal} />
最后,我们需要更新 closeModal
以重置 similarMovies
状态变量
const closeModal = () => { setSelectedMovie(null); setSimilarMovies([]); };
5. 运行应用程序
启动开发服务器,尽情享受吧!
npm run dev
我们的应用程序应该看起来像这样:
结论
恭喜!您已成功使用 Meilisearch 和 React 构建了一个 AI 驱动的电影搜索和推荐系统。让我们回顾一下您所完成的工作:
- 设置了一个 Meilisearch 项目并将其配置为 AI 驱动的搜索
- 实现了结合关键词和语义搜索能力的混合搜索
- 创建了一个用于搜索电影的 React UI
- 集成了 Meilisearch 的相似性搜索功能,用于电影推荐
下一步是什么?
为了改善用户体验并实现更精确的搜索,您可以设置一个分面搜索界面,允许用户按类型筛选电影或按发布日期排序。
当您准备好使用自己的数据构建应用程序时,请务必先配置您的索引设置以遵循最佳实践。这将优化索引性能和搜索相关性。
当您准备好用自己的数据构建应用程序时,请务必先配置您的索引设置,以遵循最佳实践。这将优化索引性能和搜索相关性,确保您的应用程序运行流畅并提供准确的结果。
Meilisearch 是一个开源搜索引擎,具有直观的开发者体验,可用于构建面向用户的搜索。您可以自托管它,或者通过Meilisearch Cloud获得优质体验。
要了解更多 Meilisearch 相关信息,您可以加入 Discord 社区或订阅新闻简报。您可以通过查看路线图和参与产品讨论来了解更多关于该产品的信息。