人們說數據是新的黃金。但要在短時間內通過龐大的數據集來滿足消費者的需求,對于后端開發人員來說仍然是一個難題。
傳統的數據庫查詢方法往往無法快速獲得準確的搜索結果。不過幸運的是,Elasticsearch為這個問題提供了解決方案。
在這篇文章中,我將向您介紹如何使用Elasticsearch來提升數據庫搜索和分析的功能,同時還能保持效率。
進行本教程學習之前,需要準備以下條件:
-
一個Node.js開發環境
-
基本的后端開發知識
好了,讓我們開始吧。但首先,什么是Elasticsearch呢?
目錄
什么是Elasticsearch?
Elasticsearch是由Apache開發的搜索引擎,它能夠對單詞和短語進行索引處理,從而提供先進的文本搜索及向量搜索功能。此外,它還具備搜索分析以及自動補全等功能。
需要注意的是,盡管Elasticsearch提供了索引功能,但它本身并不屬于數據庫——這一點與常見的數據庫是相同的。
在實際情況中,還有其他一些流行的替代工具可供選擇,例如Algolia、OpenSearch以及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的默認本地端點。此時你會在屏幕上看到類似下圖所示的成功提示信息:

完成這些步驟后,我們就可以繼續設置我們的項目,并將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應如何存儲和處理您的數據。它由兩個主要部分組成:settings和mappings。
mappings部分定義了您的文檔結構。每個字段(如title、content或author)都具有某種類型,例如text、keyword、integer或date。這些類型告訴Elasticsearch如何存儲和搜索這些字段。
對于文本字段,我們還可以定義分析器。分析器決定了在索引和搜索過程中文本是如何被分解成更小片段(即詞元)的。
在settings部分,我們為自動完成功能定義了一個自定義分析器。該分析器使用edge_ngram過濾器來生成部分匹配的結果,這樣用戶就可以在輸入內容的過程中實時看到搜索結果。我們還定義了另一個search_analyzer,以確保搜索查詢能夠被正確處理。
綜上所述,這些設置使您能夠在保持搜索結果準確性和高效性的同時,實現自動完成功能等其他特性。
搜索功能的實現
為了實現您的搜索功能,您需要構建相應的API。這包括開發業務邏輯服務以及定義API路由。您還需要使用GET請求,并將搜索詞作為查詢參數傳遞;系統返回的結果將以JSON格式呈現。
接下來,您需要實現搜索結果展示的相關功能。在這種情況下,您會利用搜索引擎的功能在索引中查找指定的短語。為了減少接收不必要的信息,建議您采用分頁技術來處理搜索結果。搜索查詢將由索引名稱、用于控制返回哪些結果的分頁參數(from和size),以及預期結果的最大規模組成。此外,您還需要附加一個查詢對象,該對象會指定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。要執行這個函數,需要傳遞三個參數:size、page和query。
size參數指定了每次搜索時要返回的最大文檔數量。默認值可以是任何整數。
查詢中使用multi_match子句來同時在多個字段中進行搜索,例如title和content。title^3這種寫法會優先匹配標題字段中的內容,使得這些匹配結果比其他字段的匹配結果更具相關性。
我們還添加了一個must子句,用于定義文檔必須滿足的條件才能被納入搜索結果中。
搜索結果通常會根據它們與查詢內容的關聯程度來進行排序。
完整代碼
通過以上步驟,你已經完成了本教程的學習,并配置好了Elasticsearch,使其能夠對你數據庫中的帖子進行索引。以下是完整的代碼:
- 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;
- 索引映射配置:
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' }
}
}
};
- 創建索引:
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;
}
};
- 刪除索引:
const deleteIndex = async () => {
try {
await esClient.indices.delete({ index: INDEX_NAME });
console.log(`${INDEX_NAME} 已被刪除");
} catch (err) {
console.error("刪除索引時出現錯誤:", err);
}
};
- 刪除文檔:
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;
}
};
- 轉換并索引文檔:
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;
}
};
- 搜索功能:
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正是實現這一目標的理想選擇。
如果你想閱讀更多有助于提升你的技術水平的文章,歡迎訪問我的網站。繼續努力學習吧!