vagrant(virtualbox)でclojureの開発環境を構築する

シゴトでclojureを使うので自宅のPCにclojureを叩くための環境を整えます。

バージョン

VirtualBox バージョン 6.1.40 r154048

Vagrant Installed Version: 2.3.0

vagrantubuntuを立てる

vagrantの設定ファイルは次のようにしました。

Vagrant.configure("2") do |config|
  # Every Vagrant development environment requires a box. You can search for
  # boxes at https://vagrantcloud.com/search.
  config.vm.box = "generic/ubuntu2204"

  config.vm.provider "virtualbox" do |vb|
    # Customize the amount of memory on the VM:
    vb.memory = "8192"
  end
end

起動して sudo apt update sudo apt upgrade します。

sdkmanでjdkをインストールする

sdkmanを使ってみようかなと。

https://sdkman.io/

sdkmanをインストールします。

vagrant@ubuntu2204:~$ curl -s "https://get.sdkman.io" | bash
vagrant@ubuntu2204:~$ source "$HOME/.sdkman/bin/sdkman-init.sh"
vagrant@ubuntu2204:~$ sdk version
==== BROADCAST =================================================================
* 2022-11-05: jreleaser 1.3.1 available on SDKMAN! https://github.com/jreleaser/jreleaser/releases/tag/v1.3.1
* 2022-10-31: layrry 1.0.0.Alpha2 available on SDKMAN! https://github.com/moditect/layrry/releases/tag/v1.0.0.Alpha2
* 2022-10-31: pomchecker 1.4.0 available on SDKMAN! https://github.com/kordamp/pomchecker/releases/tag/v1.4.0
================================================================================

SDKMAN 5.16.0

できました。

ではでは、AmazonCorrettoをインストールします。

次のコマンドでインストール可能なベンダーやバージョンを確認できます。

vagrant@ubuntu2204:~$ sdk list java

AmazonCorretto 17.0.5をインストールします。

vagrant@ubuntu2204:~$ sdk install java 17.0.5-amzn
vagrant@ubuntu2204:~$ java -version
openjdk version "17.0.5" 2022-10-18 LTS
OpenJDK Runtime Environment Corretto-17.0.5.8.1 (build 17.0.5+8-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.5.8.1 (build 17.0.5+8-LTS, mixed mode, sharing)

できました。

clojureをインストールする

公式サイトを確認したところbrewでインストールできるようなのでまずbrewをインストールします。

https://brew.sh/index_ja

vagrant@ubuntu2204:~$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
vagrant@ubuntu2204:~$ echo '# Set PATH, MANPATH, etc., for Homebrew.' >> /home/vagrant/.profile
vagrant@ubuntu2204:~$ echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> /home/vagrant/.profil
vagrant@ubuntu2204:~$ eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
vagrant@ubuntu2204:~$ sudo apt-get install build-essential
vagrant@ubuntu2204:~$ brew install gcc
vagrant@ubuntu2204:~$ brew --version
Homebrew 3.6.8
Homebrew/homebrew-core (git revision c11ea6ab4f5; last commit 2022-11-05)

できました。

ではでは、clojureをインストールします。

vagrant@ubuntu2204:~$ brew install clojure/tools/clojure
vagrant@ubuntu2204:~$ clojure --version
Clojure CLI version 1.11.1.1189
vagrant@ubuntu2204:~$ clj
Downloading: org/clojure/clojure/1.11.1/clojure-1.11.1.pom from central
Downloading: org/clojure/core.specs.alpha/0.2.62/core.specs.alpha-0.2.62.pom from central
Downloading: org/clojure/pom.contrib/1.1.0/pom.contrib-1.1.0.pom from central
Downloading: org/clojure/spec.alpha/0.3.218/spec.alpha-0.3.218.pom from central
Downloading: org/clojure/spec.alpha/0.3.218/spec.alpha-0.3.218.jar from central
Downloading: org/clojure/clojure/1.11.1/clojure-1.11.1.jar from central
Downloading: org/clojure/core.specs.alpha/0.2.62/core.specs.alpha-0.2.62.jar from central
Clojure 1.11.1
user=>

できました~。

fish shell を導入してみた

先日、Vagrant+Ubuntuで環境構築したのですが、今回はそこにfish shellを導入してみました。

fish shell とは

https://fishshell.com/

雑に一言でいうと、ユーザーフレンドリーなインタラクティブシェルです。公式ページでは次のような特徴があることが確認できます。

  • サジェスト表示
  • manページから補完表示
  • VGAカラー
  • ウェブベースで設定可能
  • インストール後設定無しですぐに使える

本番環境でお目にかかることはないと思うのですが、ローカル環境ではとても使いやすそうだな、と思います。

fish shell のインストール

fishのインストールをしていきます。こちらのページに詳しいインストール方法が書いてあったので斜め読みし、指示に従ってコマンドを打てばインストールできました。

https://launchpad.net/~fish-shell/+archive/ubuntu/release-3

sudo apt-add-repository ppa:fish-shell/release-3  
sudo apt update  
sudo apt install fish

apt-add-repository でppaなるものをゴニョゴニョしています。ppaで検索してみると、どうやらパーソナル・パッケージ・アーカイブというものみたいです。非公式のrepositoryをシステムに追加しているようです。

デフォルトのshellを変更

次のコマンドでデフォルトシェルを変更しましょう。

vagrant@ubuntu2204:~$ chsh
Password: 
Changing the login shell for vagrant
Enter the new value, or press ENTER for the default
        Login Shell [/bin/bash]: /usr/bin/fish

これで、設定は完了です。一度ログアウトし、再ログインするとfish shellでログインできます。

Ubuntuにelasticsearchをインストール、セットアップする

Vagrantで立てたUbuntuにelasticsearchをインストール、セットアップしてみようと思います。

デスクトップPC(windows)をゲーミング用で一台所有してるんですが、こちらプログラミング趣味用としても使いたいなと。ゲームパフォーマンスに影響が出るのでホストOS型でLinux(Ubuntu)を選びました。そこにelasticsearchをインストール、セットアップしていきたいと思います。

環境

Vagrant

PS D:\Vagrant> vagrant --version
Vagrant 2.3.0

また、Vagrantfileは長いので割愛しますが使用しているboxは次のとおりです。

generic/ubuntu2204

VirtualBox

バージョン 6.1.38 r153438 (Qt5.6.2)

Ubuntu

vagrant@ubuntu2204:~$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.1 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.1 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

elasticsearchのインストール

現時点で最新のelasticsearchをインストールしていきます。

公式サイトを確認したところいくつか方法があるようですね。

Installing Elasticsearch | elasticsearch guide 8.4

ここではaptによるインストールに沿ってみることにしました。

Installing Elasticsearch with Debian Package | elasticsearch guide 8.4

手順通りにコマンドを打ち込んでいけば問題なくインストールできました。

セキュリティを無効にする

デフォルトでセキュリティ機能が有効となっており、学習用途で使うには使いづらいので無効化してしまいます。

/etc/elasticsearch/elasticsearch.yml ファイルを次のとおりに編集すればOKです。

# Enable security features
xpack.security.enabled: false

起動してみる

起動して動作確認してみました。

起動は systemctl コマンドから行えます。なお、初回起動時には一度 systemctl daemon-reload することをお忘れなく。

vagrant@ubuntu2204:~$ sudo systemctl start elasticsearch
vagrant@ubuntu2204:~$ curl localhost:9200
{
  "name" : "ubuntu2204.localdomain",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "LmVzX49pRiK0PcdBSTx4fA",
  "version" : {
    "number" : "8.4.1",
    "build_flavor" : "default",
    "build_type" : "deb",
    "build_hash" : "2bd229c8e56650b42e40992322a76e7914258f0c",
    "build_date" : "2022-08-26T12:11:43.232597118Z",
    "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"
}
vagrant@ubuntu2204:~$ curl -sS -XPOST localhost:9200/demo-index/_doc -H "Content-Type: application/json" -d '{"id": 1}' | jq .
{
  "_index": "demo-index",
  "_id": "alEUUIMB83mE3HuzpOuF",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 1,
  "_primary_term": 1
}
vagrant@ubuntu2204:~$ curl -sS -XDELETE localhost:9200/demo-index | jq .
{
  "acknowledged": true
}

問題なく疎通できました。

termクエリを使ってみる

そう言えば、elasticsearch勉強したことあるけれど、あまり検索にフォーカスしていなかったなっと思いました。mappingとか、indexとかその辺りの事中心だった気がします。(もう忘れたけれど)

term って和訳すると、「用語」「言葉」って言うらしいです。

term は完全一致で検索する時に使うみたいですね。 keyword タイプのフィールドに使うのが一般的なのかも? term クエリの説明ページではそんなこと書いてありませんが、 text タイプのフィールドに使うのはオススメしないって言ってますね。

https://www.elastic.co/guide/en/elasticsearch/reference/8.3/query-dsl-term-query.html

逆に keyword 以外だとどんなフィールドに使うんだろうか? Numbers boolean とかかな? 忘れました。elasticsearchのデータタイプ全部使ったことないなと。

https://www.elastic.co/guide/en/elasticsearch/reference/8.3/mapping-types.html

公式ドキュメントに term クエリを実行したケースと、 match クエリを実行したケースの違いが載っていたので実際に手元で確認してみます。

まず、 text タイプを一つ持つインデックスを作り、ドキュメントを作成しました。

 PUT my-index
 {
   "mappings": {
     "properties": {
       "full_text": {
         "type": "text"
       }
     }
   }
 }
 
 PUT my-index/_doc/1
 {
   "full_text": "Quick Brown Foxes!"
 }

以下のように、上記のドキュメントを term クエリで検索しても結果が返ってきません。 full_text フィールドは text タイプなので、インデックスされるときにAnalyzerによって分析されるので、全く同じテキストで検索しても出てこないってことですね。

以下、公式ドキュメントより。

Because the full_text field no longer contains the exact term Quick Brown Foxes!, the term query search returns no results.

textタイプに合った検索、matchクエリを使うとこのドキュメントを取得できます。

 GET my-index/_search
 {
   "query": {
     "match": {
       "full_text": "Quick Brown Foxes!"
     }
   }
 }
 
 // response
 {
   "took" : 4,
   "timed_out" : false,
   "_shards" : {
     "total" : 1,
     "successful" : 1,
     "skipped" : 0,
     "failed" : 0
   },
   "hits" : {
     "total" : {
       "value" : 1,
       "relation" : "eq"
     },
     "max_score" : 0.8630463,
     "hits" : [
       {
         "_index" : "my-index",
         "_id" : "1",
         "_score" : 0.8630463,
         "_source" : {
           "full_text" : "Quick Brown Foxes!"
         }
       }
     ]
   }
 }

match クエリなので、一部分だけでもヒットすればドキュメントを取ることができます。(このあたりの詳しい話は忘れました。多分Analyzerで保存されているテキストも、検索するテキストも、トークン化(だっけ?)して突合していたはず!)

 GET my-index/_search
 {
   "query": {
     "match": {
       "full_text": "Quick"
     }
   }
 }

ちなみに、 term クエリでも、 match クエリでも、どちらでも検索できるようにしておきたい!というユースケースに対応できるようにelasticsearchでは、multi fieldという機能があります。こいつを使えば、どちらのクエリでもひっかけるようにできますね。

fields | Elasticsearch Guide [8.3] | Elastic

multi fieldは、一つのフィールドに対し複数のタイプを関連付けることができる機能です。なので、 textkeyword を同時に割り当てることができます(便利ですね)。おんなじ名前にはできないので、それぞれ別別のフィールド名を割り当てることになりますね。

例えば次のようにマッピングを定義しておけば、上記のドキュメントはどちらでもヒットするようにできます。

PUT my-index
{
  "mappings": {
    "properties": {
      "full_text": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

PUT my-index/_doc/1
{
  "full_text": "Quick Brown Foxes!"
}

GET my-index/_search
{
  "query": {
    "match": {
      "full_text": "Quick" // ←ヒットする!
    }
  }
}

GET my-index/_search
{
  "query": {
    "term": {
      "full_text.keyword": "Quick Brown Foxes!" // ←ヒットする!
    }
  }
}

JavaのComparatorを使ってソートする

Comparatorを使ってソートする方法について、触ってみました。

Comparator (Java Platform SE 8)

Comparatorはその名の通り、あるオブジェクト同士を比較する役割を持っています。Comparatorは導入されたのが1.2ですが、FunctionalInterfaceアノテーションが付与されているのでラムダ式を使って定義できるみたいですね。例えば、こんな感じかなと。

var comparator = (value1, value2) -> value1 - value2;

Comparatorが返す値はこう言う意味になります。よくごちゃごちゃになります。いい覚え方ないんでしょうかね。

  • 最初の引数が2番目の引数より小さい場合は負の整数。
  • 等しい場合は0。
  • 最初の引数が2番目の引数より大きい場合は正の整数。

https://docs.oracle.com/javase/jp/8/docs/api/java/util/Comparator.html#compare-T-T-

Stream API でソートしてみる

Stream APIで値をソートするのは簡単です。

https://docs.oracle.com/javase/jp/8/docs/api/java/util/stream/Stream.html#sorted-java.util.Comparator-

Stream API には sorted(Comparator) と言うメソッドが定義されているので、こいつを使ってあげればOKです。

import java.util.stream.Stream;

public class ComparatorExample1 {

    public static void main(String[] args) {
        Stream.of(10, 15, 9, 7, 12)
                .sorted((value1, value2) -> value1 - value2)
                .forEach(System.out::println);
    }

}

このようにソートされます。いたって普通ですね。まぁ、intの値をソートするユースケースは現実世界でそんなに存在しないかな。

7
9
10
12
15

もう少し複雑な例を。

import java.util.Comparator;
import java.util.stream.Stream;

public class ComparatorExample2 {

    public static void main(String[] args) {
        Stream.of(new Order("T-N001", 10),
                        new Order("T-M001", 300),
                        new Order("X-S002", 50))
                .sorted(Comparator.comparing(Order::amount))
                .forEach(System.out::println);
    }

    public record Order(String orderId, int amount) {

    }

}

いっちょ前にrecodクラスを使ってみました。

Comparator.comparing を使えば任意の型からint値を使ってソートすることができます。便利じゃないですか?ひとつ前の例(ComparatorExample1)で書いていたようなラムダ式を書かなくていいわけで、より洗礼されていて読みやすくなっていますね。意図が明確というか。

出力はこうなります。

Order[orderId=T-N001, amount=10]
Order[orderId=X-S002, amount=50]
Order[orderId=T-M001, amount=300]

きちんと amount の値でソートされましたね。

複数項目でソートしてみる

ある型を複数項目でソートしたい場合ってあると思います。そんなときもComparatorを使うとサクッとソートできるようです。

こんな感じで。

import java.util.stream.Stream;

import static java.util.Comparator.comparing;

public class ComparatorExample3 {

    public static void main(String[] args) {
        Stream.of(new Order("T-N001", 10),
                        new Order("B-M001", 300),
                        new Order("A-M001", 300),
                        new Order("X-S002", 50))
                .sorted(comparing(Order::amount).thenComparing(Order::orderId))
                .forEach(System.out::println);
    }

    public record Order(String orderId, int amount) {

    }

}

comparingメソッドはstaticインポートに変更しました。そして、thenComparingを使って2個目のソート処理を指定しました。このメソッドはかなり洗礼されているなーと感じていて、amountでソートし、orderIdでソートすることが一発で分かるようになっています。これをfor文で実装するとなるとどれだけ可読性が落ちるのか...。

出力はこうなります。きちんとソートされてますね。

Order[orderId=T-N001, amount=10]
Order[orderId=X-S002, amount=50]
Order[orderId=A-M001, amount=300]
Order[orderId=B-M001, amount=300]

Writing in progress: Quorum(定足数)による決定

Quorum-based decision making

Quorum-based decision making | Elasticsearch Guide [8.1] | Elastic

  • master-eligible nodeの基本的なタスクは次の二つ。
    • master nodeの選出
    • cluster状態の変更
  • この二つのタスクは例えいくつかのノードが使用不能になったとしても正しく動作することが求められる。
    • なぜならこれらのタスクが正常に完了しないと、cluster全体が機能しなくなるからである。
    • このような外的要因による不測の事態においても、システムが正常に稼働する性質のことをロバスト性というらしい。
  • Elasticsearchはmaster eligible nodeの定足数以上から成功を示す応答を受信し上記のタスクを進めることで、ロバスト性を担保する仕組みになっている。
    • ここでmaster eligible nodeが応答することをElasticsearchでは 「投票」(Voting) と言う。

  • master-eligible nodeは動的に追加、削除が可能。
  • master-eligible nodeの数の変化に伴い、Elasticsearchは最適な投票構成の設定を行い、堅牢性を維持しようとする。
    • 投票構成の設定についてはこちら
    • それぞれのタスクにおける決定は、投票構成の半数以上のノードが応答することにより行われる。
    • 通常、投票構成はmaster-eligible nodeと同じ数になるが異なる場合もある。

  • clusterを確実に利用し続けたいのであれば、半数以上の投票構成に含まれるnodeを一度に停止してはいけない。
    • 例えば3つ、または4つのmaster-eligible node構成でclusterを運用しているとき2つ以上が同時に停止するとclusterは正しく動作しなくなるかもしれない。
  • 通常、master-eligible nodeの追加・削除時にmaster nodeは直ぐclusterのステータスを更新し投票構成を適切な値に設定する。

Voting configurations

Voting configurations | Elasticsearch Guide [8.1] | Elastic

  • voting configurationはcluster stateとして管理、保存されている。
    • 次のAPIにより、現在の設定値を確認できる。
GET  /_cluster/state?filter_path=metadata.cluster_coordination.last_committed_config
  • 実際に試してみる。
    • 上記のリクエストのレスポンスは下記の通り。これらの値はnodeIdを表しているらしい。
{
  "metadata": {
    "cluster_coordination": {
      "last_committed_config": [
        "jRRXtWgsQ7atny8aKekv8A",
        "S7MNYHX8TJytYeTBxQGmOw",
        "TlLSYvQASLaxTeO6FCRd0Q"
      ]
    }
  }
}
  • 実際にcat apiで実際のnodeIdを取得してみる。
    • この時、全nodeは計4つでroleにmasterを付与しているノードは計3つのため、それらのnodeがvoting configurationに含まれているということになる。
[
  { "nodeId": "VgZvdqvXSKWeKxneZutYMQ" },
  { "nodeId": "TlLSYvQASLaxTeO6FCRd0Q" },
  { "nodeId": "jRRXtWgsQ7atny8aKekv8A" },
  { "nodeId": "S7MNYHX8TJytYeTBxQGmOw" }
]

Elasticsearchでcat shards APIを使いshardがどこのnodeに割り当てられているかを確認する

Elasticsearchでshardがcluster内のどこのnodeに属しているか確認したいと思うことがあったので、表示方法を調べてみました。

Elasticsearchに対するREST APIは以下のドキュメントから調べることができます。

REST APIs | Elasticsearch Guide [8.1] | Elastic

REST APIs にはElasticsearchに対するREST API操作の列挙とその操作内容について記載されています。他のドキュメントもそうですが、elastic-stackのドキュメントはよく整理されていて、例えばREST APIは操作のカテゴリごとにまとまっています。探すのが楽だし、理解しやすいので助かりますね。

cat shards API

cat shards APIを叩くことでshardごとの情報を確認できます。例えば、shardがどのindex、nodeに属しているか。primary shardか、replica shardか、等を確認できます。

cat shards API | Elasticsearch Guide [8.1] | Elastic

以下、実行例です。

GET _cat/shards?format=json

[
  {
    "index" : ".kibana-event-log-8.0.0-000001",
    "shard" : "0",
    "prirep" : "p",
    "state" : "STARTED",
    "docs" : null,
    "store" : null,
    "ip" : "172.18.0.6",
    "node" : "esmaster"
  },
  {
    "index" : ".apm-agent-configuration",
    "shard" : "0",
    "prirep" : "p",
    "state" : "STARTED",
    "docs" : null,
    "store" : null,
    "ip" : "172.18.0.6",
    "node" : "esmaster"
  },
  // 以下略

パスにindex名を指定したり、ワイルドカードを用いて検索対象のshardを絞り込むことも可能です。

GET _cat/shards/index-*?format=json

これで、レスポンスの中身を見ればどのnodeにshardが属しているかがわかりますね。

ちなみに、 prirep フィールドで示されている値は、 p の場合primary shard、 r の場合replica shardとなります。

Written with StackEdit.