Elasticsearch:创建属于自己的 Ingest processor

282 阅读7分钟

自 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-attachmentingest-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…

【2】martin-toshev.com/index.php/s…

【3】github.com/codelibs/el…

【4】 david.pilato.fr/blog/2016/1…