OpenSearch を使ってみる

この記事は「[弁護士ドットコム Advent Calendar 2021](https://qiita.com/advent-calendar/2021/bengo4com)」の 2 日目の記事です。よろしくお願いいたします。

---

# はじめに
OpenSearch は Elasticsearch と Kibana から派生した検索・分析ソフトウェアです。

https://opensearch.org/

OpenSearch が作成された理由は簡単に言うとライセンスの問題です。
詳しくは以下のページをご覧ください。

https://aws.amazon.com/jp/opensearch-service/the-elk-stack/what-is-opensearch/

# 起動
Docker Hub にあるイメージを使用します。

https://hub.docker.com/r/opensearchproject/opensearch

```sh
$ docker run \
  --name opensearch \
  -p 19200:9200 \
  -p 19600:9600 \
  -e discovery.type=single-node \
  --rm \
  opensearchproject/opensearch:1.2.0
```
- 2021/12/01 現在のバージョンは `1.2.0` です。
- Docker Hub はダウンロード回数制限 (100 回 / 6 時間) があるので注意が必要です。
    - https://docs.docker.com/docker-hub/download-rate-limit/
- `--name` は opensearch を設定しています。無指定だと適当な名前が付きます。
- `-e` で `single-node` 指定で起動します。
- 特定のネットワークを利用したい時は `--network` を指定します。
    - 無指定だと `bridge` に含まれます。
- `--net-alias` でネットワーク範囲内のエイリアスを指定することもできます。
- データを保持したい場合は `--volume` でボリュームをマウントします。

## 起動確認
```sh
$ docker ps | grep opensearch
3762adccdb82   opensearchproject/opensearch:1.2.0   "./opensearch-docker…"   About a minute ago   Up About a minute   9300/tcp, 9650/tcp, 0.0.0.0:19200->9200/tcp, 0.0.0.0:19600->9600/tcp   opensearch
```

# アクセス
```sh
$ curl -u "admin:admin" -k -X GET "https://localhost:19200"
{
  "name" : "3762adccdb82",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "ahZ6QxH0SgWDL-g11y6WGA",
  "version" : {
    "distribution" : "opensearch",
    "number" : "1.2.0",
    "build_type" : "tar",
    "build_hash" : "c459282fd67ddb17dcc545ec9bcdc805880bcbec",
    "build_date" : "2021-11-22T16:57:18.360386Z",
    "build_snapshot" : false,
    "lucene_version" : "8.10.1",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "The OpenSearch Project: https://opensearch.org/"
}

```
- `https` でアクセスします。`http` では繋がりません。
    - SSL のエラーを無視する為 `-k` オプション (insecure) を指定します。
- ユーザー認証で `-u "admin:admin"` を指定します。
    - デフォルトの管理ユーザーを使用しています。

```sh
$ curl -u "admin:admin" -k -X GET "https://localhost:19200/_cat/master?v=true"
id                     host       ip         node
MMokSh0DQNaN6o1oQvbOxQ 172.17.0.2 172.17.0.2 3762adccdb82
```

```sh
$ curl -u "admin:admin" -k -X GET "https://localhost:19200/_cat/health?v=true"
epoch      timestamp cluster        status node.total node.data discovered_master shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent
1637917926 09:12:06  docker-cluster green           1         1              true      1   1    0    0        0             0                  -                100.0%
```

```sh
$ curl -u "admin:admin" -k -X GET "https://localhost:19200/_cat/nodes?v=true"
ip         heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name
172.17.0.2           16          47   5    0.39    0.27     0.14 dimr      *      3762adccdb82
```
- `single-node` で起動しているのでマスターノードのみ起動しています。

# 作成
サンプルデータは[青空文庫](https://www.aozora.gr.jp/)を利用させていただきました。

## Index API
Index API を使ってインデックスを作成できます。

```sh
$ curl -u "admin:admin" -k -X PUT -H "Content-Type: application/json" "https://localhost:19200/books/_doc/1" -d '{
  "id": 1,
  "name": "こころ",
  "body": "私はその人を常に先生と呼んでいた。だからここでもただ先生と書くだけで本名は打ち明けない。これは世間を憚かる遠慮というよりも、その方が私にとって自然だからである。私はその人の記憶を呼び起すごとに、すぐ「先生」といいたくなる。筆を執っても心持は同じ事である。よそよそしい頭文字などはとても使う気にならない。",
  "author": "夏目漱石"
}'
```
```sh
$ curl -u "admin:admin" -k -X PUT -H "Content-Type: application/json" "https://localhost:19200/books/_doc/2" -d '{
  "id": 2,
  "name": "羅生門",
  "body": "ある日の暮方の事である。一人の下人が、羅生門の下で雨やみを待っていた。広い門の下には、この男のほかに誰もいない。ただ、所々丹塗の剥げた、大きな円柱に、蟋蟀が一匹とまっている。羅生門が、朱雀大路にある以上は、この男のほかにも、雨やみをする市女笠や揉烏帽子が、もう二三人はありそうなものである。それが、この男のほかには誰もいない。",
  "author": "芥川龍之介"
}'
```
```sh
$ curl -u "admin:admin" -k -X PUT -H "Content-Type: application/json" "https://localhost:19200/books/_doc/3" -d '{
  "id": 3,
  "name": "走れメロス",
  "body": "メロスは激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。メロスには政治がわからぬ。メロスは、村の牧人である。笛を吹き、羊と遊んで暮して来た。けれども邪悪に対しては、人一倍に敏感であった。きょう未明メロスは村を出発し、野を越え山越え、十里はなれた此のシラクスの市にやって来た。",
  "author": "太宰治"
}'
```

## Bulk API
Bulk API を使うことで複数のインデックス作成や削除を一括で実行することもできます。

```sh
$ curl -u "admin:admin" -k -X POST -H "Content-Type: application/x-ndjson" "https://localhost:19200/books/_bulk" -d '
{ "index" : { "_index" : "books", "_id" : "4" } }
{ "id": 4,"name": "坊っちゃん", "body": "親譲りの無鉄砲で小供の時から損ばかりしている。小学校に居る時分学校の二階から飛び降りて一週間ほど腰を抜かした事がある。なぜそんな無闇をしたと聞く人があるかも知れぬ。別段深い理由でもない。新築の二階から首を出していたら、同級生の一人が冗談に、いくら威張っても、そこから飛び降りる事は出来まい。弱虫やーい。と囃したからである。小使に負ぶさって帰って来た時、おやじが大きな眼をして二階ぐらいから飛び降りて腰を抜かす奴があるかと云ったから、この次は抜かさずに飛んで見せますと答えた。", "author": "夏目漱石" }
{ "index" : { "_index" : "books", "_id" : "5" } }
{ "id": 5, "name": "注文の多い料理店", "body": "二人の若い紳士が、すっかりイギリスの兵隊のかたちをして、ぴかぴかする鉄砲をかついで、白熊のような犬を二疋つれて、だいぶ山奥の、木の葉のかさかさしたとこを、こんなことを云いながら、あるいておりました。", "author": "宮沢賢治" }
{ "index" : { "_index" : "books", "_id" : "6" } }
{ "id": 6, "name": "学問のすすめ", "body": "「天は人の上に人を造らず人の下に人を造らず」と言えり。されば天より人を生ずるには、万人は万人みな同じ位にして、生まれながら貴賤上下の差別なく、万物の霊たる身と心との働きをもって天地の間にあるよろずの物を資り、もって衣食住の用を達し、自由自在、互いに人の妨げをなさずしておのおの安楽にこの世を渡らしめ給うの趣意なり。", "author": "福沢諭吉" }
'
```
- 各行の区切りに `\n` を使用する ndjson というフォーマットです。
    - http://ndjson.org/
- `"Content-Type: application/x-ndjson"` を指定します。
    - `"Content-Type: application/json"` を指定しても動きます。
- 区切りに `\n` を使用するので JSON ソース内には改行を入れずに 1 行で指定する必要があります。
- データの最終行は、必ず改行文字 `\n` で終わります。

## 確認
```sh
$ curl -u "admin:admin" -k -X GET "https://localhost:19200/_cat/indices?v=true"
health status index                        uuid                   pri rep docs.count docs.deleted store.size pri.store.size
yellow open   books                        Gymb2sviS5mOwU5ab5vRIw   1   1          6            0     36.2kb         36.2kb
green  open   .opendistro_security         jlFkWQkFQVG4UobikntRqQ   1   0          9            0     59.2kb         59.2kb
yellow open   security-auditlog-2021.12.01 PaibhuqETwCzDEFShloQWg   1   1          3            0     40.3kb         40.3kb
```
- `single-node` で起動しているので、`health` は `yellow` になります。
- この時点での index `books` の `docs.count` は 6 件です。
- 他にも index `.opendistro_security` と `security-auditlog-YYYY.MM.DD` が表示されます。

# 参照
## ユーザー追加
管理ユーザーを使い続けるのは良くないので、参照用のユーザーを追加します。

```sh
$ curl -u "admin:admin" -k -X PUT -H "Content-Type: application/json" "https://localhost:19200/_plugins/_security/api/internalusers/sampleuser" -d '{
  "password": "pass1234",
  "opendistro_security_roles": ["readall"]
}'
```
- `readall` 権限のユーザーを追加します。
    - https://opensearch.org/docs/security-plugin/access-control/users-roles#predefined-roles
- パスワードはプレーンテキスト `password` で指定します。
    - 予めハッシュ化したパスワードであれば `hash` で指定できます。
        - https://opensearch.org/docs/security-plugin/access-control/api#create-user
- 権限（ロール）は `opendistro_security_roles` に設定しています。 
    - `backend_roles` もありますが、こちらは任意の文字列や外部の認証システム (LDAP/Active Directory など) から送られてきた任意の文字列を指定するようです。
      - https://opensearch.org/docs/security-plugin/access-control/index/

## ユーザー確認
```sh
$ curl -u "admin:admin" -k -X GET "https://localhost:19200/_plugins/_security/api/internalusers/" | jq '.sampleuser'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1348  100  1348    0     0   3786      0 --:--:-- --:--:-- --:--:--  3786
{
  "hash": "",
  "reserved": false,
  "hidden": false,
  "backend_roles": [],
  "attributes": {},
  "opendistro_security_roles": [
    "readall"
  ],
  "static": false
}
```

## 1 件
```sh
$ curl -u "sampleuser:pass1234" -k -X GET "https://localhost:19200/books/_doc/1"
{"_index":"books","_type":"_doc","_id":"1","_version":1,"_seq_no":0,"_primary_term":1,"found":true,"_source":{
  "id": 1,
  "name": "こころ",
  "body": "私はその人を常に先生と呼んでいた。だからここでもただ先生と書くだけで本名は打ち明けない。これは世間を憚かる遠慮というよりも、その方が私にとって自然だからである。私はその人の記憶を呼び起すごとに、すぐ「先生」といいたくなる。筆を執っても心持は同じ事である。よそよそしい頭文字などはとても使う気にならない。",
  "author": "夏目漱石"
}}%   
```

## 全件
```sh
$ curl -u "sampleuser:pass1234" -k -X GET "https://localhost:19200/books/_search"
{"took":118,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":6,"relation":"eq"},"max_score":1.0,"hits":[{"_index":"books","_type":"_doc","_id":"1","_score":1.0,"_source":{
  "id": 1,
  "name": "こころ",
  "body": "私はその人を常に先生と呼んでいた。だからここでもただ先生と書くだけで本名は打ち明けない。これは世間を憚かる遠慮というよりも、その方が私にとって自然だからである。私はその人の記憶を呼び起すごとに、すぐ「先生」といいたくなる。筆を執っても心持は同じ事である。よそよそしい頭文字などはとても使う気にならない。",
  "author": "夏目漱石"
}},{"_index":"books","_type":"_doc","_id":"2","_score":1.0,"_source":{
  "id": 2,
  "name": "羅生門",
  "body": "ある日の暮方の事である。一人の下人が、羅生門の下で雨やみを待っていた。広い門の下には、この男のほかに誰もいない。ただ、所々丹塗の剥げた、大きな円柱に、蟋蟀が一匹とまっている。羅生門が、朱雀大路にある以上は、この男のほかにも、雨やみをする市女笠や揉烏帽子が、もう二三人はありそうなものである。それが、この男のほかには誰もいない。",
  "author": "芥川龍之介"
}},{"_index":"books","_type":"_doc","_id":"3","_score":1.0,"_source":{
  "id": 3,
  "name": "走れメロス",
  "body": "メロスは激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。メロスには政治がわからぬ。メロスは、村の牧人である。笛を吹き、羊と遊んで暮して来た。けれども邪悪に対しては、人一倍に敏感であった。きょう未明メロスは村を出発し、野を越え山越え、十里はなれた此のシラクスの市にやって来た。",
  "author": "太宰治"
}},{"_index":"books","_type":"_doc","_id":"4","_score":1.0,"_source":{ "id": 4,"name": "坊っちゃん", "body": "親譲りの無鉄砲で小供の時から損ばかりしている。小学校に居る時分学校の二階から飛び降りて一週間ほど腰を抜かした事がある。なぜそんな無闇をしたと聞く人があるかも知れぬ。別段深い理由でもない。新築の二階から首を出していたら、同級生の一人が冗談に、いくら威張っても、そこから飛び降りる事は出来まい。弱虫やーい。と囃したからである。小使に負ぶさって帰って来た時、おやじが大きな眼をして二階ぐらいから飛び降りて腰を抜かす奴があるかと云ったから、この次は抜かさずに飛んで見せますと答えた。", "author": "夏目漱石" }},{"_index":"books","_type":"_doc","_id":"5","_score":1.0,"_source":{ "id": 5, "name": "注文の多い料理店", "body": "二人の若い紳士が、すっかりイギリスの兵隊のかたちをして、ぴかぴかする鉄砲をかついで、白熊のような犬を二疋つれて、だいぶ山奥の、木の葉のかさかさしたとこを、こんなことを云いながら、あるいておりました。", "author": "宮沢賢治" }},{"_index":"books","_type":"_doc","_id":"6","_score":1.0,"_source":{ "id": 6, "name": "学問のすすめ", "body": "「天は人の上に人を造らず人の下に人を造らず」と言えり。されば天より人を生ずるには、万人は万人みな同じ位にして、生まれながら貴賤上下の差別なく、万物の霊たる身と心との働きをもって天地の間にあるよろずの物を資り、もって衣食住の用を達し、自由自在、互いに人の妨げをなさずしておのおの安楽にこの世を渡らしめ給うの趣意なり。", "author": "福沢諭吉" }}]}}%
```
- Bulk API で作成した箇所が 1 行で表示されてしまうので、見やすくしたい場合は `jq` などで整形するのが良いと思います。
 
# 検索
```sh
$ curl -u "sampleuser:pass1234" -k -X GET -H "Content-Type: application/json" "https://localhost:19200/books/_search" -d '{
  "query": {
    "simple_query_string": {
      "query": "一人",
      "fields": ["name", "body"],
      "default_operator": "and"
    }
  }
}' | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2405  100  2259  100   146  34227   2212 --:--:-- --:--:-- --:--:-- 37000
{
  "took": 5,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 3,
      "relation": "eq"
    },
    "max_score": 1.0708848,
    "hits": [
      {
        "_index": "books",
        "_type": "_doc",
        "_id": "2",
        "_score": 1.0708848,
        "_source": {
          "id": 2,
          "name": "羅生門",
          "body": "ある日の暮方の事である。一人の下人が、羅生門の下で雨やみを待っていた。広い門の下には、この男のほかに誰もいない。ただ、所々丹塗の剥げた、大きな円柱に、蟋蟀が一匹とまっている。羅生門が、朱雀大路にある以上は、この男のほかにも、雨やみをする市女笠や揉烏帽子が、もう二三人はありそうなものである。それが、この男のほかには誰もいない。",
          "author": "芥川龍之介"
        }
      },
      {
        "_index": "books",
        "_type": "_doc",
        "_id": "4",
        "_score": 0.9264894,
        "_source": {
          "id": 4,
          "name": "坊っちゃん",
          "body": "親譲りの無鉄砲で小供の時から損ばかりしている。小学校に居る時分学校の二階から飛び降りて一週間ほど腰を抜かした事がある。なぜそんな無闇をしたと聞く人があるかも知れぬ。別段深い理由でもない。新築の二階から首を出していたら、同級生の一人が冗談に、いくら威張っても、そこから飛び降りる事は出来まい。弱虫やーい。と囃したからである。小使に負ぶさって帰って来た時、おやじが大きな眼をして二階ぐらいから飛び降りて腰を抜かす奴があるかと云ったから、この次は抜かさずに飛んで見せますと答えた。",
          "author": "夏目漱石"
        }
      },
      {
        "_index": "books",
        "_type": "_doc",
        "_id": "3",
        "_score": 0.85215265,
        "_source": {
          "id": 3,
          "name": "走れメロス",
          "body": "メロスは激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。メロスには政治がわからぬ。メロスは、村の牧人である。笛を吹き、羊と遊んで暮して来た。けれども邪悪に対しては、人一倍に敏感であった。きょう未明メロスは村を出発し、野を越え山越え、十里はなれた此のシラクスの市にやって来た。",
          "author": "太宰治"
        }
      }
    ]
  }
}
```
```sh
$ curl -u "sampleuser:pass1234" -k -X GET -H "Content-Type: application/json" "https://localhost:19200/books/_search" -d '{
  "query": {
    "simple_query_string": {
      "query": "夏目",
      "fields": ["author"],
      "default_operator": "and"
    }
  }
}' | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1766  100  1626  100   140  20074   1728 --:--:-- --:--:-- --:--:-- 21802
{
  "took": 7,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 2.059239,
    "hits": [
      {
        "_index": "books",
        "_type": "_doc",
        "_id": "1",
        "_score": 2.059239,
        "_source": {
          "id": 1,
          "name": "こころ",
          "body": "私はその人を常に先生と呼んでいた。だからここでもただ先生と書くだけで本名は打ち明けない。これは世間を憚かる遠慮というよりも、その方が私にとって自然だからである。私はその人の記憶を呼び起すごとに、すぐ「先生」といいたくなる。筆を執っても心持は同じ事である。よそよそしい頭文字などはとても使う気にならない。",
          "author": "夏目漱石"
        }
      },
      {
        "_index": "books",
        "_type": "_doc",
        "_id": "4",
        "_score": 2.059239,
        "_source": {
          "id": 4,
          "name": "坊っちゃん",
          "body": "親譲りの無鉄砲で小供の時から損ばかりしている。小学校に居る時分学校の二階から飛び降りて一週間ほど腰を抜かした事がある。なぜそんな無闇をしたと聞く人があるかも知れぬ。別段深い理由でもない。新築の二階から首を出していたら、同級生の一人が冗談に、いくら威張っても、そこから飛び降りる事は出来まい。弱虫やーい。と囃したからである。小使に負ぶさって帰って来た時、おやじが大きな眼をして二階ぐらいから飛び降りて腰を抜かす奴があるかと云ったから、この次は抜かさずに飛んで見せますと答えた。",
          "author": "夏目漱石"
        }
      }
    ]
  }
}
```

## 日本語検索
デフォルトの状態でもそれなりに動いているように見えますが、精度の高い日本語検索をする為にはプラグインで `analysis-kuromoji` `analysis-icu` などをインストールして設定する必要があります。

`Dockerfile` に記載してインストールします。

```dockerfile
FROM opensearchproject/opensearch:1.2.0
RUN bin/opensearch-plugin install analysis-kuromoji \
 && bin/opensearch-plugin install analysis-icu
```
- https://opensearch.org/docs/latest/opensearch/install/plugins/

また、以下のようなマッピング定義を反映させておく必要があります。

```json
"mappings": {
  "dynamic": "strict",
  "properties": {
    "id": {
      "type": "integer"
    },
    "name": {
      "type": "text",
      "analyzer": "ja_kuromoji_index_analyzer"
    },
    "body": {
      "type": "text",
      "analyzer": "ja_kuromoji_index_analyzer"
    },
    "author": {
      "type": "text",
      "analyzer": "ja_kuromoji_index_analyzer"
    }
  }
}
```
- `"dynamic": "strict"` を指定しておくと、新しいフィールドが検出された場合に例外が発生します。
    - フィールドの増減を制御して明示的にしたい場合には指定しておくと良いかと思います。
- `"analyzer": "ja_kuromoji_index_analyzer"` は形態素解析用のアナライザーです。
    - 別途、事前に設定しておく必要があります。その際に `analysis-kuromoji` や `analysis-icu` が必要になります。
- 先にデータ投入してフィールドが作成されてしまっていると、後からマッピング定義を反映できない場合があります。
    - その場合は再インデックスが必要になります。

Elasticsearch の記事にはなりますが、日本語の全文検索については以下などを一読されると良いかと思います。

https://www.elastic.co/jp/blog/how-to-implement-japanese-full-text-search-in-elasticsearch

## Term vectors API
日本語検索の設定ができていれば、Term vectors API で用語の情報や統計を取得できます。

```sh
$ curl -s -u "sampleuser:pass1234" -k -X GET -H "Content-Type: application/json" "https://localhost:19200/books/_termvectors/1" -d '{ 
  "fields" : ["text"],
  "term_statistics": true,
  "field_statistics": true,
  "positions": false,
  "offsets": false,
  "filter": {
    "max_num_terms": 30,
    "min_term_freq": 1,
    "min_doc_freq": 1,
    "min_word_length": 1
  }
}' | jq -r '.term_vectors.text.terms | keys, [ .[].score ] | @tsv' | ruby -e 'readlines.map { |x| x.chomp.split("\t") }.transpose.sort_by { |x| x[1].to_f }.reverse.map { |x| puts "#{x[0]},#{x[1]}" }' | column -t -s ','
```

# セキュリティ
今回はデモなので、`admin` などのユーザーを使ったり `HTTPS` で通信しながら SSL 警告を無視したりしていますが、本番環境ではセキュリティ証明書や設定用 YAML ファイルを独自のものに置き換える必要があります。

`Dockerfile` に記載して置き換えることができます。

例えば、初期ユーザーを変更したい場合には `internal_users.yml` を置き換えます。

```dockerfile
FROM opensearchproject/opensearch:1.2.0
COPY --chown=opensearch:opensearch internal_users.yml /usr/share/opensearch/plugins/opensearch-security/securityconfig/
```
- https://opensearch.org/docs/latest/security-plugin/configuration/yaml/#internal_usersyml

またデモインストーラー自体を無効したい場合には、環境変数 `DISABLE_INSTALL_DEMO_CONFIG`を `true` に設定することも可能です。

セキュリティプラグインを削除したい場合は以下のように記述します。

```dockerfile
FROM opensearchproject/opensearch:1.2.0
RUN /usr/share/opensearch/bin/opensearch-plugin remove opensearch-security
COPY --chown=opensearch:opensearch opensearch.yml /usr/share/opensearch/config/
```

詳しくは以下のページなどをご覧ください。

https://opensearch.org/docs/latest/opensearch/install/docker-security/

https://opensearch.org/docs/latest/security-plugin/configuration/disable/

https://opensearch.org/docs/latest/opensearch/install/docker#customize-the-docker-image

# さいごに
今回はざっくり OpenSearch を使ってみる内容でした。
足掛かりにしていだたき OpenSearch に対する理解が進むことを期待しております。

[Amazon OpenSearch Service](https://aws.amazon.com/jp/opensearch-service/) でも利用されていますので、これからも進化することを願いつつ、便利に使っていきたいところです。

この記事が皆様の一助となれば幸いです。

---

さて、明日 (12/3) の「[弁護士ドットコム Advent Calendar 2021](https://qiita.com/advent-calendar/2021/bengo4com) | 3日目」は @k-anz さんの「[入社からフルリモートワークの9ヶ月間を振り返ってみた](https://k-anz.hatenablog.com/entry/2021/12/03/074718)」になります！お楽しみに！