如何在 React 应用中添加 AI 驱动的搜索
使用 Meilisearch 的 AI 驱动搜索构建一个 React 电影搜索和推荐应用。
在本指南中,我们将引导您构建一个 AI 驱动的电影推荐应用。与传统的关键词搜索不同,AI 驱动的搜索使用机器学习来根据查询背后的上下文和含义返回结果。
您将使用 Meilisearch 和 OpenAI 的嵌入模型构建一个搜索和推荐系统。该应用将提供即时搜索体验,结合精确的关键词匹配和语义搜索的更深层次的上下文,帮助用户找到相关的电影,即使他们的查询与电影标题或描述不完全匹配。
此外,该应用还将提供 AI 驱动的推荐,根据用户的选择推荐类似的电影,以增强他们的体验。
无论您是 Meilisearch 的新手还是正在扩展您的搜索技能,本教程都将指导您构建一个前沿的电影搜索和推荐系统。让我们开始吧!
先决条件
在开始之前,请确保您已具备
- Node.js 和 npm(包含在 Node.js 中)
- 一个正在运行的 v1.10 Meilisearch 项目——一个开箱即用即可创建相关搜索体验的搜索引擎
- 来自 OpenAI 的 API 密钥,以使用其嵌入模型(至少是第 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 驱动的搜索”框
或者,使用 experimental-features 路由通过 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://127.0.0.1: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://127.0.0.1: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
为真值时),会出现一个清除按钮 (❌)。点击此按钮会将 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 上的社区或订阅 新闻通讯。您可以通过查看 路线图 并参与 产品讨论 来了解有关该产品的更多信息。