顯示具有 ElasticSearch 標籤的文章。 顯示所有文章
顯示具有 ElasticSearch 標籤的文章。 顯示所有文章

2024年4月9日 星期二

Elasticsearch 系列 : 建立與使用 Docker 環境下的 Elasticsearch 與 Kibana

Elasticsearch 系列 : 建立與使用 Docker 環境下的 Elasticsearch 與 Kibana

由於最近半年都在從事於 Elasticsearc 方面的專案開發,因為工作上的需要,因此將會把最近用於 Elasticsearc 相關開發技術與方法記錄下來,方便日後查詢,也讓更多人可以認識這個相當優異的工具,以及如何透過 C# 最到相關的應用。

底下為 2023 所撰寫的關於 Elasticsearch 的文章清單

由於之前所寫的文章,都是採用安裝在一台後端伺服器上的 Elasticsearch 服務來進行開發,在這篇文章將會介紹如何透過 Docker 來建立 Elasticsearch 與 Kibana 的環境,讓開發者可以在本機端進行開發,並且可以透過 Kibana 來進行資料庫的管理。

建立 Elasticsearch 要用到的網路

在建立 Elasticsearch 與 Kibana 的 Docker 環境之前,我們需要先建立一個網路,讓 Elasticsearch 與 Kibana 這兩個容器可以透過這個網路來進行溝通。

請在命令提示字元中輸入以下指令

docker network create elastic

執行結果如下

建立 Elasticsearch 要用到的網路

使用底下指令來確認是否有建立成功

docker network ls

確認是否有建立成功

建立與啟動 Elasticsearch Container

接著我們需要建立 Elasticsearch 的 Docker Image,請在命令提示字元中輸入以下指令

docker pull docker.elastic.co/elasticsearch/elasticsearch:8.12.2

執行結果如下:

建立與啟動 Elasticsearch Image

接著我們需要建立 Elasticsearch 的 Docker Container,請在命令提示字元中輸入以下指令

docker run -it --name elasticsearch --net elastic -p 9200:9200 -e "discovery.type=single-node" -e ELASTIC_PASSWORD=elastic docker.elastic.co/elasticsearch/elasticsearch:8.13.1

上述的指令中,使用了 -e ELASTIC_PASSWORD=elastic 來設定 Elasticsearch 的密碼,這個密碼是用來登入 Elasticsearch 的,請記得保留這個密碼,因為之後會用到。另外,使用了 -e "discovery.type=single-node" 來設定 Elasticsearch 的節點類型為單節點,這樣就可以讓 Elasticsearch 在單節點的環境下運行。

執行結果如下:

建立與啟動 Elasticsearch Container

請在命令題是字元視窗內搜尋 [Enrollment token] 文字,就可以看到底下內容

ℹ️  Configure Kibana to use this cluster:
• Run Kibana and click the configuration link in the terminal when Kibana starts.
• Copy the following enrollment token and paste it into Kibana in your browser (valid for the next 30 minutes):
  eyJ2ZXIiOiI4LjEzLjEiLCJhZHIiOlsiMTcyLjE4LjAuMjo5MjAwIl0sImZnciI6ImUxYjUxZmZlYzkwMmZkYjcwNmM4Y2ZlYTgzMTZiOGUxZmU5NWFlNDAxNTU5MzczMTA1Y2U3Mzg3N2Q2Njc2MTciLCJrZXkiOiJ5aC1Fd0k0QlJDTTYtc3dueUJ3bTpTWlRJeEZBMFJXMmZ6eVBhUzlQOVJBIn0=

請將這段文字 eyJ2ZXIiOiI4LjEzLjEiLCJhZHIiOlsiMTcyLjE4LjAuMjo5MjAwIl0sImZnciI6ImUxYjUxZmZlYzkwMmZkYjcwNmM4Y2ZlYTgzMTZiOGUxZmU5NWFlNDAxNTU5MzczMTA1Y2U3Mzg3N2Q2Njc2MTciLCJrZXkiOiJ5aC1Fd0k0QlJDTTYtc3dueUJ3bTpTWlRJeEZBMFJXMmZ6eVBhUzlQOVJBIn0= 複製起來,因為之後會用到,這段文字稱之為 [Enrollment token]。

從執行結果中可以看到 Elasticsearch 已經成功的建立與啟動,並且可以看到底下內容:

接著我們可以透過瀏覽器來進行測試,請在瀏覽器中輸入以下網址

https://localhost:9200

執行結果如下:

首先,需要先輸入帳號與密碼,帳號為 elastic ,密碼為 elastic

測試 Elasticsearch 是否成功建立

完成後,點選登入按鈕,就會看到底下畫面

測試 Elasticsearch 是否成功建立

{
name: "eee625dddc63",
cluster_name: "docker-cluster",
cluster_uuid: "wKpWcpE8R6q9Ipu_odjzrQ",
version: {
number: "8.13.1",
build_flavor: "default",
build_type: "docker",
build_hash: "9287f29bba5e270bd51d557b8daccb7d118ba247",
build_date: "2024-03-29T10:05:29.787251984Z",
build_snapshot: false,
lucene_version: "9.10.0",
minimum_wire_compatibility_version: "7.17.0",
minimum_index_compatibility_version: "7.0.0"
},
tagline: "You Know, for Search"
}

建立與啟動 Kibana Container

使用底下指令啟動 Kibana 的 Docker Image

docker pull docker.elastic.co/kibana/kibana:8.13.1

執行結果如下:

建立與啟動 Kibana Image

接著我們需要建立 Kibana 的 Docker Container,請在命令提示字元中輸入以下指令

docker run -it --name kibana --net elastic -p 5601:5601 docker.elastic.co/kibana/kibana:8.13.1

執行結果如下:

建立與啟動 Kibana Container

從上圖的輸出文字的最後面,可以看到 Kibana has not been configured 的訊息,這表示 Kibana 還沒有設定完成,所以,接著我們可以透過瀏覽器來進行測試,請在瀏覽器中輸入以下網址

http://localhost:5601

執行結果如下:

測試 Kibana 是否成功建立

請在 [Enrollment token] 欄位中貼上剛剛複製下來的 [Enrollment token]

測試 Kibana 是否成功建立

請直接點選 [Configure Elastic] 按鈕,就會看到底下畫面

測試 Kibana 是否成功建立

在這個畫面中,可以看到需要提供驗證碼,所以,現在切換到 Kibana 的命令提示字元視窗,將會看如下圖

Kibana Verification required

最後一行的文字,將會是 Your verification code is: \u001b[30m\u001b[106m 106 918 \u001b[49m\u001b[39m,請將這段文字 106 918 複製起來,因為之後會用到,這段文字稱之為 [Verification code]。

將 [Verification code] 貼到瀏覽器上

Kibana Verification required

完成後,點選 [Verify] 按鈕,就會看到底下畫面

等候 kibana 完成初始化設定之後,就會看到底下畫面

現在可以正常連上 kibana

Kibana Verification required

請在 [Username] 欄位中輸入 elastic,在 [Password] 欄位中輸入 elastic,然後點選 [Log in] 按鈕,就可以看到底下畫面

第一次使用 Kibana

在這個畫面中,點選 [Explore on my own] 按鈕,就可以看到底下畫面

第一次使用 Kibana

其他說明

docker exec -it elasticsearch /usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s kibana

上述的指令表示了透過 elasticsearch-create-enrollment-token 來建立一個 enrollment token,這個 enrollment token 是用來讓 Kibana 與 Elasticsearch 之間進行溝通的,而 -s kibana 表示了這個 enrollment token 是用來給 Kibana 使用的。 





2023年12月27日 星期三

Elasticsearch 系列 - 使用 C# / Elasticsearch.Net 來刪除 Elasticsearch 資料庫文件記錄

Elasticsearch 系列 - 使用 C# / Elasticsearch.Net 來刪除 Elasticsearch 資料庫文件記錄

在上一篇文章中,寫了關於 Elasticsearch 系列 - 使用 C# / Elasticsearch.Net 來查詢 Elasticsearch 資料庫文件記錄 文章,這是透過 C# / NEST (Elasticsearch V7) 用戶端類別庫,進行文件查詢的作法。

接下來就是要進行 CRUD 新增、查詢、修改、刪除程式設計作法的 Retrive 修改的程式設計方式說明,在這裡,請將 Elasticsearch 系列 - 使用 C# / Elasticsearch.Net 來新增文件記錄到 Elasticsearch 資料庫 文章的新增專案再度跑一次,其目的是要確保在後端的 Elasticsearch 資料庫內的 [blogs] 索引內,有一致性的文件存在,這樣,在這篇文章中的操作,才會有可預測的結果。

建立測試專案

請依照底下的操作,建立起這篇文章需要用到的練習專案

  • 打開 Visual Studio 2022 IDE 應用程式
  • 從 [Visual Studio 2022] 對話窗中,點選右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗右半部
    • 切換 [所有語言 (L)] 下拉選單控制項為 [C#]
    • 切換 [所有專案類型 (T)] 下拉選單控制項為 [主控台]
  • 在中間的專案範本清單中,找到並且點選 [主控台應用程式] 專案範本選項

    專案,用於建立可在 Windows、Linux 及 macOS 於 .NET 執行的命令列應用程式

  • 點選右下角的 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗
  • 找到 [專案名稱] 欄位,輸入 csElasticsearchNestDelete 作為專案名稱
  • 在剛剛輸入的 [專案名稱] 欄位下方,確認沒有勾選 [將解決方案與專案至於相同目錄中] 這個檢查盒控制項
  • 點選右下角的 [下一步] 按鈕
  • 現在將會看到 [其他資訊] 對話窗
  • 在 [架構] 欄位中,請選擇最新的開發框架,這裡選擇的 [架構] 是 : .NET 8.0 (標準字詞支援)
  • 在這個練習中,需要去勾選 [不要使用最上層陳述式(T)] 這個檢查盒控制項

    這裡的這個操作,可以由讀者自行決定是否要勾選這個檢查盒控制項

  • 請點選右下角的 [建立] 按鈕

稍微等候一下,這個主控台專案將會建立完成

安裝要用到的 NuGet 開發套件

因為開發此專案時會用到這些 NuGet 套件,請依照底下說明,將需要用到的 NuGet 套件安裝起來。

安裝 Elasticsearch.Net 套件

這個 Elasticsearch.Net 套件是 Elasticsearch 的低階 .NET 客戶端,與 NEST 不同,它提供了更多的彈性和直接控制,但也意味著需要手動處理較多的細節。

請依照底下說明操作步驟,將這個套件安裝到專案內

  • 滑鼠右擊 [方案總管] 視窗內的 [專案節點] 下方的 [相依性] 節點
  • 從彈出功能表清單中,點選 [管理 NuGet 套件] 這個功能選項清單
  • 此時,將會看到 [NuGet: csElasticsearchNestDelete] 視窗
  • 切換此視窗的標籤頁次到名稱為 [瀏覽] 這個標籤頁次
  • 在左上方找到一個搜尋文字輸入盒,在此輸入 Elasticsearch.Net
  • 在視窗右方,將會看到該套件詳細說明的內容,其中,右上方有的 [安裝] 按鈕
  • 點選這個 [安裝] 按鈕,將這個套件安裝到專案內

安裝 NEST 套件

這個 NEST 套件是 Elasticsearch 的官方 .NET 高階客戶端。它是一個強大的、易於使用的 .NET 庫,旨在與 Elasticsearch 交互。NEST 提供了一個豐富的 .NET 接口,使得在 .NET 應用中與 Elasticsearch 進行通信變得容易和直觀。

請依照底下說明操作步驟,將這個套件安裝到專案內

  • 滑鼠右擊 [方案總管] 視窗內的 [專案節點] 下方的 [相依性] 節點
  • 從彈出功能表清單中,點選 [管理 NuGet 套件] 這個功能選項清單
  • 此時,將會看到 [NuGet: csElasticsearchNestDelete] 視窗
  • 切換此視窗的標籤頁次到名稱為 [瀏覽] 這個標籤頁次
  • 在左上方找到一個搜尋文字輸入盒,在此輸入 NEST
  • 在視窗右方,將會看到該套件詳細說明的內容,其中,右上方有的 [安裝] 按鈕
  • 點選這個 [安裝] 按鈕,將這個套件安裝到專案內

建立要使用的程式碼

  • 在 [方案總管] 內找到並且開啟 [Program.cs] 檔案這個節點
  • 使用底下 C# 程式碼,將原本的程式碼取代掉
using Nest;
using System.Diagnostics;

namespace csElasticsearchNestDelete;

[ElasticsearchType(IdProperty = nameof(BlogId))]
public class Blog
{
    public int BlogId { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Content { get; set; } = string.Empty;
    public DateTime CreateAt { get; set; } = DateTime.Now;
    public DateTime UpdateAt { get; set; } = DateTime.Now;
}

internal class Program
{
    static async Task Main(string[] args)
    {
        var settings = new ConnectionSettings(new Uri("http://10.1.1.231:9200/"))
            .DisableDirectStreaming()
            .BasicAuthentication("elastic", "elastic");

        var client = new ElasticClient(settings);

        string indexName = "blogs".ToLower();

        Stopwatch stopwatch = new Stopwatch();

        #region 取得需要刪除的文件
        await Console.Out.WriteLineAsync(); await Console.Out.WriteLineAsync();
        await Console.Out.WriteLineAsync($"取得需要刪除的文件");
        int needDeleteBlogId = 10006;
        stopwatch.Restart();
        var searchResponse3 = await client
            .SearchAsync<Blog>(s =>
              s.Index(indexName)
               .Size(200)
               .Query(q => q
                      .Term(t => t.BlogId, needDeleteBlogId)
                 )
            );
        stopwatch.Stop();

        Blog needDeleteBlog = null;
        if (searchResponse3.IsValid)
        {
            var documents = searchResponse3.Documents;
            needDeleteBlog = documents.FirstOrDefault();
            if (needDeleteBlog != null)
            {
                Console.WriteLine($"查詢到的文件數量為:{documents.Count} / Title : {needDeleteBlog.Title}, 花費 {stopwatch.ElapsedMilliseconds} ms");
            }
            else
            {
                Console.WriteLine($"沒有搜尋到文件, 花費 {stopwatch.ElapsedMilliseconds} ms");
            }
        }
        #endregion

        #region 進行刪除文件
        if (needDeleteBlog != null)
        {
            await Console.Out.WriteLineAsync(); await Console.Out.WriteLineAsync();
            await Console.Out.WriteLineAsync($"進行刪除文件");
            stopwatch.Restart();
            var searchResponse4 = await client
                .DeleteAsync<Blog>(needDeleteBlog.BlogId,
                  d => d.Index(indexName)
            );
            if (searchResponse4.IsValid)
            {
                Console.WriteLine($"成功刪除文件 Title : {needDeleteBlog.Title}, 花費 {stopwatch.ElapsedMilliseconds} ms");
            }
        }
        #endregion

        #region 再次查詢已經刪除的文件
        await Console.Out.WriteLineAsync(); await Console.Out.WriteLineAsync();
        await Console.Out.WriteLineAsync($"稍後3秒鐘");
        await Task.Delay(3000);
        await Console.Out.WriteLineAsync($"查詢已經刪除的文件");
        stopwatch.Restart();
        var searchResponse5 = await client
            .SearchAsync<Blog>(s =>
              s.Index(indexName)
               .Size(200)
               .Query(q => q
                      .Term(t => t.BlogId, needDeleteBlogId)
                 )
            );
        stopwatch.Stop();

        if (searchResponse5.IsValid)
        {
            var documents = searchResponse5.Documents;
            needDeleteBlog = documents.FirstOrDefault();
            if (needDeleteBlog != null)
            {
                Console.WriteLine($"查詢到的文件數量為:{documents.Count} / Title : {needDeleteBlog.Title}, 花費 {stopwatch.ElapsedMilliseconds} ms");
            }
            else
            {
                Console.WriteLine($"沒有搜尋到文件,該文件已經被刪除了, 花費 {stopwatch.ElapsedMilliseconds} ms");
            }
        }
        #endregion

    }
}

對於這個 Index 的模型,將會定義在 [Blog] 這個類別內,由於 NEST 預設將會使用 [Id] 這樣的欄位作為唯一識別碼,不過,在這個 [Blog] 類別內,卻沒有這樣的屬性存在,而是有個 [BlogId] 這樣的屬性,所以,我們需要在這個類別內使用 ElasticsearchType 這個屬性來指定這個欄位為唯一識別碼。

在此,將會在該類別之外,使用 [ElasticsearchType(IdProperty = nameof(BlogId))] 這樣的屬性來指定這個欄位為唯一識別碼。

對於這個 [Blog] 類別內的屬性,將會定義如下

  • [BlogId] : 這個屬性將會作為唯一識別碼
  • [Title] : 這個屬性將會作為標題
  • [Content] : 這個屬性將會作為內容
  • [CreateAt] : 這個屬性將會作為建立時間
  • [UpdateAt] : 這個屬性將會作為更新時間

接下來就是這個程式進入點內的程式碼,首先,將會建立一個 [ConnectionSettings] 物件,用來宣告與 Elasticsearch 進行通訊的相關設定,這個物件將會傳入一個 Uri 物件,這個 Uri 物件將會指定 Elasticsearch 的伺服器位址。

由於這台 Elasticsearch 的伺服器,有設定帳號與密碼,所以,這裡將會使用 BasicAuthentication 這個方法,來指定帳號與密碼。

一旦得到 [ConnectionSettings] 物件,接下來的工作將會是要建立一個 [ElasticClient] 這個類別,這個類別將會是與 Elasticsearch 進行通訊的主要類別,這個類別的建構式,將會需要傳入一個 [ConnectionSettings] 類別的物件,這個物件將會是用來設定與 Elasticsearch 進行通訊的相關設定。

接下來,因為底下的許多操作都需要指定在 Elasticsearch 內的索引名稱,因此,將會建立一個字串變數 [indexName],這個變數將會是用來指定要操作的 Index 名稱,這裡將會指定為 blogs。這裡有使用 ToLower() 方法,將這個字串轉換為小寫字串,這是因為在 Elasticsearch 內,對於所有的 Index 名稱,都是使用小寫字母來表示。

接下來,將會建立一個 [Stopwatch] 類別的物件,這個物件將會用來計算執行時間。

由於這裡要說明如何透過 C# 與 NEST 來做到特定文件更新的需求,所以,在這裡需要先針對要更新的文件把他搜尋出來,透過底下的程式碼,便可以做到找出 [BlogId] 欄位為 10006 的文件。

int needDeleteBlogId = 10006;
var searchResponse3 = await client
    .SearchAsync<Blog>(s =>
        s.Index(indexName)
        .Size(200)
        .Query(q => q
                .Term(t => t.BlogId, needDeleteBlogId)
            )
    );

然後,根據所找出來的文件,透過底下敘述,將這份文件從

var searchResponse4 = await client
    .DeleteAsync<Blog>(needDeleteBlog.BlogId,
        d => d.Index(indexName)
);

從回應物件的 [ApiCall.DebugInformation] 屬性,可以看到這樣的需求是透過了 HTTP DELETE: /blogs/_doc/10006 請求來完成,並且在裡沒有存在任何 Payload

一旦更新文件完成之後,就會得到底下的 JSON 物件

{
   "_index":"blogs",
   "_id":"10006",
   "_version":2,
   "result":"deleted",
   "_shards":{
      "total":2,
      "successful":2,
      "failed":0
   },
   "_seq_no":63,
   "_primary_term":1
}
  • "_index""blogs" - 這表示刪除操作發生在 "blogs" 索引上。

  • "_id""10006" - 這是被刪除文檔的唯一標識符。

  • "_version"2 - 這表示文檔在被刪除前的版本號。 在 Elasticsearch 中,每次文檔被更新或修改時,版本號會遞增。

  • "result""deleted" - 這表明操作的結果。 在這個例子中,它表明文檔已經成功被刪除。

  • "_shards":

    • "total": 2 - 表示刪除操作影響了2個分片。
    • "successful": 2 - 其中2個分片在刪除操作中成功執行。
    • "failed": 0 - 沒有分片在刪除過程中失敗。
  • "_seq_no"63 - 序列號(Sequence Number),是一個內部用於保持版本控制的數字。 每當文檔被更新或刪除時,序列號會增加。

  • "_primary_term"1 - 主術語号(Primary Term),用於標識分片的當前主副本的術語號。 它與序列號一起幫助維持 Elasticsearch 集群中的數據一致性。

總結來說,這個 JSON 結構表明一個文檔在 "blogs" 索引中成功被刪除,並且在刪除過程中沒有遇到任何分片失敗的情況。 此外,它還提供了文檔被刪除時的狀態信息,包括版本號和序列號。

執行程式碼

  • 按下 F5 鍵,開始執行這個程式
  • 請觀察 Console 視窗內的內容
取得需要刪除的文件
查詢到的文件數量為:1 / Title : Nice to meet your (Bulk) 10006, 花費 744 ms


進行刪除文件
成功刪除文件 Title : Nice to meet your (Bulk) 10006, 花費 57 ms


稍後3秒鐘
查詢已經刪除的文件 

沒有搜尋到文件,該文件已經被刪除了, 花費 23 ms