AI 驅動的混合搜尋目前處於封閉測試階段。 加入候補名單以取得搶先體驗!

前往首頁Meilisearch 的標誌
返回文章
2024 年 9 月 24 日

如何在 React 應用程式中加入 AI 驅動的搜尋功能

使用 Meilisearch 的 AI 驅動搜尋功能建立 React 電影搜尋和推薦應用程式。

Carolina Ferreira
Carolina Ferreira開發者推廣工程師 @ Meilisearch@CarolainFG
How to add AI-powered search to a React app

在本指南中,我們將引導您建構一個 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:電影海報圖像的 URL
  • release_date:電影的發行日期,以 Unix 時間戳記表示

1.2. 啟用 AI 驅動的搜尋

在 Meilisearch Cloud 儀表板中

  • 在您的專案設定中找到「實驗性功能」區段
  • 勾選「AI 驅動的搜尋」方塊

AI-powered search checkbox checked

或者,使用 experimental-features 路徑透過 API 啟用它。

1.3. 設定嵌入器

為了利用 AI 驅動搜尋的強大功能,我們需要為我們的索引設定嵌入器。

當我們設定嵌入器時,我們是在告訴 Meilisearch 如何將我們的文字資料轉換為嵌入–捕捉文字語義含義的數字表示形式。這允許進行語義相似性比較,使我們的搜尋能夠理解超越簡單關鍵字匹配的上下文和含義。

在本教學中,我們將使用 OpenAI 的模型,但 Meilisearch 與各種嵌入器相容。您可以在我們的相容性清單中探索其他選項。不知道該選擇哪個模型?我們已為您準備好,請閱讀我們關於為語義搜尋選擇最佳模型的部落格文章。

設定 嵌入器索引設定

  • 在 Cloud UI 中

Embedder configuration in 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_KEYYOUR_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"
        >
          &#x2715;
        </button>
      )}
    </div>
  );
};

export default SearchInput

SearchInput 元件接收兩個 props:querysetQuery。輸入欄位的值由 query prop 控制。當使用者在輸入框中輸入時,會觸發 onChange 事件,該事件會使用新值呼叫 setQuery

當輸入框中有任何文字時(當 query 為真值時),會出現一個清除按鈕(❌)。點擊此按鈕會將查詢設定為空字串,從而有效地清除輸入。

我們將在父元件 App.jsx 中控制 querysetQuery 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

此元件接收 urltitleoverview 作為 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 索引方法將目標電影的 idembedder 名稱作為參數。它也可以與其他搜尋參數(例如 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」鍵時,會觸發此函數。

useRefuseEffect hook 用於管理焦點和鍵盤互動,這對於無障礙功能至關重要。 aria-* 屬性進一步增強了螢幕閱讀器的彈出視窗無障礙功能。

4.2. 實作彈出視窗功能

讓我們更新主要的 App.jsx 元件,以便我們可以在點擊電影時呼叫相似電影函數並開啟彈出視窗。

首先,讓我們匯入彈出視窗和我們先前建立的 searchSimilarMovies 函數

// src/App.jsx
// ... existing imports
import MovieModal from './components/MovieModal';
import { hybridSearch, searchSimilarMovies } from './MovieSearchService';

使用 useStateselectedMovie 新增狀態

// 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

我們的應用程式應該看起來像這樣

Typing "ace ventura" in the search bar getting results with each keystroke, clicking on the movie cat of "Ace Ventura: pet detective" and a modal opens with the movie poster and the full overview and 3 similar movies: "Ace Ventura Jr: pet detective", "Ace Ventura: when the nature calls", and "The Animal".

結論

恭喜!您已使用 Meilisearch 和 React 成功建立了一個 AI 驅動的電影搜尋和推薦系統。讓我們回顧一下您已完成的事項

  1. 設定 Meilisearch 專案並將其配置為 AI 驅動的搜尋
  2. 實作結合關鍵字和語意搜尋功能的混合搜尋
  3. 建立用於搜尋電影的 React UI
  4. 整合 Meilisearch 的相似性搜尋以進行電影推薦

下一步是什麼?

為了改善使用者體驗並允許更精確的搜尋,您可以設定一個分面搜尋介面,以允許使用者按類型篩選電影或按發行日期排序。

當您準備好使用自己的資料建置應用程式時,請務必先設定您的索引設定,以遵循最佳做法。這將最佳化索引效能和搜尋相關性。

當您準備好使用自己的資料建置應用程式時,請務必先設定您的索引設定,以遵循最佳做法。這將最佳化索引效能和搜尋相關性,確保您的應用程式順利執行並提供準確的結果。


Meilisearch 是一個開源搜尋引擎,具有直觀的開發人員體驗,可以建置面向使用者的搜尋。您可以自行託管或透過Meilisearch Cloud 獲得優質體驗。

如需更多關於 Meilisearch 的資訊,您可以加入Discord 上的社群或訂閱電子報。您可以透過查看路線圖並參與產品討論來深入了解該產品。

Software Engineering Predictive Search: A Complete Guide

軟體工程預測性搜尋:完整指南

了解如何在您的軟體應用程式中實作預測性搜尋。探索關鍵概念、最佳化技術和真實世界的範例,以增強使用者體驗。

Ilia Markov
Ilia Markov2024 年 12 月 11 日
Beyond the Hype: Practical AI Search Strategies That Deliver ROI

超越炒作:提供投資報酬率的實用 AI 搜尋策略

了解如何實作驅動實際投資報酬率的 AI 驅動搜尋。透過有關預算、功能選擇和衡量成功與否的實用策略來擺脫炒作。

Ilia Markov
Ilia Markov2024 年 12 月 2 日
Searching across multiple languages

跨多種語言搜尋

了解實作進階多語言搜尋有多麼容易,並為您的使用者提供他們應得的無縫、相關的結果,而不受語言限制。

Quentin de Quelen
Quentin de Quelen2024 年 9 月 26 日