用MongoDB Atlas和MERN添加全文搜索的详细指南

222 阅读4分钟

简介

搜索功能可以帮助用户尽快找到他们要找的东西。 虽然传统的搜索会返回完全匹配的结果,但全文搜索在查询数据时提供了额外的灵活性 ,因为它返回的结果包含了查询中的一些或全部单词。

值得庆幸的是,某些管理型数据库供应商,如MongoDB,提供了一个内置的全文搜索功能。在本指南中,我们将加强我们在本迷你MERN系列的第一部分--开始使用MERN堆栈中创建的博客应用程序。用MongoDB Atlas建立一个博客-- 通过Atlas搜索添加全文本搜索。

由于我们在第一篇指南中在Koyeb上部署了应用程序,我们将受益于该平台内置的持续部署。我们所要做的就是推送我们的提交,Koyeb会自动构建和部署我们应用程序的最新版本。

要求

要成功遵循并完成MERN系列的第二部分,也是最后一部分,你需要:

操作步骤

要成功遵循本指南,你需要遵循以下步骤:

  1. 设置Atlas搜索
    • 上传样本数据
    • 创建一个搜索索引
    • 建立聚合管道来过滤帖子:使用searchsearch、limit和$project
  2. 增强搜索API
  3. 在React应用程序中添加搜索UI
  4. 在Koyeb上部署,以实现内置的持续部署

设置Atlas搜索

上传样本数据

为了利用Atlas的搜索功能,我们首先需要为我们的博客提供更好的样本数据。幸运的是,有几个在线资源可以做到这一点。我决定使用在Data.world平台上分享的政府博文。
我对样本数据进行了清理,所以它可以无缝集成到我们的网络应用中。你可以不从网站上下载JSON文件,而直接从我的GitHub上获取。

上传这些数据的第一步是使用Mongo Atlas仪表盘创建一个新的数据库和集合。从仪表板上:

  • 转到你的数据库部署
  • 点击 "浏览集合 "标签
  • 点击左边的按钮 "创建数据库"
  • 输入新的数据库和集合名称。例如,我把我的集合命名为posts

为了从你的本地设备上传JSON集合,你需要安装软件包 "mongodb-database-tools"。下面的文档提供了最常见的操作系统的简单步骤。

在终端,运行以下命令:

mongoimport --uri mongodb+srv://USERNAME:PASSWORD@CLUSTERNAME.<>.mongodb.net/DB_NAME --collection COLLECTION_NAME --type json --file SAMPLE_DATA_PATH --jsonArray

请确保用以下内容代替:

  • 连接字符串为USERNAMEPASSWORD 为你的个人用户凭证。

    注意:你可以通过点击你的MongoDB集群名称旁边的 "连接 "按钮来检索你的连接字符串。选择 "连接你的应用程序 "选项,并将你的数据库连接字符串复制到一个安全的地方供以后使用。

  • DB_NAME 和 ,用你创建的数据库的名称和 作为集合名称COLLECTION_NAME post

  • SAMPLE_DATA_PATH 用你的笔记本电脑上的index.json文件的路径

如果一切设置正确,你应该看到一个成功的消息,如10 document(s) imported successfully. 0 document(s) failed to import. 再次前往MongoDB Atlas仪表板,打开集合,你应该看到我们博客的新样本数据

创建一个搜索索引

博客搜索功能将在数据库中查询文章标题中的关键词。这意味着我们要对标题进行全文搜索,并启用自动完成的操作。

为此,我们需要在标题字段上创建一个全文搜索索引。从你的集群的MongoDB仪表板上:

  • 点击 "搜索 "标签
  • 点击 "创建搜索索引"
  • 选择JSON编辑器和 "下一步"

用下面的代码替换默认的定义:

{
  "mappings": {
    "dynamic": false,
    "fields": {
      "title": [
        {
          "foldDiacritics": true,
          "maxGrams": 15,
          "minGrams": 2,
          "tokenization": "edgeGram",
          "type": "autocomplete"
        }
      ]
    }
  }
}

你可以看到,我们正在为我们的集合的 "标题 "字段创建一个 "自动完成 "类型的索引。

在自动完成数据类型中,有几个配置选项可用,如标记化策略和双音符折叠。完整的解释可以在官方的Atlas搜索文档中找到,但在本教程中,我们使用的是以下选项:

  • foldDiactrics:当为真时,双音符被包含在索引中。
  • maxGramsminGrams :分别为每个索引序列的最大和最小字符数。
  • tokenization: edgeGram标记器将来自文本输入的左侧或 "边缘 "的输入标记为给定大小的n-grams。

为你的搜索索引命名,以便于参考,并选择你的数据库和收藏集,然后点击 "下一步"。

在你审查了你新创建的索引之后,点击 "创建搜索索引",你将被转到 "搜索 "标签。
索引的创建大约需要一分钟,当它准备好之后,其状态将自动变为 "活动"。

建立聚合管道来过滤帖子:使用searchsearch、limit和$project

在你的MongoDB集合中,点击 "聚合 "标签。我们将创建并测试你的查询。聚合用户界面的奇妙之处在于,对于管道中的每一个阶段,他们都会直接从帖子集合中向你展示结果:

MongoDB Dashboard

我们的聚合管道由3个阶段/参数组成:

  1. $search:我们定义要使用的索引、自动完成选项、字段和查询本身。
  2. $limit:我们可以决定限制结果的数量。
  3. $project:为每个帖子条目返回什么样的字段。

每当我们添加一个阶段,聚合管道将运行并实时返回结果。

使用$search

第一步是使用$searchsearch 操作符让你进行全文搜索。在下拉菜单中找到并点击它。一个基本的语法结构将显示出来供你填充:

{
    'index': 'title_autocomplete', 
    'autocomplete': {
      'query': 'open gov', 
      'path': 'title',
    }
  }
  • 将我们刚刚创建的索引的名称分配给index
  • 由于我们使用的是自动完成选项,用autocomplete 替换text
  • query 现在可以作为一个搜索查询的例子。
  • path 是要搜索的字段。

一旦完成,你应该看到用户界面自动查询集合,并返回满足上述条件的博客文章的列表。

使用$limit

使用$limit ,我们可以简单地只返回结果的一个子集,即只返回顶部的文章。在聚合用户界面中选择$limit ,然后输入5为例。
你应该看到帖子的数量被限制在5个!

使用$project

使用$project ,我们可以告诉查询返回一个帖子字段的子集。在下拉菜单中选择$project ,并添加以下代码:

'_id': 1, 
'title': 1, 
'author': 1, 
'createdAt': 1,

值 "1 "告诉管道,我们想包括特定的字段。根据查询,我们想返回post _id,title,authorcreatedAt 。这些是我们需要在博客主页上显示的唯一4个值。

为了有趣的练习,你可以去调整每个阶段的值,看看它们是如何影响最终结果的。

最后,Atlas UI提供了一个方便的功能,可以用几种语言的正确语法快速导出聚合,包括Node.js。
在 "聚合 "标签和 "整理 "按钮旁边,点击导出管道,选择Node.js。这里是产生的聚合代码:

 {
          '$search': {
            'index': 'title_autocomplete', 
            'autocomplete': {
              'query': 'open gov', 
              'path': 'title',
            }
          }
        }, {
          '$limit': 5
        }, {
          '$project': {
            '_id': 1, 
            'title': 1, 
            'author': 1, 
            'createdAt': 1,
          }
        }

请注意,我们仍然有一个硬编码的 "查询 "的 "open gov "值。在我们的后端端点中,我们将用一个来自我们在客户端建立的搜索栏的动态搜索值来代替它。

增强搜索API端点

返回到服务器代码。我们目前有一个GET端点/api/blogs ,用于查询数据库并返回全部的博客文章列表。我们将通过以下方式来扩展其功能:

  • 添加一个搜索查询参数,其中包含客户端搜索栏的查询内容
  • 当搜索参数存在时,添加额外的逻辑

运行聚合

当用户到达博客主页时,搜索栏是空的,因此第一次API调用不会发送任何搜索参数,端点将返回整个文章列表,
在搜索栏上输入将触发后续的API调用,包括额外的search params。在我们的例子中,端点不会返回整个帖子列表,而是使用mongoose的聚合功能来搜索正确的帖子。

下面是/routes/posts.js 中第一个API端点的新代码:

/* GET posts */
router.get('/', async (req, res, next) => {
  // We look for a query parameter "search"
  const { search } = req.query;
  let posts;
  if (search) { // If search exists, the user typed in the search bar
    posts = await Post.aggregate(
      [
        {
          '$search': {
            'index': 'title_autocomplete', 
            'autocomplete': {
              'query': search, // noticed we assign a dynamic value to "query"
              'path': 'title',
            }
          }
        }, {
          '$limit': 5
        }, {
          '$project': {
            '_id': 1, 
            'title': 1, 
            'author': 1, 
            'createdAt': 1,
          }
        }
      ]
    );
  } else { // The search is empty so the value of "search" is undefined
    posts = await Post.find().sort({ createdAt: 'desc' });
  }

  return res.status(200).json({
    statusCode: 200,
    message: 'Fetched posts',
    data: { posts },
  });
});

通过这一改变,我们现在已经为我们的Node服务器添加了搜索功能。

在React应用程序中添加搜索UI

本教程的倒数第二步是将搜索栏添加到我们客户端的主页上。
由于我们使用的是react-boostrap ,我们需要做的就是导入FormFormControl 组件。然后,我们要在 "onChange "事件上附加一个动作监听器,以便在用户在搜索栏中输入任何内容时调用我们的后台。

/client/src/pages 内打开home.js ,添加以下代码:

import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import Container from 'react-bootstrap/Container';
import ListGroup from 'react-bootstrap/ListGroup';
import Image from 'react-bootstrap/Image';
import http from '../lib/http';
import formatDate from '../lib/formatDate';
// Here we import the new components for the seach bar
import Form from 'react-bootstrap/Form';
import FormControl from 'react-bootstrap/FormControl';

const Home = () => {
  const [posts, setPosts] = useState([]); 
  useEffect(() => {
    async function fetchData() {
      const { data } = await http.get('/api/posts');
      setPosts(data.data.posts);
    }
    fetchData();
  }, []);
  /* We are creating a new function that calls the API endpoint
     and passing the search value as a query parameter
  */
  const searchPost = async (e) => {
    const searchValue = e.target.value;
    const { data } = await http.get(`/api/posts?search=${searchValue}`);
    // The subset of posts is added to the state that will trigger a re-render of the UI
    setPosts(data.data.posts); 
  };

  return (
    <>
      <Container className="my-5" style={{ maxWidth: '800px' }}>
        <Image
          src="avatar.jpeg"
          width="150"
          style={{ borderRadius: '50%' }}
          className="d-block mx-auto img-fluid"
        />
        <h2 className="text-center">Welcome to the Digital Marketing blog</h2>
        // Let's add the search bar under the subheader
        <Form>
          <FormControl
            type="search"
            placeholder="Search"
            className="me-5"
            aria-label="Search"
            onChange={searchPost} // onChange will trigger "search post"
          />
        </Form>
      </Container>
      <Container style={{ maxWidth: '800px' }}>
        <ListGroup variant="flush" as="ol">
          {
            posts.map((post) => {
              return (
                <ListGroup.Item key={post._id}> 
                  <div className="fw-bold h3">
                    <Link to={`/posts/${post._id}`} style={{ textDecoration: 'none' }}>{post.title}</Link>
                  </div>
                  <div>{post.author} - <span className="text-secondary">{formatDate(post.createdAt)}</span></div>
                </ListGroup.Item>
              );
            })
          }
        </ListGroup>
      </Container>
    </>
  );
};

export default Home;

感谢这段新的代码,我们只用了10多行代码就为我们的博客添加了搜索功能!

将帖子内容渲染成HTML

客户端中的最后一个代码变化是在post.js 页面中的一个纯粹的UI增强,它处理了一个帖子内容的新格式。当我们从data.world导入数据集时,内容中包含了一些符号的HTML代码,如逗号、分号等。
打开post.js ,在/client/src/pages ,修改以下一行代码:

from
 <div className="h4 mt-5">{post.content}</div>
to
 <div className="h4 mt-5" dangerouslySetInnerHTML={{__html: post.content}}></div>

通过注入HTML,我们将以正确的格式显示内容中使用的符号。

在Koyeb上部署,实现内置的持续部署

最后,你只需用git提交你的修改,Koyeb将触发重新部署具有新搜索功能的应用程序

MERN blog with full-text search

结语

恭喜你完成了MERN教程的第二部分,也是最后一部分。

在第二部分中,我们成功地对我们的帖子集进行了索引,以利用Atlas搜索功能。我们还创建了一个带有自动完成功能的聚合管道,为你的博客添加搜索功能。在创建管道的过程中,我们使用了$search,$limit$project 来创建一个满足我们要求的基本聚合。 然后,我们调整了服务器和客户端的代码,允许用户查询博客的数据库并按标题搜索文章 。

最后一步是部署最新的代码,这很容易,因为它再一次由Koyeb处理。Koyeb让开发者的工作变得极其简单。只要提交你的代码,Koyeb就会触发重新部署。