Elasticsearch 建立在 Apache Lucene 之上,于 2010 年由 Elasticsearch NV(現(xiàn)為 Elastic)首次發(fā)布。據(jù) Elastic 網(wǎng)站稱,它是一個分布式開源搜索和分析引擎,適用于所有類型的數(shù)據(jù),包括文本、數(shù)值 、地理空間、結(jié)構(gòu)化和非結(jié)構(gòu)化。Elasticsearch 操作通過 REST API 實(shí)現(xiàn)。主要功能是:
- 將文檔存儲在索引中,
- 使用強(qiáng)大的查詢搜索索引以獲取這些文檔,以及
- 對數(shù)據(jù)運(yùn)行分析函數(shù)。
Spring Data Elasticsearch 提供了一個簡單的接口來在 Elasticsearch 上執(zhí)行這些操作,作為直接使用 REST API 的替代方法。
在這里,我們將使用 Spring Data Elasticsearch 來演示 Elasticsearch 的索引和搜索功能,并在最后構(gòu)建一個簡單的搜索應(yīng)用程序,用于在產(chǎn)品庫存中搜索產(chǎn)品。
代碼示例
本文附有 GitHub 上的工作代碼示例。
Elasticsearch 概念
Elasticsearch 概念
了解 Elasticsearch 概念的最簡單方法是用數(shù)據(jù)庫進(jìn)行類比,如下表所示:
Elasticsearch | -> | 數(shù)據(jù)庫 |
---|---|---|
索引 | -> | 表 |
文檔 | -> | 行 |
文檔 | -> | 列 |
我們要搜索或分析的任何數(shù)據(jù)都作為文檔存儲在索引中。在 Spring Data 中,我們以 POJO 的形式表示一個文檔,并用注解對其進(jìn)行修飾以定義到 Elasticsearch 文檔的映射。
與數(shù)據(jù)庫不同,存儲在 Elasticsearch 中的文本首先由各種分析器處理。默認(rèn)分析器通過常用單詞分隔符(如空格和標(biāo)點(diǎn)符號)拆分文本,并刪除常用英語單詞。
如果我們存儲文本“The sky is blue”,分析器會將其存儲為包含“術(shù)語”“sky”和“blue”的文檔。我們將能夠使用“blue sky”、“sky”或“blue”形式的文本搜索此文檔,并將匹配程度作為分?jǐn)?shù)。
除了文本之外,Elasticsearch 還可以存儲其他類型的數(shù)據(jù),稱為 Field Type(字段類型)
,如文檔中 mapping-types (映射類型)部分所述。
啟動 Elasticsearch 實(shí)例
在進(jìn)一步討論之前,讓我們啟動一個 Elasticsearch 實(shí)例,我們將使用它來運(yùn)行我們的示例。有多種運(yùn)行 Elasticsearch 實(shí)例的方法:
- 使用托管服務(wù)
- 使用來自 AWS 或 Azure 等云提供商的托管服務(wù)
- 通過在虛擬機(jī)集群中自己安裝 Elasticsearch
- 運(yùn)行 Docker 鏡像
我們將使用來自 Dockerhub 的 Docker 鏡像,這對于我們的演示應(yīng)用程序來說已經(jīng)足夠了。讓我們通過運(yùn)行 Docker run 命令來啟動 Elasticsearch 實(shí)例:
docker run -p 9200:9200
-e "discovery.type=single-node"
docker.elastic.co/elasticsearch/elasticsearch:7.10.0
執(zhí)行此命令將啟動一個 Elasticsearch 實(shí)例,偵聽端口 9200。我們可以通過點(diǎn)擊 URL http://localhost:9200 來驗(yàn)證實(shí)例狀態(tài),并在瀏覽器中檢查結(jié)果輸出:
{
"name" : "8c06d897d156",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "Jkx..VyQ",
"version" : {
"number" : "7.10.0",
...
},
"tagline" : "You Know, for Search"
}
如果我們的 Elasticsearch 實(shí)例啟動成功,應(yīng)該看到上面的輸出。
使用 REST API 進(jìn)行索引和搜索
Elasticsearch 操作通過 REST API 訪問。 有兩種方法可以將文檔添加到索引中:
- 一次添加一個文檔,或者
- 批量添加文檔。
添加單個文檔的 API 接受一個文檔作為參數(shù)。
對 Elasticsearch 實(shí)例的簡單 PUT 請求用于存儲文檔如下所示:
PUT /messages/_doc/1
{
"message": "The Sky is blue today"
}
這會將消息 - “The Sky is blue today”存儲為“messages”的索引中的文檔。
我們可以使用發(fā)送到搜索 REST API 的搜索查詢來獲取此文檔:
GET /messages/search
{
"query":
{
"match": {"message": "blue sky"}
}
}
這里我們發(fā)送一個 match
類型的查詢來獲取匹配字符串“blue sky”的文檔。我們可以通過多種方式指定用于搜索文檔的查詢。Elasticsearch 提供了一個基于 JSON 的 查詢 DSL(Domain Specific Language - 領(lǐng)域特定語言)來定義查詢。
對于批量添加,我們需要提供一個包含類似以下代碼段的條目的 JSON 文檔:
POST /_bulk
{"index":{"_index":"productindex"}}{"_class":"..Product","name":"Corgi Toys .. Car",..."manufacturer":"Hornby"}{"index":{"_index":"productindex"}}{"_class":"..Product","name":"CLASSIC TOY .. BATTERY"...,"manufacturer":"ccf"}
使用 Spring Data 進(jìn)行 Elasticsearch 操作
我們有兩種使用 Spring Data 訪問 Elasticsearch 的方法,如下所示:
- Repositories:我們在接口中定義方法,Elasticsearch 查詢是在運(yùn)行時根據(jù)方法名稱生成的。
- ElasticsearchRestTemplate:我們使用方法鏈和原生查詢創(chuàng)建查詢,以便在相對復(fù)雜的場景中更好地控制創(chuàng)建 Elasticsearch 查詢。
我們將在以下各節(jié)中更詳細(xì)地研究這兩種方式。
創(chuàng)建應(yīng)用程序并添加依賴項(xiàng)
讓我們首先通過包含 web、thymeleaf 和 lombok 的依賴項(xiàng),使用 Spring Initializr 創(chuàng)建我們的應(yīng)用程序。添加 thymeleaf
依賴項(xiàng)以便增加用戶界面。
在 Maven pom.xml
中添加 spring-data-elasticsearch
依賴項(xiàng):
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
</dependency>
連接到 Elasticsearch 實(shí)例
Spring Data Elasticsearch 使用 Java High Level REST Client (JHLC) 連接到 Elasticsearch 服務(wù)器。JHLC 是 Elasticsearch 的默認(rèn)客戶端。我們將創(chuàng)建一個 Spring Bean 配置來進(jìn)行設(shè)置:
@Configuration
@EnableElasticsearch
Repositories(basePackages
= "io.pratik.elasticsearch.repositories")@ComponentScan(basePackages = { "io.pratik.elasticsearch" })
public class ElasticsearchClientConfig extends
AbstractElasticsearchConfiguration {
@Override
@Bean
public RestHighLevelClient elasticsearchClient() {
final ClientConfiguration clientConfiguration =
ClientConfiguration
.builder()
.connectedTo("localhost:9200")
.build();
return RestClients.create(clientConfiguration).rest();
}
}
在這里,我們連接到我們之前啟動的 Elasticsearch 實(shí)例。我們可以通過添加更多屬性(例如啟用 ssl、設(shè)置超時等)來進(jìn)一步自定義連接。
為了調(diào)試和診斷,我們將在 logback-spring.xml
的日志配置中打開傳輸級別的請求/響應(yīng)日志:
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, name = "name")
private String name;
@Field(type = FieldType.Double, name = "price")
private Double price;
@Field(type = FieldType.Integer, name = "quantity")
private Integer quantity;
@Field(type = FieldType.Keyword, name = "category")
private String category;
@Field(type = FieldType.Text, name = "desc")
private String description;
@Field(type = FieldType.Keyword, name = "manufacturer")
private String manufacturer;
...
}
表達(dá)文檔
在我們的示例中,我們將按名稱、品牌、價(jià)格或描述搜索產(chǎn)品。因此,為了將產(chǎn)品作為文檔存儲在 Elasticsearch 中,我們將產(chǎn)品表示為 POJO,并加上 Field 注解以配置 Elasticsearch 的映射,如下所示:
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, name = "name")
private String name;
@Field(type = FieldType.Double, name = "price")
private Double price;
@Field(type = FieldType.Integer, name = "quantity")
private Integer quantity;
@Field(type = FieldType.Keyword, name = "category")
private String category;
@Field(type = FieldType.Text, name = "desc")
private String description;
@Field(type = FieldType.Keyword, name = "manufacturer")
private String manufacturer;
...
}
@Document
注解指定索引名稱。
@Id
注解使注解字段成為文檔的 _id
,作為此索引中的唯一標(biāo)識符。id
字段有 512 個字符的限制。
@Field
注解配置字段的類型。我們還可以將名稱設(shè)置為不同的字段名稱。
在 Elasticsearch 中基于這些注解創(chuàng)建了名為 productindex 的索引。
使用 Spring Data Repository 進(jìn)行索引和搜索
存儲庫提供了使用 finder 方法訪問 Spring Data 中數(shù)據(jù)的最方便的方法。Elasticsearch 查詢是根據(jù)方法名稱創(chuàng)建的。但是,我們必須小心避免產(chǎn)生低效的查詢并給集群帶來高負(fù)載。
讓我們通過擴(kuò)展 ElasticsearchRepository 接口來創(chuàng)建一個 Spring Data 存儲庫接口:
public interface ProductRepository
extends ElasticsearchRepository<Product, String> {
}
此處 ProductRepository
類繼承了 ElasticsearchRepository
接口中包含的 save()
、saveAll()
、find()
和 findAll()
等方法。
索引
我們現(xiàn)在將通過調(diào)用 save()
方法存儲一個產(chǎn)品,調(diào)用 saveAll()
方法來批量索引,從而在索引中存儲一些產(chǎn)品。在此之前,我們將存儲庫接口放在一個服務(wù)類中:
@Service
public class ProductSearchServiceWithRepo {
private ProductRepository productRepository;
public void createProductIndexBulk(final List<Product> products) {
productRepository.saveAll(products);
}
public void createProductIndex(final Product product) {
productRepository.save(product);
}
}
當(dāng)我們從 JUnit 調(diào)用這些方法時,我們可以在跟蹤日志中看到 REST API 調(diào)用索引和批量索引。
搜索
為了滿足我們的搜索要求,我們將向存儲庫接口添加 finder
方法:
public interface ProductRepository
extends ElasticsearchRepository<Product, String> {
List<Product> findByName(String name);
List<Product> findByNameContaining(String name);
List<Product> findByManufacturerAndCategory
(String manufacturer, String category);
}
在使用 JUnit 運(yùn)行 findByName()
方法時,我們可以看到在發(fā)送到服務(wù)器之前在跟蹤日志中生成的 Elasticsearch 查詢:
TRACE Sending request POST /productindex/_search? ..:
Request body: {.."query":{"bool":{"must":[{"query_string":{"query":"apple","fields":["name^1.0"],..}
類似地,通過運(yùn)行findByManufacturerAndCategory()
方法,我們可以看到使用兩個 query_string
參數(shù)對應(yīng)兩個字段——“manufacturer”和“category”生成的查詢:
TRACE .. Sending request POST /productindex/_search..:
Request body: {.."query":{"bool":{"must":[{"query_string":{"query":"samsung","fields":["manufacturer^1.0"],..}},{"query_string":{"query":"laptop","fields":["category^1.0"],..}}],..}},"version":true}
有多種方法命名模式可以生成各種 Elasticsearch 查詢。
使用 ElasticsearchRestTemplate進(jìn)行索引和搜索
當(dāng)我們需要更多地控制我們設(shè)計(jì)查詢的方式,或者團(tuán)隊(duì)已經(jīng)掌握了 Elasticsearch 語法時,Spring Data 存儲庫可能就不再適合。
在這種情況下,我們使用 ElasticsearchRestTemplate。它是 Elasticsearch 基于 HTTP 的新客戶端,取代以前使用節(jié)點(diǎn)到節(jié)點(diǎn)二進(jìn)制協(xié)議的 TransportClient。
ElasticsearchRestTemplate
實(shí)現(xiàn)了接口 ElasticsearchOperations
,該接口負(fù)責(zé)底層搜索和集群操的繁雜工作。
索引
該接口具有用于添加單個文檔的方法 index()
和用于向索引添加多個文檔的 bulkIndex()
方法。此處的代碼片段顯示了如何使用 bulkIndex()
將多個產(chǎn)品添加到索引“productindex
”:
@Service
@Slf4j
public class ProductSearchService {
private static final String PRODUCT_INDEX = "productindex";
private ElasticsearchOperations elasticsearchOperations;
public List<String> createProductIndexBulk
(final List<Product> products) {
List<IndexQuery> queries = products.stream()
.map(product->
new IndexQueryBuilder()
.withId(product.getId().toString())
.withObject(product).build())
.collect(Collectors.toList());;
return elasticsearchOperations
.bulkIndex(queries,IndexCoordinates.of(PRODUCT_INDEX));
}
...
}
要存儲的文檔包含在 IndexQuery
對象中。bulkIndex()
方法將 IndexQuery
對象列表和包含在 IndexCoordinates
中的 Index
名稱作為輸入。當(dāng)我們執(zhí)行此方法時,我們會獲得批量請求的 REST API 跟蹤:
Sending request POST /_bulk?timeout=1m with parameters:
Request body: {"index":{"_index":"productindex","_id":"383..35"}}{"_class":"..Product","id":"383..35","name":"New Apple..phone",..manufacturer":"apple"}
..
{"_class":"..Product","id":"d7a..34",.."manufacturer":"samsung"}
接下來,我們使用 index()
方法添加單個文檔:
@Service
@Slf4j
public class ProductSearchService {
private static final String PRODUCT_INDEX = "productindex";
private ElasticsearchOperations elasticsearchOperations;
public String createProductIndex(Product product) {
IndexQuery indexQuery = new IndexQueryBuilder()
.withId(product.getId().toString())
.withObject(product).build();
String documentId = elasticsearchOperations
.index(indexQuery, IndexCoordinates.of(PRODUCT_INDEX));
return documentId;
}
}
跟蹤相應(yīng)地顯示了用于添加單個文檔的 REST API PUT
請求。
Sending request PUT /productindex/_doc/59d..987..:
Request body: {"_class":"..Product","id":"59d..87",..,"manufacturer":"dell"}
搜索
ElasticsearchRestTemplate
還具有 search()
方法,用于在索引中搜索文檔。此搜索操作類似于 Elasticsearch 查詢,是通過構(gòu)造 Query
對象并將其傳遞給搜索方法來構(gòu)建的。
Query
對象具有三種變體 - NativeQueryy
、StringQuery
和 CriteriaQuery
,具體取決于我們?nèi)绾螛?gòu)造查詢。讓我們構(gòu)建一些用于搜索產(chǎn)品的查詢。
NativeQuery
NativeQuery
為使用表示 Elasticsearch 構(gòu)造(如聚合、過濾和排序)的對象構(gòu)建查詢提供了最大的靈活性。這是用于搜索與特定制造商匹配的產(chǎn)品的 NativeQuery
:
@Service
@Slf4j
public class ProductSearchService {
private static final String PRODUCT_INDEX = "productindex";
private ElasticsearchOperations elasticsearchOperations;
public void findProductsByBrand(final String brandName) {
QueryBuilder queryBuilder =
QueryBuilders
.matchQuery("manufacturer", brandName);
Query searchQuery = new NativeSearchQueryBuilder()
.withQuery(queryBuilder)
.build();
SearchHits<Product> productHits =
elasticsearchOperations
.search(searchQuery,
Product.class,
IndexCoordinates.of(PRODUCT_INDEX));
}
}
在這里,我們使用 NativeSearchQueryBuilder
構(gòu)建查詢,該查詢使用 MatchQueryBuilder
指定包含字段“制造商”的匹配查詢。
StringQuery
StringQuery 通過允許將原生 Elasticsearch 查詢用作 JSON 字符串來提供完全控制,如下所示:
@Service
@Slf4j
public class ProductSearchService {
private static final String PRODUCT_INDEX = "productindex";
private ElasticsearchOperations elasticsearchOperations;
public void findByProductName(final String productName) {
Query searchQuery = new StringQuery(
"{"match":{"name":{"query":""+ productName + ""}}}"");
SearchHits<Product> products = elasticsearchOperations.search(
searchQuery,
Product.class,
IndexCoordinates.of(PRODUCT_INDEX_NAME));
...
}
}
在此代碼片段中,我們指定了一個簡單的 match
查詢,用于獲取具有作為方法參數(shù)發(fā)送的特定名稱的產(chǎn)品。
CriteriaQuery
使用 CriteriaQuery
,我們可以在不了解 Elasticsearch 任何術(shù)語的情況下構(gòu)建查詢。查詢是使用帶有 Criteria
對象的方法鏈構(gòu)建的。每個對象指定一些用于搜索文檔的標(biāo)準(zhǔn):
@Service
@Slf4j
public class ProductSearchService {
private static final String PRODUCT_INDEX = "productindex";
private ElasticsearchOperations elasticsearchOperations;
public void findByProductPrice(final String productPrice) {
Criteria criteria = new Criteria("price")
.greaterThan(10.0)
.lessThan(100.0);
Query searchQuery = new CriteriaQuery(criteria);
SearchHits<Product> products = elasticsearchOperations
.search(searchQuery,
Product.class,
IndexCoordinates.of(PRODUCT_INDEX_NAME));
}
}
在此代碼片段中,我們使用 CriteriaQuery
形成查詢以獲取價(jià)格大于 10.0
且小于 100.0
的產(chǎn)品。
構(gòu)建搜索應(yīng)用程序
我們現(xiàn)在將向我們的應(yīng)用程序添加一個用戶界面,以查看產(chǎn)品搜索的實(shí)際效果。用戶界面將有一個搜索輸入框,用于按名稱或描述搜索產(chǎn)品。輸入框?qū)⒕哂凶詣油瓿晒δ?,以顯示基于可用產(chǎn)品的建議列表,如下所示:
我們將為用戶的搜索輸入創(chuàng)建自動完成建議。然后根據(jù)與用戶輸入的搜索文本密切匹配的名稱或描述搜索產(chǎn)品。我們將構(gòu)建兩個搜索服務(wù)來實(shí)現(xiàn)這個用例:
- 獲取自動完成功能的搜索建議
- 根據(jù)用戶的搜索查詢處理搜索產(chǎn)品的搜索
服務(wù)類 ProductSearchService 將包含搜索和獲取建議的方法。
GitHub 存儲庫中提供了帶有用戶界面的成熟應(yīng)用程序。
建立產(chǎn)品搜索索引
productindex
與我們之前用于運(yùn)行 JUnit 測試的索引相同。我們將首先使用 Elasticsearch REST API 刪除 productindex
,以便在應(yīng)用程序啟動期間使用從我們的 50 個時尚系列產(chǎn)品的示例數(shù)據(jù)集中加載的產(chǎn)品創(chuàng)建新的 productindex
:
curl -X DELETE http://localhost:9200/productindex
如果刪除操作成功,我們將收到消息 {"acknowledged": true}
。
現(xiàn)在,讓我們?yōu)閹齑嬷械漠a(chǎn)品創(chuàng)建一個索引。我們將使用包含 50 種產(chǎn)品的示例數(shù)據(jù)集來構(gòu)建我們的索引。這些產(chǎn)品在 CSV 文件中被排列為單獨(dú)的行。
每行都有三個屬性 - id、name 和 description。我們希望在應(yīng)用程序啟動期間創(chuàng)建索引。請注意,在實(shí)際生產(chǎn)環(huán)境中,索引創(chuàng)建應(yīng)該是一個單獨(dú)的過程。我們將讀取 CSV 的每一行并將其添加到產(chǎn)品索引中:
@SpringBootApplication
@Slf4j
public class ProductsearchappApplication {
...
@PostConstruct
public void buildIndex() {
esOps.indexOps(Product.class).refresh();
productRepo.saveAll(prepareDataset());
}
private Collection<Product> prepareDataset() {
Resource resource = new ClassPathResource("fashion-products.csv");
...
return productList;
}
}
在這個片段中,我們通過從數(shù)據(jù)集中讀取行并將這些行傳遞給存儲庫的 saveAll()
方法以將產(chǎn)品添加到索引中來進(jìn)行一些預(yù)處理。在運(yùn)行應(yīng)用程序時,我們可以在應(yīng)用程序啟動中看到以下跟蹤日志。
...Sending request POST /_bulk?timeout=1m with parameters:
Request body: {"index":{"_index":"productindex"}}{"_class":"io.pratik.elasticsearch.productsearchapp.Product","name":"Hornby 2014 Catalogue","description":"Product Desc..talogue","manufacturer":"Hornby"}{"index":{"_index":"productindex"}}{"_class":"io.pratik.elasticsearch.productsearchapp.Product","name":"FunkyBuys..","description":"Size Name:Lar..& Smoke","manufacturer":"FunkyBuys"}{"index":{"_index":"productindex"}}.
...
使用多字段和模糊搜索搜索產(chǎn)品
下面是我們在方法 processSearch()
中提交搜索請求時如何處理搜索請求:
@Service
@Slf4j
public class ProductSearchService {
private static final String PRODUCT_INDEX = "productindex";
private ElasticsearchOperations elasticsearchOperations;
public List<Product> processSearch(final String query) {
log.info("Search with query {}", query);
// 1. Create query on multiple fields enabling fuzzy search
QueryBuilder queryBuilder =
QueryBuilders
.multiMatchQuery(query, "name", "description")
.fuzziness(Fuzziness.AUTO);
Query searchQuery = new NativeSearchQueryBuilder()
.withFilter(queryBuilder)
.build();
// 2. Execute search
SearchHits<Product> productHits =
elasticsearchOperations
.search(searchQuery, Product.class,
IndexCoordinates.of(PRODUCT_INDEX));
// 3. Map searchHits to product list
List<Product> productMatches = new ArrayList<Product>();
productHits.forEach(searchHit->{
productMatches.add(searchHit.getContent());
});
return productMatches;
}...
}
在這里,我們對多個字段執(zhí)行搜索 - 名稱和描述。 我們還附加了 fuzziness()
來搜索緊密匹配的文本以解釋拼寫錯誤。
使用通配符搜索獲取建議
接下來,我們?yōu)樗阉魑谋究驑?gòu)建自動完成功能。 當(dāng)我們在搜索文本字段中輸入內(nèi)容時,我們將通過使用搜索框中輸入的字符執(zhí)行通配符搜索來獲取建議。
我們在 fetchSuggestions()
方法中構(gòu)建此函數(shù),如下所示:
@Service
@Slf4j
public class ProductSearchService {
private static final String PRODUCT_INDEX = "productindex";
public List<String> fetchSuggestions(String query) {
QueryBuilder queryBuilder = QueryBuilders
.wildcardQuery("name", query+"*");
Query searchQuery = new NativeSearchQueryBuilder()
.withFilter(queryBuilder)
.withPageable(PageRequest.of(0, 5))
.build();
SearchHits<Product> searchSuggestions =
elasticsearchOperations.search(searchQuery,
Product.class,
IndexCoordinates.of(PRODUCT_INDEX));
List<String> suggestions = new ArrayList<String>();
searchSuggestions.getSearchHits().forEach(searchHit->{
suggestions.add(searchHit.getContent().getName());
});
return suggestions;
}
}
我們以搜索輸入文本的形式使用通配符查詢,并附加 * 以便如果我們輸入“red”,我們將獲得以“red”開頭的建議。我們使用 withPageable() 方法將建議的數(shù)量限制為 5??梢栽诖颂幙吹秸谶\(yùn)行的應(yīng)用程序的搜索結(jié)果的一些屏幕截圖:
結(jié)論
在本文中,我們介紹了 Elasticsearch 的主要操作——索引文檔、批量索引和搜索——它們以 REST API 的形式提供。Query DSL 與不同分析器的結(jié)合使搜索變得非常強(qiáng)大。
Spring Data Elasticsearch 通過使用 Spring Data Repositories 或 ElasticsearchRestTemplate
提供了方便的接口來訪問應(yīng)用程序中的這些操作。
我們最終構(gòu)建了一個應(yīng)用程序,在其中我們看到了如何在接近現(xiàn)實(shí)生活的應(yīng)用程序中使用 Elasticsearch 的批量索引和搜索功能。
- 本文譯自: Using Elasticsearch with Spring Boot - Reflectoring
- 有關(guān) ELK 套件請參考: ELK 教程 - 發(fā)現(xiàn)、分析和可視化你的數(shù)據(jù)
本文摘自 :https://blog.51cto.com/c