前往主页Meilisearch的Logo
返回文章
2024年9月24日

如何将AI驱动的搜索添加到React应用

使用Meilisearch的AI驱动搜索,构建一个React电影搜索和推荐应用。

Carolina Ferreira
Carolina FerreiraMeilisearch开发者布道师@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密钥,用于使用其嵌入模型(为获得最佳性能,至少需要二级密钥)

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兼容多种嵌入器。您可以在我们的兼容性列表中探索其他选项。不知道选择哪个模型?我们为您提供了帮助,请阅读我们关于选择适合语义搜索的模型的博客文章

配置嵌入器索引设置

  • 在云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 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(最初为空数组)及其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社区或订阅我们的通讯。您可以通过查看产品路线图并参与产品讨论来了解更多关于产品的信息。

🚀 How we're making AI work at Meilisearch

🚀 我们如何在Meilisearch中让AI发挥作用

还在为如何让AI在您的公司真正发挥价值而苦恼吗?了解我们如何在Meilisearch将零散的AI使用转化为系统性成功,并提供一个您可以今天就实施的实用框架。

Gillian McAuliffe
Gillian McAuliffe2025年5月13日
Why you shouldn't use vector databases for RAG

为什么你不应该将向量数据库用于RAG

关于构建更好的检索增强生成系统的逆向思考。

Thomas Payet
Thomas Payet2025年4月30日
AI-powered search: What you need to know [2025]

AI驱动的搜索:你需要知道的一切 [2025]

为您的SaaS业务解锁AI驱动搜索的力量。了解关键功能、预算技巧和实施策略,以提升用户参与度。

Ilia Markov
Ilia Markov2025年4月17日