ときどき起きる

だいたい寝ている

ElasticsearchのIngest Pipelineでtext embeddingを埋め込む & サクッとKNN+BM25のHybrid Searchを試せるリポジトリを作った

本記事は情報検索・検索技術 Advent Calendar 2022の4日目の記事です。


こんにちは、pakioです。

先日のElasticON Tokyoに参加した際、とても興味深いセッションがありました。

The search for relevance with Vector Search

内容としては以下のブログと同じかと思います。 www.elastic.co

ざっくり説明するとElasticsearch + Ingest Pipelineを使えば自前でMLモデルから特徴量を抽出するようなサービスを立ち上げる必要なく、ドキュメントにembeddingを埋め込めるよと言った内容の講演でした。 かつ、Ingest Pipelineを利用することで、リアルタイム更新にも対応しているという優れものです。これは試してみるしかと思い、今回はその検証を行ったリポジトリを公開・及び主要なポイントとハマりどころを残します。

ElasticsearchのKNN/ANNに関してはhurutoriyaさんのこちらのブログ1やfujimotoshinjiさんのこちらのzennの記事2等類似した内容の記事も数多くありますが、MLノードを用いて、かつインデキシング時にリアルタイムで埋め込む事例、及びready-to-useなデモのコードに関してはあまり公開されていない為そのあたりを重点的に記載しています。

リポジトリ

github.com

デモ画面

上記リポジトリの手順通りセットアップすると、以下のようなデモ画面が表示されます。

デモ画面

データはwikimediaからenwikiの一部を利用しています。
wikimediaからのデータ投入に関しては、さっとさんのzenn3が大変参考になりました。元記事では日本語版であるjawikiでのデータ投入方法及びmapping設定についても言及されていますので、日本語で何かしらを試される場合のテストデータ作成にも大いに役立つ記事かと思います。

モデルはHugging Faceからsentence-transformers/msmarco-MiniLM-L-12-v3を利用し、出力は384次元のベクトルとなっています。

手順

1. Elasticsearchを起動 / モデルのアップロード

Elasticsearchを起動

/Esに移動し、Elasticsearch及びKibana(オプション)を起動します。

docker-compose up -d

Ingest Pipeline内でモデルを走らせる方法では、MLノードを有効化させる必要があるため、オプションとしてnode.roles=ml,ingestを指定した状態で起動しています。yaml

また、現時点でMLノードはBasic Licenseではサポートされていないため、検証用にTrial Licenseを有効化します。

curl -X POST 'http://127.0.0.1:9200/_license/start_trial?acknowledge=true'

text embedding生成モデルのアップロード

Eland4を用いて、起動したElasticsearchにモデルをアップロードします。 ElasdではHugging FaceのモデルIDをそのまま指定しアップロードが可能なため、ファインチューニングが必要ない場合には複雑な手順を踏むことなく検証が可能です。

eland_import_hub_model --url http://127.0.0.1:9200 --hub-model-id sentence-transformers/msmarco-MiniLM-L-12-v3 --task-type text_embedding

また、アップロードが完了した時点ではモデルは有効化されていないため、_startを叩いて有効化してあげます。

curl -X POST 'http://127.0.0.1:9200/_ml/trained_models/sentence-transformers__msmarco-minilm-l-12-v3/deployment/_start'

Index/Ingest Pipeline定義

次に、Index及びIngest Pipelineを定義します。

ここでまず1つ目のハマりポイントですが、Ingest Pipelineから出力されたベクトルは、指定したフィールドそのものではなく、一つ下の.predicted_valueに格納されます。Ingest Pipelineで指定したtarget_fieldではないためご注意ください。

2つ目のハマりポイントは、Ingest Pipelineの入力に使われるフィールドの指定についてです。テキストからembeddingを生成するため当然その入力となるフィールドが必要となりますが、デフォルトのフィールド名はtext_fieldとなっています。その値自体は変更不可なため、任意のフィールドを入力に利用したい場合、フィールド名とのマッピングを指定します。

{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "properties": {
      "text": {
        "type": "text"
      },
      "url": {
        "type": "keyword"
      },
      "id": {
        "type": "keyword"
      },
      "title": {
        "type": "text"
      },
      "_vector.predicted_value": {  // ポイント①: .predicted_value必須 
        "type": "dense_vector",
        "dims": 384,
        "index": true,
        "similarity": "cosine"
      }
    }
  }
}
{
  "processors": [
    {
      "inference": {
        "model_id": "sentence-transformers__msmarco-minilm-l-12-v3",
        "target_field": "_vector",
        // ポイント②: "マッピングしたいフィールド名": "text_field"を指定する
        "field_map": {
          "title": "text_field"
        }
      }
    }
  ]
}

以上の手順でElasticsearch本体側の設定は完了です。次は実際にデータを投入します。

データの投入

1. wikimediaからデータを取得、加工

次に、/indexerに移動し、wikimediaからデータをダウンロード、インデキシング可能な形式に加工します。

加工手順の詳細については、以下の記事と同様の手順ですので、こちらを御覧ください。 zenn.dev

wget https://dumps.wikimedia.org/enwiki/20221201/enwiki-20221201-pages-articles-multistream1.xml-p1p41242.bz2

python wikiextractor/WikiExtractor.py -o output -b 10M enwiki-20221201-pages-articles-multistream1.xml-p1p41242.bz2 --json

ls ./output/AA/* -d | xargs -L 1 -P 10 bash -c './reformat_to_ndjson.py $0'

2. _bulkでデータを投入

加工後のデータを_bulk APIを用いて投入していきます。

ls ./output/AA/*_new.ndjson -d | xargs -L 1 bash -c 'echo $0 ; cat $0 | curl -s -X POST -H '\''Content-Type: application/x-ndjson'\'' '\''http://127.0.0.1:9200/wiki/_bulk?pipeline=embedding-pipeline&pretty'\'' --data-binary @-;'

ここで3つ目のハマりポイントですが、リクエストのパラメータにpipeline=embedding-pipelineを指定します。このパラメータを指定する事により、投入されたデータが即座にキューに追加され、随時処理・先程定義したパイプラインによってリアルタイムにembeddingが付与されます。

また、このコマンドを実行する際に429のステータスが返ってきた場合、処理が追いついていないことを示します。1.で WikiExtractor.pyの引数に指定した-b 10Mを小さくしてバッチサイズを減らすか、またはデータ投入時に適当なwaitを挟んで上げることで対処可能です。


ここまでの手順でデータの投入は完了です。

遊んで見る

1. 検証ツールの起動

最後に、/evalに移動し、検証ツールを起動します。ツールにはstreamlitを使用しているため、インストールがまだの方はpip install streamlitでインストールしてください。

streamlit run main.py

query部分に文字を入力すると先程投入したデータに対しての検索処理が開始されます。
また、画面上部のスライドバーを調整することで、クエリウェイトの調整も可能です。

最後に

今回のブログではElasticsearchのIngest Pipelineを用いて、MLモデルの学習/ホスティングなしにHybrid Searchを試してみました。 デモのgifにもあるように、口語文でも比較的関連性の高い結果を検索できる為大変体験がよく、今後のベクトル検索に対する個人的な期待がかなり高まる結果となりました。

Elasticsearchのタイムアウトは何をどうタイムアウトするのか追ってみる

こんにちは、pakioです。

Elasticsearchのsearch apiではQuery Parameter, Query DSLともにサポートされているtimeoutオプションが存在します。

www.elastic.co

(Optional, time units) Specifies the period of time to wait for a response from each shard. If no response is received before the timeout expires, the request fails and returns an error. Defaults to no timeout.

このドキュメントから読み取れる挙動だとタイムアウトした場合エラーが返りそうなものですが、実際にはデフォルトでtrueに設定されているallow_partial_search_resultsによって取得できた結果まで返すような挙動となるようです。

allow_partial_search_results (Optional, Boolean) If true, returns partial results if there are shard request timeouts or shard failures. If false, returns an error with no partial results. Defaults to true.

一方、ひとえに検索リクエストといってもsearch, score, aggregate, ... など様々なフェーズがあるわけですが、どこまでが打ち切られるかについては明記されていません。 実際にある程度の規模のインデックスに対してtimeoutオプションを指定した状態で重めのaggregationなどをかけると、レスポンスを受け取るまでの時間が設定した時間を大幅に上回るケースも観測しました。

そこで今回はElasticsearchのタイムアウトについて少し深堀りしてみたので、その調査結果をまとめます。

なお、本ブログはv8.5.0時点のソースコード及びドキュメントを参考に執筆しています。

TL;DR

timeoutパラメータによって

  • 中断される処理

    • search
    • scoring
    • collapsing
    • post filter
  • 中断されない処理

    • aggregation
    • rescoring
    • suggest

大まかな処理の流れについて

Elasticsearchの大まかな処理の流れについては公式のブログがわかりやすくまとめてくれています。

www.elastic.co

この中でも今回関係するのが、Searching a Shard, … Then Fetchのセクションです。

ブログ内にも書かれている通り、Elasticsearchでは各shard毎にデータを保持しているため、リクエストが振り分けられた各shard毎にfetch, score, aggregateなどの操作を行い、coordinatorに返却、結果の集約を行っています。1

ここからはさらにシャード内でどのようにタイムアウトが取り扱われているかを覗いてみます。

各シャード内でのタイムアウトの取り扱いについて

  • この周辺のコードリーディング自体が不慣れなので、もし認識に誤りがある場合ご指摘いただけると大変ありがたいです。

各シャードにリクエストが振り分けられた後、SearchService内にてcontextが生成されます。
Elasticsearchの検索処理ではこのコンテキストに様々な情報(参考:DefaultQueryContext.java)を詰めて、これをベースに様々な処理を展開していくことになる、キーとなるオブジェクトです。

その初期化処理の一貫で、もしユーザーがタイムアウトを指定していた場合、コンテキストに設定値を保存します。

        if (source.timeout() != null) {
            context.timeout(source.timeout());
        }

リンク

SearchService内から呼ばれるQueryPhase内では、このコンテキスト内にセットされたタイムアウトの値をもとに、管理オブジェクトの生成時から指定した時間経過した際にTimeExceededExceptionを返すような関数を登録します。

            boolean timeoutSet = scrollContext == null
                && searchContext.timeout() != null
                && searchContext.timeout().equals(SearchService.NO_TIMEOUT) == false;

            final Runnable timeoutRunnable;
            if (timeoutSet) {
                final long startTime = searchContext.getRelativeTimeInMillis();
                final long timeout = searchContext.timeout().millis();
                final long maxTime = startTime + timeout;
                timeoutRunnable = searcher.addQueryCancellation(() -> {
                    final long time = searchContext.getRelativeTimeInMillis();
                    if (time > maxTime) {
                        throw new TimeExceededException();
                    }
                });
            } else {
                timeoutRunnable = null;
            }

リンク

その後ContextIndexSearcherにて実際の検索フェーズに入りますが、ここで各セグメントを処理するsearchLeafの先頭にて、cancellable.checkCancelled()が呼ばれ、上記の処理で設定した関数を実行しタイムアウトのチェックを行います。

private void searchLeaf(LeafReaderContext ctx, Weight weight, Collector collector) throws IOException {
        cancellable.checkCancelled();
...

リンク

また、同様の関数はさらにsearchLeaf内のスコアリング処理 intersectScorerAndBitSetの先頭および末尾でもチェックされます。

    static void intersectScorerAndBitSet(Scorer scorer, BitSet acceptDocs, LeafCollector collector, Runnable checkCancelled)
        throws IOException {
        collector.setScorer(scorer);
        // ConjunctionDISI uses the DocIdSetIterator#cost() to order the iterators, so if roleBits has the lowest cardinality it should
        // be used first:
        DocIdSetIterator iterator = ConjunctionUtils.intersectIterators(
            Arrays.asList(new BitSetIterator(acceptDocs, acceptDocs.approximateCardinality()), scorer.iterator())
        );
        int seen = 0;
        checkCancelled.run();
        for (int docId = iterator.nextDoc(); docId < DocIdSetIterator.NO_MORE_DOCS; docId = iterator.nextDoc()) {
            if (++seen % CHECK_CANCELLED_SCORER_INTERVAL == 0) {
                checkCancelled.run();
            }
            collector.collect(docId);
        }
        checkCancelled.run();
    }

リンク

この2か所にて、実行開始から指定時間が経過していた場合、前述のとおりTimeExceededExceptionが投げられますが、この時点で走査した分のドキュメントに関してはcontextに保持された状態になっています。

また、このままだとExceptionが返る結果となってしまいますが、実際には少し戻ったQueryPhaseにてallow_partial_search_result (デフォルト: true)のチェックが行われます。ここでもし途中までの結果で返却することが許容されていた場合、Exceptionを返す代わりにsearchTimedOutをtrueにセットし、そこまでの結果で残りの処理を続行します。

        } catch (TimeExceededException e) {
            assert timeoutSet : "TimeExceededException thrown even though timeout wasn't set";
            if (searchContext.request().allowPartialSearchResults() == false) {
                // Can't rethrow TimeExceededException because not serializable
                throw new QueryPhaseExecutionException(searchContext.shardTarget(), "Time exceeded");
            }
            queryResult.searchTimedOut(true);
        }

リンク


...

ここまでの処理はすべてQueryPhaseのexecute内、executeInternalで行われます。一方、aggregationやsuggest、rescoreに関してはexecuteInternalと同レベルで別処理として実行されるため、タイムアウトの考慮時間に含まれません。= タイムアウトで指定した時間を超過しても、処理が続行されます。

    public static void execute(SearchContext searchContext) throws QueryPhaseExecutionException {
        if (searchContext.hasOnlySuggest()) {
            SuggestPhase.execute(searchContext);
            searchContext.queryResult()
                .topDocs(
                    new TopDocsAndMaxScore(new TopDocs(new TotalHits(0, TotalHits.Relation.EQUAL_TO), Lucene.EMPTY_SCORE_DOCS), Float.NaN),
                    new DocValueFormat[0]
                );
            return;
        }

        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("{}", new SearchContextSourcePrinter(searchContext));
        }

        // Pre-process aggregations as late as possible. In the case of a DFS_Q_T_F
        // request, preProcess is called on the DFS phase, this is why we pre-process them
        // here to make sure it happens during the QUERY phase
        AggregationPhase.preProcess(searchContext);
        boolean rescore = executeInternal(searchContext);

        if (rescore) { // only if we do a regular search
            RescorePhase.execute(searchContext);
        }
        SuggestPhase.execute(searchContext);
        AggregationPhase.execute(searchContext);

        if (searchContext.getProfilers() != null) {
            searchContext.queryResult().profileResults(searchContext.getProfilers().buildQueryPhaseResults());
        }
    }

追記: タイムアウトされたリクエストのキャッシュについて

Elasticsearchではシャードレベルのリクエストキャッシュやセグメント単位のクエリキャッシュが存在しますが、このタイムアウト処理によって前者のリクエストキャッシュが影響を受けるようです。

リクエストキャッシュからの読み込み、保存処理などはIndicesServiceのloadIntoContextで実行されています。その際コンテキスト内のqueryResultのタイムアウトフラグがtrueとなっていた場合、indicesRequestCacheのinvalidateが実行され、その結果のキャッシュが消されることとなります。

        } else if (context.queryResult().searchTimedOut()) {
            // we have to invalidate the cache entry if we cached a query result form a request that timed out.
            // we can't really throw exceptions in the loading part to signal a timed out search to the outside world since if there are
            // multiple requests that wait for the cache entry to be calculated they'd fail all with the same exception.
            // instead we all caching such a result for the time being, return the timed out result for all other searches with that cache
            // key invalidate the result in the thread that caused the timeout. This will end up to be simpler and eventually correct since
            // running a search that times out concurrently will likely timeout again if it's run while we have this `stale` result in the
            // cache. One other option is to not cache requests with a timeout at all...
            indicesRequestCache.invalidate(
                new IndexShardCacheEntity(context.indexShard()),
                context.getSearchExecutionContext().mappingCacheKey(),
                directoryReader,
                cacheKey
            );
            if (logger.isTraceEnabled()) {
                logger.trace(
                    "Query timed out, invalidating cache entry for request on shard [{}]:\n {}",
                    request.shardId(),
                    request.source()
                );
            }
        }

リンク

もしリクエストキャッシュでキャッシュ可能なクエリが多数あるような環境の場合、タイムアウトの設定値には慎重になったほうが良さそうです。


まとめ

本ポストではElasticsearchのタイムアウトについてその実装を追ってみましたが、以下がその結果を簡単にまとめたものになります。

中断される処理 中断されない処理
search aggregation
scoring rescoring
collapsing
post filtering

表からも、実際には他のソフトウェア文脈で利用されるような厳密なタイムアウトではなく、一部の処理にしか適用されないものだとわかります。
極端な例にはなりますがクエリは軽いがヒット件数が多い & aggregation条件が複数ある・重い ような検索だとその恩恵は受けられなさそうです。一方純粋な検索処理だけであれば十分に効果のあるパラメータではあるため、検索条件は単純だがドキュメント数が膨れなおかつ全件取得する必要がないようなケースでは有効かもしれません。


  1. この内容は、すべてsearch_type = query_then_fetchを前提としています。

2022年夏現在の関東発石垣島到着後ダイビングの状況/到着後にダイビングしたい場合のベストプラクティス


2022年7月に石垣島に行き、午前中に石垣空港到着後、午後からダイビングをしようと試みたところ、思いの外受け付けてくれるショップが無く苦労したのでその体験記です。

TL;DR

  • 到着後ダイビングは基本的に朝9時台着のJAL便を前提にしているのでLCC(Peach)を利用する場合は注意
  • まれに受け入れしているショップもあるが、基本は午後1ダイブ
  • 連日利用を前提としているケースも多く、1日のみの利用だと断られるケースも
  • 空港からの送迎はコロナの影響で基本的になし、タイミング次第で離島ターミナルからの送迎があるショップもある
  • 今回は八重山Diving Service Fieldさんで、運良く離島ターミナルの送迎付き、Peach午前便で午後2本潜れた💪

ishigaki-diving-field.com


画像元:沖縄ダイビングライセンス・ワールドダイビング

前提/留意事項

  • ライセンス : AOW
  • ダイブ本数 : ~30本
  • 航空会社 : Peach 成田 - 石垣島 午前便 (10時台着)
  • 希望 : 午後から2ダイブ

コロナ禍前の石垣島での着後ダイビングの状況は把握していませんので、その点はご了承ください。
また、本ブログの内容はファンダイブを前提としており、体験ダイビングでは異なる可能性があります。

航空会社による違いについて

2022年7月現在、東京近郊発の石垣島直行便はANAJALPeachの3社から出ています。
様々なダイビングショップに電話で問い合わせを行いましたが、到着後ダイブは基本的に9時台の到着を前提としているようで、東京近郊発だとJAL1社のみに対応していることになります。そのため、今回私が利用した10時台着のPeachは残念ながら多くのショップで断られる結果となりました。


路線別時刻表 | 飛行機に乗る | 南ぬ島石垣空港公式ウェブサイトより

東京近郊以外だと、関空発のPeach便、沖縄(那覇)発の各便が対応しているでしょうか。


移動方法について

上記の通り、かなり時間にシビアなショップが多いため、空港からショップもしくは集合場所への移動手段も限定される場合があります。
一般に手続きに要する時間の関係上レンタカーはバス・タクシーと比較して移動が開始できるまでのロスタイムが長いため、バスもしくはタクシーでの移動かどうかを確認されるケースが何件かありました。ですのでレンタカーの予約前に各ショップにおすすめの移動方法をヒアリングすることを推奨します。

1日のみの利用について

石垣島自体割と中・長期の滞在客が多いのか、問い合わせ時点で利用日数を聞かれることが多々有りました。今回は都合上1日のみダイビング可能だったためその旨をお伝えすると、中にはかなり乱暴に無理無理と断られるケースもあったので、基本的に1日のみのダイビング、かつ体験ダイビングではなくファンダイブはハードルがかなり高そうな様子です。

今回利用したショップについて

これらの点を踏まえた上で、前提条件である着後2ダイブを叶えていただけるショップを探したところ、八重山Diving Service Fieldさんであれば可能とのお返事を頂き利用させていただきました。
ishigaki-diving-field.com

石垣島の離島ターミナルからほど近く、送迎もあり徒歩でも5分、ダイビング船の乗り場も目の前とかなり好条件の揃ったショップでスタッフの方も良い人ばかりでした。本当にありがとうございました!

(その他7~8ショップにも電話・フォームで問い合わせましたが、全滅でした)



まとめ:到着後に確実に2ダイブするには

  • 9時台に到着する飛行機に乗る。(10時台はハードル高め)
  • レンタカーは手続きに時間がかかることがあるため、バスもしくはタクシーで初日は移動することを検討する
  • ダイビング可能な日を複数日設け、連続で利用したい旨を伝える

在宅勤務夫婦にはApple Watchを推したい

我が家はエンジニアとクリエイターの夫婦で、コロナ禍になって以降二人とも家で働いています。
そんなわが家が、昨年末手に入れたのがApple Watch

www.apple.com

平日の昼間も一緒に過ごすようになってから感じるようになった不便感がかなり減ったように思うので、今日はその紹介を。

声をかけるのが楽

2人で過ごす時間が増えると、何かとコミュニケーションが必要になるかと思います。
例えば、ランチは何時になにをたべるとか、今日はどれくらいまで仕事するとか、コーヒーはのむかとか。

わが家の仕事部屋は家の正反対で階もちがうため声をかけるのがかなり大変でしたが、Apple Watchにはトランシーバー機能があります。

support.apple.com

これがかなりの優れもので、スマホを手に持たずとも声をかけられ、呼ばれた側のApple Watchからスピーカーで声が聞こえます。
LINEだと通知を見逃したり仕事中は通知をオフにしていることもありますが、実際に音として聞こえるのでコミュニケーションロスがかなり減りました。

買い忘れが減る

在宅勤務が始まる前、どちらも通勤していた頃には必要になったものは帰りがけに買ってくることができました。
買ってきてほしいものもお互いLINEに書いておけば忘れることもなかったような気がします。

一方現在ではそもそも買い物に行く回数もへり、一度逃すと次は数日後とかに…

そこで我が家はリマインダーを共有し買い物リストを管理しています。
このリマインダーもApple Watchで編集でき、これがまたとても便利です。

iPhoneだけで生活していたころは、手元にスマホがないからと記録を忘れることもありましたが、Apple Watchは常に身に着けているのでその心配がありません。
音声入力もかなり優秀で、ほぼミスなく記録してくれます。


以上、在宅勤務夫婦が推すApple Watch利用方法の紹介でした。みなさまもよろしければ是非。

Elastic Contributor Program参加のすゝめ & Elastic Bronze Contributor 2022に選ばれました

こんにちは。 普段Elasticsearchを使う傍ら勉強会に参加したり、新機能やパフォーマンスについて書いているpakioです。 その甲斐あってこの度Elastic Contributor Programにて、Elastic Bronze Contributor 2022に選ばれました。

https://api.accredible.com/v1/frontend/credential_website_embed_image/certificate/46135839 https://www.credential.net/b35ba5e8-2078-4ec7-983a-20ed77493833#gs.po50tg

そもそもどういったプログラムなのかについてご存知ない方もいらっしゃる気がするので、参加方法、選出基準などについてご紹介します。

Elastic Contributor Programについて

www.elastic.co

Elastic Contributor Programとは、Elastic社から公式に提供されている、Elasticコミュニティに貢献した人向けの表彰精度です。 Contributeというとコードを連想しがちかと思いますが、ブログでの発信や登壇なども含まれており、それぞれポイントが割り振られています。

内容 ポイント
イベント開催 6p
プレゼンテーション 6p
文章コンテンツ 6p
コードでのコントリビューション 6p
動画コンテンツ 4p
公式ブログ記事の翻訳 2p
Elasitc公式QA, 及びStack Overflowでの回答 1p
他コントリビューターのコントリビュート検証 1p

アカウント登録・コントリビュート方法

アカウント登録について

Contributor Program専用のアカウントという概念はなく、Elasticアカウントを持っている方ならすぐに開始可能です。
まだ作成されていない方でも、GoogleアカウントからElasticアカウントを作成できます。

https://cloud.elastic.co/registration

コントリビュート方法

コントリビュートの提出はElastic Contributor Program上から行えます。 左側のSubmit Contributeを押すと下のような画面が開くので、自分が行ったコントリビュートについて入力します。(以下画像は2/16のElasticsearch勉強会での発表を登録する例) f:id:pakio96te:20220220202527p:plain

サブミットされたコントリビュートはすぐにポイントに反映されるわけではなく、他ユーザーに有効性を検証され承認された後に反映されます。気長に待ちましょう。
また、この承認作業でも1ポイント得られるため、たまたま自分が開いたタイミングで他人のコントリビューションがあった場合はぜひチェックすることをおすすめします。

表彰基準について

ここまでの内容だけを見ると、アカウント登録からコントリビュートまでハードルが高そうですが、実はそうでもありません。 これまで2021と2022の2回表彰が行われましたが、それぞれボーダーは以下の通りです。

2021年 2022年 順位(人)
Bronze Contributor 7p 6p 14位 ~ 41位 (18人)
Silver Contributor 74p 61p 4位 ~ 13位 (10人)
Gold Contributor 334p 307p 1位 ~ 3位 (2人)

流石にGold, Silverはアクティブな上位陣になるためハードルが高いですが、Bronze Contributorの下位であればブログ記事1回 + @で達成でき、そこまでハードルが高くありません。
また、この人数は全世界ではなく、全世界を大きく3リージョンに分けた地域ごとに表彰されるため、実質チャンスは3倍になります。 なお、このボーダーは参加人数や参加者のスコアにより上下することにはご注意ください。

副賞について

このContribute Programですが、ただ表彰されるだけでなく副賞がついてきます。 こちらについては変更の可能性がありますが、公式にはこのように案内されています。

内容 Bronze Silver Gold
記念品 o o | o
Elastic Cloudトライアル o (30日) o (30日) o (60日)
Elasticの公式トレーニング (standard) x x o
認定資格試験 x o (1回) o (2回)
バッジ・認定証 (バーチャル) o o o

www.elastic.co

バーチャルバッジだけでなく、swagが貰えたり、Silver以上だと認定資格試験などもついて来るなどかなり充実しています。認定資格試験は1回約4万円(400USD)なので、ぜひともSilver以上を目指したいものです。

まとめ

今回はElastic Contributor Programの受賞報告とプログラムについての紹介でした。コードのコントリビューションだけでなく、ブログや登壇でも獲得でき、かつコミュニティの繁栄にも貢献できる良いプログラムなのでぜひ皆様参加してみてください。