Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

JShellでフォルダ出力するワンライナーを書いてみた。

概要

こんにちは、最近JShellに嵌っているuedaです。
この記事はワンライナー自慢大会 Advent Calendar 2018 - Qiitaの19日目です。
JavaのJShellを使ってテキストに書かれたとおりの構成のフォルダを出力する
ワンライナーを紹介します。

昔はJavaワンライナーなんて考えもしませんでしたが、
Java9から使えるようになったJShell(REPL)で対話的に実行する事で、
ワンライナーっぽく書ける様になりました。
中々使う機会が無いので、勉強がてらワンライナーを作成しました。

f:id:acro-engineer:20181218044827j:plain:w200
duke

環境

Java: openjdk version "11.0.1" 2018-10-16
OS: Windows10 Home

※本稿のワンライナーはJava11のAPIを使用しているため、Java10以前では動きません。

JShellを使ったJavaコマンド実行への道

Java11のダウンロード

JDKはいくつか種類がありますが、今回はAdoptOpenJDKを使いました。
adoptopenjdk.net

パスを通す

展開したAdoptOpenJDKのbinにパスを通し、「JShell」をコマンドプロンプトから実行できるようにします。

コマンドプロンプトからJShellを経由してのJava実行

> echo System.out.plintln("Hello.") | JShell -

このように「echo」+「生のJavaコード」+パイプ~JShell~ハイフンでJavaコードの単発実行ができます。
クラスやmainメソッドを書かなくて済む事を考えると、大分楽です。

注意点として、コマンドプロンプト実行ではechoコマンドが「>」を評価してしまうため、「^」(キャレット)でエスケープする必要があります。
例えば以下のような記述になります。

例 (i -> i * 10) → (i -^^^> i * 10)

本題(フォルダ出力ワンライナー)

さて、本題のワンライナーについて。
大量のディレクトリを作んなきゃ、て事はたまにありますよね。
そんな時、さくっと生成したいと考え、
テキストに書いたとおりにフォルダを出力するワンライナーを書きました。

input.txt

Excelからぺっと貼り付けたタブインデント付のテキストです。
このテキストの構成どおりのフォルダ作成を考えます。

test
	test1
		1-1
		1-2
	test2
		1-1
		1-2
		2-1
		3-1
			images
			procedure
		3-2
			images
			procedure
		3-3
			images
			procedure

本稿の主役のワンライナー

上記input.txtと同じフォルダで下記ワンライナーを実行すると、テキストに記載された構成通りのフォルダが生成されます。

>echo Stream.of(new LinkedHashMap^^^<Integer, String^^^>()).forEach(map -^^^> {try { Files.readAllLines(Path.of("input.txt")).stream().peek(k -^^^> map.put(k.length() - k.stripLeading().length(), k.strip())).map(s -^^^> s.length() - s.stripLeading().length() == 0 ? s : String.join("/", map.values().stream().limit(s.length() - s.stripLeading().length()).collect(Collectors.toList())) + "/" + s.strip()).peek(System.out::println).forEach(r -^^^> {try {Files.createDirectory(Path.of(r));} catch (IOException e) {throw new UncheckedIOException(e);}});} catch (IOException e) {throw new UncheckedIOException(e);}}); | JShell -

解説

ワンライナーのままだと読みづらいので、展開してみます。

Stream.of(new LinkedHashMap<Integer, String>()).forEach(map -> {
	try {
		Files.readAllLines(Path.of("input.txt")).stream()
				.peek(k -> map.put(k.length() - k.stripLeading().length(), k.strip()))
				.map(s -> s.length() - s.stripLeading().length() == 0 ? s
						: String.join("/", map.values().stream().limit(s.length() - s.stripLeading().length())
								.collect(Collectors.toList())) + "/" + s.strip())
				.peek(System.out::println)
				.forEach(r -> {
					try {
						Files.createDirectory(Path.of(r));
					} catch (IOException e) {
						throw new UncheckedIOException(e);
					}
				});
	} catch (IOException e) {
		throw new UncheckedIOException(e);
	}
});
最初のStreamは、処理中で使用するLinkedHashMap生成のためだけに行います。
Stream.of(new LinkedHashMap<Integer, String>()).forEach(map -> 以降実際の出力処理
ファイルを読み込み、ファイル1行ごとの文字列に対しストリーム処理を行います。
Files.readAllLines(Path.of("input.txt")).stream()
peekはストリームに影響を与えない処理を行うため、ここでMapにファイル内容を格納しています。

 Mapはキーがタブインデントの数、バリューがフォルダ名になるよう加工しています。

.peek(k -> map.put(k.length() - k.stripLeading().length(), k.strip()))
相対パスの生成を行っています。

 タブ数が0ならそのまま、0以外なら全親フォルダのパス+自分のフォルダ名がストリームに流れます。

.map(s -> s.length() - s.stripLeading().length() == 0 ? s : String.join("/", map.values().stream().limit(s.length() - s.stripLeading().length()).collect(Collectors.toList())) + "/" + s.strip()).
生成された相対パスのDEBUG出力です。
.peek(System.out::println)
生成された相対パスに対し順次フォルダ生成を行います。
forEach(r -> {try {Files.createDirectory(Path.of(r));}


書いてみて、今回の処理は流れをさかのぼる処理があったことから、
最初のmap生成など強引な処理になっているところがあります。
この辺りワンライナーに向いていなかったのではないかと後になって気がつきました。

ちなみに

Streamを使わずに、展開して記述すると以下のようになります。

	var map = new LinkedHashMap<Integer, String>();
	var resultPathList = new ArrayList<String>();
	List<String> lines = null;
	lines = Files.readAllLines(Path.of("input.txt"));

	for (String line : lines) {
		String pathName = "";
		int tabCount = line.length() - line.stripLeading().length();
		line = line.strip();
		map.put(tabCount, line);
		if (tabCount == 0) {
			pathName = line;
		} else {
			for (int index = 0; index < tabCount; index++) {
				pathName += map.get(index) + "/";
			}
			pathName += line;
		}
		resultPathList.add(pathName);
	}

	for (String resultPath : resultPathList) {
		System.out.println(resultPath);
		Files.createDirectory(Path.of(resultPath));
	}

24Lineぐらいありますね。是に比べるとStreamを使ったコードはスッキリしています。
こちらのほうが読みやすいですが。。

まとめ

以上、JShellで書いたワンライナー、いかがでしたでしょうか。
Javaはあまり向いていないとは思いますが、
ストリームを書く練習にはちょうどよいのではないかと思います。

ではでは。

Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

世界初のElastic認定エンジニアと一緒に働きたい人Wanted! - Acroquest Technology株式会社のエンジニア中途・インターンシップの求人 - Wantedlywww.wantedly.com

Re:VIEW 3.0でレイアウトをカスタマイズしたい!

こんにちは、@snuffkinです。
このエントリは技術同人誌 その2 Advent Calendar 2018の18日目の記事です。

みなさん、技術同人誌、書いてますか~
私は技術書典5で初めて技術同人誌を作成しました。
acro-engineer.hatenablog.com
acro-engineer.hatenablog.com


このとき初めてRe:VIEWに触れたのですが、とても便利ですね。
Markdownライクな独自フォーマットで記述した原稿から、書籍の形式のPDFを簡単に生成できます。

ただ、技術書典5では原稿を書くためにパワーの大半を使ってしまい、書籍のレイアウトにはあまり時間を割けませんでした。
その後、Re:VIEWが3.0にバージョンアップしたのを機に、レイアウトをカスタマイズするために必要な知識を学びました。
この記事では、Re:VIEWでPDFを生成したことがあり、TeXの知識が多少ある方に向けて、Re:VIEWのレイアウト修正方法についてご紹介します。

Re:VIEWの情報源

Re:VIEWのサイトはこちらになります。
Re:VIEW - Digital Publishing System for Books and eBooks(https://reviewml.org/ja/)

カスタマイズして利用したい方は、こちらのナレッジベースが参考になります。
Re:VIEW ナレッジベース — Re:VIEW knowledge ドキュメント(https://review-knowledge-ja.readthedocs.io/ja/latest/)

デフォルト設定でPDFを出力

PDFを生成するには、config.ymlのtexdocumentclassにmedia=ebookを指定し、

texdocumentclass: ["review-jsbook", "media=ebook,paper=a5"]

次のコマンドを実行します。

rake pdf

生成されたPDFは次のようなレイアウトになっています。
f:id:acro-engineer:20181216132648p:plain

このレイアウトには気になる点があったため、赤枠で囲いました。
デフォルトの設定では、各ページのヘッダに章名と節名が表示されています。
私の感覚としては、

  • 偶数ページの左側に章名
  • 奇数ページの右側に章名

とし、赤枠の部分には何も出力されないようにしたいです。

レイアウトを変更

そこで、レイアウトを変更してみましょう。
Re:VIEWのレイアウト情報はstyディレクトリにTeXで記載されています。
そのため、レイアウトをカスタマイズするには、TeXの知識が必要になります。

ページヘッダはsty/review-style.styの次の部分になります。

\RequirePackage{fancyhdr}
\pagestyle{fancy}
\lhead{\gtfamily\sffamily\bfseries\upshape \leftmark}
\chead{}
\rhead{\gtfamily\sffamily\bfseries\upshape \rightmark}

TeXのfancyhdrパッケージを使っています。
lhead、rheadの仕様は

  • \lhead[偶数ページのヘッダの左側]{奇数ページのヘッダの左側}
  • \rhead[偶数ページのヘッダの右側]{奇数ページのヘッダの右側}

であり、[]を省略すると偶数ページ・奇数ページ共に同じヘッダになります。

ヘッダのレイアウトを偶数ページと奇数ページで分ければいいんですね。

Re:VIEWでスタイルを修正したい場合は、sty/review-custom.styに記載します。
review-custom.styは他のstyより後に読み込まれるため、ここに記載した内容でデフォルト設定を上書きすることができます。

\lhead[\gtfamily\sffamily\bfseries\upshape \leftmark]{}
\rhead[]{\gtfamily\sffamily\bfseries\upshape \rightmark}

さて、これでrake pdfしてみると、、、
期待通りのヘッダになりました!
f:id:acro-engineer:20181216132704p:plain

このように、TeXの知識があればレイアウトを変更することができます。
ちょっとレイアウトを変更したいのであれば、このような流れで対応できますね。

おまけ: @<m>でTeXを使うときに「}」はエスケープしなくて良いです!

TeXで「\sum_{x=1}^n x」と出力するとき、次のように書きます。

\sum_{x=1}^n x

Re:VIEWにインライン命令でTeX記法を使うときは「@<m>」を使いますが、多くのサイトで

@<m>{\sum_{x=1\}^n x}

と書くよう説明されています。
「}」をエスケープする必要があるため、「x=1」の後ろの「}」を「\}」と書く必要があります。
TeXは大量の「}」を使うので、いちいちエスケープするのは非常につらいです。

これは最近のRe:VIEWでは改善されていて、「@<m>」の開始と終了に「$」を指定できるようになっています。
「$」を使えば「}」はエスケープしなくて良いです!

@<m>$\sum_{x=1}^n x$

これで、Re:VIEWの原稿にTeXを書きやすくなりますね。

最後に

Re:VIEWはとても素晴らしいツールで、手軽に技術書を書くことができます。
是非皆さんも技術書を書いてみませんか。

では、次回の技術書典で(?)お会いしましょう~

Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
モノリシックなアプリケーションをマイクロサービス化したいエンジニア募集! - Acroquest Technology株式会社のWeb エンジニア中途・インターンシップ・契約・委託の求人 - Wantedlywww.wantedly.com

JJUG CCC 2018 Fall で発表しました

皆さんこんにちは、しんどーです。

この記事はJava Advent Calendar 2018の17日目のエントリーです。
16日目はdo-m-gatoruさんの「JJUG CCC 2018 Fall」に参加してきた。でした。

はじめに

先日の12/15(土)に行われたJJUG CCC 2018 Fallに参加し、登壇させていただきました。
JJUG CCCは日本Javaユーザグループ主催のコミュニティイベントです。
参加者は登録数で1000人超と、ユーザカンファレンスとしては最大級の規模です。
Javaのイベントではありますが、ほとんどJavaに関係ないセッションもあったりでJavaユーザ以外でも十分に楽しめる内容だと思います。

発表について

朝イチで『ふつうのJavaアプリ開発のための自動テスト戦略』というタイトルで発表しました。

ふつうのJavaアプリ開発のための自動テスト戦略 / JJUG CCC 2018 Fall - Speaker Deck



前回のJJUG CCC 2018 SpringではJUnit 5について発表しており、2回連続でテストネタと、特に意識はしてなかったのですが続編的なものになってしまいました。
まだまだ間に合う!JUnit 5入門 / JJUG CCC 2018 Spring - Speaker Deck

ありがたいことに、登壇初心者枠のセッションにも関わらず多くの方にご参加いただき満席御礼となりました。僕に何をそこまで期待していたのでしょうか!?

参加してくださった方々、あとから資料を見てくださった方々には大変感謝感激です。
はてブもたくさんついてるようで、何やら恥ずかしさがこみ上げてきました。

f:id:acro-engineer:20181217003124j:plain
発表の様子

裏話的なこと

テストネタで発表しておきながら、実は僕はあまりテストが好きではありませんw
ただ嫌いなりにテスト(特にユニットテスト)のことに向き合って、いかに楽にできるか、いかに楽しく開発できるかを考えながら発表させていただきました。

CFPでは「実際のコードも交えながらお話します」とか書きましたが、結果コードは1行も出てきていませんww
「話が違うじゃねーか!」と発表中に靴を投げ込まれるのではないかと内心ビクビクしていました。

ノウハウや知識よりも、考え方や自分の想いを伝えるセッションにしたつもりですので、パッション的なものを感じていただければ何よりです。

さいごに

1年前はJJUG CCCに登壇するなんて考えもしなかったのですが、なんと今年は2回もしてしまいました。裏で多くの方に背中を押していただいて、こういう経験をできたのはエンジニアとしてとても貴重なことだと思います。

ちなみにですが、こんな超個人的な内容を会社ブログに上げさせてくれる弊社は自由だなーと思いました。


明日18日目のJava Advent Calendarの担当はnabedgeさんです。
登壇上級者からのJJUG CCCの感想が楽しみです!



Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

モノリシックなアプリケーションをマイクロサービス化したいエンジニア募集! - Acroquest Technology株式会社のWeb エンジニア中途・インターンシップ・契約・委託の求人 - Wantedlywww.wantedly.com



ElasticsearchのRanking Evaluation APIについて整理してみた

概要

こんにちは、shin0higuchiです😊
この記事はElastic stack (Elasticsearch) Advent Calendar 2018 - Qiitaの17日目です。
ElasticsearchのRanking Evaluation APIについて整理してみました 。

バージョン情報など

Elasticsearch : 6.5.1
Kibana : 6.5.1

Ranking Evaluation APIとは

Ranking Evaluation APIは、検索クエリに対する検索結果の妥当性を評価するためのAPIです。
Elasticsearchのバージョン6.2以降で利用することができます。


突然ですが、「良い検索」とはどういったものでしょうか?
一般的に、指定した検索条件に対して、より関連度の高いドキュメントが検索結果の上位に来るほど、良い検索システムであると言えます。
このAPIでは、「関連度の高いドキュメント」を事前にテストデータセットとして定義することで、実際の検索結果を評価します。

使いどころとしては、「index時に利用しているanalyzerの設定が適切か?」「アプリケーションがElasticsearchに対して発行するクエリは適切か?」などを定量的に評価したい場合に利用するのが良いと思います。

使い方

データ登録

今回は、日本語のWikipediaデータをElasticsearchに登録しておきます。
https://dumps.wikimedia.org/jawiki/20181201/jawiki-20181201-pages-articles6.xml-p2534193p3873913.bz2
からデータをダウンロードし、Logstashを使って取り込みました。

Kibanaから確認してみたところ、wikiというindexが作成されています。

f:id:acro-engineer:20181216212547p:plain:w700
title一覧

約60万件の記事が登録されていることがわかります。

また、タイトル一覧を取得してみると、非常に多岐にわたる記事が登録されていますね。

f:id:acro-engineer:20181216213806p:plain:w700
title一覧

リクエストの形式

Ranking Evaluation APIを利用する際のリクエスト形式は次のようになります。

GET /wiki/_rank_eval
{
    "requests": [ ... ], 
    "metric": { ... }
}

詳細については後述しますが、リクエストボディは大まかに2つの要素からなります。

  • requests : 検索クエリおよび、そのクエリで検索結果の上位に来て欲しいドキュメントのリスト
  • metrics : 検索結果の評価方法の定義

では、それぞれの定義内容について説明します。

requests

requestsの形式は次の通り。

{
            "id": " ... ",
            "request": {
                "query": { ... }
            },
            "ratings": [
                { "_index": "wiki", "_id": "doc1", "rating": 0 }.
                { "_index": "wiki", "_id": "doc2", "rating": 1 },
                { "_index": "wiki", "_id": "doc3", "rating": 2 }
            ]
        }

request内にクエリを記述し、そのクエリで検索結果の上位に来て欲しいドキュメントのリストはratingsの中に記述します。indexとdocument idを指定し、ratingに関連度を記述します。ratingの数値は整数で、0であれば検索にヒットして欲しくないドキュメント(クエリに関連のないドキュメント)、数値が大きいほど関連のある(検索上位に来て欲しい)ドキュメントとなります。

上記の例だと、_idがdoc3とdoc2のものが検索結果上位に来て、doc1はヒットしないのが理想ということです。
Ranking Evaluation APIでの評価は、実際にクエリを実行した時に、ratingに定義したリストにどれだけ近いかを数値化するものです。
厳密にはこの後に説明するmetricで評価方法を決定します。

metric

評価指標としては次の4つがサポートされています。

  • Precision at K
  • Mean Reciprocal Rank (MRR)
  • Discounted Cumulative Gain (DCG)
  • Expected Reciprocal Rank (ERR)

詳細については、本記事では割愛します。
もし評価方法論について興味があれば、個人的には下記の書籍がオススメです。
情報アクセス評価方法論-酒井哲也-

この記事ではもっともシンプルなPrecision at K(以下、P@K)を用います。
これは実際の検索結果の上位Kドキュメントの適合率を表します。

metricにP@Kを設定するには下記のように記述します。

"metric": {
      "precision": {
        "k" : 5,
        "relevant_rating_threshold": 1
      }
   }

この例では、検索結果の上位5件に、rank1以上のドキュメントが何割あるかを評価します。
5件全てが、関連のあるドキュメント(rank1以上)であれば適合率は100%、つまりP@Kは1となります。

実行してみる

では、実際に今回のケースに適用してみます。

例えば検索ユーザーが、「Javascriptフレームワークに関連するドキュメントが欲しい」と考えたとしましょう。ユーザーは「Javascript Web フレームワーク」などと検索すると仮定します。このクエリで検索した時の結果を評価してみましょう。実際のリクエストは下記のようになります。

GET wiki/_rank_eval
{
  "requests": [
    {
      "id": "javascriptのフレームワークを探すクエリ例",
      "request": {
        "query": {
          "query_string": {
            "query": "Javascript Web フレームワーク"
          }
        }
      },
      "ratings": [
        {
          "_index": "wiki",
          "_id": "3440787",
          "rating": 1
        },
        {
          "_index": "wiki",
          "_id": "3387491",
          "rating": 1
        },
        {
          "_index": "wiki",
          "_id": "2611804",
          "rating": 1
        },
        {
          "_index": "wiki",
          "_id": "2922947",
          "rating": 1
        },
        {
          "_index": "wiki",
          "_id": "3672684",
          "rating": 1
        }
      ]
    }
  ],
  "metric": {
      "precision": {
        "k" : 5,
        "relevant_rating_threshold": 1
      }
   }
}

ここでは関連のあるドキュメント上位5件のIDをratingsに登録しています。
実際に実行してみると次のようなレスポンスが返ってきます。

{
  "metric_score" : 0.4,
  "details" : {
    "javascriptのフレームワークを探すクエリ例" : {
      "metric_score" : 0.4,
      "unrated_docs" : [
        {
          "_index" : "wiki",
          "_id" : "2828794"
        },
        {
          "_index" : "wiki",
          "_id" : "3447215"
        },
        {
          "_index" : "wiki",
          "_id" : "3609062"
        }
      ],
      "hits" : [
        {
          "hit" : {
            "_index" : "wiki",
            "_type" : "page",
            "_id" : "2828794",
            "_score" : 32.780136
          },
          "rating" : null
        },
        {
          "hit" : {
            "_index" : "wiki",
            "_type" : "page",
            "_id" : "3440787",
            "_score" : 32.139748
          },
          "rating" : 1
        },
        {
          "hit" : {
            "_index" : "wiki",
            "_type" : "page",
            "_id" : "3447215",
            "_score" : 31.709887
          },
          "rating" : null
        },
        {
          "hit" : {
            "_index" : "wiki",
            "_type" : "page",
            "_id" : "3609062",
            "_score" : 28.539764
          },
          "rating" : null
        },
        {
          "hit" : {
            "_index" : "wiki",
            "_type" : "page",
            "_id" : "3387491",
            "_score" : 28.265165
          },
          "rating" : 1
        }
      ],
      "metric_details" : {
        "precision" : {
          "relevant_docs_retrieved" : 2,
          "docs_retrieved" : 5
        }
      }
    }
  },
  "failures" : { }
}

"metric_score" : 0.4という結果から分かるように、適合率は40%、つまり5件中の2件のみが関連するドキュメントで、残りの3件は関連がない若しくはrateingされていないドキュメントということになります。

実際、unrated_docsというブロックを見ると、3件のドキュメントが入っています。
これは、想定していなかった(リクエスト時にrating指定していなかった)ドキュメントが、検索結果の上位5件に入ってきたことを意味します。
ドキュメントを確認してみたところ、「Vue.js」「Angular.js」などの他に、「シングルページアプリケーション」などがヒットしていました。

今回のケースでは、「シングルページアプリケーション」よりも上位に来て欲しい他のフレームワークなどもありました。
実際には、ここからanalyzerやscoringなどを調整して理想の検索結果に近づくようチューニングをしてゆくことになるでしょう。
その時に再びRanking Evaluation APIを利用して精度が上がったかどうかを客観的数値で測ることができます。
今回の記事の中では、長くなってしまうためチューニングについては割愛します。

さて、ここまでRanking Evaluation APIの使い方を簡単に説明してきました。
真面目に検索システムとして評価するとなると、通常は1つのクエリだけでなく、複数のクエリを用いて総合的に評価するでしょうし、ratingに関してもより網羅性を上げる必要があると思います。ただ、全ドキュメントにレートをつけるのは現実的ではないので、通常はどの程度までratingするものなのか、詳しい人がいたら是非話してみたいものです。

まとめ

  • Ranking Evaluation APIを使って、検索システムとしての定量的評価ができる
  • 事前のRatingなど、テストデータセットの準備が手間

以上です。ツッコミ等あれば是非お願いいたします。
お読みいただきありがとうございました。

Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

世界初のElastic認定エンジニアと一緒に働きたい人Wanted! - Acroquest Technology株式会社のエンジニア中途・インターンシップの求人 - Wantedlywww.wantedly.com

開発者が見落としがちなユーザビリティアンチパターン

こんにちは。QAエンジニアの @yuki_shiro_823 です。

このエントリはソフトウェアテスト Advent Calendar 2018の15日目の記事です。

きっかけ

私は社内で色々なプロジェクトを渡り歩き、横断的にテストを行っています。
テストをしていると、

  • 機能的には問題ないけれども、なんとなく使いづらい
  • お客様の業務に照らし合わせると、もっとこうした方がいいのでは?

という事例を時々見かけます。
「開発者にもユーザ視点で、開発時から基本的なユーザビリティを向上させられるようになってほしい!」
という思いから、いくつかアンチパターンをまとめてみました。
その一部を紹介します。

ユーザビリティの定義と本エントリで扱う範囲

ユーザビリティの定義は、ヤコブ・ニールセンによる定義やISO25010などいくつかあります。
本エントリではISO-25010の「システム/ソフトウェア製品品質」で定義されている「使用性」の一部「適切度認識性」にまつわるアンチパターンをとりあげます。
f:id:acro-engineer:20181214014259p:plain:w450

適切度認識性のアンチパターン

適切度認識性について

ISO25010では、「ニーズに適した利用かどうか認識できる度合」と定義されています。

損なわれるとどうなるか

ユーザがシステムを利用して、明示された作業及び目的の達成を実行できなくなる、と考えられます。

アンチパターン1:探し物はどこですか?

f:id:acro-engineer:20181214020750p:plain:w200

内容

表やドロップダウンリストの並びが何の順番になっているのか不明で、自分の見たいものがどこにあるのか分からない、というケースです。
業務上、最新の結果から順番に表示してほしいのに、件名の昇順になっていて使いにくいということがありました。
また、サイトを訪れる人のほとんどが日本国籍と思われるサイトで、国籍を選択するドロップダウンリストになかなか「日本」が出てこないというのもよく見ます。
f:id:acro-engineer:20181214235724p:plain

対処策

一覧であれば、業務での使用順に合わせて並んでいる、ドロップダウンリストであれば、選択肢が使用頻度の高いものから並んでいるという構成にしてほしいです。上記の例では「JP 日本」がドロップダウンリストの上位に表示されていると、ユーザにとっては使いやすいですね。
要件をヒアリングする段階で、ユーザが一覧にした内容で何をするのか、が明確にできていると後々助かります。(コーディングの段階で気づくと、修正が厄介な場合が多い問題です)

アンチパターン2:反応がない…

f:id:acro-engineer:20181214021830p:plain:w200

内容

「登録」や「検索」などのボタンをクリックしたものの、画面に何も反応がなく、処理が行われているか分からないというケースです。ユーザがボタンを連打してしまい、二重登録などが発生してしまう恐れがあります。

対処策

処理中であることが、ユーザに明確に分かるようにしてください。
こちらは、はてなブログの編集画面にある「公開する」ボタンです。
f:id:acro-engineer:20181215002713p:plain
一度「公開する」ボタンを押下すると、次の通りボタンの色が変わります。
f:id:acro-engineer:20181215004535p:plain
いかにもクリックできない様子になりました。
このように、

  • ボタンをクリックできなくする/ボタンの色を変える
  • プログレスバーなどを表示し、処理中であること示す

などがあると分かりやすくなります。
登録画面などの場合は、「登録ボタンは一度だけクリックしてください」などの注意書きがあるだけでも、連打の防止に効果があるそうです。

アンチパターン3:キャンセルのキャンセル?

f:id:acro-engineer:20181214030515p:plain:w200

内容

ユーザの操作に対して、何らかのメッセージが出るものの、次に何をしてよいか明確でない、すぐに操作が行えない、というケースです。
f:id:acro-engineer:20181215000125p:plain
パッと見ただけでは、「キャンセル」ボタンをクリックしたときに、どちらになるのか分かりません。
キャンセルをキャンセルするので、結果は「キャンセルされない」になるのですが、キャンセルがゲシュタルト崩壊しそうですね。
こちらは、キャンセルのキャンセル問題という有名なアンチパターンです。詳しく知りたい方はぜひ、以下のサイトをどうぞ。
キャンセルのキャンセル問題から考えるダイアログデザイン

対処策

メッセージの場合は、「xxxに誤りがあります。ご確認ください」など次に何をすればよいか明示するようにしてください。
また、メッセージ中の動詞とボタンの動詞をそろえるのも効果的です。

アンチパターン4:どこへも行けない

f:id:acro-engineer:20181214025231p:plain:w200

内容

ユーザがある画面から、他の画面に戻りたいと思ったときに移動する手段がないケースです。例えば、次のような画面がありました。

  • 「戻る」ボタンがない
  • ダイアログ表示で「戻る」または「閉じる」ボタンがない

f:id:acro-engineer:20181215000220p:plain:w300
こちらのキャプチャの例は、半分ネタですが、ダイアログのエリア外を押すとダイアログが消えることを知らないと、ユーザは何もできなくなってしまいます。

対処策

明確に「戻る」や「閉じる」のボタンを設けてください。
ボタンを設けない場合は、「ブラウザのバックボタンでお戻りください」など、ユーザが次の操作を行えるような指示を記載してください。

最後に

4点のアンチパターンを紹介しましたが、気を抜くと誰しもやってしまいがちなものだと思います。
何かの折にふと思い出してもらい、ユーザビリティの高いシステムを作るヒントにしてもらえると幸いです。

Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
モノリシックなアプリケーションをマイクロサービス化したいエンジニア募集! - Acroquest Technology株式会社のWeb エンジニア中途・インターンシップ・契約・委託の求人 - Wantedlywww.wantedly.com

最初に知っておけば良かったFilebeatの設定

こんにちは。

最近、Filebeatによるログ収集について興味を持ちつつ、
いろいろと調べながら使っているsawaです。

この記事は、Elastic stack (Elasticsearch) Advent Calendar 2018 - Qiitaの、13日目の記事になります。

はじめに

Elastic Stackを使ったログ収集を行うには、
Logstashを使う、Beatsシリーズを使うといった、選択肢があります。

本記事では、Filebeatを使う際に知っておくと良い設定を紹介したいと思います。

Filebeatとは?

f:id:acro-engineer:20181212180846j:plain

Filebeatには次のような特徴があります。

  1. データ収集ツールとして、軽量に扱える。
  2. 用途が限定されているため、シンプルに使える。
  3. ミドルウェアに応じたプラグインを適用することで、自動でダッシュボードの生成までも行える。

詳細についてはドキュメントに記載があります。
www.elastic.co

大量のログファイルを収集する際に陥りがちな問題

手元にある大量のファイルをElasticsearchにインデクシングする必要があり、Filebeatを使いました。
ひとまずFilebeatをデフォルト設定のまま使ったところ、うまく収集できず、Filebeatのログにこんな出力がされました。

A:

Harvester could not be started on new file: /var/log/foo_xxxx.log, 
Err: error setting up harvester: Harvester setup failed. Unexpected file opening error: 
Failed opening /var/log/foo_xxxx.log: open /var/log/foo_xxxx.log: too many open files

B:

Failed to connect to backoff(elasticsearch(http://localhost:9200)): Get http://localhost:9200: 
dial tcp [::1]:9200: socket: too many open files

Aはログファイルを開けない事象、BはElasticsearchへの接続が行えない事象を指しています。

例えば、3,000個のログファイルを読み込んでしばらくすると、オープンファイル数が1,000近くになることがFilebeatのログから分かりました。
それにより、OSのファイルディスクリプタのソフトリミットである1,024近くになっていたことが分かりました。
ファイルディスクリプタを使う数を減らしたいですね。

Filebeatにおける収集の仕組みと対策

対策するために、まず、Filebeatによる収集の仕組みを押さえます。

収集の仕組み

Filebeatは、以下の流れでログを収集します。

f:id:acro-engineer:20181213103928j:plain

①収集対象ファイルの一覧取得
 デフォルト10秒間隔で、タイムスタンプが新しいファイルを収集対象に入れます。
 (一覧取得間隔はfilebeat.ymlの「scan_frequency」で変更可能)
 一覧に入ったファイルは、②で収集開始します。

②収集の開始(ファイルハンドラの確保)
 1ログファイルにつき1つずつのファイル収集のためのインスタンスが立ち上がります。
 これを「ハーベスタ」といいます。

③更新の検知と送信
 デフォルト1秒間隔で、新しい行が追加されていれば差分を外部に送信します。
 (更新の検知間隔はfilebeat.ymlの「backoff」で変更可能)

④収集の終了(ファイルハンドラの解放)
 filebeat.ymlで定義された条件を満たすと、ハーベスタによる収集が終了します。


上記の④で収集を終了(ファイルハンドラを解放する)条件の設定は以下のドキュメントで紹介されています。
 Log input | Filebeat Reference [6.5] | Elastic

  • 一定時間以上の更新が無い場合(filebeat.ymlの「close_inactive」で変更可能)
  • ファイルが削除された場合
  • ファイルがリネームされた場合
  • ファイルがEOFに達した場合
  • ファイルハンドラを作って一定時間が経過したら強制的に解放

これら設定は、ユースケースに応じて柔軟にカスタマイズが必要ですね。
Filebeatを利用する前にチェックしてみてください。

今回の場合は5分以上更新が無いログファイルなので、「一定時間以上の更新が無い場合」(close_inactive)の時間を短縮することでファイルディスクリプタの枯渇によるエラーの発生を抑えることができそうです。
(もちろん、OSの設定を変更するという手段もあります)

以下の条件を満たすログを収集する場合は、close_inactiveの短縮が有効と言えるでしょう。

  • 更新頻度がさほど少ない
  • サイズが小さめ
  • 1,000以上ある大量の数のログが収集対象

「close_inactive」による対策

「close_inactive」を具体的に紹介します。
デフォルトは5m(5分間)で、単位に分(m)や秒(s)を指定可能です。

短くすることでより早くファイルハンドラが解放されるようになります。
ファイルハンドラが解放されることで、待ち状態の次のファイルが次々と読み込まれていきます。

ただし、短くし過ぎると、次に更新が掛かった後にファイルハンドラを再取得するまでのタイムラグが生じます。
(①で説明したscan_frequency分待つ必要があります)

今回の検証では、1mに短縮してみます。

  close_inactive: 1m

今回はこれで解決できました。
この設定を先に知っておけば良かったですね。

最後に

Filebeatを使うために知っておけば良かった設定について紹介しました。
Filebeatを使う際に、こんな設定あったなと、思い出して、このページを見て頂ければ幸いです。

それではまた。

Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
ユーザに最高の検索体験を提供したいエンジニアWanted! - Acroquest Technology株式会社のエンジニア中途・インターンシップの求人 - Wantedlywww.wantedly.com

LambdaからRekognitionを使ってみたら意外なところでハマった話

本エントリは、AWS #2 Advent Calendar 2018の12日目です。

こんにちは! 気付けばAcroquestに入社してもう10年目、最近はAWSを使った開発やDevOpsの活動に携わっているiidaです。

今回は、少し前に上司から「Amazon Rekognition Imageを使うことになるかも知れない」と言われて触ってみたら、意外なところで嵌った話をしたいと思います。

AWS Rekognitonとは

人工知能機械学習に基づく画像認識・画像分析サービスです。こう書くと何やら難しそうですが、百聞は一見にしかず、AWSコンソールのデモを見てみましょう。

f:id:acro-engineer:20181209232237p:plain:w800

これは「オブジェクトとシーンの検出」で、画像中にどんなオブジェクト(CarやPersonなど)があるのか、あるいはどんなシーン(TransportationやSportsなど)なのかを検出し、その位置まで教えてくれるようです。

私は機械学習についてはほとんど知識がありませんが、それでもこのようは画像解析ができるようになるなんて、すごい世の中になったものですね!

Pythonから呼んでみる

さてさて、このようなサービスがあるということは分かりましたが、エンジニアとしてはやはり自分の書いたコードから使ってみたいものですよね!

ということで、早速PythonからRekognitionを呼び出すコードを書いてみました。

import json
import boto3

# ローカルの画像ファイルを読み込む。
with open('sample.jpeg', 'rb') as image_file:
    image_bytes = image_file.read()
# Rekognitionのラベル検出を呼び出す。
rekognition = boto3.client('rekognition', 'ap-northeast-1')
response = rekognition.detect_labels(Image={'Bytes': image_bytes})
print(json.dumps(response, indent=2))

たったこれだけ。簡単!

f:id:acro-engineer:20181209231438j:plain:w600

試しにこの画像を解析してみましょう。先日のHappy360(全体査定)の時の一コマです。どんなレスポンスが返って来たのかというと…。

{
  "Labels": [
    {
      "Name": "Human",
      "Confidence": 99.71063232421875,
      "Instances": [],
      "Parents": []
    },
    {
      "Name": "Person",
      "Confidence": 99.71063232421875,
      "Instances": [
        {
          "BoundingBox": {
            "Width": 0.31008321046829224,
            "Height": 0.6783191561698914,
            "Left": 0.626418948173523,
            "Top": 0.31440863013267517
          },
          "Confidence": 99.71063232421875
        },
            :(以下省略)

なるほど、Nameがオブジェクトやシーンの種類で、BoundingBoxが画像中の位置のようです。HumanとPersonの違いがよく分かりませんが、Humanは「人が写っている」ということを表しているのでしょうか?

結果を画像に描画してみる

これだけだと分かりづらいので、デモみたいにラベルを画像に描画してみましょう。

import cv2

# (ここに先ほどのコードが入ります。)

# CV2で画像ファイルを読み込む。
np_image = cv2.imread('sample.jpeg')
height, width = np_image.shape[:2]

# ラベルの中から人と思われるものを探して四角で囲う。
for label in response['Labels']:
    if label['Name'] not in ['People', 'Person', 'Human']:
        continue

    for person in label['Instances']:
        box = person['BoundingBox']
        x = round(width * box['Left'])
        y = round(height * box['Top'])
        w = round(width * box['Width'])
        h = round(height * box['Height'])
        cv2.rectangle(np_image, (x, y), (x + w, y + h), (255, 255, 255), 3)
        cv2.putText(np_image, label['Name'], (x, y - 9),
                    cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 3)

cv2.imwrite('./sample_result.jpeg', np_image)

BoundingBoxの値は、画像の幅と高さに対する割合となっているので、少し計算が必要です。これを実行してみると…。

f:id:acro-engineer:20181209231555j:plain:w600

Great! 手前の2人だけでなく、奥にひっそりといる人もちゃんと検出されていますね!

Lambdaから実行してみる

ここまで来たら、Lambda上でも動かしてみたくなりますよね!(えっ、ならないですか?)

S3にアップロードされた画像ファイルを自動で解析するLambda関数を書いてみました。

import os

import boto3
import cv2

s3 = boto3.resource('s3')


def handle_request(event, content):

    # S3にアップされた画像の情報を取得する。
    bucket_name = event['Records'][0]['s3']['bucket']['name']
    object_key = event['Records'][0]['s3']['object']['key']
    file_name = os.path.basename(object_key)

    # 画像をRekognitionで解析する。
    rekognition = boto3.client('rekognition')
    response = rekognition.detect_labels(Image={
        'S3Object': {
            'Bucket': bucket_name,
            'Name': object_key
        }
    })

    # S3から画像をダウンロードする。
    tmp_dir = os.getenv('TMP_DIR', '/tmp/')
    bucket = s3.Bucket(bucket_name)
    bucket.download_file(object_key, tmp_dir + file_name)

    # 検出した人物に枠を描画する。
    image = cv2.imread(tmp_dir + file_name)
    height, width = image.shape[:2]

    for label in response['Labels']:
        if label['Name'] not in ['People', 'Person', 'Human']:
            continue

        for person in label['Instances']:
            box = person['BoundingBox']
            x = round(width * box['Left'])
            y = round(height * box['Top'])
            w = round(width * box['Width'])
            h = round(height * box['Height'])
            cv2.rectangle(image, (x, y), (x + w, y + h), (255, 255, 255), 3)
            cv2.putText(image, label['Name'], (x, y - 4),
                        cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 3)

    cv2.imwrite(tmp_dir + file_name, image)

    # S3に描画後の画像をアップロードする。
    bucket.upload_file(tmp_dir + file_name, 'result/' + file_name)
    os.remove(tmp_dir + file_name)

ちなみに、CV2(opencv-python)は非Pure Pythonなので、Amazon Linux上でビルドまたはpip installしたものをデプロイパッケージに含めるか、Serverless Framworkを使用する場合はServerless Python RequirementsプラグインでdockerizePipを有効にする必要があります(ここでも少し嵌った)。

デプロイパッケージが50MB近くになってしまいましたが、無事デプロイが完了したので、早速S3バケットに画像ファイルをアップロードしてみます。すると…。

'Instances': KeyError
Traceback (most recent call last):
  File "/var/task/person_detector.py", line 44, in handle_request
    for person in label['Instances']:
KeyError: 'Instances'

あれ? ラベルにInstancesが無いって怒られてしまいました。ローカルで動かした時にはこんなエラーは出なかったのですが、そんなこともあるんですかね?

        if label['Name'] not in ['People', 'Person', 'Human'] or 'Instances' not in label:
            continue

突貫ですが、処理するラベルの条件式をこんな風に変えてみました。

f:id:acro-engineer:20181209231438j:plain:w600

すると、出力フォルダに画像はできたのですが、ローカルでは描画されていたラベルがありません。何故だ?!

    response = rekognition.detect_labels(Image={'Bytes': image_bytes})
    print(json.dumps(response, indent=2))

こうなったらログ出力しかありません。Rekognitionからのレスポンスを表示してみると…。

{
  "Labels": [
    {
      "Name": "Person",
      "Confidence": 99.57161712646484
    },
    {
      "Name": "Human",
      "Confidence": 99.57161712646484
    },
        :(以下省略)

なんと、ローカルで実行した時よりも取得できる情報が少ないではありませんか!

こんなことが起こり得るのかと思って調べてみたところ、どうやら古いバージョンのBoto3だとこれらの情報しか取得できないようです。あれ、そう言えばLambdaではデフォルトでBoto3が使えますが、あれってもしかして…。

f:id:acro-engineer:20181209231248j:plain:w600

これだ!

確かに言われてみれば当たり前ですが、今まで意識したことがなかったので盲点でした。バージョンによって関数の有無があるのは想像できますが、レスポンスが異なるなんてこともあるんですね。

f:id:acro-engineer:20181209231555j:plain:w600

その後、最新のboto3とbotocore、そしてurllib3もデプロイパッケージに含めたところ、ローカルと同じくラベルの描画された画像が出力されました。(デプロイパッケージが50MBを超えてしまいましたが。。)

Amazon Rekognitionだけでなく、普段使っているAWS Lambdaについても理解を深められ、とても良い勉強になったと思います。LambdaからRekognitionを使おうと思っている方はお気を付け下さい! 以上、AWS #2 Advent Calendar 2018の12日目でした!

Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
ユーザに最高の検索体験を提供したいエンジニアWanted! - Acroquest Technology株式会社のエンジニア中途・インターンシップの求人 - Wantedlywww.wantedly.com