1. 程式人生 > 實用技巧 >.NET Core 下使用 ElasticSearch

.NET Core 下使用 ElasticSearch

快速入門

Elasticsearch 快速入門

ElasticSearch 是一個開源的搜尋引擎,建立在一個全文搜尋引擎庫 Apache Lucene™ 基礎之上。 Lucene 可以說是當下最先進、高效能、全功能的搜尋引擎庫,無論是開源還是私有。

但是 Lucene 僅僅只是一個庫。為了充分發揮其功能,你需要使用 Java 並將 Lucene 直接整合到應用程式中。 更糟糕的是,您可能需要獲得資訊檢索學位才能瞭解其工作原理。Lucene 非常 複雜。

ElasticSearch 也是使用 Java 編寫的,它的內部使用 Lucene 做索引與搜尋,但是它的目的是使全文檢索變得簡單, 通過隱藏 Lucene 的複雜性,取而代之的提供一套簡單一致的 RESTful API。

然而,Elasticsearch 不僅僅是 Lucene,並且也不僅僅只是一個全文搜尋引擎。 它可以被下面這樣準確的形容:

  • 一個分散式的實時文件儲存,每個欄位 可以被索引與搜尋
  • 一個分散式實時分析搜尋引擎
  • 能勝任上百個服務節點的擴充套件,並支援 PB 級別的結構化或者非結構化資料

官方客戶端在Java、.NET、PHP、Python、Ruby、Nodejs和許多其他語言中都是可用的。根據 DB-Engines 的排名顯示,ElasticSearch 是最受歡迎的企業搜尋引擎,其次是Apache Solr,也是基於Lucene。

ES 開發指南

中文文件請參考:《Elasticsearch: 權威指南》

英文文件請參考:《Elasticsearch Reference》

下載: https://www.elastic.co/cn/downloads/

ES API文件

API Conventions

Document APIs

Search APIs

Indices APIs

cat APIs

Cluster APIs

Javascript api

Logstash

Logstash Reference

Configuring Logstash

Input plugins

Output plugins

Filter plugins

Kibana DevTools 快捷鍵

  • Ctrl+i 自動縮排
  • Ctrl+Enter 提交
  • Down 開啟自動補全選單
  • Enter 或 Tab 選中項自動補全
  • Esc 關閉補全選單

pretty = true在任意的查詢字串中增加pretty引數,會讓 Elasticsearch 美化輸出(pretty-print)JSON響應以便更加容易閱讀。

Kibana 命令

// 查詢叢集的磁碟狀態
GET _cat/allocation?v

// 獲取所有索引
GET _cat/indices

// 按索引數量排序
GET _cat/indices?s=docs.count:desc
GET _cat/indices?v&s=index

// 叢集有多少節點
GET _cat/nodes

// 叢集的狀態
GET _cluster/health?pretty=true
GET _cat/indices/*?v&s=index

//獲取指定索引的分片資訊
GET logs/_search_shards

...

叢集狀態

curl -s -XGET 'http://<host>:9200/_cluster/health?pretty'

//系統正常,返回的結果
{
  "cluster_name" : "es-qwerty",
  "status" : "green",
  "timed_out" : false,
  "number_of_nodes" : 3,
  "number_of_data_nodes" : 3,
  "active_primary_shards" : 1,
  "active_shards" : 2,
  "relocating_shards" : 0,
  "initializing_shards" : 0,
  "unassigned_shards" : 0,
  "delayed_unassigned_shards" : 0,
  "number_of_pending_tasks" : 0,
  "number_of_in_flight_fetch" : 0,
  "task_max_waiting_in_queue_millis" : 0,
  "active_shards_percent_as_number" : 100.0
}

檢索文件

POST logs/_search
{
  "query":{
    "range":{
      "createdAt":{
        "gt":"2020-04-25",
        "lt":"2020-04-27",
        "format": "yyyy-MM-dd"
      }
    }
  },
  "size":0,
  "aggs":{
    "url_type_stats":{
      "terms": {
        "field": "urlType.keyword",
        "size": 2
      }
    }
  }
}

POST logs/_search
{
  "query":{
    "range":{
      "createdAt":{
        "gte":"2020-04-26 00:00:00",
        "lte":"now",
        "format": "yyyy-MM-dd hh:mm:ss"
      }
    }
  },
  "size":0,
  "aggs":{
    "url_type_stats":{
      "terms": {
        "field": "urlType.keyword",
        "size": 2
      }
    }
  }
}

POST logs/_search
{
  "query":{
    "range": {
      "createdAt": {
        "gte": "2020-04-26 00:00:00",
        "lte": "now",
         "format": "yyyy-MM-dd hh:mm:ss"
      }
    }
  },
  "size" : 0,
  "aggs":{
    "total_clientIp":{
      "cardinality":{
        "field": "clientIp.keyword"
      }
    },
    "total_userAgent":{
      "cardinality": {
        "field": "userAgent.keyword"
      }
    }
  }
}

POST logs/_search
{
  "size" : 0,
  "aggs":{
    "date_total_ClientIp":{
      "date_histogram":{
        "field": "createdAt",
        "interval": "quarter",
        "format": "yyyy-MM-dd",
        "extended_bounds":{
          "min": "2020-04-26 13:00:00",
          "max": "2020-04-26 14:00:00",
        }
      },
      "aggs":{
        "url_type_api": {
          "terms": {
            "field": "urlType.keyword",
            "size": 10
          }
        }
      }
    }
  }
}

POST logs/_search
{
  "size" : 0,
  "aggs":{
    "total_clientIp":{
      "terms":{
        "size":30,
        "field": "clientIp.keyword"
      }
    }
  }
}

刪除文件

// 刪除
POST logs/_delete_by_query {"query":{"match_all": {}}}

// 刪除索引
DELETE logs

建立索引

資料遷移本質是索引的重建,重建索引不會嘗試設定目標索引,它不會複製源索引的設定。 所以在操作之前設定目標索引,包括設定對映,分片數,副本等。

資料遷移

Reindex from Remoteedit

// Reindex支援從遠端Elasticsearch叢集重建索引:
POST _reindex
{
  "source": {
    "remote": {
      "host": "http://lotherhost:9200",
      "username": "user",
      "password": "pass"
    },
    "index": "source",
    "query": {
      "match": {
        "test": "data"
      }
    }
  },
  "dest": {
    "index": "dest"
  }
}

// host引數必須包含scheme、host和port(例如https://lotherhost:9200)
// username和password引數可選

使用時需要在elasticsearch.yml中配置 reindex.remote.whitelist 屬性。可以設定多組(例如,lotherhost:9200, another:9200, 127.0.10.*:9200, localhost:*)。

具體使用可參考 Reindex from Remoteedit

Elasticsearch-Dump

Elasticsearch-Dump是一個elasticsearch資料匯入匯出開源工具包。安裝、遷移相關執行可以在相同可用區的雲主機上進行,使用方便。

需要node環境,npm安裝elasticdump

npm install elasticdump -g
elasticdump

// Copy an index from production to staging with analyzer and mapping:
elasticdump \
  --input=http://production.es.com:9200/my_index \
  --output=http://staging.es.com:9200/my_index \
  --type=analyzer
elasticdump \
  --input=http://production.es.com:9200/my_index \
  --output=http://staging.es.com:9200/my_index \
  --type=mapping
elasticdump \
  --input=http://production.es.com:9200/my_index \
  --output=http://staging.es.com:9200/my_index \
  --type=data

// Copy a single shard data:
elasticdump \
  --input=http://es.com:9200/api \
  --output=http://es.com:9200/api2 \
  --params='{"preference" : "_shards:0"}'

elasticdump 命令其他引數使用參考 Elasticdump Options

深度分頁

  • elasticsearch 超過10000條資料的分頁查詢會報異常,官方提供了 search_after 的方式來支援
  • search_after 要求提供上一頁兩個必須的排序標識
//https://www.elastic.co/guide/en/elasticsearch/reference/5.6/search-request-search-after.html
GET logs/_search
{
  "from":9990,
  "size":10,
  "_source": ["url","clientIp","createdAt"],
  "query":{
    "match_all": {}
  },
  "sort":[
    {
      "createdAt":{
        "order":"desc"
      }
    },
    {
      "_id":{
        "order":"desc"
      }
    }
    ]
}

GET logs/_search
{
  "from":-1,
  "size":10,
  "_source": ["url","clientIp","createdAt"],
  "query":{
    "match_all": {}
  },
  "search_after": [1588042597000, "V363vnEBz1D1HVfYBb0V"],
  "sort":[
    {
      "createdAt":{
        "order":"desc"
      }
    },
    {
      "_id":{
        "order":"desc"
      }
    }
    ]
}

安裝

  • docker下安裝Elasticsearch
docker pull docker.elastic.co/elasticsearch/elasticsearch:7.8.1
docker run -p 9200:9200 --name elasticsearch -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.8.1
docker pull docker.elastic.co/kibana/kibana:7.8.1
docker run -p 5601:5601 --name kibana --link 14e385b1e761:elasticsearch -e "elasticsearch.hosts=http://127.0.0.1:9200" -d docker.elastic.co/kibana/kibana:7.8.1

接入使用

新建一個webapi專案,然後安裝兩個元件。

Install-Package NEST
Install-Package Swashbuckle.AspNetCore

通過NEST來實現操作Elasticsearch,開源地址:https://github.com/elastic/elasticsearch-net,同時將swagger也新增以下方便後面呼叫介面。

接下來演示一個對Elasticsearch的增刪改查操作。

新增實體類:VisitLog.cs

using System;

namespace ESDemo.Domain
{
    public class VisitLog
    {
        public string Id { get; set; }

        /// <summary>
        /// UserAgent
        /// </summary>
        public string UserAgent { get; set; }

        /// <summary>
        /// Method
        /// </summary>
        public string Method { get; set; }

        /// <summary>
        /// Url
        /// </summary>
        public string Url { get; set; }

        /// <summary>
        /// Referrer
        /// </summary>
        public string Referrer { get; set; }

        /// <summary>
        /// IpAddress
        /// </summary>
        public string IpAddress { get; set; }

        /// <summary>
        /// Milliseconds
        /// </summary>
        public int Milliseconds { get; set; }

        /// <summary>
        /// QueryString
        /// </summary>
        public string QueryString { get; set; }

        /// <summary>
        /// Request Body
        /// </summary>
        public string RequestBody { get; set; }

        /// <summary>
        /// Cookies
        /// </summary>
        public string Cookies { get; set; }

        /// <summary>
        /// Headers
        /// </summary>
        public string Headers { get; set; }

        /// <summary>
        /// StatusCode
        /// </summary>
        public int StatusCode { get; set; }

        /// <summary>
        /// Response Body
        /// </summary>
        public string ResponseBody { get; set; }

        public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
    }
}

確定好實體類後,來包裝一下Elasticsearch,簡單封裝一個基類用於倉儲的整合使用。

新增一個介面類IElasticsearchProvider

using Nest;

namespace ESDemo.Elasticsearch
{
    public interface IElasticsearchProvider
    {
        IElasticClient GetClient();
    }
}

ElasticsearchProvider中實現IElasticsearchProvider介面。

using Nest;
using System;

namespace ESDemo.Elasticsearch
{
    public class ElasticsearchProvider : IElasticsearchProvider
    {
        public IElasticClient GetClient()
        {
            var connectionSettings = new ConnectionSettings(new Uri("http://localhost:9200"));

            return new ElasticClient(connectionSettings);
        }
    }
}

新增Elasticsearch倉儲基類,ElasticsearchRepositoryBase

using Nest;

namespace ESDemo.Elasticsearch
{
    public abstract class ElasticsearchRepositoryBase
    {
        private readonly IElasticsearchProvider _elasticsearchProvider;

        public ElasticsearchRepositoryBase(IElasticsearchProvider elasticsearchProvider)
        {
            _elasticsearchProvider = elasticsearchProvider;
        }

        protected IElasticClient Client => _elasticsearchProvider.GetClient();

        protected abstract string IndexName { get; }
    }
}

也就是一個抽象類,當我們整合此基類的時候需要重寫protected abstract string IndexName { get; },指定IndexName。

完成上面簡單封裝,現在新建一個IVisitLogRepository倉儲介面,裡面新增四個方法:

using ESDemo.Domain;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ESDemo.Repositories
{
    public interface IVisitLogRepository
    {
        Task InsertAsync(VisitLog visitLog);

        Task DeleteAsync(string id);

        Task UpdateAsync(VisitLog visitLog);

        Task<Tuple<int, IList<VisitLog>>> QueryAsync(int page, int limit);
    }
}

所以接下來不用說你也知道改幹嘛,實現這個倉儲介面,新增VisitLogRepository,程式碼如下:

using ESDemo.Domain;
using ESDemo.Elasticsearch;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ESDemo.Repositories
{
    public class VisitLogRepository : ElasticsearchRepositoryBase, IVisitLogRepository
    {
        public VisitLogRepository(IElasticsearchProvider elasticsearchProvider) : base(elasticsearchProvider)
        {
        }

        protected override string IndexName => "visitlogs";

        public async Task InsertAsync(VisitLog visitLog)
        {
            await Client.IndexAsync(visitLog, x => x.Index(IndexName));
        }

        public async Task DeleteAsync(string id)
        {
            await Client.DeleteAsync<VisitLog>(id, x => x.Index(IndexName));
        }

        public async Task UpdateAsync(VisitLog visitLog)
        {
            await Client.UpdateAsync<VisitLog>(visitLog.Id, x => x.Index(IndexName).Doc(visitLog));
        }

        public async Task<Tuple<int, IList<VisitLog>>> QueryAsync(int page, int limit)
        {
            var query = await Client.SearchAsync<VisitLog>(x => x.Index(IndexName)
                                    .From((page - 1) * limit)
                                    .Size(limit)
                                    .Sort(x => x.Descending(v => v.CreatedAt)));
            return new Tuple<int, IList<VisitLog>>(Convert.ToInt32(query.Total), query.Documents.ToList());
        }
    }
}

現在去寫介面,新增一個VisitLogControllerAPI控制器,程式碼如下:

using ESDemo.Domain;
using ESDemo.Repositories;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;

namespace ESDemo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class VisitLogController : ControllerBase
    {
        private readonly IVisitLogRepository _visitLogRepository;

        public VisitLogController(IVisitLogRepository visitLogRepository)
        {
            _visitLogRepository = visitLogRepository;
        }

        [HttpGet]
        public async Task<IActionResult> QueryAsync(int page = 1, int limit = 10)
        {
            var result = await _visitLogRepository.QueryAsync(page, limit);

            return Ok(new
            {
                total = result.Item1,
                items = result.Item2
            });
        }

        [HttpPost]
        public async Task<IActionResult> InsertAsync([FromBody] VisitLog visitLog)
        {
            await _visitLogRepository.InsertAsync(visitLog);

            return Ok("新增成功");
        }

        [HttpDelete]
        public async Task<IActionResult> DeleteAsync([Required] string id)
        {
            await _visitLogRepository.DeleteAsync(id);

            return Ok("刪除成功");
        }

        [HttpPut]
        public async Task<IActionResult> UpdateAsync([FromBody] VisitLog visitLog)
        {
            await _visitLogRepository.UpdateAsync(visitLog);

            return Ok("修改成功");
        }
    }
}

大功告成,最後一步不要忘記在Startup.cs中新增服務,不然無法使用依賴注入。

...
services.AddSingleton<IElasticsearchProvider, ElasticsearchProvider>();
services.AddSingleton<IVisitLogRepository, VisitLogRepository>();
...

一切準備就緒,現在滿懷期待的執行專案,開啟swagger介面。

按照新增、更新、刪除、查詢的順序依次呼叫介面。新增可以多來幾次,因為預設是沒有資料的,多新增一點可以測試分頁是否ok,這裡就不再演示了。

如果你有安裝kibana,現在可以滿懷驚喜的去檢視一下剛才新增的資料。

GET _cat/indices

GET visitlogs/_search
{}

可以看到,資料已經安安靜靜的躺在這裡了。

本篇簡單介紹Elasticsearch在.NET Core中的使用,關於檢索資料還有很多語法沒有體現出來,如果在開發中需要用到,可以參考官方的各種資料查詢示例:https://github.com/elastic/elasticsearch-net/tree/master/examples