ElasticsearchのmappingからGoのTypeを作る(1/2)
Overview
-
- Elasticsearchの概要について説明する
-
- Elasticsearchについて、JSONを格納したり引き出したりする場合に必要な知識を調査し、明示する
-
- 以下を達成する型を作るcode generatorを作成する
- mapping.jsonからindexに格納されたJSONを容易に生成/消費できる
- Plain / Rawと二つに分け、アプリの決定事項を反映した型、Elasticsearchが受け入れるすべての値からUnmarshalできる型とそれぞれする
- 相互を適切に変換するメソッドを設ける
-
"dynamic"
値が"strict"
以外の時にmapping.jsonに載っていない数値を格納できる
-
- 作成中に見つけたjenniferによるcode generationのポイントを述べる
この記事は1.
と2.
を述べ、3.
、4.
はpart2で述べます。
成果物
- Helper Types: Elasticsearchにドキュメントとして格納するJSONのフィールドをmarshal / unmarshalする型
- Code Generator: mappingからGoのstructを作るcode generator
を作りました。
成果物はこちらです。
以下でインストールし、
# go install github.com/ngicks/estype/cmd/genestype@latest
以下のようなオプションを受け付けます。
# genestype --help
Usage of genestype:
-c string
path to config file.
see definition of github.com/ngicks/estype/generator.GeneratorOption.
-m string
path to mapping.json.
You can use one that can be fetched from '<index_name>/_mapping',
or one that you've sent when creating index.
-o string
[optional] path to output generated code. (default "--")
-p string
package name of generated code.
サンプルで用意してあるmapping.jsonとオプションは以下に格納され
それをgenestype
に食わせて以下のコードを生成してあります。
前提知識
以下を有する
- Go programming languageを使って開発が行える程度の理解度。
-
Elasticsearchとやり取りするアプリケーションを開発できる程度の理解度。
- indexの作成
- documentの格納/取得
Elasticsearchついて、基本的な概要からJSONを生成、消費することにかかる説明はこの記事で行いますが十分足りてるかは不明です。Goに関しては一切説明を行いません。
- 本投稿のごく一部で何の説明もなしにTypeScriptの型表記がでてきます。知っている人か、でなければなんとなくで読んでください。
対象読者
- Elasticsearchとやり取りするアプリを書いてJSON構造がよくわからなくて困った人
- 格納できるJSONの生成に関するという観点のみだが、Elasticsearchのmappingに関する細かい話が知りたい人
環境
作り出した時期が大分前なので、elasticsearchは8.4.3を対象に作られています。
ドキュメントもすべて8.4のものを参照しています。
# go version
go version go1.20.6 linux/amd64
# curl ${ELASTICSEARCH_URL}
{
"name" : "cdf7a5d86cb7",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "raebKhvuRay4SB_eC704NQ",
"version" : {
"number" : "8.4.3",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "42f05b9372a9a4a470db3b52817899b99a76ee73",
"build_date" : "2022-10-04T07:17:24.662462378Z",
"build_snapshot" : false,
"lucene_version" : "9.3.0",
"minimum_wire_compatibility_version" : "7.17.0",
"minimum_index_compatibility_version" : "7.0.0"
},
"tagline" : "You Know, for Search"
}
背景
筆者は業務でElasticsearchとやり取りするアプリをNode.jsで開発しています。その中で、いろいろなことで困りました。例えば:
- あるフィールドが
undefined
(JSONにキーが存在しない),null
,T
(keywordの場合stringのような),T[]
のどれでも取りうることがあるため、typescriptの型定義が非常に煩雑であること - JSON.parseしただけではDate型がjavascriptのDate型に変換できず、アプリケーションの中で変換コードが散らばってしまう。
-
Boolean型が
true
/false
だけでなく"true"
/"false"
を受け付けるため、アプリ内で非常に煩雑なコードで判別することになってしまう。
後ろの二つは明らかに筆者が未熟でした。JSON.parseした後に、アプリに都合のいいフォーマットに変換する変換部を設けるべきでした。後悔は先に立たないものですね。(最近はzodを使えばいい感じにできるかもしれませんん。)
一方で、Goではある型がjson.Marshaler,json.Unmarshalerを実装している場合、json.Marshal()
,(*json.Encoder).Encode()
, json.Unmarshal()
,(*json.Decoder).Decode()
などの対応する関数の中で、デフォルトの挙動の代わりにそれらが呼び出されるという挙動があります。
GoはEncode / Decodeの界面での変換、validationに関して意識しやすい設計になっているため、ここにElasticsearchのindexに格納できるJSON documentとGo structと変換するいいブリッジをかけば同じ轍を踏まずに済みそうです。
目的
- mapping.jsonを解析してそこからJSONを生成/消費できる型を生成するcode generatorを作成する
目標
この記事では、そのために以下を調べます
- Elasticsearchがmappingに対してどのようなJSONを受け付けるのか
- 逆にどのようなJSONを受け付けないのか
- その中で
string
やint
やmap[string]any
などで表現できない特定のJSONのサブフィールドを必要とするfield data type(s)
さらに、
- そのfield data typeを表現する型を作成する
Elasticsearch
コンテキストを共有するためにElasticsearchの概要について説明します。筆者はElasticsearchに明るくないので基本的には引用元をあたってください。
概要
引用:
https://www.elastic.co/guide/en/elasticsearch/reference/8.4/elasticsearch-intro.htmlElasticsearch is the distributed search and analytics engine at the heart of
the Elastic Stack. Logstash and Beats facilitate collecting, aggregating, and
enriching your data and storing it in Elasticsearch.(中略)
Elasticsearch provides near real-time search and analytics for all types of
data. Whether you have structured or unstructured text, numerical data, or
geospatial data, Elasticsearch can efficiently store and index it in a way
that supports fast searches.
Elasticsearchは検索と分析を行う分散型ドキュメントストアであり、
REST APIを通じてJSONをDocumentとして格納することができます。
格納されたドキュメントはデフォルトでは1秒程度で検索可能状態となり、謳われる通りnear real-time
です。
引用:
https://www.elastic.co/guide/en/elasticsearch/reference/8.4/documents-indices.htmlElasticsearch uses a data structure called an inverted index that supports
very fast full-text searches.
格納されたドキュメントは、設定に基づいて解析され、
Inverted Indexに変換されます。これにより高速な検索を実現してるとのことです。
引用:
https://www.elastic.co/guide/en/elasticsearch/reference/8.4/search-analyze.htmlWhile you can use Elasticsearch as a document store and retrieve documents and
their metadata, the real power comes from being able to easily access the full
suite of search capabilities built on the Apache Lucene search engine library.
Apache Luceneを包むことでこれらの挙動を実現していると述べており、種々の事情がここから透けてきています。ElasticsearchがShardという単位で管理を行う部分がありますが、これはLucene Indexのことのようです(参考)。
ElasticsearchのIndexというのはRDBで言うところのTableに近く、
同じschemaを共有するドキュメントを格納することができ、indexに対して検索をかけることができます。
このschemaを決めるのがmappingであり、mappingによってIndexに収めるJSON Documentの形を完全に、あるいは部分的に決めることができます。
dynamic mapping / explicit mapping
引用:
https://www.elastic.co/guide/en/elasticsearch/reference/8.4/documents-indices.htmlElasticsearch also has the ability to be schema-less, which means that
documents can be indexed without explicitly specifying how to handle each of
the different fields that might occur in a document.(中略)
You can define rules to control dynamic mapping and explicitly define mappings
to take full control of how fields are stored and indexed.
Elasticsearchはschema-lessで稼働して検索されることも可能です。この場合はDynamic field mappingで述べられるように、格納されたJSONの内容から自動的にmappingを推定します。データの形が推定されるために手元のデータをとりあえず検索可能にするなどのユースケースには便利なのかもしれません。
一方で、推定されるために
- date detectionに検知されないフォーマットの時間が時間としてindexされない
-
"true"
/"false"
がbooleanとしてindexされない
などのドローバックがあると述べられています。
mappingはindex作成時などに明確に指定することができます。
mappingの指定/更新
IndexはCreate index APIで作成します。このAPIで同時にmappingを指定することができます。このmappingがこの記事で呼ぶmapping.json
のことです。
一度作られたindexのフィールド(mapping)は減らすはできません。
追加はUpdate mapping APIにより可能です。
"dynamic": "strict"
によるmappingの固定
mappingはしばしば完全に固定にされる("dynamic":"strict"
)ことがあります。
引用: https://www.elastic.co/guide/en/elasticsearch/reference/8.4/dynamic.html#dynamic-parameters
strict
If new fields are detected, an exception is thrown and the document is rejected. New fields must be explicitly added to the mapping.
推定を防ぐことで後々のmapping拡張を許したり、ill-formedなJSONを検知するなどの目的でこの設定が行われると考えられます。また、自由なフィールド追加を許すとmapping explosionという現象が起きる可能性があります。これはindexに任意の入力を許すことによりmappingが増殖してパフォーマンスが劣化することです。
ユーザーの任意な値を収める場合はflattenedフィールドを使うか、objectのmapping設定で"index":false
にするなどをしたほうが良いです。
この記事の目的は主に、"dynamic":"strict"
なmappingからGoの型を生成することで知識をコードにすることです。
お品書き
mapping.json
から型を作るcode generatorを作るにあたり以下の作業が必要でした。
-
undefined | null | T | (null | T)[]
をunmarshalできる型を作る。 - Elasticsearchに格納できるJSONの形を調査する。
- Helper typeを実装する: ElasticsearchのField data typesのうち、単なる
string
やint
などでない型に対するhelperを作成する。 - date formatの変換: Elasticsearchの理解するtime formatをGoが理解するそれに変換する部分を実装する。
- code generatorの作成: github.com/dave/jenniferを使ったcode generatorを作る。
5.
はpart2で述べます。
undefined | null | T | (null | T)[]
をunmarshalできる型を作る。
すべてのフィールドはundefined
であることが許され、ほとんどのfield data type(s)において、null
を受け付けます。Arraysの項に説明される通り、ほとんどのfield data typesにおいてT[] | T[][]
も同じく受け付けられます。T[][]
はT[]
にflattenされると述べられています。
上記のすべてはオペレーション上混在しうることが考えられるため、これらすべてからunmarshalできる型があるとGoのコードからの扱いがよくなります。(T[][]
はサポートしないことにしました)
混在する状況として考えらるのは
- 全くデータを指定しない(
{}
)で初期化されたドキュメント- すべてのフィールドが
undefined
- すべてのフィールドが
- mappingが追加されてフィールドが拡張された場合の、それ以前にindexされたドキュメントのそのフィールドは
undefined
- フィールドをクリアするためにupdate APIのpartial updateで
[]
、もしくはnull
で上書きされた場合 - サービスで運用されていくうちに、もとは
T
だったフィールドがT[]
にしたい場合、T
とT[]
は混在
undefined
とnull
、T
とT[]
の混在は、reindexとpipelineなどででデータをいじって、aliasをreindex先に切り替えれば無停止でそれぞれ片方に統一できます。しかし、ドキュメント数が多いとメモリやCPU使用率が高い状態が長時間続くため、あえてやりたい理由はないでしょう。
上記の型を実装するには前回の記事でも述べた、github.com/ngicks/undを拡張し、Elastic[T]
として実装しました。
前回の記事で提示したUndefinedable[T]
とNullable[T]
の組み合わせで上記のすべての型を表現することを実現しました。
UnmarshalJSON
の実装でnull, T and (null | T)[]
のいずれも受け付けられるようにしてあります。
とある通り、値がある場合は必ずT[]
に向けてエンコードされるように設計されています。
値が一つだけの場合にT
にmarshalすべきか、T[]
にすべきかは型レベルでは判別のつきづらい要素であったためです。
Elasticsearchに格納できるJSONの形を調査する
Elasticsearchのmappingとフィールドの形
前述のとおり、ElasticsearchではIndexごとに格納するJSON documentの形をmapping.json
によって決めることができます。specによればトップは必ずJSON Objectであり、各フィールドはfield data type(s)を指定することで、格納するデータの型とそれの意味が決められます。
前述のとおり、mappingはindex生成時などに事前に定義することができ、設定によって部分的に、あるいは完全にJSONの形を固定することができます。
mappingはJSONとしてPUT /<index_name>
にsettingとともに渡すことができます。
例えば、以下のようなmappingの場合
{
"mappings": {
"dynamic": "strict",
"properties": {
"name": {
"type": "text"
},
"blob": {
"type": "binary"
},
"range": {
"type": "integer_range"
}
}
}
}
以下のJSONをそのIndexに格納することできます
{
"name": "alice",
"blob": "Zm9vYmFyYmF6",
"range": {
"gte": 3,
"lt": 5
}
}
このようにmappingによってJSONの形と意味が決められます。
ドキュメントによると
-
textはその言葉の通り、
analyzer
にって解析されるfull-textコンテンツ - binaryはbase64 encodeされたstring
-
rangeはサブフィールドに
gt
,gte
,lt
,lte
を持つことでrangeを表現できる型
text
やkeyword
はstring
、integer
やdouble
はint32
やfloat64
に単にすればよいですが、それ以外は大なり小なりフォーマットに意味があります。
Elasticsearchの各field data typeのふるまい
8.4のfield data typesの項をすべて読み、以下を一通りチェックしました。tableの以下の各項目と対応しています。
- built-in / std
- => Goのbuilt-in typeとstdの範疇で表現できるか
- 要mapping解析
- => mappingの値によって型が変わったりcode generationで生成される内容が変わるか。
- multi-value
- => 複数値を受け付けるか
- null
- =>
null
をそのフィールドに格納できるか
- =>
- single valueのarrayを受け付けるか
-
dense_vector
以外は許されました。おそらくArraysの項に説明がある通り、T[][]
はT[]
にflattenされるからでしょう。
-
field data type | built-in / std | 要mapping解析 | multi-value | null | 備考 |
---|---|---|---|---|---|
aggregate_metric_double | ⭕️ | ❌ | |||
alias | N/A |
⭕️ | ❌ | ❌ | いかなる値も受け付けない |
binary | []byte |
||||
boolean | |||||
completion | string |
❌ | |||
date/date_nanos | ⭕️ | ||||
dense_vector | ⭕️ | ❌ | ❌ |
[[1,2,3]] は受け付けない |
|
flattened | map[string]any |
||||
geo_point | |||||
geo_shape | |||||
histogram | ❌ | ||||
ip | netip.Addr |
||||
join | ⭕️ | ❌ | ❌ | ||
keyword | string |
||||
constant_keyword | string |
⭕️ | ❌ | mapping.jsonで入れたワードしか受け付けない | |
wildcard | string |
||||
nested | ⭕️ | code generatorからするとobject と同じ |
|||
byte | int8 |
||||
double | float64 |
||||
float | float32 |
||||
half_float | float32 |
goはnativeでfloat16 をサポートしない |
|||
integer | int32 |
||||
long | int64 |
||||
scaled_float | float64 |
||||
short | int16 |
||||
object | ⭕️ | ||||
percolator | map[string]any |
❌ | ❌ | QueryDSLを格納する用途 | |
point | |||||
date_range | ⭕️ | ||||
double_range | |||||
float_range | |||||
integer_range | |||||
ip_range | |||||
long_range | |||||
rank_feature | float64 |
❌ | |||
rank_features | map[string]float64 |
❌ | 同じキーが複数のobjectにあるとエラー | ||
search_as_you_type | string |
||||
shape | |||||
text | string |
||||
match_only_text | string |
||||
token_count | おそらくfieldプロパティ―以外では使えない? | ||||
unsigned_long | uint64 |
||||
version | string |
semverパッケージ使うほうがいいかもしれない |
Helper Typeを実装する
必要性
上記のように、ものによっては単なるbuilt-in typeでElasticsearchのfield data typeを表現できません。
前記のtext, binary, rangeのうち、text
は単なるstring
,binary
は[]byte
とすればよいでしょう。
引用: https://pkg.go.dev/encoding/json#Marshal
Array and slice values encode as JSON arrays, except that []byte encodes as a
base64-encoded string, and a nil slice encodes as the null JSON value.
とあるように、json.Marshal()
が[]byte
をbase64にエンコードするように設計されているためです。
もし仮にぎりぎりまでデコードを遅延したいならばそれ用の特別な型が必要ですが、ひとまずはそのことは考えないようにしましょう。
他方、rangeなど、決められた形のJSONを収める型に関しては、例えば以下のような型が定義されてあると、勘違いが少なく、実装の手間も少ないわけです。
type Range[T comparable] struct {
Gt *T `json:"gt,omitempty"`
Gte *T `json:"gte,omitempty"`
Lt *T `json:"lt,omitempty"`
Lte *T `json:"lte,omitempty"`
}
実装
前述のテーブルのbuilt-in / std
が空白であった型に関してhelper typeを定義します。
力尽きてしまったのでjoinとpointは未実装です。自分で使う機会があれば実装するかもしれません。
aggregate metric double
aggregate_metric_doubleは、mappingの"metrics"
で定義した値のみ値を格納できるようです。
// 引用: https://www.elastic.co/guide/en/elasticsearch/reference/8.4/aggregate-metric-double.html
{
"mappings": {
"properties": {
"my-agg-metric-field": {
"type": "aggregate_metric_double",
"metrics": ["min", "max", "sum", "value_count"],
"default_metric": "max"
}
}
}
}
4種類のサブフィールドの組み合わせですから、15種類の型を定義しておけばよいです。そこで:
以上のように、フラグのon/offの全パターン網羅はfor
文で容易に実装できます。これによって事前にすべてのパターンを事前に生成しておけばよいのです。
この型のうちmappingに対し適切なものをcode generatorによって選択してもらえばいいわけですね。
boolean
booleanは以下の値を受けれるとドキュメントされています。
- trueして:
true
,"true"
- falseとして:
false
,"false"
,""
boolをbase typeと持つ型とし、MarshalJSON
/ UnmarshalJSON
を実装すればよいでしょう。
困ったことに、stringの"true"
/ "false"
を好むプロジェクトが存在する(筆者が実際に参加していました)ため、MarshalJSON
で出すのがboolean literalになる型とstring literalになる型をそれぞれ作ってcode generatorの設定値でどちらを使うか決める決断を下しました。
geo_point
geo_pointは以下の6つのフォーマットを受け付けます。
- GeoJSON
- Well-Known Text
{"lat":123,"lon":456}
[lon, lat]
"lat,lon"
- geohash
多いですね。
頑張って実装しました。これで少なくとも公式のサンプルを全部パーズできます。
この型はシンプルな{"lat":123,"lon":456}
フォーマットにMarshalします。
boolと同じく、どのフォーマットに対してmarshalするかを設定で決められるようにすればよかったと思いますが、力尽きてしまいました・・・。
geo_shape
geo_shapeはGeoJSONかWell-Known Textを受け付けます。
にある通り、GeoJSON Typeのうち
"Feature"
と"FeatureCollection"
以外を受け付けるとあります。
そこで実装は、
特にデータフォーマットを制限したりせず、github.com/go-spatial/geomに委譲してしまう実装にしました。内部の実装を読む限り、wktはbboxをサポートしていないのでそれを使われるとデコードできないですが、それ以外は網羅できています。
histogram
histogramはドキュメントによるとalgorithm agnosticな値のセットであり、あらかじめaggregateされている値を入れておくものらしいです。
これはシンプルに
というだけです。
point / shape
見たところgeo_point, geo_shapeとほぼ同じなのですが、ドキュメントを読んでる時間がなく、shapeはgeo_shapeをそっくり使いまわします決断をしました。pointがgeo_pointとどう違うか確認が取れなかったため、実装を先送りしています。
range
rangeはその名の通り数値のrangeを表現するものです。8.4の時点では
integer_range
float_range
long_range
double_range
date_range
ip_range
があります。
version_range
が存在しないのがちょっと気になるところですが、semverを数値に変換すれば実現可能なので優先度が低いんでしょうか。
各フィールドはnull
を許容しないため、,omitempty
が必要です。試してないですがGt
とGte
、Lt
とLte
は同時に存在してはいけないはずです。これに関しては特に型やメソッドによってvalidationをかけられるようにはしていません。
型制限をこれ以上きつくできなかった話
実際には以下のようにしたほうが型レベルできついのですが
type Range[T interface {
constraints.Integer | constraints.Float | netip.Addr | time.Time
}] struct{}
後述するElasticsearchのdateフォーマットをすべて理解できる型を作る際に以下のように、エラーになるため、できませんでした。
type builtinDate time.Time
func init() {
var n Range[builtinDate]
// builtinDate does not satisfy interface{constraints.Integer | constraints.Float | netip.Addr | time.Time}
// (builtinDate missing in ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | net/netip.Addr | time.Time)
}
筆者の理解が正しければ、 structをunderlying typeとする型を、 base
typeとする型をconstraintにすることは現状できません。
つまり
type Range[T interface {
constraints.Integer | constraints.Float | ~netip.Addr | ~time.Time
}] struct{}
// invalid use of ~ (underlying type of netip.Addr is struct{addr netip.uint128; z *intern.Value})
// invalid use of ~ (underlying type of time.Time is struct{wall uint64; ext int64; loc *time.Location})
というエラーです。実際この型にtype paramを入れるのはcode
generatorなのでこの制限は特に問題ないとみなし、とりあえずでcomparable
にしてあります。
date helper typeの実装
dateおよびdate_nanos field data typeは"format"
フィールドで指定されたフォーマットに従うstring
もしくはnumber
を収めることができ、フォーマット通りに解釈されて時間としてインデックスされます。
// 引用: https://www.elastic.co/guide/en/elasticsearch/reference/8.4/date.html#multiple-date-formats
{
"mappings": {
"properties": {
"date": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
}
}
}
}
- フォーマットはDateTimeFormatterのフォーマットに従う(Cunstom Date Formats)
||
区切りの複数のフォーマットを指定できる- 特定の文字列(e.g.
strict_date_optional_time
)はbuilt in formatとして認識される -
epoch_millis
とepoch_second
を指定すると、Epochからのミリ秒、秒をJSONのnumber
で指定できる。 -
"format"
を指定しない場合デフォルトは"strict_date_optional_time||epoch_millis"
,"strict_date_optional_time_nanos||epoch_millis"
にそれぞれなる-
"strict_date_optional_time"
はyyyy-MM-dd'T'HH:mm:ss.SSSSSSZ or yyyy-MM-dd
です。 - と、ドキュメントには載っていますが実際には9桁までデータを格納できます。おそらくIndex時にデータがドロップしています。
-
したがって以下が必要となります。
-
"format"
フィールドを読んでフォーマットの解析 - built in formatの事前展開展開
- DateTimeFormatterが理解するフォーマットをGoの
time.Parse
が理解するフォーマットに変換- optional section (
[
,]
)の展開 - トークンごとの変換
- optional section (
- 複数のフォーマットでパーズができる型を定義
- Marshal / Unmarshal時、フォーマットに
epoch_*
が含まれている場合numberも解釈する必要がある。
- Marshal / Unmarshal時、フォーマットに
"format"
フィールドの展開や、built in formatの内容からレイアウト変換などはcode generatorの行いますのでこのセクションでは述べません。
optional sectionの展開
この機能はoptionalstring
という名前でパッケージにまとめてあります。
パパっと調べた限り、特定のトークンで囲まれたstringをoptionalとみなして展開し列挙する、というほしい機能を備えたパッケージは見つかりませんでした。
探し方が悪いだけな可能性が高いですが、いいんですこれは趣味プロジェクトなんだから作ってしまえば。
このパッケージはgithub.com/prataprc/goparsecというパーザコンビネータを利用して文字列を木構造に変更、木構造を展開してoptional sectionなしの文章に列挙します。
このパッケージは事前処理のために使われるため、パフォーマンスは重視されていません。あまり賢い実装をしているとは思えませんし、実際頭がこんがらがりながら木構造を展開する処理をかいていました。実際パーザコンビネータの吐くトークン列から文字列を列挙をすればいいのに木構造に落としなおしている時点で非効率なはずです。
実装の不備やバグは探せばいくらでもあると思いますがdate formatを展開するという用途には現状問題なく動作しています。
time tokenの変換
こちらは以下のファイルで実装されています。
中身はtime.Parse
を簡易化したような実装をしており、愚直にswitch-caseを書いてパフォーマンスを求めるより、トークンをテーブル化して実装の負担を減らす方針でいきました。
すなわち
func convert(input string) string {
var prefix, token, suffix, out string
for len(input) > 0 {
prefix, token, suffix = nextToken(input)
input = suffix
out += prefix + fromJavaLikeToGoToken(token)
}
return out
}
のような感じです。time.Parse
では上記のnextToken
に当たる関数の中で膨大なswitch-case
を走らせてトークンを抽出しています。
こういったtableを作ることで、switch-caseの量を大分減らせます。
doc commentでも述べていますが、Goが同じ機能を持つトークンを持たない以下はサポートされません
とくにweekyear系トークンがないのでbuilt in date formatの中にいくつか使えないものが出てきます。
複数フォーマットでパーズできる型を定義
複数のstringフォーマットを持つパーザ
これ以下のファイルで実装しました。
これはとっても簡単ですね。
複数のレイアウトを保持
イニシャライズ時にlengthでdescending, 文字コードでdescendingでソート、dedupe,
validateし、
順番にパーズを試みて成功したらそのまま値を返します。
順番にレイアウトを試していくのはElasticsearch自身のソースを参考にしました(すみません、出展なしです)。どうやっているんだろう、と思って見に行くと単にパーザーをイテレートしながらパーズを繰り返していたので、なるほど、と思いに似たような処理にしています。
numberもパーズ/フォーマットできるパーザ
これは前述のMultiLayoutとnumberを変換できるパーザを組み合わせます。
numberのパーザ/フォーマッタはElasticsearchのそれと一致したstring typeであると非常に楽です。つまり
switch-caseによってtime.UnixMilli
とtime.Unix
を呼び出せば所望の動作を実現できます。
まとめ
-
- Elasticsearchの概要について説明した
-
- Elasticsearchについて、JSONを格納したり引き出したりする場合に必要な知識を調査し、明示した
- 単に
string
,int
などにできない型について型を定義し、必要であればjson.Marshaler
,json.Unmarshaler
を実装した。 - date / date_nanosのために時間フォーマットの変換部と複数レイアウトを保持してパーズができる型を実装した
part2でmapping.json
から型を生成するcode generatorを実装します。
Discussion