人們說數據是新的黃金。但要在短時間內通過龐大的數據集來滿足消費者的需求,對于后端開發人員來說仍然是一個難題。

傳統的數據庫查詢方法往往無法快速獲得準確的搜索結果。不過幸運的是,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.