quarkusを使う(inject編)

こんばんは、エキサイト株式会社の中尾です。

前回の続きです。 serviceを入れていきます。 serviceには@Singletonをつけて、@Injectで呼び出します。 ※サンプルです。

  • interface
package org.my.hobby.service;

import org.my.hobby.core.Book;

public interface BookService {
    Book find(String title);
}
  • impl
package org.my.hobby.service;

import javax.inject.Singleton;

import org.my.hobby.core.Book;

@Singleton
public class BookServiceImpl implements BookService {
    @Override
    public Book find(String title) {
        if (!"hobby".equals(title)) {
            return new Book("", "");
        }

        return new Book("hobby", "s-nakao");
    }
}
  • model
package org.my.hobby.core;

import io.netty.util.internal.StringUtil;

public record Book(String title, String author) {
    public boolean isFind(){
        return !StringUtil.isNullOrEmpty(title);
    }

    public String message(){
        if(isFind()){
            return "";
        };
        return "not found book";
    }
}
  • コントローラー

前回のコントローラーをちょっと改良します。

package org.my.hobby;

import javax.inject.Inject;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.constraints.NotBlank;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.Set;
import java.util.stream.Collectors;

import org.my.hobby.core.Book;
import org.my.hobby.service.BookService;

record BookSearchRequest(
        @NotBlank(message = "Title may not be blank") String title) {
}

record BookSearchResponse(
        boolean success,
        String message,
        Book book) {
    record Book(String title, String author) {
    }
}

@Path("/book/search")
public class BookController {

    @Inject
    Validator validator;

    @Inject
    BookService bookService;

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    public BookSearchResponse search(BookSearchRequest bookSearchRequest) {
        Set<ConstraintViolation<BookSearchRequest>> violations = validator.validate(bookSearchRequest);
        if (!violations.isEmpty()) {
            final String errorMessage = violations
                    .stream()
                    .map(e -> e.getMessage())
                    .collect(Collectors.joining(","));
            final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book("", "");
            return new BookSearchResponse(false, errorMessage, hobbyBook);
        }

        final Book book = bookService.find(bookSearchRequest.title());
        final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book(book.title(), book.author());
        return new BookSearchResponse(book.isFind(), book.message(), hobbyBook);
    }
}
  • curlでアクセス
shogo.nakao@localhost:(App-Db-Blog-Fan) $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
        "title": "hobby"
}'

{"success":true,"message":"","book":{"title":"hobby","author":"s-nakao"}}%
shogo.nakao@localhost:(App-Db-Blog-Fan) $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
        "title": "s-nakao"
}'

{"success":false,"message":"not found book","book":{"title":"","author":""}}%
shogo.nakao@localhost:(App-Db-Blog-Fan) $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
}'

{"success":false,"message":"Title may not be blank","book":{"title":"","author":""}}%

取得できました!よかったですね!

次回、repositoryを作成します。

最後に、弊社では採用もバシバシ実施しているので興味のあるかたがいましたらご応募ください。

www.wantedly.com



Advent Calendar 2021を引き続き楽しんでいただけると嬉しいです。

qiita.com

SpringBoot x Spring Shell で簡易バッチ処理を作る

エキサイト株式会社エンジニアの佐々木です。2021年エキサイトホーディングスアドベントカレンダー7日目を担当させていただきます。

以前、SpringBootで対話的インターフェースをSpring Shellで実装するで対話的なアプリケーションについては記載しており、今回は簡易的なバッチ処理の方を記載します。

はじめに

前回の対話的なアプリケーションのコードを再掲します。(一部修正していますが、メソッド名部分のみになります。)

@ShellComponent(value = "demo")
public class DemoShellController {

    @ShellMethod(value = "足し算をする", key = "addCalc" , group = "calc")
    public Integer add(int a, @Max(10) int b, @ShellOption(value = "--optional_c" , defaultValue = "0") int c){
        return a + b + c;
    }
}

こちらになりますが、引数を渡されたものを足し算する簡単なものになります。前回のままだと対話的なアプリケーションになってしまい、Cron等で動作させられないので、これを改造していきます。

SpringShell内の実装方法

SpringShellではどうやって対話的アプリケーションにしているかの実装をみていきます。

InteractiveShellApplicationRunner.java

@Component
@Order(InteractiveShellApplicationRunner.PRECEDENCE)
public class InteractiveShellApplicationRunner implements ApplicationRunner {

        // 省略

    @Override
    public void run(ApplicationArguments args) throws Exception {
        boolean interactive = isEnabled();
        if (interactive) {
            InputProvider inputProvider = new JLineInputProvider(lineReader, promptProvider);
            shell.run(inputProvider);
        }
    }

        // 省略
}

ApplicationRunnerのインターフェースを用いて実装されているのがわかります。@Orderを用いて、実行順序を制御しています。簡易バッチの方の処理は、ApplicationRunnerインタフェースを用いて、且つ@Orderで対話型アプリケーションよりも早く動かないといけません。

簡易バッチの実装

下記のような実装コードになります。

NonInteractiveRunner.java

@Component
@Order(InteractiveShellApplicationRunner.PRECEDENCE - 1)    //  ① InteractiveShellApplicationRunnerより1つ優先順位を上げる
public class NonInteractiveRunner implements ApplicationRunner {

    private Shell shell;
    private ConfigurableEnvironment configurableEnvironment;

    public NonInteractiveRunner(Shell shell, ConfigurableEnvironment configurableEnvironment) {
        this.shell = shell;
        this.configurableEnvironment = configurableEnvironment;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        if (args.getNonOptionArgs().isEmpty() || args.getNonOptionArgs().stream().anyMatch(s -> s.startsWith("@"))) {
            // ②ここに入った場合は、通常の対話型アプリケーションを実行する
            return;
        }

        InteractiveShellApplicationRunner.disable((configurableEnvironment));  // ③ ここで対話型アプリケーションを無効にする
        final Object evaluate = shell.evaluate(() -> String.join(" ", args.getSourceArgs()));  // ④ここで簡易バッチアプリケーションを実行する
        if (evaluate != null) {
            System.out.println(evaluate);    // ⑤出力するものがあればここで出力する
        }

    }
}

対話型アプリケーションの機能も残したい為、引数がなかったら or 引数に@があれば対話型アプリケーションの動きをするようにしています。(※対話型アプリケーションでは@があると、ファイルに記載されているコマンドを対話型アプリケーションの中で展開する仕様になっています。)

それ以外の場合は、対話型アプリケーションを無効にし、渡された引数でメソッド等を呼び出しバッチとして処理するようなものにします。

 $ java -jar build/libs/batch-0.0.1-SNAPSHOT.jar addCalc 1 2 4

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.0)

2021-12-05 17:46:45.958  INFO 76623 --- [           main] com.example.batch.BatchApplication       : Starting BatchApplication using Java 11.0.13 on 61-17616.local with PID 76623 (/Users/kohei.sasaki/git/sample/batch/build/libs/batch-0.0.1-SNAPSHOT.jar started by kohei.sasaki in /Users/kohei.sasaki/git/sample/batch)
2021-12-05 17:46:45.963  INFO 76623 --- [           main] com.example.batch.BatchApplication       : No active profile set, falling back to default profiles: default
2021-12-05 17:46:46.911  INFO 76623 --- [           main] com.example.batch.BatchApplication       : Started BatchApplication in 6.416 seconds (JVM running for 6.881)

7

答えは7になっています。 これで、SpringShellで簡易的なバッチ処理を作ることができました。対話型アプリケーションもコマンドアプリケーションもできるので、お得な実装になっているかと思います。

おまけ

上記のログにでている、起動時のSpringBootのバナーが邪魔ですね。これはメインメソッド内で消せるのでその実装を行います。

BatchApplication.java

@SpringBootApplication
public class BatchApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(BatchApplication.class)
                .bannerMode(Banner.Mode.OFF)  // ここでバナーをオフにする
                .run(args);
    }
}

上記のように、bannerMode()OFFにすれば表示されません。これで邪魔ではなくなりました。 下記のように、起動してもバナーがでないようになります。下記が実行結果です。

$ ./gradlew assemble && java -jar build/libs/batch-0.0.1-SNAPSHOT.jar addCalc 1 2 4

2021-12-06 09:33:25.406  INFO 85570 --- [           main] com.example.batch.BatchApplication       : Starting BatchApplication using Java 11.0.13 on 61-17616.local with PID 85570 (/Users/kohei.sasaki/git/sample/batch/build/libs/batch-0.0.1-SNAPSHOT.jar started by kohei.sasaki in /Users/kohei.sasaki/git/sample/batch)
2021-12-06 09:33:25.411  INFO 85570 --- [           main] com.example.batch.BatchApplication       : No active profile set, falling back to default profiles: default
2021-12-06 09:33:26.228  INFO 85570 --- [           main] com.example.batch.BatchApplication       : Started BatchApplication in 6.197 seconds (JVM running for 6.6)
7

まとめ

重要データに関しては、SpringBatch等をつかった方が、動作履歴やレジューム(途中から再開)機能があるのでそちらを使ったほうがいいですが、ちょっとしたバッチ処理であれば使ってみるのもいいと思います。対話型のアプリケーションと簡易型のコマンドラインアプリケーションがついてくるので、お得かもしれません。

最後に

2021年エキサイトホーディングスアドベントカレンダー7日目でした。引き続きエキサイトホーディングスのアドベントカレンダーをお楽しみいただけると幸いです。 qiita.com

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

カジュアル面談はこちらになります! meety.net

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

【タイポグラフィ・ポスター製作に】簡単計算!Illustratorでグリッドを作る方法

f:id:excite_ny:20211201131504p:plain

はじめに

エキサイト21卒デザイナーの山﨑です😊

こちらはエキサイトホールディングス Advent Calendar 2021の6日目の記事になります!

qiita.com

今回はIllustratorでのポスターデザインに必須なグリッドシステム(グリッドレイアウト)を解説していきたいと思います。

グリッドシステム・制作方法について

はじめに、グリッドシステムとはどんな物なのか説明します。

グリッドシステムとは?

色々意味はありますが、画面やページを格子状に分割して、規則的な直線を基準にデザイン要素を配置していくデザイン手法です。

ポスターをデザインする前にグリッドレイアウトを作っておくと、デザインが圧倒的に作りやすくなります。

今回エキサイト社内に掲示するバリューポスターをグリッドレイアウトを活用してデザイン制作したので、例として出してみます。

f:id:excite_ny:20211204234145p:plain

右がグリッドレイアウトです。このグリッドがある程度のデザインの配置の目安になっています。

一見適当な正方形を並べて配置しただけに見えますが、実は計算して配置しています。

では、早速解説して行きましょう。

①フォントサイズ・行送り・行間・グリッドに入れる行を決める

まずはこの四つの数字をきめましょう。

製作する文字のフォントサイズを元に決めると作りやすいです。

正直数字はなんでも良いのですが、あまりに数字が大きすぎるとグリッドの正方形が大きくなって組みづらいグリッドレイアウトになりがちなので、注意が必要です。

ひとまず今回は【フォントサイズ21pt、行送り25pt・行間4pt・グリッドに入れる行数は3行】に設定しました。 (グリッドに入れる行は5行・4行・3行から選ぶのが基本的です)

②その数字をもとに計算してグリッドサイズ・グリッド間を割り出す

そうすると、ざっくりこんな感じの図になります。

f:id:excite_ny:20211205005126j:plain

グリッドの正方形の中に3行文が入るとなると、

21pt+4pt+21pt+4pt+21pt=71pt (行送り25pt+行送り25pt+フォント21pt=71ptでも可)

f:id:excite_ny:20211206102305p:plain

3行目の行間4ptがはみ出る形になるので、グリッド間のサイズは

4pt+21pt+4pt=29pt

f:id:excite_ny:20211206103205p:plain

というわけで、正方形のサイズは71pt・正方形と正方形の間は29ptになりました。

③グリッドを作ってみる

f:id:excite_ny:20211205010752p:plain

71ptの正方形を一つ作ります。

f:id:excite_ny:20211205010812p:plain

環境設定>一般>キー入力で「71pt(グリッドサイズ)+29pt(グリッド間)」を入力してください。

このときに数字だけではなくpt (ポイント)という単位も入れましょう。

f:id:excite_ny:20211205010933p:plain

最初の正方形をクリックしてaltを押しながら→を押すと間隔が29ptに保たれた正方形が量産されます。

f:id:excite_ny:20211205011044p:plain

→を押し続けるとすぐに横一列のグリッドが完成しました。次はこの横一列のグリッドを全て選択して、今度はaltを押しながら↓を押します。

f:id:excite_ny:20211205011220p:plain

グリッドが完成しました。

このグリッドをアートボードの中心に合わせます。

この正方形を全て選択し、Command+5を推してグリッド化させれば完成です。

f:id:excite_ny:20211205011311p:plain

終わりに

いかがだったでしょうか?

慣れるまで少し複雑なのですが、仕組みを覚えれば意外と簡単に組めると思います😊

グリッドに合わせすぎるとデザインが窮屈な印象になってしまうので、ご注意ください。

グリッドレイアウトはあくまで目安なので、グリッドから外れてしまっても気にせずデザインするのが良いと思いました😊

それでは、ありがとうございました!

quarkusを使う(バリデーション編)

こんばんは、エキサイト株式会社の中尾です。

前回の続きです。 必要そうなツールを入れていきます。 まず、バリデーションを追加します。

  • gradleに以下を追加
implementation 'io.quarkus:quarkus-hibernate-validator'

java17にしているのでもちろんRecordを使います。

package org.my.hobby;

import javax.inject.Inject;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.constraints.NotBlank;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.Set;
import java.util.stream.Collectors;

record BookSearchRequest(
        @NotBlank(message = "Title may not be blank") String title) {
}

record BookSearchResponse(
        boolean success,
        String message,
        Book book) {
    record Book(String title, String author) {
    }
}

@Path("/book/search")
public class BookController {

    @Inject
    Validator validator;

    /**
     * 書籍をタイトルで検索
     *
     * @param bookSearchRequest
     * @return
     */
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    public BookSearchResponse search(BookSearchRequest bookSearchRequest) {
        Set<ConstraintViolation<BookSearchRequest>> violations = validator.validate(bookSearchRequest);
        if (!violations.isEmpty()) {
            final String errorMessage = violations
                    .stream()
                    .map(e -> e.getMessage())
                    .collect(Collectors.joining(","));
            final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book("", "");
            return new BookSearchResponse(false, errorMessage, hobbyBook);
        }

        if (!"hobby".equals(bookSearchRequest.title())) {
            final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book("", "");
            return new BookSearchResponse(false, "not found book", hobbyBook);
        }

        final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book("hobby", "s-nakao");
        return new BookSearchResponse(true, "", hobbyBook);
    }
}
  • postmanでアクセス
shogo.nakao@localhost: $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
        "title": "hobby"
}'
{"success":true,"message":"","book":{"title":"hobby","author":"s-nakao"}}%
shogo.nakao@localhost: $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
        "title": "s-nakao"
}'
{"success":false,"message":"not found book","book":{"title":"","author":""}}%
shogo.nakao@localhost: $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
}'
{"success":false,"message":"Title may not be blank","book":{"title":"","author":""}}%
shogo.nakao@localhost: $

ちゃんとバリデーションできていますね!嬉しい嬉しい!

springbootと違ってbindingResultは使えないですが、、、(もしかしたら使える方法があるかも?今後調べます)

次回、さらに必要そうなツールを入れるに移動します。

最後に、弊社では採用もバシバシ実施しているので興味のあるかたがいましたらご応募ください。

www.wantedly.com



Advent Calendar 2021を引き続き楽しんでいただけると嬉しいです。

qiita.com

JavaとLombokの@Withでイミュータブルなデータ型を生成する

エキサイト株式会社エンジニアの佐々木です。2021年アドベントカレンダー5日目を担当します。 サービスを開発している上で、データをなるべくイミュータブルにするというのは、バグを作り込まない上で重要だと思います。Java15でRecord型が導入されていますが、まだ未対応なライブラリがあると思います。今回はLombokでは@Withを使って簡単に実装できます。

ミュータブルなデータ型

ミュータブルなデータ型は、Lombokでは@Dataを使って簡単にできます。

@Data
class Form {
    private final Long id;
    private final String name;
}

使用時:
Form form = new Form();
form.setId(1L);
form.setName("sample");

form.setId(2L);   // form.id が 2にかわる
form.setName("sample2"); // form.name が 2に変わってしまう

コード見れば明らかですが、途中でオブジェクトの状態が変わっています。メソッドの引数で渡して、再代入等を特にしていない場合でも変わってしまうので、バグの1つの原因になります。

イミュータブルなデータ型(@Value)

@Value
class Form {
    private final Long id;
    private final String name;
}

使用時:
Form form = new Form(1L, "sample");

form.setId(2L);  // メソッドが存在しないので、エラーになる
form.setName("sample2"); // メソッドが存在しないので、エラーになる

Form form2 = new Form(2L, "sample2"); // 代入 or 再代入をする

コンストラクタを使用したときだけ値を設定できるようにすると、1度定義したオブジェクトの状態は途中で変えることができません。途中で変えるには、再度コンストラクタ呼び出しを行い、再代入等が必要です。しかし、引数が少ないときはいいですが、多くなってくると辛くなってきます。そこで@Withが登場します。

イミュータブルなデータ型(@With)

@With
@Value
class Form {
    private final Long id;
    private final String name;
}

使用時
Form form = new Form(1L, "sample");
form.withId(2L);
form.withName("sample");

log.info("{}", form);  // Form(id=1, name=sample);

Form form2 = form.withId(2L).withName("sample2");
log.info("{}", form);  // Form(id=2, name=sample2);   

withId(), withName() を使用して値変更しても、formオブジェクトの値は変更されていません。setterとは違い、新規オブジェクトが返ってきます。では、@Withがどういう処理をしているのかをdelombokをして見ていきます。

public Form withId(Long id) {
    return this.id == id ? this : new Form(id, this.name);
}

public Form withName(String name) {
    return this.name == name ? this : new Form(this.id, name);
}

このようなコードが裏側で実行されています。値が同じ時は自分の参照を返し、値が変更されている場合は、新規のオブジェクトを生成して返しています。これでイミュータブルなオブジェクトになって安全です。

まとめ

Java15でRecord型が導入され、イミュータブルなオブジェクト専用の型が追加されています。状態が変更される副作用を抑えるべく導入されているかと思います。周辺ライブラリが対応するまではLombokを駆使して、ミュータブル、イミュータブルなオブジェクト生成の使い分けをする感じになるかと思います。

最後に

引き続きエキサイトホールディングスのアドベントカレンダーをお楽しみいただければ幸いです。 Calendar for エキサイトホールディングス Advent Calendar 2021 | Advent Calendar 2021 - Qiita

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

カジュアル面談はmeetyを公開していますので、よろしくお願いいたします! meety.net

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

quarkusを使う(環境構築編)

初めまして、Red Hat大好きエキサイト株式会社の中尾です。

趣味でquarkusを使っていこうと思うので、その記録を残そうと思います。

quarkus cliは使いません。

まずは環境作りです。

quarkusでbuildツールgradleを使おうと思います。 mvnよりもgradleのほうが柔軟なので、、、(宗教戦争ありそうですが。)

  • mvnインストール

インストールするんかい!!って疑問あるかもしれないですが、quarkus自体を入れる最初の儀式です。

brewで入れてください。

brew install maven
  • 初期構築

mvn使って構築します。 buildToolにgradleを指定すると、gradle使えます。

mvn io.quarkus.platform:quarkus-maven-plugin:2.5.1.Final:create \
    -DprojectGroupId=quarkus \
    -DprojectArtifactId=hobby \
    -DprojectVersion=v1.0.0 \
    -DclassName="org.my.hobby.Resource" \
    -Dextensions="resteasy,resteasy-jackson" \
    -DbuildTool=gradle
  • 起動
./gradlew --console=plain quarkusDev

開きました!嬉しい嬉しい!

http://localhost:8080/

http://localhost:8080/hello

次回、必要そうなツールを入れるに移動します。

最後に、弊社では採用もバシバシ実施しているので興味のあるかたがいましたらご応募ください。
www.wantedly.com

Advent Calendar 2021を引き続き楽しんでいただけると嬉しいです。

qiita.com

リモートワークを快適に行うためにデスクまわりを整備した

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 普段は既存サービスのリビルド(PHP / BEAR.Sunday → Java / SpringBoot)を担当しています。

エキサイトホールディングス Advent Calendar 2021の3日目の記事です! qiita.com

現在エキサイトでは、月8回は出社する必要があり、その他の曜日は自宅から作業をしています。 本記事では、自分がリモートワークを快適に行うために購入してよかったものを紹介します。

LG 4Kモニター

f:id:excite-kazuki:20211202190018j:plain

このLGの4Kモニター(32UN500-W)を購入する前は、DellのWQHDモニター(P2720DC)を使っていました。 Dellのモニターでも十分に満足していましたが、不慮の事故によりディスプレイを壊してしまい、新しく購入することになりました。 下記条件を満たすモニターがないか調べていたところ、LGの4Kモニター(32UN500-W)がこれらを満たしていました。

  • 4Kの解像度
  • 枠が細い
  • スピーカー内臓
  • モニターアームが取り付けられる(VESA規格対応)
  • 5万円以内

現状では大画面で作業することができてとても満足していますが、チームメンバーから「EIZOのモニターはいいぞ!」とおすすめされることも多く、今後買い換えるかもしれないです🤣

Anker PowerExpand 9-in-1

f:id:excite-kazuki:20211202191001j:plain

このドッキングステーションは「MacBookとモニターをケーブル1本で繋いで配線を整理したいな」と考えて、購入しました。 現在使用しているMacBookにはType-Cのポートしかないため、給電、ディスプレイ出力、Webカメラ接続などをハブで行うと配線がゴチャゴチャしてしまい、スッキリしていませんでした。PowerExpand 9-in-1を購入してからは、配線がスッキリしたことや、私用のMacBookと社用のMacBookを簡単に切り替えることができることなど、とても満足しています。

エルゴトロン LXデスクマウントアーム

f:id:excite-kazuki:20211202185627j:plain

このモニターアームは「モニターアームがカッコイイ!欲しい!」という単純な理由で購入しました💪 実際に使ってみると、モニターを最大まで低く配置することができたり、モニターの下にキーボードを置くことができたりしてよかったです。あとは見た目がカッコイイのが最高!

AfterShokz OpenMove

f:id:excite-kazuki:20211202185543j:plain

この骨伝導イヤホンは、AirPods Proを使ってミーティングをしていたときに、耳の中を傷つけてしまったため、耳をふさがないイヤホンが欲しいと考えて、購入しました。 「骨伝導イヤホンってしっかり音が聞こえるのかな...?」と懐疑的でしたが、ミーティングは問題なく行えています。 OpenMoveは外部の音をあまり拾わないことや、Type-Cで充電できることなど魅力がたくさんあります。 たまに、大きな音が流れてくると、イヤホンが震えるため、ゾワッ としてしまうのがつらいところ...😵 好きな音楽を流しながら集中して作業したい場合は、ヘッドホンに切り替えるとよさそうです。

オカムラ コンテッサセコンダ

f:id:excite-kazuki:20211202193621j:plain

このオフィスチェアを購入するまでは、IKEAの3000円ほどで購入した椅子を使っていました。 ほぼ毎日がリモートワークということもあり、「腰は大事にしたほうがよい」「よい椅子を買うと幸せになれる」といった記事を多く見たため、「そこまで言うなら買うぞ!」と思いオフィスチェアを購入しました。

長時間作業しても腰が痛くなることはなく、座り心地も最高なため、ずっと座って作業できます! ただ、座りっぱなしは身体によくなたいめ、定期的に立ったり身体を動かすように気をつけていきたいです。

IKEA デスク

f:id:excite-kazuki:20211202184723j:plain

このデスクは天板と2本の脚と引き出しユニットを組み合わせて構築しています。 当初、昇降式デスクも視野に入れていましたが、「結局使うことないかな〜」と思い、それなら引き出しを置こうと思ってこの構成にしています。 横幅が186cmもあるため、一般的な横幅120cmほどのデスク比べてもかなり広いため、余裕をもって色々なものを置くことができたり、引き出しに収納できたりして満足しています。

おわりに

リモートワークを快適に行うために、色々なものを購入してきました。 購入金額の合計を出すのが怖いですね...💸 自宅では職場と違って色々とカスタマイズできるのが魅力的かなと思います。

ここまで読んでいただきありがとうございました!

AWS SDK for PHP を利用してSESで添付ファイル付きのメールを送信する

こんにちは 👋

エキサイトホールディングス Advent Calendar 2021の3日目は、エキサイト株式会社の伊藤(🐦 @motokiito2)が担当させていただきます!

今回は、AWS SDK for PHP を利用して、SESで添付ファイル付きのメールを送信する方法について書かせていただきたいと思います!

なお、SDKの導入や SesClient() の利用方法については本記事では割愛させていただきます。

添付ファイルつきメールを送信する方法

AWS公式のチュートリアルで利用されている sendEmail() は宛先、送信元、本文、件名、文字コード等を渡すだけでメールを送信することができます。

しかし、 sendEmail() には添付ファイルを指定するパラメータが存在しないため、メールのheaderとbodyを自分で成形して送信する sendRawEmail() を利用する必要があります。

docs.aws.amazon.com

sendRawEmail() の利用

sendRawEmail()sendEmail() とは異なり、メールヘッダおよびコンテンツを自前で整形して渡す必要があります。

この例では、送信元 $from , 宛先 $to を渡していますが、必須パラメータは RawMessageのみなので、RawMessageのヘッダ内に含めても問題ないです。

<?php

$this->sesClient->sendRawEmail([
    'Destinations' => [
        $values['to'],
    ],
    'Source'     => $values['from'],
    'RawMessage' => [
        'Data' => $rawMessage,
    ],
]);

sendRawEmail() の詳細は下記ドキュメントを参照してください。

https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-email-2010-12-01.html#sendrawemail

RawMessage

RawMessageを用意する方法はいろいろありますが、この例ではシンプルに文字列で用意します。

SESの設定セットを利用している場合は、ヘッダー部に X-SES-CONFIGURATION-SET: を含める必要があります。

また、sendEmail() がよしなにやってくれている部分を自前で用意する必要があるので、注意してください。

  • SubjectのMIMEエンコードを忘れない

  • 添付ファイルを入れる場合にはメッセージの Content-type を multipart/mixed にしておく必要がある

など..

<?php

private function generateRawMessage(
    string $from,
    string $to,
    string $subject,
    string $body,
    string $fileName,
    string $fileStringBase64Encoded
) : string {
    $boundaryString = '__BOUNDARY__';

    return 'To: ' . $to . "\n"
         . 'From: ' . $from . "\n"
         . 'X-SES-CONFIGURATION-SET: ' . $this->config['aws']['ses']['configuration_set'] . "\n"
         . 'Subject: ' . mb_encode_mimeheader($subject, 'UTF-8') . "\n"
         . "MIME-Version: 1.0\n"
         . 'Content-Type: multipart/mixed; boundary="' . $boundaryString . '"' . "\n\n"
         . '--' . $boundaryString . "\n"
         . "Content-Type: text/html; charset=\"UTF-8\"\n\n"
         . nl2br($body) . "\n\n"
         . '--' . $boundaryString . "\n"
         . 'Content-Type: application/octet-stream; name="' . $fileName . '"' . "\n"
         . "Content-Transfer-Encoding: base64\n"
         . 'Content-Disposition: attachment; filename="' . $fileName . '"' . "\n\n"
         . $fileStringBase64Encoded
         . '--' . $boundaryString . "\n";
}

添付ファイル

添付ファイルは、ファイルをbase64エンコードした文字列を用意します。

<?php

private function generateFileStringBase64Encoded(
    string $fileAbsolutePath
) : string {
    if (!file_exists($fileAbsolutePath)) {
        return false;
    }

    return chunk_split(base64_encode(file_get_contents($fileAbsolutePath)));
}

メール送信メソッドサンプル

これまでの実装を利用した送信メソッドのサンプルコードをのせます。 (SesClientとLoggerはクラス内でコンストラクタインジェクションしています)

<?php

private function sendEmailWithAttachment(
    string $from,
    string $to,
    string $subject,
    string $messageBody,
    string $fileAbsolutePath,
    string $fileName
) : void {
    $fileStringBase64Encoded = $this->generateFileStringBase64Encoded($fileAbsolutePath);

    if (!$fileStringBase64Encoded) {
        throw MailSendFailedException::factoryMailSendFailedException('attachment file is invalid');
    }

    $rawMessage = $this->generateRawMessage(
        $from,
        $to,
        $subject,
        $messageBody,
        $fileName,
        $fileStringBase64Encoded
    );

    try {
        $result = $this->sesClient->sendRawEmail([
            'Destinations' => [
                $to,
            ],
            'Source'     => $from,
            'RawMessage' => [
                'Data' => $rawMessage,
            ],
        ]);
        $messageId = $result['MessageId'];
        $this->logger->info("Completed to sent, messageId: $messageId");
    } catch (AwsException $e) {
        $this->logger->error('[MailSendError] ErrorMessage: ' . $e->getMessage());

        throw MailSendFailedException::factoryMailSendFailedException('mail send error');
    }
}

最後に

AWS SDK for PHPの SesClientは、シンプルなメールを送信する場合は実装が楽なのですが、メールヘッダを弄る必要があったり、今回のようにファイルを添付したい場合などはRawMessageを扱う必要があり、実装が少し複雑になります。

同じようなケースで sendRawEmail() を利用しなければいけない場合に、この記事が参考になってくれればとても嬉しいです!


弊社アドベントカレンダーの他の記事も是非!

qiita.com

WebViewの高さをローディング時に動的に変える方法

はじめに

2021年度アドベントカレンダーの2日目の担当の高野です。メディア事業部のアプリではWebViewを使うことが多く、その備忘録として残します。

動作バージョン

Flutter 2.5.1
Dart 2.14.0
webview_flutter 2.1.1

実装

今回はwebview_flutterを使用していますが、他のWebViewライブラリでもおそらく同じだと思います。
まず初めに動的にWebViewの高さを変更するためにはSizedBoxまたはContainerでWebViewを包みます。

Container(
  height: controller.webViewHeight.value,
  child: WebView(
    navigationDelegate: (request) => controller.onRequestNavigation(request: request),
    javascriptMode: JavascriptMode.unrestricted,
    onPageFinished: (_) => controller.onPageFinished(),
    onWebViewCreated: (webViewController) => controller.onWebViewCreated(controller: webViewController),
  ),
);

ここのheightにWebViewの高さを取得した後に入れることで動的に高さを変えることができます。
このWebViewのonPageFinishedの発火時に以下コードを発火させることによって取得できます。Javascriptを使用するので JavascriptMode.unrestrictedjavascriptMode に指定しておいてください。

Future<double> _getWebViewHeight() async {
  try {
    final rawWebviewHeight = await controller!.evaluateJavascript('document.body.offsetHeight;');
    final webviewHeight = double.parse(rawWebviewHeight);
    return webviewHeight;
  } catch (e) {
    Logger.error(e);
    return 1000;
  }
}

これであとはこの関数の返り値をContainerやSizedBoxのheightに指定してあげれば取得時にbuild()が走るので更新が行われます。

まとめ

どこが一番重要かと言うと

await controller!.evaluateJavascript('document.body.offsetHeight;');

ここですね、これによってロードしたページ(body)の高さを取得することができます。

最後に、弊社では採用もバシバシ実施しているので興味のあるかたがいましたらご応募ください。
https://www.wantedly.com/companies/excite

Advent Calendar 2021を引き続き楽しんでいただけると嬉しいです。

SpringBoot x Gradleマルチプロジェクトで環境構築をする

エキサイト株式会社佐々木です。アドベントカレンダー2021の2ページ目の1日目になります。メディア事業部では、SpringBoot x Gradleマルチプロジェクトを使用して、モジュラーモノリスのような構成で各メディアのリビルドの開発を進めています。既存のリポジトリは1メディアで30〜50リポジトリが存在し、リポジトリの行き来で見通しが悪くなっているので、リビルドではモノリポ x マルチプロジェクト形式にしています。

Gradleマルチプロジェクトとは

Gradleマルチプロジェクトは、1つのメインプロジェクト内にいくつものサブプロジェクトが存在する構成になります。

設定

設定には、build.gradleとsettings.gradleを使用します。

settings.gradle

マルチプロジェクトで運用するには、settings.gradleにプロジェクト名を追記します。

仮に、web,batch,api,usecase,service,repositoryのように各層ごとに分けるようにすると下記のようになります。

rootProject.name = 'demo'

include "web"
include "batch"
include "api"
include "service"
include "repository"
include "domain"

build.gradle

settings.gradleで分割したモジュールをbuild.gradleに反映していきます。

plugins {
    id 'org.springframework.boot' version '2.6.1'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

subprojects {   // ★1

    apply plugins: "java"    // ★2
    apply plugin: "org.springframework.boot"
    apply plugin: "io.spring.dependency-management"

    group = 'com.example'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '11'

    repositories {
        mavenCentral()
    }

    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }

    dependencies {
        compileOnly 'org.projectlombok:lombok'
        developmentOnly 'org.springframework.boot:spring-boot-devtools'
        annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
        annotationProcessor 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }

    test {
        useJUnitPlatform()
    }

}


project(":web") {    // ★3

    bootJar {    // ★4
        enabled = true
    }

    dependencies {    // ★5
        implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.boot:spring-boot-starter-webflux'
        implementation project(":service")   // ★6
        implementation project(":domain")   // ★6
        testImplementation 'io.projectreactor:reactor-test'
    }
}

project(":api") {

    bootJar {
        enabled = true
    }

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.boot:spring-boot-starter-webflux'
        implementation project(":service")
        testImplementation 'io.projectreactor:reactor-test'
    }
}

project(":service") {

    bootJar {
        enabled = false
    }

    dependencies {
        implementation project(":repository")
    }
}

project(":repository") {

    bootJar {
        enabled = false
    }

    dependencies {
        implementation project(":domain")
    }
}

project(":domain") {
    bootJar {
        enabled = false
    }
}

上記のような設定になります。★ごとに解説します。

★1

Gradleマルチプロジェクト構成にすると、プロジェクト直下以外は、すべてサプブロジェクトとなります。この時に、サブプロジェクト共通で読み込みたいようなライブラリや設定があるときに、 subprojects {} の中に書いておくと全てのサブプロジェクトに適用できます。ほぼすべてのプロジェクトで必要そうなものを記述しておきます。(LombokやSpringBootのAnnotationまわりの設定等)

★2

Gradleのプラグインの設定はサブプロジェクトごとにプラグインを使うか使わないかを決められます。逆に設定しないと使えないです。ここでよくハマっているのをみます。

★3

各サブプロジェクトごとの設定は、 project(":プロジェクト名") {} の内側に記述します。ここには、プロジェクト固有のものを設定します。例では、project(":web"){...} では、テンプレートエンジンのThymeleafとSpringBootのWeb系のライブラリを読み込んでいます。

★4

SpringBootはコンパイルすると実行可能なjarの生成が可能です。ですが、serviceプロジェクトやrepositoryプロジェクトでは、実行形式にする必要がありません(エンドポイントがないので)。そういうときには、bootJar{} を使用して、実行可能なjarを生成する・しないの設定を追加できます。webapiプロジェクトは、エンドポイントがそれぞれあるので、bootJar{enabled=true}を指定して、実行可能なjarを生成するようにします。

★5

サブプロジェクトごとに依存関係を定義できます。ここで定義したものは、そのサブプロジェクト内でしか使えないことに注意してください。

★6

ここで、サブプロジェクト内で別のサブプロジェクトを依存関係に含めています。これを行うことで、サブプロジェクト内のコードを使えるようになります。例ではwebプロジェクトは、serviceサブプロジェクトとdomainサブプロジェクトのコードにアクセスができますが、repositoryサブプロジェクトのコードにはアクセスできないことになります。

Gradleマルチプロジェクトのメリット

Gradleマルチプロジェクトのメリットは、アクセス制御が可能になことです。依存関係をサブプロジェクト単位で定義できることによって、モジュラーモノリスやミニサービスの構成を簡単に実現できます。依存関係に定義していないサブプロジェクトやライブラリを使おうとしてもコンパイルエラーになりますので、管理が楽になります。

Gradleマルチプロジェクトのような機能がない状態で、厳密に依存関係を制御しようとすると、コードレビューをするまたはチェック用のコードを書く等のことをしなければならないですが、Gradleの依存関係の設定のみでそれが可能になります。JavaにもArchUnitというライブラリはありますが、テストコードを記述する必要があります。

最後に

アドベントカレンダー1日目を書かせていただきました。他社同様エキサイトHDでもアドベントカレンダーをやっていますので、ご覧いただけると幸いです。(https://qiita.com/advent-calendar/2021/excite-hd)

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

カジュアル面談はこちらになります! meety.net

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

Spring Bootで、AWSパラメータストアから簡単にDBのパスワードを取得する方法

こんにちは。 エキサイト株式会社の三浦です。

いよいよ今年も12月になりました。 「師走」の文字通り、忙しくしている方も多いのではないでしょうか。

この「12月」ですが、我々エンジニアにとっては別の意味も持っています。 すなわち、アドベントカレンダーの時期です!

というわけで、エキサイトホールディングスの今年のアドベントカレンダーの第一回目のブログを作成させていただくことになりました!

第一回目となる今回は、Spring Bootにおける、DBのパスワード管理問題のAWSパラメータストアを使用した簡単な解決方法について書かせていただきます。

DBのパスワード管理問題

アプリケーションでは、特に一定以上の規模になってくれば、多くの場合DBからデータを取得したり書き込んだりする必要が出てくるでしょう。 この時問題になってくるのが、「DBのパスワードをどうやって管理するか」です。

方法自体は以下のようにいくつか考えられますが、DBのデータの改竄はアプリケーションにとって致命的な問題となりうるので、パスワードが漏れてしまうことは絶対に避ける必要があります。

Github等で、その他のコードと一緒にバージョン管理する

Github等でバージョン管理してしまえば管理自体は簡単ですが、何かしらの事情でパスワードが漏れてしまうリスクが増加してしまいます。 可能であれば避けるべきでしょう。

デプロイサーバ等で管理し、デプロイのたびに書き込む形式にする

例えばデプロイサーバの環境変数やどこかのファイルに保存しておき、デプロイ時にアプリケーションコードにそのパスワードを書き込む方法です。 もちろんこれでも可能ですが、何かしらの理由でデプロイサーバのデータが失われたときに、パスワードのデータが消えてしまう可能性があります。 また、デプロイサーバやデプロイ方法が変更となった場合に、移行しなければならないでしょう。

AWS RDSの場合)IAMを使用した認証方法にする

AWS RDSでは、パスワードではなくIAMを使って接続をすることができます。 これができればそもそもパスワード自体が不要ではありますが、RDSでしか使用できないことや、IAMを使用した認証方法だと接続数によっては追加オーバーヘッドが発生してしまう場合があるという問題があります。

AWSパラメータストアを使用する

AWSには、パラメータストアというサービスがあります。 これは、AWS上で様々なデータを保存しておくことが出来るものですが、

  1. パラメータストアにパスワードを置いておく
  2. それをデプロイ時に取得し、アプリケーションコードに書き込む

とすることで、安全にパスワードを管理することができます。

問題点として、パラメータストアからデータを取得するのが面倒なこと、取得したデータをアプリケーションコードに書き込むのが面倒なことが上げられますが、実はSpring Bootであれば、それらを劇的に簡単にする方法があります。

そこで今回は、最後の「AWSパラメータストアを使用」してDBのパスワードを管理する方法について、Spring Bootで簡単に行うやり方について説明します。

Spring Cloudを使ったパラメータストアとの統合

Spring Bootには、Spring Cloudというプロジェクトが存在します。 ここではAWSとの統合を容易にする様々なライブラリが提供されているのですが、その中にパラメータストアとの統合を容易にするものも存在しているのです。

以下の設定だけで、パラメータストアからパスワードを取得出来るようになります。

アプリケーションコードの設定

アプリケーションコードの設定を行います。 必要なのは、 build.gradleapplication.yml のみです。

build.gradle

バージョンは適宜変更してください。

implementation 'io.awspring.cloud:spring-cloud-starter-aws-parameter-store-config:2.3.2'

application.yml

以下以外の設定は通常通りで大丈夫です。

spring:
  application:
    name: sample.application # パラメータストアのキーに使用します
  config:
    import: 'aws-parameterstore:'
  datasource:
    password: xxxx # ここにパスワードが入ります。上書きされるので、何を書いていても大丈夫です。

aws:
  paramstore:
    region: ap-northeast-1 # 使用したいパラメータストアのregionを設定してください
    prefix: /config # パラメータストアのキーのprefixに使用します

コードとしては、なんと実質これだけで「パラメータストアからのパスワードの取得」と「取得したパスワードのアプリケーションコードへの書き込み」ができたことになります!

AWSと接続するための認証

コードとしては上記で完成ですが、パラメータストアからデータを取得するためには、AWSと接続するための認証の設定が必要となります。 方法例として、以下のものが挙げられます。

環境変数で設定する

AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY という名前の環境変数を作成し、それぞれにパラメータストアにアクセスできる権限を持つIAMユーザのアクセストークン・シークレットトークンを入力します。

クレデンシャルファイルを使用する

AWS CLIaws configure コマンド等を使って、パラメータストアにアクセスできる権限を持つIAMユーザのクレデンシャルファイルを作成します。

ECSやEC2インスタンスプロファイルのクレデンシャルを使用する

実行環境のECSやEC2インスタンスプロファイルに、パラメータストアからデータを取得できるポリシーを追加します。

詳しくはこちらを御覧ください。

とりあえずローカルで確認するだけなら、環境変数かクレデンシャルファイルを使用すると良いでしょう。

パスワードの保存

ここまででパラメータストアからデータを取得する準備は整いました! 最後に、パラメータストアにデータを保存します。

キー名は、

{$aws.paramstore.prefix}/{$spring.application.name}_{$プロファイル}/spring.datasource.password

となります。 もし実行時に local というプロファイルを使用するのであれば、今回の設定の場合は

/config/sample.application_local/spring.datasource.password

というキーの値にDBのパスワードを入れれば大丈夫です。 念の為、「安全な文字列」として保存しておくと良いでしょう。

最後に

DBのパスワードは、バージョン管理する管理方法が他の方法と比べてあまりに簡単すぎるので、まだそうしてしまっているアプリケーションも多いのではないでしょうか。 保存しているGithubリポジトリをprivateにしておけばとりあえずは安全にも見えますが、例えば何かしらのヒューマンエラーでpublicにしてしまって、その間に見られてしまう可能性も考えられます。 そして、それによってDBのデータが改竄されてしまった場合、損害は計り知れないものとなってしまう場合もありうるでしょう。

幸いSpring Bootであれば、上記のように比較的簡単にセキュアな方法で管理ができるので、ぜひやってみてはいかがでしょうか。


さて、私の今回のブログは以上ですが、アドベントカレンダーはまだまだ続きます! ぜひ明日以降も御覧ください!

qiita.com

PHPを使って形態素解析と文章の類似度を出してみる

ご無沙汰しております。
taanatsuです。

今回は珍しくPHPの記事を書いていこうと思います。
ExciteといえばPHPですからね!しらんけど。

形態素解析

皆さんは「形態素解析」という言葉を耳にしたことがありますでしょうか?
機械学習だ!AIだ!と騒がれる昨今、文章の解析で使われる手法の一つがこの形態素解析です。
私は漢字が4つ以上並ぶと読めなくなるので、形態素解析という言葉が苦手ではあります。

形態素解析とは、文章を「形態素」、いわゆる名詞・動詞・形容詞・副詞のような、日本語の最小単位の単語に分割する処理のことを言います。

形態素解析器「MeCab

形態素解析を行ってくれるツールです。
今回はよく使われる「MeCab」を利用していきたいと思います。

で、Windowsの方はすいません。。。
会社のPCがMacなので、この記事はMac用になります。
私個人はWindows機を利用していて、できることは知っていますので、Windowsの方は頑張ってください!
(確かバイナリをダウンロードして、ダブルクリックしてインストールするだけだったはず…!)

MeCabのインストール

Homebrewを使えば一発です。

$ brew install mecab

以上!
……と言いたいところですが、文章を単語に分割するための「辞書」が必要になります。
辞書も入れましょう。

$ brew install mecab-ipadic

以上です。
ターミナル上でmecabとタイプしてEnterを押してみてください。
入力待ちになります。

この状態で「すもももももももものうち」と入力してEnterを押してみましょう。

$ mecab            
すもももももももものうち
すもも   名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
の 助詞,連体化,*,*,*,*,の,ノ,ノ
うち  名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ

きれいに分割されていますね!
これが形態素解析です。

PHPからMeCabを使う

さて、こういう分野ではPythonブイブイ言わせていますが、弊社はPHPが盛んな会社ですので、PHPで使っていきましょう。 PHP7.1くらいまでは php-mecabというPHPMeCabを使えるようにするバインディングツールがあったのですが、どうやらPHP8には対応していなさそう…
なので、ちょっと強引ですが、PHPexecを使っていきます。
参考

<?php
$result = [];

$text = 'すもももももももものうち';
exec('echo ' . $text . ' | mecab', $result);

var_dump($result);

array(8) {
  [0] =>
  string(61) "すもも    名詞,一般,*,*,*,*,すもも,スモモ,スモモ"
  [1] =>
  string(40) "も        助詞,係助詞,*,*,*,*,も,モ,モ"
  [2] =>
  string(49) "もも      名詞,一般,*,*,*,*,もも,モモ,モモ"
  [3] =>
  string(40) "も        助詞,係助詞,*,*,*,*,も,モ,モ"
  [4] =>
  string(49) "もも      名詞,一般,*,*,*,*,もも,モモ,モモ"
  [5] =>
  string(40) "の        助詞,連体化,*,*,*,*,の,ノ,ノ"
  [6] =>
  string(63) "うち      名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ"
  [7] =>
  string(3) "EOS"
}

形態素解析を使った文章の類似度

それでは形態素解析を使って文章の類似度を出してみましょう。
2つの文章に登場した形態素に対して、文章がその形態素を持っていれば1、持っていなければ0として計算してみましょう。

<?php
$text1 = '新宿は豪雨';
$text2 = '渋谷は豪雨';

echo cosineSimilarity($text1, $text2);

/**
 * 分かち書きのリストを作成します
 * 
 * @param string $text 分かち書きを作成したい文章
 */
function getWakachiList(string $text): array {
    $result = '';
    exec('echo ' . $text . ' | mecab -Owakati', $result);

    if (!is_array($result) || count($result) !== 1) {
        return [];
    }

    return explode(' ', $result[0]);
}


/**
 * 文章の類似度をコサイン類似度を用いて求めます
 * 
 * @param string $text1 文章1つ目
 * @param string $text2 文章2つ目
 */
function cosineSimilarity(string $text1, string $text2): float {
    // 文章を形態素に分解
    $text1Corpus = getWakachiList($text1);
    $text2Corpus = getWakachiList($text2);

    // 2つの文章の形態素を抽出
    $allCorpus = array_unique(array_merge($text1Corpus, $text2Corpus));

    // コサイン類似度の計算に必要な分子分母の変数
    $c = 0;
    $m1 = 0;
    $m2 = 0;

    foreach ($allCorpus as $word) {
        // 文章1に対象の形態素があるかどうか(あれば1、なければ0)
        $n1 = (array_search($word, $text1Corpus) !== false) ? 1 : 0;
        // 文章2に対象の形態素があるかどうか(あれば1、なければ0)
        $n2 = (array_search($word, $text2Corpus) !== false) ? 1 : 0;

        // コサイン類似度に利用する分子分母の数値を計算
        $c += ($n1 * $n2);
        $m1 +=  $n1 * $n1;
        $m2 += $n2 * $n2;
    }

    // コサイン類似度の計算
    if ($m1 === 0 || $m2 === 0) {
        return 0;
    }

    return $c / (sqrt($m1) * sqrt($m2));
}

新宿は豪雨
渋谷は豪雨

以上のテキストから形態素を抽出します。
また各文章にその形態素があるかないかも確認します。

新宿 豪雨 渋谷
テキスト1 1(形態素を持っている) 1 1 0(形態素を持っていない)
テキスト2 0 1 1 1

上記の1と0を使って、コサイン類似度を算出します。

このように形態素解析を使うと、単語単位で分解できるため、精度良く文章の類似度を出すことができます。
それでは今回はこのあたりで!

業種交流LT会【広告チーム編】を開催しました📃

iXITの小長谷です。

11月19日に、2回目の開催となる業種交流LT会を行いました🎉

今回は広告チーム編というテーマで、普段広告に関わる業務を行っている方々に登壇していただきました!

今年8月には業種交流LT会【クリエイティブ編】も開催しています。そちらも以下からぜひご覧ください。 

tech.excite.co.jp

発表内容

メディア事業部から2名、ヘルスケア事業部から2名の、計4名に普段の業務内容などを発表していただきました。

メディア事業部の広告について

メディア事業部の2名からは、それぞれ編集担当、営業担当として行っていること、その中で工夫している点・苦労している点などを発表していただきました。

営業では、案件獲得のために代理店・広告主となる企業へのアプローチを行っていること、編集部では、営業の方と一緒に案件獲得から始まり、記事制作、公開、継続提案までを行っていることをお話しいただきました。

苦労している点として

  • コロナ禍でオフラインによる新規担当者との出会いが減ってしまったこと

  • 確定案件を制作しながら、自主提案企画も併行して進めていること

  • 並行して担当している案件が多く、スケジュール管理が大変であること

などがあり、工夫している点として

  • クライアントと、社内の編集部の方との齟齬が無いように連携を大切にすること

  • 自身の経験や周囲の体験を企画に落とし込むこと

  • 営業と編集で常に情報共有、意見交換を活発に行うこと

などがあるとのことでした。

ヘルスケア事業部の広告について

ヘルスケア事業部の2名からは、広告計測のこと、担当サービスの新規課金を促すための広告運用について発表していただきました。

広告の計測については、間接コンバージョンと直接コンバージョンの説明や、異なる広告媒体でも平等に計測することができる「アドコード」の設定などについてお話しいただきました。

広告運用では、リスティング広告についての説明から、広告の表示順位に関わる「広告ランク」に関してのことと、広告ランクを上げるために取り組んでいる「広告とランディングページの品質」などについてお話しいただきました。

まとめ

他業種のことを知り、交流するきっかけになることを目的とした業種交流LT会の第2回目として、今回は「広告」というテーマで開催しました。

普段なかなか知る機会がない業務内容や、そこで働く方々の知見を得ることができ、とても良い機会になったと感じています!

今後のLT会レポートもお楽しみに!

ECSで複数のターゲットグループを紐付けるときの注意点

こんにちは。 エキサイト株式会社の三浦です。

AWSをインフラとしている場合、アプリケーションを動かす環境としてECSは選択肢の大きな一つです。 それがWebアプリケーションであれば、ECSとALBのターゲットグループを紐付けることが多いでしょう。

実はECSは、2つ以上のターゲットグループと紐付けることができます。 ですが、1つのみ紐付ける場合と比べると注意点があります。

今回は、その注意点について説明していきます。

なおこちら、2021/11/22現在の情報であり、将来的には変わっている可能性があるのでご注意ください。

ECSとターゲットグループ

ECSは、AWSが提供しているコンテナ稼働のマネージドサービスです。 公式ページには、以下のように説明されています。

Amazon ECS は、フルマネージドコンテナオーケストレーションサービスであり、コンテナ化されたアプリケーションを簡単にデプロイ、管理、およびスケーリングできます。

マネージドであり管理が非常に容易なため、AWSをインフラとしていてアプリケーションをコンテナでデプロイしたい場合は、ECSを使うことが選択肢の大きな一つとなります。 また、提供したいアプリケーションがWebサービスである場合、ECSとALBのターゲットグループを紐付けることによりALB経由でアクセスを流すことができます。

多くの場合、ECSとターゲットグループは 1 : 1 で紐付けると思います。 ただ、例えばinternal(VPC内のみ)とexternal(internet経由)の両方のALBを使用する必要があるなど、2つ以上のターゲットグループをECSに紐付けたいこともありえます。

実はそうした場合、1つのECSに対して2つ以上のターゲットグループを紐付けることができます。

f:id:excite-takayuki-miura:20211122113324p:plain
ターゲットグループが1つの場合

f:id:excite-takayuki-miura:20211122114121p:plain
ターゲットグループが2つの場合

非常に便利な機能なのですが、1つ注意点があります。

2つ以上のターゲットグループを紐付ける注意点

ECSには「オートスケーリング」という機能があります。 これは文字通り、コンテナへのアクセス量や負荷に応じて自動的にコンテナ数を増減してくれる機能であり、例えば時間や情勢によってアクセス量が変わりうるWebサービスなどで非常に有用な機能です。

このオートスケーリングでは、基本的に3つの指標をもとにコンテナの増減数を決定します。

ECSServiceAverageCPUUtilization

平均のCPU使用率が指定した値を超えたらコンテナ数を増やす

ECSServiceAverageMemoryUtilization

平均のメモリ使用率が指定した値を超えたらコンテナ数を増やす

ALBRequestCountPerTarget

ALBからの1コンテナ平均リクエスト数が指定した値を超えたらコンテナ数を増やす

これらはそれぞれサービスの特性に応じて使い分ければよいのですが、実は2つ以上のターゲットグループに紐付けたコンテナでは、最後の ALBRequestCountPerTarget を使用するのは危険です。

f:id:excite-takayuki-miura:20211122114906p:plain
ターゲットグループが1つの場合

f:id:excite-takayuki-miura:20211122125321p:plain
ターゲットグループが2つの場合

選択自体はできますが、実はこちら、片方のターゲットグループしか計算されていません。

計算されている方のターゲットグループにアクセスがあった場合

f:id:excite-takayuki-miura:20211122131831p:plain
計算されている方のターゲットグループにアクセスがあった場合のグラフ

f:id:excite-takayuki-miura:20211122131924p:plain
計算されている方のターゲットグループにアクセスがあった場合のコンテナ数

計算されていない方のターゲットグループにアクセスがあった場合

f:id:excite-takayuki-miura:20211122150139p:plain
計算されていない方のターゲットグループにアクセスがあった場合のグラフ

f:id:excite-takayuki-miura:20211122150210p:plain
計算されていない方のターゲットグループにアクセスがあった場合のコンテナ数

このように、片方のターゲットグループしか計算されないので、基本的に2つ以上のターゲットグループを紐付けたECSでは、 ALBRequestCountPerTarget は使わないほうが良いでしょう。

最後に

こちら、実は昔はそもそも2つ以上のターゲットグループを紐付けたECSでは ALBRequestCountPerTarget を選択すること自体が不可能でした。 それが選べるようになったということは、徐々に改修がされていっているのかもしれません。

こちらは2021/11/22現在の情報ですが、将来的には適切に処理がなされるようになるかもしれないので、気になる方は注意してみると良いでしょう。

SpringBootで対話的インターフェースをSpring Shellで実装する

エキサイト株式会社エンジニアの佐々木です。SpringBootは、Webアプリケーションフレームワークというのが一般的な認識可と思いますが、結構なんでもできたります。今回はマイナーな、インタラクティブCUIアプリケーションを作る、SpringShellの機能についてご紹介します。(https://docs.spring.io/spring-shell/docs/2.0.1.RELEASE/reference/htmlsingle/)

はじめに

運用フェーズで極稀に操作することとかあるとおもいます(あまりしたくはないですが)。そういうときに、Shellが役に立つことがあります。

設定

build.gradle

依存関係を解消するのに、下記のように修正します。

依存関係の解決と、 ./gradlew bootRun で実行したときに標準入力を有効にするオプションを追加します。

dependencies {
    ....
    implementation 'org.springframework.shell:spring-shell-starter:2.0.1.RELEASE'   // これを追加する
    ....
}

bootRun {
    standardInput = System.in   // ./gradlew bootRun で実行したときに、標準入力を有効にする為
}

application.properties

下記を入れないと循環参照エラーで起動しないのを回避するのに追加します。

....
spring.main.allow-circular-references=true
....

コード

ここからはWebアプリケーションと同じようなエンドポイントのようなものを書いていきます。

package com.example.batch.controller;

import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;

@ShellComponent
public class DemoShellController {

    @ShellMethod(value = "足し算をする", key = "sampleAdd" , group = "calc")
    public Integer add(int a, @Max(10) int b, @ShellOption(value = "--optional_c" , defaultValue = "1") int c){
        return a + b + c;
    }
}

このような感じで記述できます。@ShellMedthodでShellのエンドポイントを定義できます。valueアトリビュートは必須設定ですが、その他は任意になります。コマンド名はデフォルトメソッド名ですが、keyをつけることにより変更できます。@MaxでValidationも付与でき、@ShellOptionで引数名変更やデフォルト値を入れることも可能です。デフォルト値を入れないと、必須パラメータ になるので、注意してください。

実行

動作結果としてはこのような感じになります。 補完が効くのも便利です。

shell:>sampleAdd 1 10
12
shell:>sampleAdd 1 11
The following constraints were not met:
        --b int : 10 以下の値にしてください (You passed '11')
shell:>sampleAdd 1 11 10
The following constraints were not met:
        --b int : 10 以下の値にしてください (You passed '11')
shell:>sampleAdd 1 10 10
21
shell:>

f:id:earu:20211122100917g:plain

まとめ

Shellアプリケーションが簡単に実装できたかとおもいます。Java + SpringBoot + Gradleではマルチプロジェクト、モジュラーモノリス構成が簡単に構築できますので、内部の実装はWebアプリケーションで開発した実装を使用することが可能です。SpringBoot依存にはなってしまいますが、サービスが大きくなってきたらマイクロサービスに切り出すでいいかと思っております。

今回は、インタラクティブ(対話的)なShellアプリケーションでしたが、同じソースコードバッチ処理も可能ですので、次回ご紹介いたします。

おわりに

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

カジュアル面談はこちらになります! meety.net

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

以上となります。