用 Mahout 和 Elasticsearch 实现推荐系统

图片 24

如果您使用过 Apache Lucene
或 Apache Solr,就会知道它们的使用体验非常有趣。尤其在您需要扩展基于
Lucene 或 Solr
的解决方案时,您就会了解 Elasticsearch 项目背后的动机。Elasticsearch(构建于
Lucene
之上)在一个容易管理的包中提供了高性能的全文搜索功能,支持开箱即用地集群化扩展。您可以通过标准的
REST
API
或从特定于编程语言的客户端库与 Elasticsearch 进行交互。

原文地址

原文地址:http://www.dotnetcurry.com/aspnet/1354/elastic-search-kibana-in-docker-dotnet-core-app

本教程将展示 Elasticsearch 的实际工作原理。首先从命令行访问该 REST API
来了解它的基本信息。然后设置一个本地 Elasticsearch
服务器,并从一个简单的 Java
应用程序与它交互。请参见 下载 部分,获取有关的示例代码。

本文内容

  • 软件
  • 步骤
  • 控制相关性
  • 总结
  • 参考资料

本文介绍如何用带 Apache Mahout 的 MapR
Sandbox for Hadoop

Elasticsearch
搭建推荐引擎,只需要很少的代码。

This tutorial will give step-by-step instructions on how to:

  • 使用的电影评分数据位于
  • 使用 Apache Mahout 的协同过滤(collaborative
    filtering)搭建和训练机器学习模型
  • 使用 Elasticsearch 的搜索技术简化推荐系统的开发

想要轻松地通过许多不同的方式查询数据,甚至是从未预料到的方式?想要以多种方式可视化日志?同时支持基于时间、文本和其他类型的即时过滤器?
借助于 ** Elastic ** stack
的卓越性能和可扩展方式的优点,我们将通过两个示例轻松实现。

前提条件

要理解本教程的所有示例,需要在您的系统上安装
Elasticsearch。下载针对您的平台的 最新 Elastic Search
程序包。将该包解压到一个方便的位置。在
UNIX 或 Linux 上,通过以下命令启动该实例:

/elastic-search-dir/bin/elasticsearch

在 Windows 上,运行

/elastic-search-dir/bin/elasticsearch.bat

在看到日志消息 started 时,该节点已准备好接受请求。

对于 Java
示例,还需要安装 Eclipse 和 Apache
Maven。如果您的系统上还没有它们,请下载和安装它们。

您还需要 cURL。在 Microsoft Windows 上,我使用 Git
Bash shell
来运行 cURL。

软件


该文章运行在 MapReduce Sandbox。还要求在 Sandbox 上安装 Elasticsearch 和
Mahout。

  • 从 下载 10M MovieLens
    数据
  • 安装 Mahout
  • 安装 Elasticsearch

本文由 DNC Magazine for Developers and Architects 发布。
从这里下载此杂志
*[PDF] 或
*免费订阅本杂志
下载所有以前和当前的版本版本。

使用 cURL 执行 REST 命令

可以对 Elasticsearch 发出 cURL 请求,这样很容易从命令行 shell
体验该框架。

“Elasticsearch
是无模式的。它可以接受您提供的任何命令,并处理它以供以后查询。”

Elasticsearch
是无模式的,这意味着它可以接受您提供的任何命令,并处理它以供以后查询。Elasticsearch
中的所有内容都被存储为文档,所以您的第一个练习是存储一个包含歌词的文档。首先创建一个索引,它是您的所有文档类型的容器
— 类似于 MySQL
等关系数据库中的数据库。然后,将一个文档插入该索引中,以便可以查询该文档的数据。

步骤


在这篇文章中,我将介绍流行的搜索引擎 Elasticsearch,其配套的可视化应用
Kibana,并展示如何对.NET核心可以轻松地与 Elastic stack 整合在一块。

创建一个索引

Elasticsearch 命令的一般格式是:REST VERBHOST:9200/index/doc-type
其中 REST VERB 是 PUTGET 或 DELETE。(使用
cURL -X 动词前缀来明确指定 HTTP 方法。)

要创建一个索引,可在您的 shell 中运行以下命令:

curl -XPUT "http://localhost:9200/music/"

Step 1: 索引(Index)电影元数据到 Elasticsearch

在 Elasticsearch
中,默认情况下,文档的所有字段都会被索引。最简单的文档是只有一级 JSON
结构。文档包含在索引中,文档中的类型告诉 Elasticsearch
如何解释文档中的字段。

你可以把 Elasticsearch
的索引看做是关系型数据库中的数据库实例,而类型看做是数据库表,字段看做表定义(但是这个字段,在
Elasticsearch 中的意义更广泛),文档看做是表的某行记录。

针对本例,文档类型是
film。并具有如下字段:电影ID(id)、标题(title)、上映时间(year)、电影类型/标签(genre,基因)、指示(indicators)、indicators数组的数量(numFields):

{

 "id": "65006",

 "title": "Impulse",

 "year": "2008",

 "genre": ["Mystery","Thriller"],

 "indicators": ["154","272",”154","308", "535", "583", "593", "668", "670", "680", "702", "745"],

 "numFields": 12

}

通过 9200 端口访问 Elasticsearch RESTful API 与其通信,或者命令行用 curl
命令。参看 Elasticsearch REST
interface
和 Elasticsearch 101
tutorial。

curl -X<VERB> 'http://<HOST>/<PATH>?<QUERY_STRING>' -d '<BODY>'

使用 Elasticsearch’s REST API 的 put
mapping
命令可以定义文档的类型。下面的请求在 bigmovie 索引中创建名为 film
的映射(mapping)。该映射定义一个类型为 integer 类型的 numFields
字段。默认情况,所有字段都被存储并索引,整型也如此。

curl -XPUT 'http://localhost:9200/bigmovie' -d '

{

  "mappings": {

    "film" : {

      "properties" : {

        "numFields" : { "type" :   "integer" }

      }

    }

  }

}'

电影信息包含在 movies.dat
文件中。文件的每行表示一部电影,字段的含义如下所示:

MovieID::Title::Genres

例如:

65006::Impulse (2008)::Mystery|Thriller

图片 1

图 1
电影《冲动(Impulse)》(2008)、类型“悬疑/惊悚”

下面 Python 脚本把 movies.dat 文件中的数据转换成 JSON 格式,以便导入
Elasticsearch:

import re

import json

count=0

with open('movies.dat','rb') as csv_file:

   content = csv_file.readlines()

   for line in content:

        fixed = re.sub("::", "t", line).rstrip().split("t")

   if len(fixed)==3:

          title = re.sub(" (.*)$", "", re.sub('"','', fixed[1]))

          genre = fixed[2].split('|')

          print '{ "create" : { "_index" : "bigmovie", "_type" : "film",

          "_id" : "%s" } }' %  fixed[0]

          print '{ "id": "%s", "title" : "%s", "year":"%s" , "genre":%s }'

          % (fixed[0],title, fixed[1][-5:-1], json.dumps(genre))

运行该 Python 文件,转换结果输出到 index.json:

$ python index.py > index.json

将产生如下 Elasticsearch 需要的格式:

{ "create" : { "_index" : "bigmovie", "_type" : "film", "_id" : "1" } }

{ "id": "1", "title" : "Toy Story", "year":"1995" , "genre":["Adventure", "Animation", "Children", "Comedy", "Fantasy"] }

{ "create" : { "_index" : "bigmovie", "_type" : "film", "_id" : "2" } }

{ "id": "2", "title" : "Jumanji", "year":"1995" , "genre":["Adventure", "Children", "Fantasy"] }

文件中的每行创建索引和类型,并添加电影信息。这是利用 Elasticsearch
批量导入数据。

Elasticsearch 批量 API 可以执行对索引的操作,用同一个 API,不同的 http
请求(如 get、put、post、delete)。下面命令让 Elasticsearch 批量加载
index.json 文中的内容:

curl -s -XPOST localhost:9200/_bulk --data-binary @index.json; echo

加载电影信息后,你就可以利用 REST API 进行查询了。你也可以使用 Chrome 的
Elasticsearch 插件——Sense
进行操作(Kibana 4 提供的一个插件)。示例如下所示:

图片 2

下面是检索 id 为 1237的电影:

图片 3

图片 4

模式可选

尽管 Elasticsearch 是无模式的,但它在幕后使用了
Lucene,后者使用了模式。不过 Elasticsearch
为您隐藏了这种复杂性。实际上,您可以将 Elasticsearch
文档类型简单地视为子索引或表名称。但是,如果您愿意,可以指定一个模式,所以您可以将它视为一种模式可选的数据存储。

Step 2: 使用 Mahout 从用户评分数据中创建 Movie indicators

评分包含在 ratings.dat
文件中。该文件每行表示某个用户对某个电影的评分,格式如下所示:

UserID::MovieID::Rating::Timestamp

例如:

71567::2294::5::912577968

71567::2338::2::912578016

ratings.data 文件用 “::” 做分隔符,转换成 tab 后 Mahout 才能使用。可以用
sed 命令把 :: 替换成 tab:

sed -i 's/::/t/g' ratings.dat

该命令打开文件,把”::” 替换成”t” 后,重新保存。Updates are only
supported with MapR NFS and thus this command probably won’t work on
other NFS-on-Hadoop implementations. MapR Direct Access NFS allows files
to be modified (supports random reads and writes) and accessed via
mounting the Hadoop cluster over NFS.

sed 命令会产生如下格式的内容,该格式可以作为 Mahout 的输入:

71567    2294    5    912580553

71567    2338    2    912580553

一般格式为:item1 item2 rating timestamp,即“物品1 物品2
评分”,本例不使用 timestamp。

启动 Mahout 物品相似度(itemsimilarity)作业,命令如下所示:

 mahout itemsimilarity 

  --input /user/user01/mlinput/ratings.dat 

  --output /user/user01/mloutput 

  --similarityClassname SIMILARITY_LOGLIKELIHOOD 

  --booleanData TRUE 

  --tempDir /user/user01/temp

The argument “-s SIMILARITY_LOGLIKELIHOOD” tells the recommender to use
the Log Likelihood Ratio (LLR) method for determining which items
co-occur anomalously often and thus which co-occurrences can be used as
indicators of preference. 相似度默认是 0.9;this can be adjusted based
on the use case with the –threshold parameter, which will discard pairs
with lower similarity (the default is a fine choice). Mahout
通过启动很多 Hadoop MapReduce
作业计算推荐,最后将产生输出文件,该文件位于 /user/user01/mloutput
目录。输出文件格式如下所示

64957   64997   0.9604835425701245
64957   65126   0.919355104432831
64957   65133   0.9580439772229588

一般格式为:item1id item2id similarity,即“物品1 物品2 相似度”。

Elasticsearch和.Net Core

插入一个文档

要在 /music 索引下创建一个类型,可插入一个文档。在第一个示例中,您的文档包含数据(包含一行)“Deck
the Halls” 的歌词,这是一首最初由威尔士诗人 John Ceirog Hughes 于 1885
年编写的传统的圣诞歌曲。

要将包含 “Deck the Halls”
的文档插入索引中,可运行以下命令(将该命令和本教程的其他 cURL
命令都键入到一行中):

curl -XPUT "http://localhost:9200/music/songs/1" -d '
{ "name": "Deck the Halls", "year": 1885, "lyrics": "Fa la la la la" }'

前面的命令使用 PUT 动词将一个文档添加到 /songs 文档类型,并为该文档分配
ID 1。URL 路径显示为 index/doctype/ID

Step 3: 添加 Movie indicators 到 Elasticsearch 的电影文档

下一步,我们从上面的输出文件添加 indicators 到 Elasticsearch 的 film
文档。例如,把电影的 indicators 放到 indicators 字段:

{

  "id": "65006",

  "title": "Impulse",

  "year": "2008",

  "genre": ["Mystery","Thriller"],

  "indicators": ["1076", "1936", "2057", "2204"],

  "numFields": 4

}

左面的表显示文档中包含 indicator 的内容,右边的表显示哪些文档包含某个
indicator:

图片 5

图 2 文档与 indicator

如果想要检索 indicator 为 1237551 的电影,那么本例将返回 id
为 8298 的文档(电影)。如果检索 1237551,那么将返回 id 为
8298、3 和 64418 的电影。

图片 6

下面脚本将读取 Mahout 的输出文件 part-r-00000,为每部电影创建 indicator
数组,然后输出 JSON 文件,用该文件更新 Elasticsearch bigmovie 索引的
film 类型的 indicator 字段。

import fileinput

from string import join

import json

import csv

import json

### read the output from MAHOUT and collect into hash ###

with open('/user/user01/mloutput/part-r-00000','rb') as csv_file:

    csv_reader = csv.reader(csv_file,delimiter='t')

    old_id = ""

    indicators = []

    update = {"update" : {"_id":""}}

    doc = {"doc" : {"indicators":[], "numFields":0}}

    for row in csv_reader:

        id = row[0]

        if (id != old_id and old_id != ""):

            update["update"]["_id"] = old_id

            doc["doc"]["indicators"] = indicators

            doc["doc"]["numFields"] = len(indicators)

            print(json.dumps(update))

            print(json.dumps(doc))

            indicators = [row[1]]

        else:

            indicators.append(row[1])

        old_id = id

下面命令会执行 update.py 的 Python 脚本,并输出 update.json:

$ python update.py > update.json

上面 Python 脚本将创建如下内容的文件:

{"update": {"_id": "1"}}

{"doc": {"indicators": ["75", "118", "494", "512", "609", "626", "631", "634", "648", "711", "761", "810", "837", "881", "910", "1022", "1030", "1064", "1301", "1373", "1390", "1588", "1806", "2053", "2083", "2090", "2096", "2102", "2286", "2375", "2378", "2641", "2857", "2947", "3147", "3429", "3438", "3440", "3471", "3483", "3712", "3799", "3836", "4016", "4149", "4544", "4545", "4720", "4732", "4901", "5004", "5159", "5309", "5313", "5323", "5419", "5574", "5803", "5841", "5902", "5940", "6156", "6208", "6250", "6383", "6618", "6713", "6889", "6890", "6909", "6944", "7046", "7099", "7281", "7367", "7374", "7439", "7451", "7980", "8387", "8666", "8780", "8819", "8875", "8974", "9009", "25947", "27721", "31660", "32300", "33646", "40339", "42725", "45517", "46322", "46559", "46972", "47384", "48150", "49272", "55668", "63808"], "numFields": 102}}

{"update": {"_id": "2"}}

{"doc": {"indicators": ["15", "62", "153", "163", "181", "231", "239", "280", "333", "355", "374", "436", "473", "485", "489", "502", "505", "544", "546", "742", "829", "1021", "1474", "1562", "1588", "1590", "1713", "1920", "1967", "2002", "2012", "2045", "2115", "2116", "2139", "2143", "2162", "2296", "2338", "2399", "2408", "2447", "2616", "2793", "2798", "2822", "3157", "3243", "3327", "3438", "3440", "3477", "3591", "3614", "3668", "3802", "3869", "3968", "3972", "4090", "4103", "4247", "4370", "4467", "4677", "4686", "4846", "4967", "4980", "5283", "5313", "5810", "5843", "5970", "6095", "6383", "6385", "6550", "6764", "6863", "6881", "6888", "6952", "7317", "8424", "8536", "8633", "8641", "26870", "27772", "31658", "32954", "33004", "34334", "34437", "39419", "40278", "42011", "45210", "45447", "45720", "48142", "50347", "53464", "55553", "57528"], "numFields": 106}}

在命令行,用 curl 命令调用 Elasticsearch REST
bulk
请求,把该文件 update.json 作为输入,就可以更新 indicator 字段:

$ curl -s -XPOST localhost:9200/bigmovie/film/_bulk --data-binary @update.json; echo

我们将开始探索 Elasticsearch 的 REST API
,通过索引和查询某些数据。接着,我们将使用Elasticsearch官方的 .Net API
完成类似的练习。一旦熟悉 Elasticsearch 及其 API 后,我们将使用 .Net Core
创建一个日志模块,并将数据发送到 Elasticsearch
。Kibana紧随其中,以有趣的方式可视化 Elasticsearch 的索引数据。
我迫切希望你会认为这篇文章十分有趣,并且想要了解更多关于Elastic的强大之处。

查看文档

要查看该文档,可使用简单的 GET 命令:

curl -XGET "http://localhost:9200/music/songs/1"

Elasticsearch 使用您之前 PUT 进索引中的 JSON 内容作为响应:

{"_index":"music","_type":"songs","_id":"1","_version":1,"found":true,"_source":
{ "name": "Deck the Halls", "year": 1885, "lyrics": "Fa la la la la" }}

Step 4: 检索 Film 索引的 indicator 字段进行推荐

现在,你就可以检索 film 的 indicator
字段进行查询并推荐。例如,某人喜欢电影 1237 和
551,你想推荐类似的电影,可以执行如下 Elasticsearch
查询获得推荐,将返回indicator 数组为 1237 和 551 的电影,即
1237=Seventh Seal(第七封印),551=Nightmare Before
Christmas(圣诞夜惊魂)

curl 'http://localhost:9200/bigmovie/film/_search?pretty' -d '

{

  "query": {

    "function_score": {

      "query": {

         "bool": {

           "must": [ { "match": { "indicators":"1237 551"} } ],

           "must_not": [ { "ids": { "values": ["1237", "551"] } } ]

         }

      },

      "functions":[ {"random_score": {"seed":"48" } } ],

      "score_mode":"sum"

    }

  },

  "fields":["_id","title","genre"],

  "size":"8"

}'

上面查询 indicator 为 1237 或 551,并且不是 1237 或 551
的电影。下面示例使用 Sense 插件进行查询,右边是检索结果,推荐结果是 “A
Man Named Pearl(这个是纪录片)” 和 “Used People(寡妇三弄)”。

图片 7

本文假设您已经了解 C#和 REST API 的基本知识。使用 Visual
Studio,Postman 和 Docker 等工具,但您可以轻松使用 VS Code 和 Fiddler
等替代方案。

更新文档

如果您认识到日期写错了,并想将它更改为 1886
怎么办?可运行以下命令来更新文档:

curl -XPUT "http://localhost:9200/music/lyrics/1" -d '{ "name": 
"Deck the Halls", "year": 1886, "lyrics": "Fa la la la la" }'

因为此命令使用了相同的唯一 ID 1,所以该文档会被更新。

控制相关性

全文检索引擎根据相关度排序,Elasticsearch 用 _score
字段表示文档的相关度分数(relevance
score)。function_score
允许你查询时修改该分数。random_score
用一个种子变量使用散列生成分数。Elasticsearch
查询如下所示,random_score 函数用于把变量添加到检索结果,以便完成
dithering:

  "query": {

    "function_score": {

      "query": {

         "bool": {

           "must": [ { "match": { "indicators":"1237 551"} } ],

           "must_not": [ { "ids": { "values": ["1237", "551"] } } ]

         }

      },

      "functions":[ {"random_score": {"seed":"48" } } ],

      "score_mode":"sum"

    }

  }

相关性抖动(dithering)有意地包含排名靠,但相关性较低的结果,以便拓展训练数据,提供给推荐引擎。如果没有
dithering,那么明天的训练数据仅仅是教模型今天已经知道的事情。增加
dithering, 会帮助拓展推荐模型。如果模型给出的答案接近优秀的,那么
dithering 可以帮助找到正确答案。有效的 dithering
会减少今天的准确性,而改进明天的训练数据(和未来的性能,算法的准确性也属于性能的范畴),换句话说,为了让将来的推荐准确,需要减少过去对将来的影响。

Elasticsearch – 简介#

Elasticsearch
作为核心的部分,是一个具有强大索引功能的文档存储库,并且可以通过 REST
API 来搜索数据。它使用 Java 编写,基于 Apache
Lucene,尽管这些细节隐藏在
API 中。
通过被索引的字段,可以用许多不同的聚合方式找到任何被存储(索引)的文档。
但是,ElasticSearch不仅仅只提供对这些被索引文档的强大搜索功能。
快速、分布式、水平扩展,支持实时文档存储和分析,支持数百台服务器和 PB
级索引数据。同时作为 Elastic stack (aka ELK) 的核心,提供了诸如
LogStash、Kibana 和更多的强大应用。
Kibana 是 Elasticsearch
中专门提供强有力的可视化查询Web应用程序。使用Kibana,能非常简单地为
Elasticsearch 中索引的数据创建查询、图表和仪表盘。
Elasticsearch开放了一个 REST API,你会发现许多文档示例是 HTTP
调用,你可以尝试使用 curl 或 postman 等工具。当然,这个 API
的客户端已经用许多不同的语言编写,包括.Net、Java、Python、Ruby和JavaScript等。
如果你想阅读更多,Elasticsearch
官方网站
可能是最好的地方。

删除文档(但暂时不要删除)

暂时不要删除该文档,知道如何删除它就行了:

curl -XDELETE "http://localhost:9200/music/lyrics/1"

总结


We showed in this tutorial how to use Apache Mahout and Elasticsearch
with the MapR Sandbox to build a basic recommendation engine. You can go
beyond a basic recommender and get even better results with a few simple
additions to the design to add cross recommendation of items, which
leverages a variety of interactions and items for making
recommendations. You can find more information about these technologies
here:

Docker是在本地运行的最简方式#

在这篇文章中,我们需要先连接到一个 Elasticsearch
(和后面的Kibana)的服务器。如果您已经有一个在本地运行或可以使用的服务器,那很好。否则需要先搭建一个服务器。
您可以选择在您的本地机器或可以使用的 VM 或服务器中下载和安装
Elasticsearch 和 Kibana 。不过,建议您使用最简单最纯粹的方式,使用Docker
搭建 Elasticsearch 和 Kibana 。
您可以直接运行以下命令,获取包含Elasticsearch和Kibana的容器。

docker run -it --rm -p 9200:9200 -p 5601:5601 --name esk nshou/elasticsearch-kibana
  • -it 表示以交互模式启动容器,并附加到终端。
  • –rm 表示从终端退出后,容器将被移除。
  • -p 将容器中的端口映射到主机中的端口
  • –name 给容器一个名称,当您不使用的情况下可以用 –rm 手动停止/删除
  • nshou/elasticsearch-kibana 是 Docker
    Hub中的一个镜像的名称,已经有人帮你准备好了Elasticsearch和Kibana
  • 如果你喜欢在后台运行的话,你可以使用参数-d 代替 –it
    –rm,并且手动停止/删除容器。

在同一个容器中运行多个应用程序,就像我们现在这种做法,非常适用本文,但不是推荐用于生产容器!

您应该意识到,一旦你删除容器,你的数据就会消失(一旦你使用-rm选项就删除它了)。虽然有利于本地实验,但在实际环境中,如果您不想丢失数据,请参照
“data container” 模式。

Docker是一个很棒的工具,我鼓励你更多地了解它,特别是如果你想做更重要的事情,而不仅仅是跟随本文,在本地快速搭建
Elasticsearch 服务器。在之前的文章
Building DockNetFiddle using
Docker and .NET
Core

中已经对 .NET Core 搭配 Docker 有很好的介绍。

只需打开
http://localhost:9200

http://localhost:5600
,检查Elasticsearch 和 Kibana 是否都可以使用。(如果您使用docker
toolbox,请使用托管Docker的虚拟机ip替换localhost,您可以在命令行中运行
docker-machine env default )。

图片 8

在docker中运行 Elasticsearch

图片 9

kibana也准备好了

从文件插入文档

这是另一个技巧。您可以使用一个文件的内容来从命令行插入文档。尝试此方法,添加另一首针对传统歌曲
“Ballad of Casey Jones” 的文档。将清单 1 复制到一个名为 caseyjones.json
的文件中;也可以使用示例代码包中的 caseyjones.json
文件(参见 下载)。将该文件放在任何方便对它运行
cURL 命令的地方。(在下载的代码中,该文件位于根目录中。)

参考资料


若想学习更多关于推荐引擎的组件和逻辑,参看 “An Inside Look at the
Components of a Recommendation
Engine”,该文章详细描述了推荐引擎的架构、Mahout
协同过滤(collaborative filtering)和 Elasticsearch 检索引擎。

更多关于推荐引擎、机器学习和 Elasticsearch 的资源,如下所示:

  • 实践机器学习: Innovations in
    Recommendations
  • An Invitation to Practical Machine
    Learning
  • 构建一个简单的推荐器
  • Jump-Start Your Recommendation Engine on
    Hadoop
  • MapR 快速解决方案 –
    推荐引擎演示
  • Elasticsearch: The Definitive
    Guide
  • Mahout
    基于物品推荐的介绍

Tutorial Category Reference:

  • ElasticSearch
  • Mahout

在 Elasticsearch 中索引和查询

在我们开始编写任何 .Net 代码之前,我们先了解一下一些基本知识。先在
Elasticsearch
索引一些文档(类似于存到数据库),以便我们对它们运行不同的查询。

*在这里,我将使用Postman向我们的 Elasticsearch 服务器发送 HTTP
请求,但您可以使用任何其他类似的工具,如
[Fiddler]()
*curl

我们要做的第一件事是请求 Elasticsearch 创建一个新的索引
(译者语:类似创建一个表) 并索引一些文档 (译者语:类似于在数据中插入数据)
。这类似于将数据存储在表/集合中,主要区别(和目的)是让 Elasticsearch
集群 (这里只是一个节点) 可以分析和搜索文档数据。
被索引的文档在 Elasticsearch
中以索引和类型进行组织。以往,被拿来和数据库表做对比,往往会令人困惑。如这篇文章所述,索引由Lucene处理,在分布式跨
分片 中,与类型紧密地联系在一起。
发送以下两个请求以创建索引,并在该索引中插入文档 (请记住
toolbox,如果使用docker ,请使用托管Docker的虚拟机ip而不是localhost) :

  • 创建一个名为 “default” 的新索引。

PUT localhost:9200/default
  • 在 “default”
    索引中索引文档。请注意,我们需要知道我们存储哪种类型的文档(”product”)和该文档的ID
    (如 1,尽管您可以使用任何值,只要它是唯一的)

PUT localhost:9200/default/product/1
{ 
    "name": "Apple MacBook Pro",
    "description": "Latest MacBook Pro 13",
    "tags": ["laptops", "mac"]
}

图片 10

创建一个新索引

图片 11

索引新文档

在我们验证搜索功能和查询数据之前,再索引几个 “product”。尝试使用不同的
“tags”,如 “laptops”和 “laptops”,并记得使用不同的ids!
完成后,让我们按名称排序的搜索所有被索引的文档。您可以使用查询字符串或
GET/POST 同样的内容,下面两个请求是等效的:

GET http://localhost:9200/default/_search?q=*&sort=name.keyword:asc

POST http://localhost:9200/default/_search
{
  "query": { "match_all": {} },
  "sort": [
    { "name.keyword": "asc" }
  ]
}

让我们尝试一些更有趣的东西,例如搜索 “description” 字段中含有 “latest”
,同时 “tags” 字段中含有 “laptops” 的所有文档:

POST http://localhost:9200/default/_search
{
  "query": { 
      "bool": {
      "must": [
        { "match": {"description": "latest"} },
        { "match": { "tags": "laptops" } }
      ]
    }
  },
  "sort": [
    { "name.keyword": "asc" }
  ]
}

图片 12

搜索结果

清单 1. “Ballad of Casey Jones” 的 JSON 文档
{
  "artist": "Wallace Saunders",
  "year": 1909,
  "styles": ["traditional"],
  "album": "Unknown",
  "name": "Ballad of Casey Jones",
  "lyrics": "Come all you rounders if you want to hear
The story of a brave engineer
Casey Jones was the rounder's name....
Come all you rounders if you want to hear
The story of a brave engineer
Casey Jones was the rounder's name
On the six-eight wheeler, boys, he won his fame
The caller called Casey at half past four
He kissed his wife at the station door
He mounted to the cabin with the orders in his hand
And he took his farewell trip to that promis'd land

Chorus:
Casey Jones--mounted to his cabin
Casey Jones--with his orders in his hand
Casey Jones--mounted to his cabin
And he took his... land"
}

运行以下命令,将此文档 PUT 到您的 music 索引中:

$ curl -XPUT "http://localhost:9200/music/lyrics/2" -d @caseyjones.json

在该索引中时,将清单 2 的内容(包含另一手民歌 “Walking Boss”)保存到
walking.json 文件中。

Kibana 可视化数据

作为介绍的最后部分,我们将对 Kibana 的相关知识蜻蜓点水。
假设您在上一步已经索引了几个文档,通过访问
http://localhost:5601
中打开在 Docker 的 Kibana 服务器。你会注意到,Kibana
要求你提供默认的索引模式,所以必须告诉它使用的 Elasticsearch 索引:

  • 我们在上一节中创建了一个名为 “default” 的索引,因此可以使用
    “default” 作为索引模式。
  • 您还需要取消 “索引包含基于时间的事件 (Index contains time-based
    events ) ” 选项,因为我们的文档不包含任何时间字段。

图片 13

在 Kibana 中添加索引模式

完成后,使用左侧菜单打开 ” 发现 (Discover) ”
页面,您应该会看到上一节中插入的所有最新文档。尝试选择不同的字段,在搜索栏中输入相关的字段或某个过滤器:

图片 14

在 kibana 中可视化数据

最后,我们创建一个饼图,显示 “laptops” 或 “desktops”
的销量百分比。利用之前索引的数据,在左侧菜单新建一个 “饼图 (Pie Chart)”

您可以在 饼图 (Pie Chart)的页面上配置。将 ” Count ” 作为切片的大小,并在
” buckets ” 部分中选择 ” split slices ” 。将 ” filters ”
作为聚合类型,添加两个过滤器:tags =”laptop” 和 tags =”desktoptops”
。单击运行,您将看到类似于下图:

图片 15

在Kibana中创建饼图

确保在搜索栏中输入包含已过滤的项目的搜索关键词,并注意到可视化图形如何变化。

清单 2. “Walking Boss” JSON
{
  "artist": "Clarence Ashley",
  "year": 1920
  "name": "Walking Boss",
  "styles": ["folk","protest"],
  "album": "Traditional",
  "lyrics": "Walkin' boss
Walkin' boss
Walkin' boss
I don't belong to you

I belong
I belong
I belong
To that steel driving crew

Well you work one day
Work one day
Work one day
Then go lay around the shanty two"
}

将此文档推送到索引中:

$ curl -XPUT "http://localhost:9200/music/lyrics/3" -d @walking.json

Elasticsearch .Net API

在简要介绍Elasticsearch和Kibana之后,我们来看看我们如何用 .Net
应用程序索引和查询我们的文档。
您可能想知道为什么要这样做,而不是直接使用 HTTP API
。我可以提供几个理由,我相信你可以自己找几个:

  • 你不想直接暴露 Elasticsearch 集群
  • Elasticsearch
    可能不是您的主数据库,您可能需要结合来自主数据库的结果。
  • 你希望包含来自存储/生产服务器中的被索引文档

首先需要注意的是打开
这个文档
,有两个官方提供的 APIs : Elasticsearch.NetNEST ,都支持 .Net
Core 项目。

  • Elasticsearch.Net 提供了一个用于与
    Elasticsearch连接的低级API,提供构建/处理请求和响应的功能。它是 .Net
    瘦客户端。
  • NEST 在 Elasticsearch.Net 之上,提供了更高级别的 API
    。它可以将对象映射到请求/响应中,提供强大查询功能,将索引名称、文档类型、字段类型用于构建与
    HTTP REST API 的匹配查询。
![](https://upload-images.jianshu.io/upload_images/3913171-0e885efa9cf839fd.png)

Elasticsearch .Net API

由于我使用的是 NEST,所以第一步是创建一个新的 ASP .Net Core
应用程序,并使用 Package Manager 安装NEST。

搜索 REST API

是时候运行一次基本查询了,此查询比您运行来查找 “Get the Halls”
文档的简单 GET 要复杂一些。文档 URL
有一个内置的 _search 端点用于此用途。在歌词中找到所有包含单词 you 的歌曲:

curl -XGET "http://localhost:9200/music/lyrics/_search?q=lyrics:'you'"

q 参数表示一个查询。

响应是:

{"took":107,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":2,"max
_score":0.15625,"hits":[{"_index":"music","_type":"songs","_id":"2","_
score":0.15625,"_source":{"artist": "Wallace Saunders","year": 1909,"styles":
["traditional"],"album": "Unknown","name": "Ballad of Casey Jones","lyrics": "Come all you rounders
if you want to hear The story of a brave engineer Casey Jones was the rounder's name.... Come all
you rounders if you want to hear The story of a brave engineer Casey Jones was the rounder's name
On the six-eight wheeler, boys, he won his fame The caller called Casey at half past four He kissed
his wife at the station door He mounted to the cabin with the orders in his hand And he took his
farewell trip to that promis'd land Chorus: Casey Jones--mounted to his cabin Casey Jones--with his
orders in his hand Casey Jones--mounted to his cabin And he took his... land"
}},{"_index":"music","_type":"songs","_id":"3","_score":0.06780553,"_source":{"artist": "Clarence
Ashley","year": 1920,"name": "Walking Boss","styles": ["folk","protest"],"album":
"Traditional","lyrics": "Walkin' boss Walkin' boss Walkin' boss I don't belong to you I belong I
belong I belong To that steel driving crew Well you work one day Work one day Work one day Then go
lay around the shanty two"}}]}}

使用Nest开始索引数据

我们将在新的 ASP.Net Core 应用程序中完成之前手动发送 HTTP
请求的一些步骤。如果需要,请重新启Docker 容器,从而清理数据;或通过 HTTP
API 和 Postman 手动删除文档/索引。
我们首先为产品创建一个POCO模型:

public class Product
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public string[] Tags { get; set; }        
}

接下来,我们创建一个新的控制器 ProductController,它具有添加新的
“Product” 的方法和基于单个关键词查找 “Product” 的方法:

[Route("api/[controller]")]
public class ProductController : Controller
{

    [HttpPost]
    public async Task< IActionResult > Create([FromBody]Product product)
    {
    }

    [HttpGet("find")]
    public async Task< IActionResult > Find(string term)
    {
    }
}

为了实现这些方法,我们需要先连接到 Elasticsearch。这里有一个
ElasticClient 连接的正确示范。
由于该类是线程安全的,所以推荐的方法是在应用程序中使用单例模式,而不是按请求创建新的连接。

为了简洁起见,我现在将使用带有硬编码设置的私有静态变量。在 .Net Core
中使用依赖注入配置框架,或查看
Github中
的代码

可以想到的是,至少需要提供被连接的 Elasticsearch
集群的URL。当然,还有其他可选参数,用于与您的群集进行身份验证、设置超时、连接池等。

private static readonly ConnectionSettings connSettings =
    new ConnectionSettings(new Uri("http://localhost:9200/"));        
private static readonly ElasticClient elasticClient = 
    new ElasticClient(connSettings);

建立连接后,索引文档只是简单地使用 ElasticClient 的Index/IndexAsync
方法:

[Route("api/[controller]")]
public class ProductController : Controller
{

    [HttpPost]
    public async Task<IActionResult> Create([FromBody]Product product)
    {
    }

    [HttpGet("find")]
    public async Task<IActionResult> Find(string term)
    {
    }
}

很简单,对吧?不幸的是,如果您向Postman发送以下请求,您将看到失败。

POST http://localhost:65113/api/product
{ 
    "name": "Dell XPS 13",
    "description": "Latest Dell XPS 13",
    "tags": ["laptops", "windows"]
}

这是因为NEST无法确定在索引文档时使用哪个索引!如果您想起手动使用 HTTP
API 的做法,那么需要在URL指出文档的索引、文档的类型和ID,如
localhost:9200/default/product/1

NEST能够推断文档的类型(使用类的名称),还可以默认对字段进行索引(基于字段的类型),但需要一些索引名称的帮助。您可以指定默认的索引名称,以及特定类型的特定索引名称。

connSettings = new ConnectionSettings(new Uri("http://192.168.99.100:9200/"))
    .DefaultIndex("default")
    //Optionally override the default index for specific types
    .MapDefaultTypeIndices(m => m
        .Add(typeof(Product), "default"));

进行这些更改后再试一次。您将看到 NEST
创建索引(如果尚未存在),并将文档编入索引。如果你切换到
Kibana,你也可以看到该文档。需要注意的是:

  • 从类的名称推断文档类型,如 Product
  • 在类中将Id属性推断为标识
  • 将所有公开的属性发送到 Elasticsearch

图片 16

使用NEST索引的文档

在我们查询数据之前,重新考虑创建索引的方式。
如何创建索引?
现在我们得到一个事实,即如果这个索引不存在,也会被创建。然而映射字段的索引方式很重要,并直接定义了
Elasticsearch 如何索引和分析这些字段。这对于字符串字段尤其明显,因为在
Elasticsearch v5 中提供了两种不同字段类型的 “Text” 和 “Keyword”:

  • Text 类型的字段将会被分析和分解成单词,以便用于更高级的
    Elasticsearch 搜索功能
  • 另一方面,Keyword 字段将 “保持原样”
    而不进行分析,只能通过其精确值进行搜索。

您可以使用 NEST 索引映射属性来生成POCO模型:

public class Product
{
    public Guid Id { get; set; }
    [Text(Name="name")]
    public string Name { get; set; }
    [Text(Name = "description")]
    public string Description { get; set; }
    [Keyword(Name = "tag")]
    public string[] Tags { get; set; }        
}

然而,我们需要先创建索引,必须使用 ElasticClient API
手动创建和定义索引的映射。这是非常简单的,特别是如果我们只是使用属性:

if (!elasticClient.IndexExists("default").Exists)
{
    elasticClient.CreateIndex("default", i => i
        .Mappings(m => m
            .Map<Product>(ms => ms.AutoMap())));
}

直接向Elasticsearch发送请求(GET
localhost:92000/default),并注意与我们想要的映射是否相同。

图片 17

使用NEST创建索引映射

使用其他比较符

还有其他各种比较符可供使用。例如,找到所有 1900 年以前编写的歌曲:

curl -XGET "http://localhost:9200/music/lyrics/_search?q=year:<1900

此查询将返回完整的 “Casey Jones” 和 “Walking Boss” 文档。

使用Nest查询数据

现在,我们有一个使用 NEST 对 “products” 进行索引的 ProductController
控制器。是时候,为这个控制器添加 Find action,用于使用 NEST 向
Elasticsearch 查询文档。
我们只是用到一个字段来实现一个简单的搜索。您应该观察所有字段:

  • 映射为 “Text” 类型的字段可以被分析,您可以在 “name” /
    “description” 字段内搜索特定的单词
  • 映射为 “Keywords” 的字段是保持原样的,未进行分析。您只能在 “tags”
    字段中完全匹配。

NEST 提供了一个查询 Elasticsearch 的丰富 API,可以转换成标准的 HTTP API
。实现上述查询类型与使用Search/SearchAsync方法一样简单,并构建一个
SimpleQueryString 作为参数。

[HttpGet("find")]
public async Task<IActionResult> Find(string term)
{
    var res = await elasticClient.SearchAsync<Product>(x => x
        .Query( q => q.
            SimpleQueryString(qs => qs.Query(term))));
    if (!res.IsValid)
    {
        throw new InvalidOperationException(res.DebugInformation);
    }

    return Json(res.Documents);
}

使用PostMan测试您的新操作:

图片 18

使用nest查询

正如您可能已经意识到的那样,我们的操作行为与手动发送请求到 Elasticsearch
一样:

GET http://localhost:9200/default/_search?q=*&

限制字段

要限制您在结果中看到的字段,可将 fields 参数添加到您的查询中:

curl -XGET "http://localhost:9200/music/lyrics/_search?q=year:>1900&fields=year"

在 .Net Core 中创建一个Elasticsearch日志提供程序

现在我们了解了 NEST
的一些基础知识,让我们尝试一些更有野心的事情。我们已经创建了一个 ASP.Net
Core 的应用程序,借助.NET
Core的日志框架,实现我们的日志提供程序,并将信息发送到Elasticsearch。
新的日志 API 在日志 (logger) 和日志提供程序 (logger provider)
方面的区别:

  • 日志 (logger) 记录信息和事件,如用于控制器中
  • 可以为应用程序添加并启用多个日志提供程序 (provider)
    ,并可以配置独立的记录级别和记录相应的信息/事件。

该日志框架内置了一些对事件日志、Azure 等的日志提供程序
(provider),但正如您将看到的,创建自己的并不复杂。有关详细信息,请查阅.NET
Core
关于日志的官方文档。
在本文的最后部分,我们将为Elasticsearch创建一个新的日志提供程序,在我们的应用程序中启用它,并使用Kibana来查看记录的事件。

检查搜索返回对象

清单 3 给出了 Elasticsearch 从前面的查询返回的数据。

为Elasticsearch添加一个新的日志提供程序

首先要做的是定义一个新的POCO对象,我们将使用它作为使用NEST进行索引的文档,类似于之前创建的
“Product” 类。
这将包含有关可能发生的任何异常以及相关请求数据的记录信息、可选信息。记录请求数据将会派上用场,因为我们可以根据具体请求查询/可视化我们记录的事件。

public class LogEntry
{
    public DateTime DateTime { get; set; }
    public EventId EventId { get; set; }
    [Keyword]
    [JsonConverter(typeof(StringEnumConverter))]
    public Microsoft.Extensions.Logging.LogLevel Level { get; set; }
    [Keyword]
    public string Category { get; set; }
    public string Message { get; set; }

    [Keyword]
    public string TraceIdentifier { get; set; }
    [Keyword]
    public string UserName { get; set; }        
    [Keyword]
    public string ContentType { get; set; }
    [Keyword]
    public string Host { get; set; }         
    [Keyword]
    public string Method { get; set; }        
    [Keyword]
    public string Protocol { get; set; }
    [Keyword]
    public string Scheme { get; set; }
    public string Path { get; set; }
    public string PathBase { get; set; }
    public string QueryString { get; set; }
    public long? ContentLength { get; set; }
    public bool IsHttps { get; set; }
    public IRequestCookieCollection Cookies { get; set; }
    public IHeaderDictionary Headers { get; set; }

    [Keyword]
    public string ExceptionType { get; set; }        
    public string ExceptionMessage { get; set; }
    public string Exception { get; set; }
    public bool HasException { get { return Exception != null; } }
    public string StackTrace { get; set; }
}

下一步是在一个新类上实现ILogger接口。如您所想,这将需要记录的数据映射到一个新的
LogEntry 对象,并使用 ElasticClient 对其进行索引。

  • 我们将使用IHttpContextAccessor,以便我们可以获取当前的HttpContext并提取相关的请求属性。

在这里就不写连接到Elasticsearch并创建索引的代码,这与之前的操作,没有什么不同。使用不同的索引或删除上一节中索引的
“products” 。

注意:
您可以使用依赖注入和配置文检查Github*中
*的配套代码。

实现的主要方法是Log
<TState>,这是我们创建一个LogEntry并用NEST进行索引:

public void Log< TState >(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func< TState, Exception, string > formatter)
{
    if (!IsEnabled(logLevel)) return;

    var message = formatter(state, exception);
    var entry = new LogEntry
    {
        EventId = eventId,
        DateTime = DateTime.UtcNow,
        Category = _categoryName,
        Message = message,
        Level = logLevel
    };

    var context = _httpContextAccessor.HttpContext;
    if (context != null)
    {                
        entry.TraceIdentifier = context.TraceIdentifier;
        entry.UserName = context.User.Identity.Name;
        var request = context.Request;
        entry.ContentLength = request.ContentLength;
        entry.ContentType = request.ContentType;
        entry.Host = request.Host.Value;
        entry.IsHttps = request.IsHttps;
        entry.Method = request.Method;
        entry.Path = request.Path;
        entry.PathBase = request.PathBase;
        entry.Protocol = request.Protocol;
        entry.QueryString = request.QueryString.Value;
        entry.Scheme = request.Scheme;

        entry.Cookies = request.Cookies;
        entry.Headers = request.Headers;
    }

    if (exception != null)
    {
        entry.Exception = exception.ToString();
        entry.ExceptionMessage = exception.Message;
        entry.ExceptionType = exception.GetType().Name;
        entry.StackTrace = exception.StackTrace;
    }

    elasticClient.Client.Index(entry);
}

您还需要额外实现 BeginScope 和 IsEnabled 方法。

  • 为了本文的目的,忽略 BeginScope,只返回null。
  • 更新您的构造函数,以便它接收一个日志级别(LogLevel),如果接收到大于或等于构造函数中的日志级别,则实现
    IsEnabled 并返回 true。

你可能会问为什么需要分类?这是一个用于标识日志是哪种类型的字符串。默认情况下,每次注入ILogger
<T>的实例时,该类别默认分配为T的分类名称。例如,获取ILogger
<MyController>并使用它来记录某些事件,意味着这些事件将具有
“MyController ” 名称。
这可能派上用场,例如为不同的类设置不同的日志级别,以过滤/查询记录的事件。我相信您可能还想到更多的用法。

这个类的实现将如下所示:

public class ESLoggerProvider: ILoggerProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly FilterLoggerSettings _filter;

    public ESLoggerProvider(IServiceProvider serviceProvider, FilterLoggerSettings filter = null)
    {
        _httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
        _filter = filter ?? new FilterLoggerSettings
        {
            {"*", LogLevel.Warning}
        };
    }

    public ILogger CreateLogger(string categoryName)
    {
        return new ESLogger(_httpContextAccessor, categoryName, FindLevel(categoryName));
    }

    private LogLevel FindLevel(string categoryName)
    {
        var def = LogLevel.Warning;
        foreach (var s in _filter.Switches)
        {
            if (categoryName.Contains(s.Key))
                return s.Value;

            if (s.Key == "*")
                def = s.Value;
        }

        return def;
    }

    public void Dispose()
    {
    }
}

最后,我们创建一个扩展方法,可以用于在启动类中注册我们的日志提供程序:

public static class LoggerExtensions
{
    public static ILoggerFactory AddESLogger(this ILoggerFactory factory, IServiceProvider serviceProvider, FilterLoggerSettings filter = null)
    {
        factory.AddProvider(new ESLoggerProvider(serviceProvider, filter));
        return factory;
    }
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"))
        .AddDebug()
        .AddESLogger(app.ApplicationServices, new FilterLoggerSettings
        {
            {"*", LogLevel.Information}
        });
    …
}

请注意我如何覆盖默认设置并按日志级别分类记录。这样,我们可以轻松地为每个请求索引一些事件。

清单 3. 查询结果
{
    "took": 6,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
    },
    "hits": {
        "total": 2,
        "max_score": 1.0,
        "hits": [{
            "_index": "music",
            "_type": "lyrics",
            "_id": "1",
            "_score": 1.0,
            "fields": {
                "year": [1920]
            }
        }, {
            "_index": "music",
            "_type": "lyrics",
            "_id": "3",
            "_score": 1.0,
            "fields": {
                "year": [1909]
            }
        }]
    }
}

在结果中,Elasticsearch 提供了多个 JSON
对象。第一个对象包含请求的元数据:看看该请求花了多少毫秒 (took)
和它是否超时 (timed_out)。_shards 字段需要考虑 Elasticsearch
是一个集群化服务的事实。甚至在这个单节点本地部署中,Elasticsearch
也在逻辑上被集群化为分片。

继续查看清单 3 中的搜索结果,可以观察到 hits 对象包含:

  • total 字段,它会告诉您获得了多少个结果
  • max_score,用于全文搜索
  • 实际结果

实际结果包含 fields 属性,因为您将 fields 参数添加到了查询中。否则,结果中会包含 source,而且包含完整的匹配文档。_index_type 和 _id 的用途不言自明;_score 指的是全文搜索命中长度。这
4 个字段始终会在结果中返回。

在Kibana中可视化数据

现在我们已经在Kibana中记录了事件,我们来探索数据可视化吧!
首先,在Kibana中重建索引,这次确保选择” Index contains time-based
events ( Index包含基于时间的事件 )
“,选择字段dateTime作为”Time-field
name (时间字段名称)”。
接下来,启动您的应用程序,浏览一些页面以获取一些事件日志。还可以在某个端点随意添加抛出异常的代码,以便我们可以看到被记录的异常数据。
在这之后,请转到Kibana的 **发现 (Discover) ** 页面,您可以看到由
“dateTime”
字段排序的多个事件(默认情况下,数据被过滤为最近15分钟,但您可以在右上角更改):

图片 19

Kibana可视化中记录的事件

试着在搜索栏中输入 “exception”,并注意任何一个被分析的文本字段中包含
“exception”
的事件。然后尝试搜索特定的异常类型(记住我们使用了一个关键字字段!)。
您还可以尝试搜索特定的URL,如以 ” /Home/About “和” /Home/About”
路径的两种搜索方式 。您会注意到第一种情况包括引用者是 “/Home/About”
的事件,而第二种情况则只能正确返回路径为 “/Home/About” 的事件。
一旦你熟悉了数据,以及如何查询数据,那么可以用数据创建一些有趣的图形。
首先,我们将创建一个图表,显示每分钟记录的异常数。

  • 转到Kibana的** 可视化 (Visualize) 页面,并创建一个新的
    垂直条形图 (Vertical bar chart) **。
  • 选择Y轴作为计数,X轴上为日期的直方图。
  • 将间隔设置为每分钟,最后在搜索框中添加一个过滤器
    “hasException:true” 。

一个很棒的图表,显示每分钟记录的异常数目:

图片 20

每分钟记录的异常数目

接下来,显示每个 category 随时间记录的消息数量,限于前5个 category :

  • 转到Kibana的** 可视化 (Visualize) **页面,并创建一个新的
    **线型图 (Line chart) **。
  • 再次选择Y轴作为计数,X轴上为日期的直方图,选择dateTime作为字段,间隔为每分钟。
  • 现在添加一个 sub-bucket 并选择 “split lines” 。使用 “significant
    terms” 作为聚合,category 为字段,单位为5个。

这将绘制类似于以下的图表:

图片 21

随着时间的推移

尝试在搜索框中添加一些过滤器,并查看它对结果的影响。
最后,我们添加另一个图表,我们将看到前五个出现最多的消息和前五个
categories 的消息。

  • 转到Kibana的** 可视化 (Visualize) **页面,并创建一个新的
    **饼图 (Pie chart) **。
  • 像之前一样,选择Y轴的计数
  • 现在,将 “Terms” 作为聚合,将 “category”
    作为字段,数量作为单位,限制前五个,画出图表。
  • 然后将 “Terms” 作为聚合来分割切片,”message.keyword”
    作为字段,数量作为单位,限制前五个。
    一旦你有了这些设置,你会看到一个类似于这个图表:

图片 22

每个 category 中最常见的消息

花时间观察下数据(百分比,message/category
显示在图表元素上)。例如,您将观察到由
DeveloperExceptionPageMiddleware类记录的异常。

使用 JSON 查询 DSL

基于查询字符串的搜索很快会变得很复杂。对于更高级的查询,Elasticsearch
提供了一种完全基于 JSON 的特定于领域的语言
(DSL)。例如,要搜索 album 值为 traditional 的每首歌曲,可创建一个包含以下内容的
query.json 文件:

{
    "query" : {
        "match" : {
            "album" : "Traditional"
        }
    }
}

然后运行:

curl -XGET "http://localhost:9200/music/lyrics/_search" -d @query.json

结论

Elasticsearch是一个强大的数据索引和查询平台。虽然它本身相当令人印象深刻,但与其他应用程序(如Kibana)相结合,可以很好地分析、报告和可视化数据。只要您开始使用,只是蜻蜓点水都能的到非凡的结果。
对于 .Net 和 .Net Core,Elasticsearch 官方的 API 已经覆盖,因为它们支持
.Net Standard 1.3和更高版本(他们仍然在为1.1提供支持)。
正如我们已经看到的,在 ASP.Net Core 项目中使用这个 API
是很方便的,我们可以轻松地将其 REST API
作为存储,以及在应用程序中作为日志提供程序。
最后但并非不重要的一点,我希望您使用Docker。尝试使用
Elasticsearch,同时思考Docker可以为您和您的团队做些什么。

下载本文的全部源代码(Github)。


本文采用 知识共享署名-非商业性使用-相同方式共享 3.0
中国大陆许可协议
转载请注明:作者 张很水

图片 23

打赏作者

从 Java 代码使用 Elasticsearch

“Elasticsearch 强大功能会在通过语言 API 使用它时体现出来。”

Elasticsearch 强大功能会在通过语言 API 使用它时体现出来。现在我将介绍
Java
API,您将从一个应用程序执行搜索。请参见 下载 部分,获取相关的示例代码。该应用程序使用了
Spark 微型框架,所以可以很快设置它。

示例应用程序

为一个新项目创建一个目录,然后运行(将该命令键入到一行上):

mvn archetype:generate -DgroupId=com.dw -DartifactId=es-demo 
-DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

要生成一个项目来在 Eclipse 中使用,可通过 cd 进入 Maven
创建的项目目录,并运行 mvn eclipse:eclipse

在 Eclipse 中,选择 File > Import > Existing Project
into Workspace
。导航到您使用 Maven
的文件夹,选择该项目,单击 Finish

在 Eclipse 中,您可以看到一个基本的 Java 项目布局,包括根目录中的
pom.xml 文件和一个 com.dw.App.java 主要类文件。将您所需的依赖项添加到
pom.xml 文件中。清单 4 给出了完整的 pom.xml 文件。

清单 4. 完整的 pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.dw</groupId>
  <artifactId>es-demo</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>es-demo</name>
  <url>http://maven.apache.org</url>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <compilerVersion>1.8</compilerVersion>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <dependency>
    <groupId>com.sparkjava</groupId>
    <artifactId>spark-core</artifactId>
    <version>2.3</version>
</dependency>
<dependency>
    <groupId>com.sparkjava</groupId>
    <artifactId>spark-template-freemarker</artifactId>
    <version>2.3</version>
</dependency>
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>2.1.1</version>
</dependency>
  </dependencies>
</project>

清单 4 中的依赖项获取 Spark 框架核心、Spark Freemarker 模板支持和
Elasticsearch。另请注意,我将 <source> 版本设置为 Java 8,Spark
需要该版本(因为它大量使用了 lambda)。

我不知道您的情况,但我不久前构建了许多 RESTful
应用程序,所以为了改变以下步调,您将为应用程序提供一个更加传统的
“提交和加载 (submit-and-load)” UI。

在 Eclipse 中,在导航器中右键单击项目,选择 Configure > Convert
to Maven Project
,以便 Eclipse 可以解析 Maven
依赖项。转到项目,右键单击该项目,然后选择 Maven > Update
Project

Java 客户端配置

Elasticsearch 的 Java
客户端非常强大;它可以建立一个嵌入式实例并在必要时运行管理任务。但我在这里将重点介绍如何运行针对您已运行的节点的应用程序任务。

运行一个 Java 应用程序和 Elasticsearch
时,有两种操作模式可供使用。该应用程序可在 Elasticsearch
集群中扮演更加主动或更加被动的角色。在更加主动的情况下(称为 Node
Client),应用程序实例将从集群接收请求,确定哪个节点应处理该请求,就像正常节点所做的一样。(应用程序甚至可以托管索引和处理请求。)另一种模式称为
Transport Client,它将所有请求都转发到另一个 Elasticsearch
节点,由后者来确定最终目标。

获取 Transport Client

对于演示应用程序,(通过 App.java 中执行的初始化)选择 Transport
Client,并保持 Elasticsearch 执行最低级别的处理:

Client client = TransportClient.builder().build()
   .addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("localhost"), 9300));

如果连接到一个 Elasticsearch
集群,构建器可以接受多个地址。(在本例中,您只有一个 localhost
节点。)连接到端口 9300,而不是像之前在 REST API 的 cURL 中一样连接到
9200。Java 客户端将会使用这个特殊端口,使用端口 9200 不起作用。(其他
Elasticsearch 客户端,Python 客户端就是其中之一,将会 使用 9200 来访问
REST API。)

在服务器启动时创建该客户端,并在整个请求处理过程中使用它。Spark 通过
Mustache 模板引擎的 Java 实现来呈现该页面,而且 Spark 定义了请求端点 —
但我不会太多地解释这些简单的用例。(请参见 参考资料,获取
Spark 的详细信息的链接。)

该应用程序的索引页面显示了 Java 客户端的功能:

图片 24

UI:

  • 呈现现有歌曲的列表
  • 提供一个添加歌曲的按钮
  • 实现按艺术家和歌词进行搜索
  • 返回突出显示了匹配内容的结果

搜索和处理结果

在清单 5 中,根 URL / 被映射到 index.mustache 页面。

清单 5. 基本搜索
Spark.get("/", (request, response) -> {
        SearchResponse searchResponse = 
            client.prepareSearch("music").setTypes("lyrics").execute().actionGet();
        SearchHit[] hits = searchResponse.getHits().getHits();

            Map<String, Object> attributes = new HashMap<>();
            attributes.put("songs", hits);

            return new ModelAndView(attributes, "index.mustache");
        }, new MustacheTemplateEngine());

清单 5 中的有趣部分始于:

SearchResponse searchResponse = client.prepareSearch("music").setTypes("lyrics").execute().actionGet();

这一行显示了搜索 API
的简单用法。使用 prepareSearch 方法指定一个索引(在本例中为 music),然后执行查询。查询基本上显示为
“Give me all of the records in
the music index.”。另外,将文档类型设置为 lyrics,但在这个简单用例中没有必要这么做,因为索引仅包含一种文档类型。在更大的应用程序,需要执行这种设置。这个
API
调用类似于您之前看到的 curl -XGET "http://localhost:9200/music/lyrics/_search" 调用。

SearchResponse 对象包含有趣的功能(例如命中数量和评分),但就目前而言,您只想要一个结果数组,可使用searchResponse.getHits().getHits(); 获得它。

最后,将结果数组添加到视图上下文中,并让 Mustache 呈现它。Mustache
模板如下所示:

清单 6. index.mustache
<html>
<body>
<form name="" action="/search">
  <input type="text" name="artist" placeholder="Artist"></input>
  <input type="text" name="query" placeholder="lyric"></input>
  <button type="submit">Search</button>
</form>
<button onclick="window.location='/add'">Add</button>
<ul>
{{#songs}}
  <li>{{id}} - {{getSource.name}} - {{getSource.year}}
    {{#getHighlightFields}} -
      {{#lyrics.getFragments}}
        {{#.}}{{{.}}}{{/.}}
      {{/lyrics.getFragments}}
    {{/getHighlightFields}}
  </li>
{{/songs}}
</ul>

</body>
</html>

突出显示高级查询和匹配内容

要支持突出显示更高级的查询和匹配内容,可以使用 /search,如下所示:

清单 7. 搜索和突出显示
Spark.get("/search", (request, response) -> {
        SearchRequestBuilder srb = client.prepareSearch("music").setTypes("lyrics");

        String lyricParam = request.queryParams("query");
        QueryBuilder lyricQuery = null;
        if (lyricParam != null && lyricParam.trim().length() > 0){
            lyricQuery = QueryBuilders.matchQuery("lyrics", lyricParam);
        }
        String artistParam = request.queryParams("artist");
        QueryBuilder artistQuery = null;
        if (artistParam != null && artistParam.trim().length() > 0){
          artistQuery = QueryBuilders.matchQuery("artist", artistParam);
        }

        if (lyricQuery != null && artistQuery == null){
          srb.setQuery(lyricQuery).addHighlightedField("lyrics", 0, 0);
        } else if (lyricQuery == null && artistQuery != null){
          srb.setQuery(artistQuery);
        } else if (lyricQuery != null && artistQuery != null){
          srb.setQuery(QueryBuilders.andQuery(artistQuery, 
              lyricQuery)).addHighlightedField("lyrics", 0, 0);
        }

        SearchResponse searchResponse = srb.execute().actionGet();

SearchHit[] hits = searchResponse.getHits().getHits();

    Map<String, Object> attributes = new HashMap<>();
    attributes.put("songs", hits);

    return new ModelAndView(attributes, "index.mustache");
}, new MustacheTemplateEngine());

在清单 7 中,要注意的第一个有趣的 API
用法是 QueryBuilders.matchQuery("lyrics", lyricParam);。这是您设置对 lyrics 字段的查询的地方。另外要注意的是 QueryBuilders.andQuery(artistQuery, lyricQuery),它是将查询的 artist 和 lyrics 部分合并到
AND 查询中的一种方法。

.addHighlightedField("lyrics", 0, 0); 调用告诉 Elasticsearch
生成 lyrics 字段上的搜索命中突出显示结果。第二和第三个参数分别指定无线大小的分段和无限数量的分段。

在呈现搜索结果时,将突出显示结果放入 HTML 中。使用 Elasticsearch
就能生成有效的 HTML,使用 <em> 标记来突出显示匹配字符串所在的位置。

插入文档

让我们来看看如何以编程方式将文档插入索引中。清单 8 给出了添加过程。

清单 8. 插入索引中
Spark.post("/save", (request, response) -> {
      StringBuilder json = new StringBuilder("{");
      json.append(""name":""+request.raw().getParameter("name")+"",");
      json.append(""artist":""+request.raw().getParameter("artist")+"",");
      json.append(""year":"+request.raw().getParameter("year")+",");
      json.append(""album":""+request.raw().getParameter("album")+"",");
      json.append(""lyrics":""+request.raw().getParameter("lyrics")+""}");

      IndexRequest indexRequest = new IndexRequest("music", "lyrics",
          UUID.randomUUID().toString());
      indexRequest.source(json.toString());
      IndexResponse esResponse = client.index(indexRequest).actionGet();

      Map<String, Object> attributes = new HashMap<>();
      return new ModelAndView(attributes, "index.mustache");
    }, new MustacheTemplateEngine());

使用 StringBuilder 直接生成一个 JSON
字符串来创建它。在生产应用程序中,可使用 Boon 或 Jackson
等库。

执行 Elasticsearch 工作的部分是:

IndexRequest indexRequest = new IndexRequest("music", "lyrics", UUID.randomUUID().toString());

在本例中,使用了 UUID 来生成 ID。

结束语

您已快速掌握了如何从命令行和在 Java 应用程序中使用
Elasticsearch。您现在已经熟悉了索引、查询、突出显示和多字段搜索。Elasticsearch
在一个相对容易使用的包中提供了大量的功能。作为一个项目,Elasticsearch
带来了一些您可能也会感兴趣的结果。具体地讲,所谓的 ELK 堆栈,即
Elasticsearch、Logstash(用于日志管理)和
Kibana(用于报告/可视化),正在迅速发展。

示例代码下载:es-demo.zip

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图