Raspberry Pi 2でクラスタ構築 その3(Hadoop編)

前回の構成ではNameNodeが単一障害点になっていたため、HDFS高可用性(HA)機能を試してみる。

サーバー構成を検討

スタンバイのNameNodeがSecondary NameNodeの代わりになるため、Secondary NameNodeは不要になる。

master1・・・Zookeeper,NameNode(Active), ResourceManager
master2・・・Zookeeper,NameNode(Standby)
master3・・・Zookeeper,NameNode(Standby)
slave1・・・DataNode, NodeManager
slave2・・・DataNode, NodeManager
slave3・・・DataNode, NodeManager
slave4・・・DataNode, NodeManager

Zookeeperをインストール

master1~3にインストールする。

wget https://downloads.apache.org/zookeeper/zookeeper-3.6.2/apache-zookeeper-3.6.2-bin.tar.gz
tar xvfz apache-zookeeper-3.6.2-bin.tar.gz
ln -nfs /home/xxx/apache-zookeeper-3.6.2-bin zookeeper-latest

master1~3のconf/zoo.cfgを編集する。

tickTime=2000
initLimit=5
syncLimit=2
dataDir=/home/xxx/zookeeper-latest/data
clientPort=2181
server.1=master1:2888:3888
server.2=master2:2888:3888
server.3=master3:2888:3888

master1~3の~/zookeeper-latest/data/myidファイルを作成し、server.xで指定した数字と同じ数字を記入する。
その後、master1~3のZookeeperを順次起動する。

~/zookeeper-latest/bin/zkServer.sh start

HDFS高可用性(HA)機能の設定

master1~3のetc/hadoop/hdfs-site.xmlを編集する。

  <property>
    <name>dfs.nameservices</name>
    <value>rpcluster</value>
  </property>
  <property>
    <name>dfs.ha.namenodes.rpcluster</name>
    <value>nn1,nn2,nn3</value>
  </property>
  <property>
    <name>dfs.namenode.rpc-address.rpcluster.nn1</name>
    <value>master1:8020</value>
  </property>
  <property>
    <name>dfs.namenode.rpc-address.rpcluster.nn2</name>
    <value>master2:8020</value>
  </property>
  <property>
    <name>dfs.namenode.rpc-address.rpcluster.nn3</name>
    <value>master3:8020</value>
  </property>
  <property>
    <name>dfs.namenode.http-address.rpcluster.nn1</name>
    <value>master1:9870</value>
  </property>
  <property>
    <name>dfs.namenode.http-address.rpcluster.nn2</name>
    <value>master2:9870</value>
  </property>
  <property>
    <name>dfs.namenode.http-address.rpcluster.nn3</name>
    <value>master3:9870</value>
  </property>
  <property>
    <name>dfs.namenode.shared.edits.dir</name>
    <value>qjournal://master1:8485;master2:8485;master3:8485/rpcluster</value>
  </property>
  <property>
    <name>dfs.client.failover.proxy.provider.rpcluster</name>
    <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
  </property>
  <property>
    <name>dfs.ha.fencing.methods</name>
    <value>sshfence</value>
  </property>
  <property>
    <name>dfs.ha.fencing.ssh.private-key-files</name>
    <value>/home/ubuntu/.ssh/id_rsa</value>
  </property>
  <property>
    <name>dfs.ha.fencing.methods</name>
    <value>shell(/bin/true)</value>
  </property>
  <property>
    <name>dfs.ha.automatic-failover.enabled</name>
    <value>true</value>
  </property>
  <property>
    <name>ha.zookeeper.quorum</name>
    <value>master1:2181,master2:2181,master3:2181</value>
  </property>
  <property>
    <name>dfs.journalnode.edits.dir</name>
    <value>/home/ubuntu/hadoop-latest/dfs/journalnode</value>
  </property>
  <property>
    <name>dfs.namenode.name.dir</name>
    <value>/home/ubuntu/hadoop-latest/dfs/namenode</value>
  </property>
  <property>
    <name>dfs.replication</name>
    <value>3</value>
  </property>

slave1~4のetc/hadoop/hdfs-site.xmlを編集する。

   <property>
    <name>dfs.nameservices</name>
    <value>rpcluster</value>
  </property>
  <property>
    <name>dfs.ha.namenodes.rpcluster</name>
    <value>nn1,nn2,nn3</value>
  </property>
  <property>
    <name>dfs.namenode.rpc-address.rpcluster.nn1</name>
    <value>master1:8020</value>
  </property>
  <property>
    <name>dfs.namenode.rpc-address.rpcluster.nn2</name>
    <value>master2:8020</value>
  </property>
  <property>
    <name>dfs.namenode.rpc-address.rpcluster.nn3</name>
    <value>master3:8020</value>
  </property>
  <property>
    <name>dfs.namenode.http-address.rpcluster.nn1</name>
    <value>master1:9870</value>
  </property>
  <property>
    <name>dfs.namenode.http-address.rpcluster.nn2</name>
    <value>master2:9870</value>
  </property>
  <property>
    <name>dfs.namenode.http-address.rpcluster.nn3</name>
    <value>master3:9870</value>
  </property>
  <property>
    <name>dfs.namenode.shared.edits.dir</name>
    <value>qjournal://master1:8485;master2:8485;master3:8485/rpcluster</value>
  </property>
  <property>
    <name>dfs.client.failover.proxy.provider.rpcluster</name>
    <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
  </property>
  <property>
    <name>dfs.ha.fencing.methods</name>
    <value>sshfence</value>
  </property>
  <property>
    <name>dfs.ha.fencing.ssh.private-key-files</name>
    <value>/home/ubuntu/.ssh/id_rsa</value>
  </property>
  <property>
    <name>dfs.ha.fencing.methods</name>
    <value>shell(/bin/true)</value>
  </property>
  <property>
    <name>dfs.ha.automatic-failover.enabled</name>
    <value>true</value>
  </property>
  <property>
    <name>ha.zookeeper.quorum</name>
    <value>master1:2181,master2:2181,master3:2181</value>
  </property>
  <property>
    <name>dfs.datanode.data.dir</name>
    <value>/mnt/usbhdd/hadoop/data</value>
  </property>

master1~3、slave1~4のetc/hadoop/core-site.xmlを編集する。

  <property>
    <name>fs.defaultFS</name>
    <value>hdfs://rpcluster</value>
  </property>
  <property>
    <name>ha.zookeeper.quorum</name>
    <value>master1:2181,master2:2181,master3:2181</value>
  </property>

HDFSのフォーマット

master1~3のJournalNodeのみ起動する。

$HADOOP_HOME/bin/hdfs --daemon start journalnode

master1でNameNodeをフォーマットする。

$HADOOP_HOME/bin/hdfs namenode -format
$HADOOP_HOME/bin/hdfs namenode -initializeSharedEdits -force
$HADOOP_HOME/bin/hdfs zkfc -formatZK -force

master1のNameNodeのみ起動する。

$HADOOP_HOME/bin/hdfs --daemon start namenode

master2、3でスタンバイ用のNameNodeを作成する。

$HADOOP_HOME/bin/hdfs namenode -bootstrapStandby -force

HDFSの起動

全てのNameNode、JournalNodeを停止する。

$HADOOP_HOME/bin/hdfs --daemon stop namenode
$HADOOP_HOME/bin/hdfs --daemon stop journalnode

master1で下記コマンドを実行し、HDFSを再起動する。

$HADOOP_HOME/sbin/start-dfs.sh

次回、ResourceManagerをHA化する。

Raspberry Pi 2でクラスタ構築 その2(Hadoop編)

前回、Hadoop環境を構築しStandaloneモードで動作確認出来たため、今度は完全分散モードで動作を確認する。

準備

各サーバーからパスワード無しで接続出来るようにする。この作業を行っておくとマスターの起動とスレーブの起動を1コマンドで実行出来るようになる。

#鍵を生成
ssh-keygen -t rsa
#各サーバーに公開鍵をコピー
ssh-copy-id -i /home/foo/.ssh/id_rsa.pub foo@192.168.0.xxx
#パスワード無しでログイン出来ることを確認
ssh 192.168.0.xxx

データ保存用のUSBハードディスクを起動時にマウントするようにする。

#USBハードディスクのパスを確認
sudo fdisk -l
#UUIDを確認
sudo blkid /dev/sda1 #パスは環境毎に異なる可能性があるため注意
sudo vi /etc/fstab
#下記の1行を追記
UUID=xxx-xxx-xxx-xxx-xxx /mnt/usbhdd ext4 defaults 0 0 #UUIDは機器毎に異なるため注意
#再起動して自動的にマウントされることを確認
sudo shutdown -r now

ホスト名を/etc/hostsに記載する。Hadoopの設定でIPアドレスを使用するとエラーが発生するため、ここでホスト名を設定しておく。

sudo vi /etc/hosts

192.168.0.xx1 master1
192.168.0.xx2 master2
192.168.0.xx3 master3
192.168.0.xx4 slave1
192.168.0.xx5 slave2
192.168.0.xx6 slave3
192.168.0.xx7 slave4

クラスターの構成を検討

手元にには7台のRaspberry Pi 2があり、そのうち4台にはUSBハードディスクを接続している。

Hadoopの構築に必要な機能は以下の通りであることから、

  • NameNode
  • DataNode
  • Secondary NameNode
  • ResourceManager
  • NodeManager
  • WebAppProxy
  • Map Reduce Job History Server

各サーバーに下記の様に割り振ることにした。Zookeeperによる自動フェイルオーバーについては今回は設定しない。

master1・・・NameNode, ResourceManager
master2・・・Secondary NameNode
master3・・・今回は使用しない
slave1・・・DataNode, NodeManager
slave2・・・DataNode, NodeManager
slave3・・・DataNode, NodeManager
slave4・・・DataNode, NodeManager
※slave1~4にはUSBハードディスクが接続されている。

Hadoopデーモンの環境設定

~/.bashrcに下記を追記する。

export JAVA_HOME="$HOME/jdk-11-latest"
export HADOOP_HOME="$HOME/hadoop-latest"
if [ -d "$HADOOP_HOME" ] ; then
    export HADOOP_CONF_DIR="$HADOOP_HOME/etc/hadoop"
fi

全サーバーのetc/hadoop/hadoop-env.shを編集する。

export JAVA_HOME=/home/xxx/jdk-11-latest
export HADOOP_HOME=/home/xxx/hadoop-latest
export HADOOP_HEAPSIZE_MAX=200m

HDFSの設定

全サーバーのetc/hadoop/core-site.xmlを編集する。

<configuration>
  <property>
    <name>fs.defaultFS</name>
    <value>hdfs://master1:9000/</value>
  </property>
</configuration>

master1のetc/hadoop/hdfs-site.xmlを編集する。

<configuration>
  <property>
    <name>dfs.replication</name>
    <value>3</value>
  </property>
  <property>
    <name>dfs.namenode.secondary.http-address</name>
    <value>master2:50090</value>
  </property>
  <property>
    <name>dfs.namenode.name.dir</name>
    <value>/home/xxx/hadoop-latest/name</value>
  </property>
</configuration>

master1のetc/hadoop/workersを編集する。

slave1
slave2
slave3
slave4

slave1~4のetc/hadoop/hdfs-site.xmlを編集する。

<configuration>
  <property>
    <name>dfs.datanode.data.dir</name>
    <value>/mnt/usbhdd/hadoop/data</value>
  </property>
</configuration>

master1で下記を実行する。

$HADOOP_HOME/bin/hdfs namenode -format <cluster_name>
$HADOOP_HOME/sbin/start-dfs.sh

正常動作を確認出来たら、下記を実行し停止する。

$HADOOP_HOME/sbin/stop-dfs.sh

この時下記のWarningが出たため、

OpenJDK Server VM warning: You have loaded library /home/xxx/hadoop-3.2.2/lib/native/libhadoop.so.1.0.0 which might have disabled stack guard. The VM will try to fix the stack guard now.
It's highly recommended that you fix the library with 'execstack -c <libfile>', or link it with '-z noexecstack'.

下記の対応を行った。

sudo apt-get install prelink
sudo execstack -c /home/xxx/hadoop-3.2.2/lib/native/libhadoop.so.1.0.0
sudo execstack -c /home/xxx/hadoop-3.2.2/lib/native/libhdfs.so.0.0.0
sudo execstack -c /home/xxx/hadoop-3.2.2/lib/native/libnativetask.so.1.0.0

YARNの設定

注意:メモリの設定は参考にしないでください。物理メモリが1GBしかないRaspberry Pi 2で無理やり動くように設定したものなので、正しい設定方法ではないです。

master1、slave1~4のetc/hadoop/yarn-site.xmlを編集する。(yarn.scheduler.~はmaster1のみ追記、yarn.nodemanager.~はslave1~4のみ追記)

<configuration>
  <property>
      <name>yarn.resourcemanager.hostname</name>
      <value>master1</value>
  </property>
  <property>
      <name>yarn.scheduler.minimum-allocation-mb</name>
      <value>200</value>
  </property>
  <property>
      <name>yarn.scheduler.maximum-allocation-mb</name>
      <value>800</value>
  </property>
  <property>
    <name>yarn.nodemanager.resource.memory-mb</name>
    <value>200</value>
  </property>
  <property>
    <name>yarn.nodemanager.aux-services</name>
    <value>mapreduce_shuffle</value>
  </property>
  <property>
    <name>yarn.nodemanager.env-whitelist</name>
    <value>JAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_MAPRED_HOME</value>
  </property>
</configuration>

master1のetc/hadoop/mapred-site.xmlを編集する。

<configuration>
  <property>
    <name>mapreduce.framework.name</name>
    <value>yarn</value>
  </property>
  <property>
    <name>yarn.app.mapreduce.am.resource.mb</name>
    <value>200</value>
  </property>
  <property>
    <name>mapreduce.map.memory.mb</name>
    <value>200</value>
  </property>
  <property>
    <name>mapreduce.map.java.opts</name>
    <value>-Xmx200M</value>
  </property>
  <property>
    <name>mapreduce.reduce.memory.mb</name>
    <value>200</value>
  </property>
  <property>
    <name>mapreduce.reduce.java.opts</name>
    <value>-Xmx200M</value>
  </property>
  <property>
    <name>yarn.app.mapreduce.am.env</name>
    <value>HADOOP_MAPRED_HOME=/home/xxx/hadoop-latest</value>
  </property>
  <property>
    <name>mapreduce.map.env</name>
    <value>HADOOP_MAPRED_HOME=/home/xxx/hadoop-latest</value>
  </property>
  <property>
    <name>mapreduce.reduce.env</name>
    <value>HADOOP_MAPRED_HOME=/home/xxx/hadoop-latest</value>
  </property>
</configuration>

master1で下記を実行する。

$HADOOP_HOME/sbin/start-dfs.sh
$HADOOP_HOME/sbin/start-yarn.sh

問題なく両方の起動が確認できたので、実際に処理を実行してみる。

$HADOOP_HOME/bin/hadoop jar /home/xxx/hadoop-latest/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.2.2.jar pi 10 10000

下記の様に処理が進んで行き、

2021-02-18 08:12:21,597 INFO mapreduce.Job:  map 0% reduce 0%
2021-02-18 08:13:06,983 INFO mapreduce.Job:  map 19% reduce 0%
2021-02-18 08:13:38,044 INFO mapreduce.Job:  map 31% reduce 0%
2021-02-18 08:13:39,081 INFO mapreduce.Job:  map 38% reduce 0%
2021-02-18 08:14:09,121 INFO mapreduce.Job:  map 44% reduce 0%
2021-02-18 08:14:10,159 INFO mapreduce.Job:  map 56% reduce 0%
2021-02-18 08:14:41,193 INFO mapreduce.Job:  map 69% reduce 0%
2021-02-18 08:14:48,435 INFO mapreduce.Job:  map 69% reduce 23%
2021-02-18 08:15:12,225 INFO mapreduce.Job:  map 81% reduce 23%
2021-02-18 08:15:13,260 INFO mapreduce.Job:  map 81% reduce 27%
2021-02-18 08:15:42,218 INFO mapreduce.Job:  map 88% reduce 27%
2021-02-18 08:15:43,256 INFO mapreduce.Job:  map 94% reduce 27%
2021-02-18 08:15:44,290 INFO mapreduce.Job:  map 94% reduce 31%
2021-02-18 08:16:14,264 INFO mapreduce.Job:  map 100% reduce 31%
2021-02-18 08:16:15,298 INFO mapreduce.Job:  map 100% reduce 67%
2021-02-18 08:16:17,369 INFO mapreduce.Job:  map 100% reduce 100%

最終的に下記の結果が表示された。やはりかなり遅い。

Job Finished in 320.709 seconds
Estimated value of Pi is 3.14157500000000000000

次回、NameNodeをHA化する。

Raspberry Pi 2でクラスタ構築

動機

以前、Raspberry Pi 2でTomcatのクラスターサーバーを構築したときの機材が放置してあったので、そのままにしておくのはもったいないと思い、もう一度何かしらのクラスターを構築してみようと思った。

使用しなくなっていた原因

  • Raspberry Pi 2はかなり非力で、重いJavaアプリケーションを動作させることが不適であったこと
  • セッションはサーバー毎で別になるため、負荷を分散する恩恵をあまり受けられなかったこと(セッションを共有することも出来るが、メモリの消費が増えるため断念)
  • データ保存用のSDカードが壊れた

何を構築するか

今回のクラスターの構築は実務で使うより、勉強の意味合いが大きい。まず、最近はどのようなものが流行っているのか調査する。また、Raspberry Pi 2は非力なため1台1台別々に動作させるのではなく、今回は複数サーバーを束ねて1台のサーバーとして動作させるようにしたい。

  • Kubernetes
     機能説明は省略、次回以降調べて記載する。
     Googleの検索でヒットするのは、殆どこのクラスター構築に関する記事。実際にRaspberry Piで構築している人も多いため、構築方法は見つけやすいと思う。ただし、古いRaspberry Pi 2でも動作させることが出来るかはやってみないと分からない。
  • Hadoop
     大量データを分散処理するためのプラットフォーム。Javaで動作するため構築に関しては問題ないはず。分散処理のため使用用途は限られるかも知れない。
  • その他
     アプリケーションの機能限定でクラスター化するもの。以前構築したTomcatのクラスター、MySQLや、Apache HTTPサーバーなどに用意されているクラスター化機能を使用する。

取り敢えず今回はHadoopを構築した後に、Kubernetesの構築も試してみる。

Raspberry Pi 2の動作確認

しばらく動かしていなかったため、まずは正常に起動するか確認する。

下記からインストーラーをダウンロードし、SDカードにインストールする。
https://ubuntu.com/download/raspberry-pi

アップデートや固定IPの設定をしたら、ここでSDカードのイメージバックアップを取得しておく。他のRaspberry Piにはそのイメージをコピーして構築の手間を省く。

# 固定IP化
sudo vi /etc/netplan/50-cloud-init.yaml
# 下記を追記
addresses: [192.168.0.xx/24]
gateway4: 192.168.0.xx
nameservers:
  addresses: [192.168.0.xx]
  search: []
dhcp4: false
# 変更を適用
sudo netplan apply
# 最新化
sudo apt update
sudo apt upgrade
# Firmwareを最新化(通常運用する場合は実施しないこと)
sudo apt install python3-pip
sudo pip3 install setuptools
sudo pip3 install vcgencmd

sudo curl -L --output /usr/bin/rpi-update https://raw.githubusercontent.com/Hexxeh/rpi-update/master/rpi-update && sudo chmod +x /usr/bin/rpi-update

sudo rpi-update

Hadoop構築

# 必要なソフトウェアをインストール
sudo apt install ssh
sudo apt install pdsh

下記からJDKをダウンロードする。
https://adoptopenjdk.net/releases.html

Raspberry Pi 2はArm 32bitプロセッサのため、上記をダウンロードした。

# 解凍し、リンクを作成
tar xvfz OpenJDK11U-jdk_arm_linux_hotspot_11.0.10_9.tar.gz
ln -nfs /home/xxx/jdk-11.0.10+9 jdk-11-latest
# バージョン表示確認
java -version
openjdk version "11.0.10" 2021-01-19
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.10+9)
OpenJDK Server VM AdoptOpenJDK (build 11.0.10+9, mixed mode)

下記からHadoopをダウンロードする。
https://hadoop.apache.org/releases.html

# 解凍
tar xvfz hadoop-3.2.2.tar.gz
# JAVA_HOMEを設定
export JAVA_HOME=/home/xxx/jdk-11-latest
# Standaloneモードで動作確認
cd hadoop-3.2.2
mkdir input
cp etc/hadoop/*.xml input
bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-3.2.2.jar grep input output 'dfs[a-z.]+'
cat output/*

1       dfsadmin

Standaloneモードでの動作を確認できたので今回はここまで。

次回、完全分散モードで動作を確認する。

Hibernate Search 6.0 Migration

自作のアプリのマイグレーションする過程で問題になった個所を纏めておく。

Hibernate Search 6の変更点

APIが大幅に変わっているため、コード修正なしに移行は不可。APIをElasticsearchに最適化したそうなので、Luceneを使用してきたがこれを機にElasticsearchに変更することを検討しても良いかも知れない。

インデックスの再作成
// Hibernate5
FullTextEntityManager txtentityManager = Search.getFullTextEntityManager(entityManager);
MassIndexer massIndexer = txtentityManager.createIndexer();

// Hibernate6
SearchSession searchSession = Search.session(entityManager);
MassIndexer massIndexer = searchSession.massIndexer();

// 共通
massIndexer.start()
ファセットの作成

全く同じように移行は出来なかった。5の場合は戻り値がList<Facet>で、6の場合はMap<String, Long>になる。Stringの部分はフィールドの属性毎に変更する必要がある。

// Hibernate5
FullTextEntityManager txtentityManager = Search.getFullTextEntityManager(entityManager);
SearchFactory searchFactory = txtentityManager.getSearchFactory();
QueryBuilder builder = searchFactory.buildQueryBuilder().forEntity(searchedEntity).get();
FacetingRequest categoryFacetingRequest = builder.facet()
    .name(field + searchedEntity.getSimpleName()).onField(field).discrete()
    .orderedBy(FacetSortOrder.COUNT_DESC).includeZeroCounts(false).maxFacetCount(maxCount)
    .createFacetingRequest();

Query luceneQuery = builder.all().createQuery();
FullTextQuery fullTextQuery = txtentityManager.createFullTextQuery(luceneQuery);
FacetManager facetManager = fullTextQuery.getFacetManager();
facetManager.enableFaceting(categoryFacetingRequest);

return facetManager.getFacets(field + searchedEntity.getSimpleName());

// Hibernate6
SearchSession searchSession = Search.session(entityManager);
AggregationKey<Map<String, Long>> countByKey = AggregationKey.of(field);

SearchResult<?> result = searchSession.search(User.class)
    .where(f -> f.matchAll())
    .aggregation(countByKey, f -> f.terms()
        .field(field, String.class)
        .orderByCountDescending()
        .minDocumentCount(1)
        .maxTermCount(maxCount))
    .fetch(20);

result.hits();

return result.aggregation(countByKey);
アナライザーの変更

@Analyzer(impl = JapaneseAnalyzer.class)はなくなり、@FullTextField(analyzer = “japanese”)の様にでフィールド毎に指定する必要がある。しかも、カスタムアナライザーを別途自分で定義する必要がある。@NormalizerDefも使用出来なくなっているため、アノテーションで細かく処理を指定出来なくなっている。

public class CustomLuceneAnalysisConfigurer implements LuceneAnalysisConfigurer {

    @Override
    public void configure(LuceneAnalysisConfigurationContext context) {
        context.analyzer("japanese").instance(new JapaneseAnalyzer());
    }
}
hibernate.search.backend.analysis.configurer = <パッケージ>.CustomLuceneAnalysisConfigurer

LuceneAnalysisConfigurerを定義して、以下の様に指定する。

@FullTextField(analyzer = "japanese")
private String text;
FullTextQueryの廃止

今までは全文検索用クエリ(FullTextQuery)を作成するためにluceneを使用する場面があったが、それらはバックエンドに移動したから、今後は使用しないようにということか。luceneのQueryを使用しなくても検索ロジックを記述することが出来るようになった。

Search.session(entityManager).search(User.class)
        .where(f -> {
            if (userSearchCriteria.getUsername() == null && userSearchCriteria.getEmail() == null) {
                return f.matchAll();
            } else {
                return f.bool(b -> {
                    if (userSearchCriteria.getUsername() != null) {
                        b.should(f.match().field(UserSearchCriteria.USERNAME_FIELD)
                                .matching(userSearchCriteria.getUsername()));
                    }
                    if (userSearchCriteria.getEmail() != null) {
                        b.should(f.match().field(UserSearchCriteria.EMAIL_FIELD)
                                .matching(userSearchCriteria.getEmail()));
                    }
                });
            }
        })
        .sort(f -> f.field(UserSearchCriteria.USERNAME_FIELD + "Sort"))
        .fetchHits(Long.valueOf(pageRequest.getOffset()).intValue(), pageRequest.getPageSize());
起動設定の変更
// Hibernate5
<prop key="hibernate.search.lucene_version">LUCENE_CURRENT</prop>
<prop key="hibernate.search.default.directory_provider">filesystem</prop>
<prop key="hibernate.search.default.locking_strategy">simple</prop>
<prop key="hibernate.search.default.exclusive_index_use">true</prop>
<prop key="hibernate.search.default.indexBase">${hibernate.search.indexBase}</prop>

// Hibernate6
<prop key="hibernate.search.backend.lucene_version">LATEST</prop>
<prop key="hibernate.search.backend.directory.type">local-filesystem</prop>
<prop key="hibernate.search.backend.directory.root">${hibernate.search.indexBase}</prop>
<prop key="hibernate.search.backend.analysis.configurer">common.dao.impl.CustomLuceneAnalysisConfigurer</prop>

Beanアノテーションの変更
// Hibernate5
@Field
@Field(name = "usernameSort", normalizer = @Normalizer(definition = "userSort"))
@SortableField(forField = "usernameSort")
private String username;

@Field(name = "firstNameFacet", analyze = Analyze.NO)
@Facet(forField = "firstNameFacet")
private String firstName;

// Hibernate6
@FullTextField(analyzer = "japanese")
@KeywordField(name = "usernameSort", sortable = Sortable.YES)
private String username;

@KeywordField(name = "firstNameFacet", aggregable = Aggregable.YES)
private String firstName;
Spring Boot

マニュアル通り下記を追加すると、エラーが発生してしまう。まだ、対応していないのかもしれない。
Caused by: org.hibernate.search.util.common.AssertionFailure: Unexpected duplicate key: enabled — this may indicate a bug or a missing test in Hibernate Search. Please report it: https://hibernate.org/community/

implementation 'org.apache.lucene:lucene-analyzers-kuromoji:8.7.0'
implementation 'org.hibernate.search:hibernate-search-mapper-orm:6.0.0.Final'
implementation 'org.hibernate.search:hibernate-search-backend-lucene:6.0.0.Final'

Thymeleaf Layout Dialectのth:withに関するエラー

バグなのか仕様変更なのか後で調べるためのメモ。バージョンアップ後から下記のような使い方するとエラーが発生する様になった。

user.html
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
    layout:decorate="~{templates/layout}"
    th:with="currentMenu = ${param.from} != null and ${param.from[0]} == 'list' ? 'admin' : 'userSaveForm'">
layout.html
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
    th:with="lang = ${#locale.language}"
    th:lang="${#locale.language}">
Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Could not parse as assignation sequence: "currentMenu=(((${param.from} !,lang=${#locale.language}" (template: "templates/layout" - line 5, col 5)

user.htmlのth:withを単純な代入式にすると、エラーにならない。

th:with="currentMenu = 'admin'"

Token authentication requirements for Git operations

Githubから以下のようなメールが届いた。どうやら、2021/8/13でパスワードによる認証が出来なくなるらしい。

[GitHub] Deprecation Notice
Basic authentication using a password to Git is deprecated and will soon no longer work.
※詳しくはこちらのURLを参照。

当方ではEclipseでpush/pullするときにパスワード認証を使用しているので、Token authenticationを使用するように変更する。(Jenkinsでパスワード認証を使用している場合も同様の設定が必要になる)

Github側の設定

  1. Settings > Developer settingsのPersonal access tokensを開き、Generate new tokenをクリックする。
  2. 必要な権限にチェックを入れ、Generate tokenをクリックする。
  3. 作成されたtokenをコピーしておく。

Jenkins側の設定

  1. プロジェクトの設定画面を開き、認証情報の追加をクリックする。
  2. 先ほどコピーしたTokenをパスワードに入力する。

Eclipse側の設定

  1. 先ほど生成したTokenをパスワードに入力する。

Eclipse起動構成の移行方法

起動構成のエクスポートは実行構成のウィンドウにボタンがあるので分かりやすかったのだが、インポートが分かりづらかったので、メモを残しておく。

エクスポート方法

1.メニューの実行>実行構成をクリックする。
2.左上のエクスポートボタンをクリックする。

3.保存先、エクスポート対象を選んで完了をクリックする。

インポート方法

1.メニューのファイル>インポートをクリックする。
2.インポート・ウィザードから起動構成を選択して、次へをクリックする。

3.起動構成を保存してあるフォルダを選択し、インポート対象を選び完了をクリックする。

分類問題を機械学習で解く

今回の目的

理論とか細かいことは後回し、取り敢えず実行してみて、機械学習がどのようなものか感じをつかむ。

分類問題とは

データを複数のクラスに分類すること。

今回は、機械学習の手法の一つである教師あり学習を使って分類する。教師あり学習とは、予め用意された問題と正解の傾向を学習することで、未知の問題に対する正解を推測する手法をいう。

必要なもの

  • データセット
  • 分類器 (データを分類する機械学習モデル)
    • k-近傍法(k-NN)
    • 決定木
    • サポートベクターマシン(SVM)
    • ロジスティック回帰など

データの傾向を学習させる必要があるため、目的に合わせたデータセットを事前に用意する。分類器はライブラリとして既に実装されているものを利用する。

機械学習を試すとき、まずはデータを準備することが一つのハードルになるが、scikit-learnにはいくつかの標準的なデータセットが付属しているので、自分で用意しなくても試すことが出来る。

今回はsklearn.datasetsのload_iris(アヤメの花のデータセット)を使用する。がくや花びらの大きさとアヤメの種類がデータに含まれていて、がくや花びらの大きさからアヤメの種類を予測する。

Pythonで実装

from sklearn.datasets import load_iris

iris = load_iris()
# データの内容を確認する
iris
{'DESCR': '(データの説明は省略)',
 'data': array([[5.1, 3.5, 1.4, 0.2],
        [4.9, 3. , 1.4, 0.2],
        [4.7, 3.2, 1.3, 0.2],
        [4.6, 3.1, 1.5, 0.2],
        [5. , 3.6, 1.4, 0.2],
        (中略)
        [5.9, 3. , 5.1, 1.8]]),
 'feature_names': ['sepal length (cm)', # がくの長さ
  'sepal width (cm)',                   # がくの幅
  'petal length (cm)',                  # 花びらの長さ
  'petal width (cm)'],                  # 花びらの幅
 'filename': '/usr/local/lib/python3.6/dist-packages/sklearn/datasets/data/iris.csv',
 # アヤメの種類 0: 'setosa', 1: 'versicolor', 2: 'virginica'
 'target': array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
        2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
        2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]),
 'target_names': array(['setosa', 'versicolor', 'virginica'], dtype='<U10')} 

この後のデータの解析を行いやすくするため、pandasのDataFrameに変換する。

import pandas as pd

# DataFrameに変換する
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['target'] = iris.target_names[iris.target]

# 先頭5行を表示する
df.head()
sepal length (cm)sepal width (cm)petal length (cm)petal width (cm)target
05.13.51.40.2setosa
14.93.01.40.2setosa
24.73.21.30.2setosa
34.63.11.50.2setosa
45.03.61.40.2setosa

ざっとデータの内容を確認する。欠損値もないしそのまま使えるように既に整えられている。

df.info()

RangeIndex: 150 entries, 0 to 149
Data columns (total 5 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   sepal length (cm)  150 non-null    float64
 1   sepal width (cm)   150 non-null    float64
 2   petal length (cm)  150 non-null    float64
 3   petal width (cm)   150 non-null    float64
 4   target             150 non-null    object 
dtypes: float64(4), object(1)
memory usage: 6.0+ KB
df.describe()
sepal length (cm)sepal width (cm)petal length (cm)petal width (cm)
count150150150150
mean5.8433333.0573333.7581.199333
std0.8280660.4358661.7652980.762238
min4.3210.1
25%5.12.81.60.3
50%5.834.351.3
75%6.43.35.11.8
max7.94.46.92.5
df['target'].value_counts()

virginica     50
setosa        50
versicolor    50
Name: target, dtype: int64

次に説明変数と目的変数に分ける。
説明変数とは目的変数を説明する変数のこと。これをもとに予測する。
目的変数とは予測したい変数のこと。

# 説明変数
X = df.drop('target', axis=1)
# 目的変数
y = df['target']

さらに、トレーニング用データセットとテスト用データセットに分ける。未知の値について予測する性能をテストするため、テスト用のデータはトレーニングで使用してはいけない。

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

分類器にはサポートベクターマシンを使用する。scikit-learnサポートベクターマシンを実装したライブラリがあるので、それをそのまま使用する。

from sklearn.svm import SVC

svc = SVC()
# 問題と正解の傾向を学習させ、学習済みモデルを作成する
svc.fit(X_train, y_train)

学習済みモデルが作成出来たら、テストデータを使ってアヤメの種類を予測してみる。

svc.predict(X_test)

array(['virginica', 'versicolor', 'setosa', 'virginica', 'setosa',
       'virginica', 'setosa', 'versicolor', 'versicolor', 'versicolor',
       'virginica', 'versicolor', 'versicolor', 'versicolor',
       'versicolor', 'setosa', 'versicolor', 'versicolor', 'setosa',
       'setosa', 'virginica', 'versicolor', 'setosa', 'setosa',
       'virginica', 'setosa', 'setosa', 'versicolor', 'versicolor',
       'setosa', 'virginica', 'versicolor', 'setosa', 'virginica',
       'virginica', 'versicolor', 'setosa', 'virginica'], dtype=object)

どのくらい正解しているのかは下記で確認出来る。

svc.score(X_test, y_test)

0.9736842105263158

97%正しいアヤメの種類を予測出来ている。

まとめ

上記はかなり単純な例です。このように高い率で予測出来ているのは、使用しやすく既にデータが整備されていたためです。本来データには欠損値であったり、ノイズであったり、そもそも予測するための説明変数が不足していたりしていますので、まず学習で使用するデータの作成に時間がかかります。
また、分類器も今回はサポートベクターマシンを使用しましたが、最適な分類器ではないかもしれません。どの分類器が最適か探すこともあるでしょうし、そのパラメーターのチューニングも必要になるかもしれません。
これらを詰めていき正解率を上げていく作業はなかなか楽しいです。興味があればさらに詳しく調べてみてください。

Recommendation Systemを考える

以下の機械学習の勉強の過程で小説の特徴を抽出することは出来た。ここからユーザーにおすすめの小説を推薦するにはどのような仕組みが必要なのか考える。

小説を読もうの累計ランキングをDoc2Vecで解析する その5

Recommendation System(推薦システム)とは

過去の行動をもとに、あるアイテムを好む可能性を予測するシステム。
例えば、Netflixでは、映画を見たり、投票したりするような過去の行動に応じて、新しい映画を提案している。
新しいアイテムを推奨するという点が重要となる。

・ユーザーベースの推薦システム

自分の経験と他の人の経験を組み合わせて推薦する。ユーザ間の類似度を計算し、類似度の高いユーザーが好む作品の中でまだ見ていない作品を推薦する。

このシステムを実現するには複数のユーザー情報が必要になるため、今回は不採用。

・アイテムベースの推薦システム

類似のユーザを見つけるのではなく、映画や物などのアイテムを比較する。アイテム間の類似度を計算する。

・どのような推薦システムを作成するか

ユーザーが入力した小説に似ている小説を推薦するシステムは以前作成したDoc2Vecのモデルを使用すれば実現可能である。しかし、このシステムではユーザーが毎回小説を入力しなければいけない点が煩わしい。

例えば、最近投稿された小説の中から自分の好みに合う小説があったら、自動的に通知が来るようなシステムであると良い。

このシステムは以前作成したモデルのみでは実現不可能であり、さらに自分の好みを学習させる必要がある。これまでと違い教師あり学習になるので、どのような手法があるか調べるところから始めることにする。

Recommendation Systemを考える その2へ続く。

GreenMail Ver. 1.6 Migration

GreenMailを1.6にバージョンアップするとAssertionErrorが発生する場合は、 以下の依存関係が悪さをしているので、

    <dependency>
      <groupId>com.sun.mail</groupId>
      <artifactId>javax.mail</artifactId>
    </dependency>

以下の様に変更すれば良い。

    <dependency>
      <groupId>com.sun.mail</groupId>
      <artifactId>jakarta.mail</artifactId>
    </dependency>

Ver. 1.6から依存関係にあるjavamailが変更された。