AI 驱动的混合搜索目前处于封闭测试阶段。 加入候补名单 以获得早期访问权限!

返回主页Meilisearch 的标志
返回文章
2024 年 9 月 24 日

如何在 React 应用中添加 AI 驱动的搜索

使用 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 密钥,以使用其嵌入模型(至少是第 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 组件接受两个属性:querysetQuery。输入字段的值由 query 属性控制。当用户在输入框中键入内容时,它会触发 onChange 事件,该事件会使用新值调用 setQuery

当输入框中有任何文本时(即 query 为真值时),会出现一个清除按钮 (❌)。点击此按钮会将 query 设置为空字符串,从而有效地清除输入框。

我们将在父组件 App.jsx 中控制 querysetQuery 属性的状态和行为。

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 作为属性。该组件使用 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 索引方法将目标电影的 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 个属性

  • movie: 一个包含所选电影详细信息的对象。此属性用于显示模态框的主要内容。
  • similarMovies: 一个电影对象数组,表示与主电影相似的电影。我们重用 ResultCard 组件来展示每部推荐的电影。
  • onClose: 一个在应关闭模态框时调用的函数。当点击关闭按钮或按下“Escape”键时,会触发此函数。

useRefuseEffect 钩子用于管理焦点和键盘交互,这对于可访问性至关重要。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 日