人们说数据是新的黄金。但要在短时间内通过庞大的数据集来满足消费者的需求,对于后端开发人员来说仍然是一个难题。

传统的数据库查询方法往往无法快速获得准确的搜索结果。不过幸运的是,Elasticsearch为这个问题提供了解决方案。

在这篇文章中,我将向您介绍如何使用Elasticsearch来提升数据库搜索和分析的功能,同时还能保持效率。

进行本教程学习之前,需要准备以下条件:

  • 一个Node.js开发环境

  • 基本的后端开发知识

好了,让我们开始吧。但首先,什么是Elasticsearch呢?

目录

什么是Elasticsearch?

Elasticsearch是由Apache开发的搜索引擎,它能够对单词和短语进行索引处理,从而提供先进的文本搜索及向量搜索功能。此外,它还具备搜索分析以及自动补全等功能。

需要注意的是,尽管Elasticsearch提供了索引功能,但它本身并不属于数据库——这一点与常见的数据库是相同的。

在实际情况中,还有其他一些流行的替代工具可供选择,例如AlgoliaOpenSearch以及MeiliSearch等。

Elasticsearch核心术语

在这一节中,我们将介绍一些在Elasticsearch中常用的术语。为了帮助您更好地理解这些概念,我会引用一些常见的数据库相关术语来进行说明。

  • 索引:索引是用于存储数据的区域,它可以被视为Elasticsearch中的“数据库”。与传统的数据库一样,索引也具有唯一性等特性。

  • 文档:文档是索引中存储的最小信息单位。它的结构与基于MongoDB的文档相同,也类似于SQL数据库中的行。

  • 映射规则:映射规则定义了文档和字段在Elasticsearch索引中的存储方式。

  • 得分:Elasticsearch会生成得分值,用来表示搜索查询与存储在索引中的数据之间的相关性程度。

  • 分析器:当数据被发送到Elasticsearch引擎进行索引处理时,首先会经过分析器的处理。这一过程通过过滤器和支持分词的工具来实现。

  • 分词工具:这些工具会将输入到Elasticsearch引擎中的非结构化数据转换成结构化的数据格式,以便进一步进行处理和存储。

  • 聚合器:聚合器能够对索引中存储的数据进行深入分析,从而生成有用的数据洞察。这是Elasticsearch引擎的一大优势,MongoDB的聚合器也提供了类似的功能。

  • 过滤器:过滤器是一组用于修改分词结果指令,这些指令可能包括删除填充字符、调整大小写等操作。

  • 批量索引:批量索引是指一次性对多个文档进行索引处理。当需要对已经存在内容的数据库进行索引时,通常会使用这种方式。

如何设置Elasticsearch

在本教程中,我们将在本地机器上使用Elasticsearch的可安装软件。当然,也存在在线托管版本的Elasticsearch,使用它们也同样非常方便。

这里提供了关于如何在Windows系统上设置Elasticsearch的详细说明。对于非Windows用户来说,也可以在Linux/Mac OS系统中安装Elasticsearch,或者使用Docker来部署它。

注意:对于Windows用户来说,请确保以管理员身份运行Elasticsearch程序,这样才能避免安装过程中出现错误。

安装成功后,你可以通过访问localhost:9200来测试Elasticsearch是否能够正常工作。这个地址是Elasticsearch的默认本地端点。此时你会在屏幕上看到类似下图所示的成功提示信息:

elastic search localhost homepage

完成这些步骤后,我们就可以继续设置我们的项目,并将ElasticSearch集成到演示项目中去了。

如何设置演示项目

为了方便进行本教程的学习,我们将使用一个用Node Express JS构建的、已经开发完成的论坛后端应用程序。项目的链接如下。

要启动并运行这个项目,请克隆相应的代码包,然后运行

npm start

在本教程中,MySQL将被作为默认数据库使用。接下来我们进入下一节内容吧。

如何在你的项目中设置Elasticsearch

现有的演示项目是一个论坛后端应用,它允许用户发布文本内容,并通过分类主题来开展讨论。

Elasticsearch能够帮助用户快速筛选这些帖子和讨论主题,从而利用特定的关键词准确找到所需信息。这种方式比使用传统的数据库搜索查询更为高效,因为后者往往操作起来比较繁琐。

要设置Elasticsearch,请首先安装npm包中的Elasticsearch插件。具体操作方法是在你的项目目录中运行以下命令:

npm install @elastic/elasticsearch

安装成功后,创建一个config.js文件,在其中配置用于连接Elasticsearch应用的各项参数。

const { Client } = require('@elastic/elasticsearch');

const esClient = new Client({
  node: 'http://localhost:9200',
  auth: {
    username: process.env.ELASTICSEARCH_USERNAME,
    password: process.env.ELASTICSEARCH_PASSWORD
  },
  maxRetries: 5,
  requestTimeout: 60000,
  tls: {
    rejectUnauthorized: process.env.NODE_ENV !== 'development'
  }
});

module.exports = esClient;

要在后端应用程序中访问并使用Elasticsearch的功能,您需要设置和配置相应的驱动程序。具体细节在上述配置文件中有所说明。

如前所述,Elasticsearch运行在localhost:9200端口上。因此,您的Elasticsearch节点会连接到这个本地端口。在线托管的Elasticsearch节点在类似情况下也能正常工作。

在配置文件中,您还需要提供访问Elasticsearch所需的认证信息。用户名和密码需要通过Auth对象进行设置。如果您是在本地运行Elasticsearch,除非启用了安全功能,否则可能不需要进行认证。

在这里,MaxRetries表示尝试访问Elasticsearch时允许的最大失败次数。我们将其设置为5次。而requestTimeout则表示如果请求在指定时间内未被处理,系统将自动终止该请求所花费的时间(单位为毫秒)。

配置文件设置完成后,在后端应用程序启动时,您需要导入这些配置信息并初始化Elasticsearch客户端。

如何在Elasticsearch中操作索引

在开始充分利用Elasticsearch的功能之前,我们首先需要在项目后端对其进行定制,以便使其能够满足我们的需求。这包括在Elasticsearch引擎中创建一个索引,用于存储所有提交到后端应用程序的数据。

const esClient = require('./config');

const setupIndex = async () => {
  try {
    const indexExists = await esClient.indices.exists({
      index: INDEX_NAME
    });

    if (indexExists) {
      console.log(`索引 "${INDEX_NAME}" 已经存在`);
      return;
    }

    await esClientindices.create({
      index: INDEX_NAME,
      ...indexMapping
    });

    console.log(`索引 "${INDEX_NAME}" 已创建`);
  } catch (err) {
    console.error(err);
    throw err;
  }
};

上述代码演示了如何创建一个新的索引。首先,需要调用setupIndex()函数,在该函数中指定索引的名称。Elasticsearch会检查该名称是否已经存在。

如果索引名称已经存在,函数会终止执行(以避免重复创建相同的索引);但如果名称不存在,系统就会使用该名称创建一个新的索引,并同时设置相应的索引映射规则(我们稍后会进一步讨论这些规则)。

成功创建索引后,您会在应用程序的控制台中看到相应的成功提示信息。

如何删除索引

过了一段时间后,某个索引可能就不再需要了,这时您就可以将其从Elasticsearch中删除。

const deleteIndex = async () => {
try {
await esClientindices.delete({ index: INDEX_NAME });
console.log(`${INDEX_NAME} 已被删除`);
} catch (err) {
console.error("删除索引时出现错误:", err);
}
};

如何删除索引中的帖子

有时,帖子会被删除或修改。此外,用户也可能会被封禁,在这种情况下,您就需要将他们的内容从存储的数据库中移除。

在这种情况下,您需要确保这些内容真正被删除——也就是说,既要从数据库中删除,也要从Elasticsearch的索引中删除。

const deletePost = async (postId) => {
try {
await esClient.delete({
index: INDEX_NAME,
id: postId.toString(),
});

console.log("帖子已成功删除");
return { success: true, postId };
} catch (err) {
console.error(err);
throw err;
}
};

如何为帖子创建索引

在设置好了Elasticsearch索引之后,您就需要将添加到数据库中的帖子自动纳入该索引中。

const transformPostToESDoc = (post) => {
return {
id: post.id,
title: post.title,
content: post.body,
author: post.author,
category: post.category,
tags: post.tags,
views: post.views || 0,
published_at: post.created_at
};

const indexPost = async (postId) => {
try {
const postRepo = await getPostRepo();
const post = await postRepo.findOne({ where: { id: postId } });

if (!post) {
throw new Error("帖子不存在");
}

const esDocument = transformPostToESDoc(post);

await esClient.index({
index: INDEX_NAME,
id: post.id.toString(),
document: esDocument
});

console.log("帖子已成功创建索引");
return { success: true, postId };
} catch (err) {
console.error(err);
throw err;
}
};

要被创建索引的帖子必须具有唯一的ID。为了方便使用,我们采用了常规数据库中默认存在的唯一标识机制;当然,您也可以使用UUID库来生成唯一的帖子ID。

const indexPost = async (postId) => {
// ... 其他代码 ...

const transformPostToESDoc = (post) => {
// ... 其他代码 ...

随后,这些帖子信息会被作为要被索引的文档传递给esClient.index()函数。同时,我们还设置了相应的错误处理机制,以防止在操作失败时导致应用程序崩溃。

如何定义Elasticsearch映射规则

Elasticsearch的映射规则决定了数据是如何被存储和索引的。这些规则指定了每个字段的数据类型,同时也规定了文本在搜索时应该如何被分析处理。

在下面的示例中,我们将定义一种索引配置方案,该配置包括用于自动完成的自定义分析器,以及针对每个帖子字段(如标题、内容和作者)所设置的映射规则。const indexMapping = {
settings: {
analysis: {
analyzer: {
autocomplete: {
type: 'custom',
tokenizer: 'standard',
filter: ['lowercase', 'autocomplete_filter']
},
autocomplete_search: {
type: 'custom',
tokenizer: 'standard',
filter: ['lowercase']
}
},
filter: {
autocomplete_filter: {
type: 'edge_ngram',
min_gram: 2,
max_gram: 10
}
}
}
},
mappings: {
properties: {
id: { type: 'integer' },
title: {
type: 'text',
analyzer: 'autocomplete',
search_analyzer: 'autocomplete_search',
fields: {
keyword: { type: 'keyword' },
standard: { type: 'text' }
}
},
content: {
type: 'text',
analyzer: 'standard'
},
category: {
type: 'keyword'
},
tags: { type: 'keyword' },
author: {
type: 'text',
fields: {
keyword: { type: 'keyword' }
}
},
views: { type: 'integer' },
published_at: { type: 'date' }
}
}
};

indexMapping对象定义了Elasticsearch应如何存储和处理您的数据。它由两个主要部分组成:settingsmappings
mappings部分定义了您的文档结构。每个字段(如titlecontentauthor)都具有某种类型,例如textkeywordintegerdate。这些类型告诉Elasticsearch如何存储和搜索这些字段。
对于文本字段,我们还可以定义分析器。分析器决定了在索引和搜索过程中文本是如何被分解成更小片段(即词元)的。
settings部分,我们为自动完成功能定义了一个自定义分析器。该分析器使用edge_ngram过滤器来生成部分匹配的结果,这样用户就可以在输入内容的过程中实时看到搜索结果。我们还定义了另一个search_analyzer,以确保搜索查询能够被正确处理。
综上所述,这些设置使您能够在保持搜索结果准确性和高效性的同时,实现自动完成功能等其他特性。

搜索功能的实现

为了实现您的搜索功能,您需要构建相应的API。这包括开发业务逻辑服务以及定义API路由。您还需要使用GET请求,并将搜索词作为查询参数传递;系统返回的结果将以JSON格式呈现。
接下来,您需要实现搜索结果展示的相关功能。在这种情况下,您会利用搜索引擎的功能在索引中查找指定的短语。为了减少接收不必要的信息,建议您采用分页技术来处理搜索结果。搜索查询将由索引名称、用于控制返回哪些结果的分页参数(fromsize),以及预期结果的最大规模组成。此外,您还需要附加一个查询对象,该对象会指定Elasticsearch引擎应使用的搜索方式。

const searchElastic = async (query, page = 1, size = 10) => {
  const searchQuery = {
    index: INDEX_NAME,
    from: (page - 1) * size,
    size,
    query: {
      bool: {
        must: [
          {
            multi_match: {
              query,
              fields: ["title^3", "content"],
              type: "best_fields",
              fuzziness: "AUTO"
            }
          }
        ]
      }
    }
  };

  const result = await esClient.search(searchQuery);
  return result.hitshits;
};

在上面的代码中,这个函数的名称是searchElastic。要执行这个函数,需要传递三个参数:sizepagequery

size参数指定了每次搜索时要返回的最大文档数量。默认值可以是任何整数。

查询中使用multi_match子句来同时在多个字段中进行搜索,例如titlecontenttitle^3这种写法会优先匹配标题字段中的内容,使得这些匹配结果比其他字段的匹配结果更具相关性。

我们还添加了一个must子句,用于定义文档必须满足的条件才能被纳入搜索结果中。

搜索结果通常会根据它们与查询内容的关联程度来进行排序。

完整代码

通过以上步骤,你已经完成了本教程的学习,并配置好了Elasticsearch,使其能够对你数据库中的帖子进行索引。以下是完整的代码:

  1. Elasticsearch客户端(config.js):
const { Client } = require('@elastic/elasticsearch');

const esClient = new Client({
  node: 'http://localhost:9200',
  auth: {
    username: process.env.ELASTICSEARCH_USERNAME,
    password: process.env.ELASTICSEARCH_PASSWORD
  },
  maxRetries: 5,
  requestTimeout: 60000,
  tls: {
    rejectUnauthorized: process.env.NODE_ENV !== 'development'
  }
});

module.exports = esClient;
  1. 索引映射配置:
const indexMapping = {
  settings: {
    analysis: {
      analyzer: {
        autocomplete: {
          type: 'custom',
          tokenizer: 'standard',
          filter: ['lowercase', 'autocomplete_filter']
        },
        autocomplete_search: {
          type: 'custom',
          tokenizer: 'standard',
          filter: ['lowercase']
        }
      },
      filter: {
        autocomplete_filter: {
          type: 'edge_ngram',
          min_gram: 2,
          max_gram: 10
        }
      }
    }
  },
  mappings: {
    properties: {
      id: { type: 'integer' },
      title: {
        type: 'text',
        analyzer: 'autocomplete',
        search_analyzer: 'autocomplete_search',
        fields: {
          keyword: { type: 'keyword' },
          standard: { type: 'text' }
        }
      },
      content: {
        type: 'text',
        analyzer: 'standard'
      },
      category: {
        type: 'keyword'
      },
      tags: { type: 'keyword' },
      author: {
        type: 'text',
        fields: {
          keyword: { type: 'keyword' }
        }
      },
      views: { type: 'integer' },
      published_at: { type: 'date' }
    }
  }
};
  1. 创建索引:
const setupIndex = async () => {
  try {
    const indexExists = await esClient.indices.exists({
      index: INDEX_NAME
    });

    if (indexExists) {
      console.log(`索引 "${INDEX_NAME}" 已经存在`);
      return;
    }

    await esClientindices.create({
      index: INDEX_NAME,
      ...indexMapping
    });

    console.log(`索引 "${INDEX_NAME}" 已创建`);
  } catch (err) {
    console.error(err);
    throw err;
  }
};
  1. 删除索引:
const deleteIndex = async () => {
  try {
    await esClient.indices.delete({ index: INDEX_NAME });
    console.log(`${INDEX_NAME} 已被删除");
  } catch (err) {
    console.error("删除索引时出现错误:", err);
  }
};
  1. 删除文档:
const deletePost = async (postId) => {
  try {
    await esClient.delete({
      index: INDEX_NAME,
      id: postId.toString()
    });

    console.log("文档已成功删除");
    return { success: true, postId };
  } catch (err) {
    console.error(err);
    throw err;
  }
};
  1. 转换并索引文档:
const transformPostToESDoc = (post) => {
  return {
    id: post.id,
    title: post.title,
    content: post.body,
    author: post.author,
    category: post.category,
    tags: post.tags,
    views: post.views || 0,
    published_at: post.created_at
  };

  const indexPost = async (postId) => {
    try {
      const postRepo = await getPostRepo();
      const post = await postRepo.findOne({ where: { id: postId } });

      if (!post) {
        throw new Error("文档不存在");
      }

      const esDocument = transformPostToESDoc(post);

      await esClient.index({
        index: INDEX_NAME,
        id: post.id.toString(),
        document: esDocument
      });

      console.log("文档已成功索引");
      return { success: true,postId };
    } catch (err) {
      console.error(err);
      throw err;
    }
  };
  1. 搜索功能:
const searchElastic = async (query, page = 1, size = 10) => {
  const searchQuery = {
    index: INDEX_NAME,
    from: (page - 1) * size,
    size,
    query: {
      bool: {
        must: [
          {
            multi_match: {
              query,
              fields: ["title^3", "content"],
              type: "best_fields",
              fuzziness: "AUTO"
            }
          }
        ]
      }
    }
  };

  const result = await esClient.search(searchQuery);
  return result.hitshits;
};

总结

现在您已经了解了如何使用Elasticsearch来提升您的Web应用程序中的搜索功能。Elasticsearch具有很强的通用性,因此您可以将其应用于各种编程语言和框架中。此外,它拥有庞大的社区支持,这些社区资源提供了许多有用的用户指南,从而帮助您更轻松地开始使用Elasticsearch。

要想进一步发挥Elasticsearch的强大功能,你可以探索ELK技术栈中的其他工具(包括Elasticsearch、Log Stash和Kibana),这些工具能够帮助你为数据生成高质量的数据可视化报表,尤其是对于企业级应用而言。

结论

在当今的Web应用中,一个快速且可靠的搜索引擎是必不可少的。Elasticsearch正是实现这一目标的理想选择。

如果你想阅读更多有助于提升你的技术水平的文章,欢迎访问我的网站。继续努力学习吧!

Comments are closed.