Meilisearch v1.14 发布啦 ✨ 在我们的博客上阅读更多信息

转到主页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 密钥,用于使用其嵌入模型(至少是 Tier 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

或者,通过 API 激活它,使用 experimental-features 路由

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(通常是 http://localhost: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 || 'http://localhost: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 hooks 用于管理焦点和键盘交互,这对于可访问性至关重要。 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(最初是一个空数组)及其 setter 函数 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 上的社区或订阅新闻通讯。您可以通过查看 路线图 并参与 产品讨论来了解有关该产品的更多信息。

The 10 best AI enterprise search tools and platforms [2025]

10 大 AI 企业搜索工具和平台 [2025 年]

了解当今市场上十大最佳 AI 企业搜索工具。了解它们在功能、能力、用例、定价等方面的比较。

Ilia Markov
Ilia Markov2025 年 4 月 15 日
Building the future of search with Meilisearch AI

使用 Meilisearch AI 构建搜索的未来

我们正在通过 Meilisearch AI 改变开发者构建搜索的方式。不再有复杂的基础架构——只有开箱即用的强大、智能的搜索。

Quentin de Quelen
Quentin de Quelen2025 年 3 月 24 日
What are vector embeddings? A complete guide [2025]

什么是向量嵌入?完整指南 [2025 年]

了解您需要了解的有关向量嵌入的所有信息。了解它们的含义、不同类型、如何创建它们、应用等等。

Carolina Ferreira
Carolina Ferreira2025 年 3 月 20 日