自 Elasticsearch 5.0开始,你可以使用 ingest node 实现建立索引之前对文档进行预处理。 此预处理由 ingest node 进行,该 ingest node 节点拦截批量 ( bulk )和索引请求,应用转换,然后将文档传递回索引或批量API。这极大地提供了 Elasticsearch 对数据的处理能力。在很多的情况下,这种方法非常有效。它非常具有灵活性。在某些时候,甚至可以取代在之前只能使用 Logstash 才能完成的工作。目前 Ingest node 所提供的 processor 非常广泛。这也给我们实战的开发者提供无限的遐想。那么我们是否可以开发属于我们自己的 ingest processor呢?
答案是肯定的。在今天的文章中,我们将讲述如何从零开始来设计一个属于自己的 ingest processor。目前 Elastic 提供了很多的processor,但是我觉得很重要的一个就是 script processor. 它更像是一个通用的可以通过编程完成我们自己想要的数据处理。你可以找到一个具体的应用例子“Beats:运用Elastic Stack分析COVID-19数据并进行可视化分析 - 续”
创建一个框架 plugin
为了开始,你可以简单地检查一下 Elasticsearch 源代码并复制粘贴另一个 ingest processor 插件。 例如,ingest-attachment,ingest-user-agent 或 ingest-geoip 插件)是可以在 Elasticsearch 中找到的现有 ingest 插件。 但是,这意味着你将需要删除大量代码,然后才开始编码。
替代方法是使用一个称为 cookiecutter 的工具。 Cookiecutter 是从模板创建项目的命令行工具。 利用这样的模板,以使你更轻松地编写自己的 ingest processor。 因此,让我们开始使用 cookiecutter 模板创建一个插件。 如果尚未安装 cookiecutter,请运行:
pip install cookiecutter
或者:
pip3 install cookiecutter
依赖于你的 python 语言的版本。
一旦完成了我们上面的安装后,我们可以通过如下的命令来生产一个框架的 processor 样板。
cookiecutter gh:spinscale/cookiecutter-elasticsearch-ingest-processor
你将被问到四个问题。 插件的名称,程序包目录(请勿更改),描述,你的名称(需要填入到许可里)和要运行的 Elasticsearch 版本。 如果您选择 sample 作为插件的名称,则本文档中的所有命名约定也将适用于你的本地设置。比如下面的就是我的示范:
$ cookiecutter gh:spinscale/cookiecutter-elasticsearch-ingest-processor
You've downloaded /Users/liuxg/.cookiecutters/cookiecutter-elasticsearch-ingest-processor before. Is it okay to delete and re-download it? [yes]: yes
processor_type [awesome]: sample
package_dir [sample]:
description [Ingest processor that is doing something awesome]: This is a sample
developer_name [YOUR REAL NAME]: Xiaoguo Liu
elasticsearch_version [7.6.2]:
在我们运行的当前的目录下,你会发现一个叫做 ingest-sample 的目录,我们进入该目录:
$ ls
LICENSE.txt README.md gradle gradlew.bat src
NOTICE.txt build.gradle gradlew settings.gradle
我们会发现这个是一个 gradle 的项目。在 src 目录里含有需要建立一个 ingest processor 所有需要的代码:
正如上图所示,有两个源文件:IngestSamplePlugin.java 及 SampleProcessor.java。
如果你检出新创建的目录,则会发现一个功能完善的插件,甚至可以直接进行一些测试。
您可以运行 gradle clean check 并查看测试通过。 现在,我们的下一步应该是添加依赖关系并编写测试。在 Mac OS 上,我们运行如下的命令:
./gradlew clean check
上面显示,我们的测试已经通过了。事实上,这个框架就是一个完整的 processor。我们首先来查看一下文件 SampleProcessor.java:
SampleProcessor.java
package org.elasticsearch.plugin.ingest.sample;
import org.elasticsearch.ingest.AbstractProcessor;
import org.elasticsearch.ingest.IngestDocument;
import org.elasticsearch.ingest.Processor;
import java.io.IOException;
import java.util.Map;
import static org.elasticsearch.ingest.ConfigurationUtils.readStringProperty;
public class SampleProcessor extends AbstractProcessor {
public static final String TYPE = "sample";
private final String field;
private final String targetField;
public SampleProcessor(String tag, String field, String targetField) throws IOException {
super(tag);
this.field = field;
this.targetField = targetField;
}
@Override
public IngestDocument execute(IngestDocument ingestDocument) throws Exception {
String content = ingestDocument.getFieldValue(field, String.class);
// TODO implement me!
ingestDocument.setFieldValue(targetField, content);
return ingestDocument;
}
@Override
public String getType() {
return TYPE;
}
public static final class Factory implements Processor.Factory {
@Override
public SampleProcessor create(Map<String, Processor.Factory> factories, String tag, Map<String, Object> config)
throws Exception {
String field = readStringProperty(TYPE, tag, config, "field");
String targetField = readStringProperty(TYPE, tag, config, "target_field", "default_field_name");
return new SampleProcessor(tag, field, targetField);
}
}
}
上面的代码其实蛮简单的。在 Factory class 里,它创建一个 SampleProcessor 的实例。我们的 processor 的类型叫做 sample,而真正对数据进行处理的工作其实就在 SampleProcessor 里的execute方法里。在它里面,尽管目前没有什么复杂的处理。它只是把一个指定的字段的内容,赋予给一个新的由 target_field 所指定的字段里。我们首先来编译这个简单的 processor。在项目的跟目录中,我们打入如下的命令:
./gradlew build
上面显示我们编译是成功的。我们可以通过如下的命令来显示已经生产的 procssor 的压缩包:
$ ls ./build/distributions/ingest-sample-0.0.1-SNAPSHOT.zip
./build/distributions/ingest-sample-0.0.1-SNAPSHOT.zip
上面显示:我们已经成功地编译出来一个叫做 ingest-sample-0.0.1-SNAPSHOT.zip 的压缩包。在这里含有我们安装这个 sample processor 的所有文档。我们把这个 zip 文件拷入到 Elasticsearch 的根目录中,然后我们打入如下的命令进行拷贝:
cp ./build/distributions/ingest-sample-0.0.1-SNAPSHOT.zip ~/elastic0/elasticsearch-7.6.2
上面的路径依赖于你的 Elasticsearch 安装的位置不同而不同。
然后进入到 Elasticsearch 的根目录中,我们打入如下的命令来进行安装:
./bin/elasticsearch-plugin install file:Users/liuxg/elastic0/elasticsearch-7.6.2/ingest-sample-0.0.1-SNAPSHOT.zip
上面一定要注意这里的 file:/// 。在我刚测试的时候,由于忘记添加这个,从而导致安装一直不成功。
在安装的时候,提示我们安全的警告。我们选择 yes。上面显示我们的安装是成功的。我们可以打入如下的命令来检查 plugin 是否已经被成功地安装:
./bin/elasticsearch-plugin list
$ ./bin/elasticsearch-plugin list
ingest-sample
显然,我们的插件已经被成功地安装。当我们安装好这个 plugin 后,请一定要记得重启我们的 Elasticsearch 以使得它起作用。
使用 sample processor
在上面,我们已经创建了一个属于我们自己的 processor。那么我们该如何来使用呢?首先,我们打开 Kibana,并创建我们的 pipeline:
PUT /_ingest/pipeline/my_sample
{
"processors": [
{
"filter": {
"field": "content",
"target_field": "newField"
}
}
]
}
我们运行上面的命令,并使用如下的命令来创建一个文件:
PUT my_index/_doc/1?pipeline=my_sample
{
"content": "I like this"
}
我们再接着使用如下的命令来查看刚刚导入的这个文档:
{
"_index" : "my_index",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"newField" : "I like this",
"content" : "I like this"
}
}
从上面,我们可以看出来:在 source 里有一个新的字段叫做 newField,而且它的内容就是和 content 字段的内容是一样的。这个和我们之前的 Java 代码是一样的啊:
@Override
public IngestDocument execute(IngestDocument ingestDocument) throws Exception {
String content = ingestDocument.getFieldValue(field, String.class);
// TODO implement me!
ingestDocument.setFieldValue(targetField, content);
return ingestDocument;
}
是不是有点小兴奋啊。虽然我们目前啥也没有做,但是亲眼见识了这个没有做任何改动的 ingest processor 在起作用。如果你对目前的代码感兴趣的话,请参考我的代码:github.com/liu-xiao-gu…
URL 提取 ingest processor
接下来,我们来创建一个进一步修改上面的 plugin。我们想做的事是把输入的文档中指定的字段进行扫描,如果里面有url,那么我们把它们存放到指定的字段中,这样便于我们来提取。在这里,我们需要使用到 autolink 这个帮助库,它可以帮我们提取这些url。首先,我们打开 build.gradle 文件,并添加这个库:
build.gradle:
dependencies {
compile 'org.nibor.autolink:autolink:0.6.0'
compile 'org.elasticsearch:elasticsearch:7.6.2'
testCompile 'org.elasticsearch.test:framework:7.6.2'
}
在上面,我们添加了 compile 'org.nibor.autolink:autolink:0.6.0' 这句。
我们接下来修改 SampleProcessor.java 这个文件:
SampleProcessor.java
package org.elasticsearch.plugin.ingest.sample;
import org.elasticsearch.ingest.AbstractProcessor;
import org.nibor.autolink.LinkExtractor;
import org.nibor.autolink.LinkType;
import java.util.EnumSet;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.elasticsearch.ingest.IngestDocument;
import org.elasticsearch.ingest.Processor;
import java.io.IOException;
import java.util.Map;
import static org.elasticsearch.ingest.ConfigurationUtils.readStringProperty;
public class SampleProcessor extends AbstractProcessor {
public static final String TYPE = "sample";
private final String field;
private final String targetField;
public SampleProcessor(String tag, String field, String targetField) throws IOException {
super(tag);
this.field = field;
this.targetField = targetField;
}
@Override
public IngestDocument execute(IngestDocument ingestDocument) throws Exception {
String content = ingestDocument.getFieldValue(field, String.class);
LinkExtractor linkExtractor = LinkExtractor.builder().linkTypes(EnumSet.of(LinkType.URL)).build();
List<String> links = StreamSupport.stream(linkExtractor.extractLinks(content).spliterator(), false)
.map(link -> content.substring(link.getBeginIndex(), link.getEndIndex()))
.collect(Collectors.toList());
ingestDocument.setFieldValue(targetField, links);
return ingestDocument;
}
@Override
public String getType() {
return TYPE;
}
public static final class Factory implements Processor.Factory {
@Override
public SampleProcessor create(Map<String, Processor.Factory> factories, String tag, Map<String, Object> config)
throws Exception {
String field = readStringProperty(TYPE, tag, config, "field");
String targetField = readStringProperty(TYPE, tag, config, "target_field", "default_field_name");
return new SampleProcessor(tag, field, targetField);
}
}
}
在这里,我们修改了 execute 这个方法。通过 helper 库,把指定字段的内容进行扫描,并把所有的 url 存于一个 List 列表,并最终赋予给指定的 target_field 字段。
接下来,我们不想进行 test 我们的代码,所以我们就 SampleProcessorTests.java 的文件进行修改,并注释掉大部分的代码:
SampleProcessorTests.java
package org.elasticsearch.plugin.ingest.sample;
import org.elasticsearch.test.ESTestCase;
public class SampleProcessorTests extends ESTestCase {
public void testThatProcessorWorks() throws Exception {
}
}
我们接下来的工作就是编译我们的 processor。我们使用如下的命令来进行编译:
./gradlew build -x integTes
上面的选项 -x integTes 就是不去做 integration 测试。等我们编译完后,按照我们上面讲述的方法再重新进行部署,并重启 Elasticsearch。这样我们新版本的 Sample ingest processor 就完成了。
我们可以通过如下的方式来进行测试:
1) 创建一个 pipeline:
PUT /_ingest/pipeline/my_sample
{
"processors": [
{
"sample": {
"field": "content",
"target_field": "newField"
}
}
]
}
运行上面的命令。请注意上面的 sample 就是我们的 processor 的类型。
2)创建一个文档,并使用刚才的 pipeline:
PUT my_index/_doc/1?pipeline=my_sample
{
"content": "this is a test field pointing to http://elastic.co and http://example.org/foo"
}
在上面的 content 字段里,我们的文档里含有两个 url。
3)使用如下的命令来进行查找:
GET my_index/_doc/1
显示结果:
{
"_index" : "my_index",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"_seq_no" : 1,
"_primary_term" : 2,
"found" : true,
"_source" : {
"newField" : [
"http://elastic.co",
"http://example.org/foo"
],
"content" : "this is a test field pointing to http://elastic.co and http://example.org/foo"
}
}
从上面我们可以看出来在 newField 里含有两个 url 的数组,这两个 url 都是从 content 这个字段里提前出来的。
如果你对这个例子感兴趣的话,请参阅我的代码:github.com/liu-xiao-gu…
你也可以参考我的另外一篇文章 “Elasticsearch:创建一个Elasticsearch Ingest 插件”。
参考:
【1】www.elastic.co/blog/writin…