如何在 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
元件接收兩個 props:query
和 setQuery
。輸入欄位的值由 query
prop 控制。當使用者在輸入框中輸入時,會觸發 onChange
事件,該事件會使用新值呼叫 setQuery
。
當輸入框中有任何文字時(當 query
為真值時),會出現一個清除按鈕(❌)。點擊此按鈕會將查詢設定為空字串,從而有效地清除輸入。
我們將在父元件 App.jsx
中控制 query
和 setQuery
props 的狀態和行為。
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
作為 props。該元件使用 url
prop 顯示電影海報,然後是 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
hook,每當查詢變更時,它會觸發一個 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 個 props
movie
:一個包含所選電影詳細資訊的物件。此 prop 用於顯示彈出視窗的主要內容。similarMovies
:一個電影物件陣列,表示與主要電影相似的電影。我們重複使用ResultCard
元件來展示每部推薦的電影。onClose
:一個在應關閉彈出視窗時呼叫的函數。當點擊關閉按鈕或按下「Escape」鍵時,會觸發此函數。
useRef
和 useEffect
hook 用於管理焦點和鍵盤互動,這對於無障礙功能至關重要。 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 上的社群或訂閱電子報。您可以透過查看路線圖並參與產品討論來深入了解該產品。