AWS Copilot CLIのScheduled JobとSpring Shellを組み合わせて一回限りのタスクを実行する

はじめに

エキサイト株式会社 バックエンドエンジニアの山縣(@zsp2088dev)です。 エキサイトホールディングス - Qiita Advent Calendar 2024 - Qiita の18日目を担当します。

エキサイトブログの開発では、新機能の開発や既存機能の改修時に、データベースに新たなテーブルを作成し既存のデータを移行することがあります。 データ移行を何度か経験していく中で、より快適なデータ移行環境を目指し、AWS Copilot CLIのScheduled JobとSpring Shellを組み合わせたデータ移行環境を構築しました。 本記事では、このような環境を用意した背景や、実際に取り組んでいることについて紹介します。

背景

一般に、あるテーブルから別のテーブルへデータを移行する場合、複雑な加工処理をしないのであればINSERT... SELECTで済むことが多いです。 一方で、スキーマ間のデータ移行や複雑な加工処理を必要とするデータ移行では、アプリケーション内でデータの加工をしたほうが柔軟性が高くなります。 例えば、データの正規化や結合、不要なデータのフィルタリングなどを必要とする場合、SQLだけでは記述が複雑になりがちです。 また、アプリケーションコードを書くことで、テストコードを書くことができたり、ログ出力ができたりなどの利点があります。

エキサイトブログの開発でも、昨年のブログテーマ機能の改修のときに以下のような取り組みをしていました。

tech.excite.co.jp

当時は、EC2上でgit cloneしてアプリケーションコードを手動実行していました。 データ移行は繰り返し行うものではなく一回限りの実行であるため、煩わしさを感じつつも「頻度が高くないからこのままでいいな」と考えていました。 しかし、今後も同様の操作をすることを考えたときに、より快適なデータ移行環境を目指したほうがよいと考え、新たなデータ移行環境を検討するようになりました。

環境

本記事で扱う環境は以下の通りです。 記事内では実際にデータ移行しているところまでは触れず、「データ移行を実行する環境」について触れていきます。

Spring Shellの設定

Spring Shell 3系では@Commandアノテーションを使用してコマンド登録ができます。 下記のコードに対して、./gradlew bootRun --args="hello"と実行したときに、test1と出力されることが確認できます。 同様に、./gradlew bootRun --args="world"と実行したときに、test2と出力されます。 このように、引数に応じて実行したいメソッドを切り替えることができます。

package com.example.command;

import lombok.RequiredArgsConstructor;
import org.springframework.shell.command.annotation.Command;

@Command
@RequiredArgsConstructor
public class SampleCommand {
    @Command(command = "hello")
    public String hello() {
        return "test1"
    }

    @Command(command = "world")
    public String hello() {
        return "test2"
    }
}

データ移行時は、本メソッド内にデータの読み込み、書き込み、加工処理を書いていました。

Spring Bootをコンテナ化する

jibを使用して、Spring Bootで作成したアプリケーションをコンテナ化しています。 argsプロパティに対して、@Commandアノテーションに定義したcommandの値を設定することで、コンテナ実行時に任意のメソッドを実行できるようになります。 これを利用して、./gradlew jibDockerBuild -Pargs=helloとコンテナ化することで、コンテナ実行時にtest1と出力するコンテナイメージを作成できます。

jib {
    container {
        if (project.hasProperty("args")) {
            args = [
                    project.property('args')
            ]
        }
    }
}

jibの詳細やその他オプションについては下記のドキュメントをご参照ください。

github.com

AWS Copilot CLIの設定

Amazon ECS内でコンテナ化されたアプリケーションを実行するために、AWS Copilot CLIScheduled Jobを使用しています。 Scheduled Jobマニフェストscheduledフィールドにnoneを設定することで、タスクが定期実行されないように設定できます。

on:
  schedule: "none"

aws.github.io

上記の設定により、タスクは定期実行されません。 Scheduled Jobのドキュメント内には、noneを設定するのは一時的無効化したい場合と記載されているため、本来の用途とは違った使い方をしています。 copilot job runコマンドを実行すると、任意のタイミングでタスクを実行することができます。 これにより、タスクを実行したいときだけcopilot job runコマンドを実行すればよいです。

aws.github.io

GitHub Actionsから実行する

上記の内容までを実装することで、ローカル環境上でcopilot job runと実行するとタスクを実行できます。 これを、ローカル環境上にcopilotコマンドが無くても実行できるように、GitHub Actions上で実行できるようにしていきます。

ここでは、ワークフローを実行するのにGitHub Actionsのworkflow_dispatchイベントと、workflow_dispatch.inputsを使用します。 以下のようにworkflow_dispatch.inputsを定義すると、ワークフロー内で ${{ inputs.ARGS }}として扱えます。

on:
  workflow_dispatch:
    inputs:
      ARGS:
        description: ジョブ名を入力してください
        required: true
        type: string
      ENV:
        description: デプロイ先の環境を選択してください
        default: test
        required: true
        type: choice
        options:
          - test
          - prod

これにより、下記のようにhelloと入力し、ワークフロー内でコンテナをビルド、コンテナレジストリにプッシュ、copilot job run と実行することで、ECS上でtest1と出力できるようになります。

おわりに

本記事では、AWS Copilot CLIのScheduled JobとSpring Shellを組み合わせた一回限りのタスクを実行する方法について紹介しました。 サンプルでは標準出力をしただけですが、我々の開発ではデータベースからデータを読み取り、アプリケーション内で加工、データを書き込みといった操作をしています。 一度環境を整備してしまえば、いつでもすぐに利用できるのが非常に嬉しいです。

一方で、本記事では事業部内で知見のあるAWS Copilot CLIを使用していますが、以下のissueの通り、開発体制が不安定な状況にあるようです。 もし本記事と同様のことを実現したい場合は、以下の内容についてご留意いただけますと幸いです。

github.com

採用アナウンス

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しています。 また、長期インターンも歓迎しています。

カジュアル面談からもOKです。少しでもご興味がございましたら、お気軽にご連絡頂ければ幸いです。

▼ 募集職種一覧 ▼ recruit.jobcan.jp

Reactで良い感じのモーダルを作成する

こんにちは、エキサイト株式会社の奥川です。

エキサイトホールディングス・アドベントカレンダー2024シリーズ2の17日目を担当します。

qiita.com

今回は、Next.jsでいい感じのモーダルを作成する方法をご紹介します。

完成形

コンポーネントと呼び出し側のコードは下記になります。スタイルは適当です。

モーダルコンポーネント

// Modal.tsx
import { FC, ReactNode, useRef, useEffect } from 'react'
import { createPortal } from 'react-dom'

type ModalProps = {
  isOpen: boolean
  onClose: () => void
  children: ReactNode
}

const Modal: FC<ModalProps> = ({ isOpen, onClose, children }) => {
  const dialogRef = useRef<HTMLDialogElement>(null)

  useEffect(() => {
    if (isOpen) {
      // body要素にoverflow: hiddenを設定するとモーダル表示時に背景がスクロールしなくなる
      document.body.style.overflow = 'hidden'
      dialogRef.current?.showModal()
    } else {
      dialogRef.current?.close()
    }
  }, [isOpen])

  if (!isOpen) return null

  const handleClose = () => {
    onClose()
    // body要素のoverflow: hiddenを戻す
    document.body.style.overflow = ''
    // ボタンのフォーカスを外す
    document.activeElement instanceof HTMLElement && document.activeElement.blur()
  }

  return (
    <>
      {createPortal(
        <dialog>
            {children}
            <button 
              onClick={handleClose} 
              style={{ marginTop: '1rem', padding: '0.5rem 1rem', cursor: 'pointer' }}
            >
              閉じる
            </button>
        </dialog>,
        document.body,
      )}
    </>
  )
}
export default Modal

呼び出し部分の例

import { useState } from 'react'
import Modal from './Modal'

const ModalExample = () => {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <h1>Modal表示の例</h1>
      <button
        onClick={() => seIsOpen(true)}
        style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}
      >
        モーダルを開く
      </button>

      <Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
        <h2>モーダルの中身</h2>
        <p>ここに任意のコンテンツを入れることができます。</p>
      </Modal>
    </div>
  )
}

モーダル作成時に意識した点

DOM上ではbody要素末尾だが、コード上ではコンポーネント内に書きたい

モーダルは次の観点からDOM上ではbody要素直下の末尾に記載されることが多いです。

  • 全ての要素より優先して表示させたい
  • 他の場所とスタイルを独立させたい

しかし、画面全体を複数コンポーネントで分けて実装している場合、コンポーネントの中でモーダルを書こうとするとbody要素の途中でモーダルを書くことになります。ReactではcreatPortalを利用することで、DOM上の別の場所にコードを移動させられるため、コンポーネントの中でモーダルを実装してもbodyの直下に配置させることができます。

ja.react.dev

dialog要素を活用する

モーダルはdialog要素を用いて実装します。

developer.mozilla.org

dialog要素を利用すると、backdropで背景のCSSを設定できたり、アクセシビリティ周りの設定をしなくてもブラウザがモーダルと認識してくれるため、非常に便利です。dialog要素を利用する場合、モーダルの開閉作業はHTMLDialogElement.showModal()/close()をそれぞれ利用します。ただ、Reactではコンポーネントを純粋に保つことが推奨されます。そのため、モーダルの開閉状態はpropsで管理し、実際の開閉処理はHTMLDialogElement.showModal()/close()を利用しています。

おわりに

今回はReactでモーダル用のコンポーネントを作成する方法を紹介しました。開閉ロジックをカスタムフックにしたり、まだ改善の余地はあるかと思います。まだ、dialog要素を利用する場合はcreatePortalでbody直下に飛ばさなくても問題なかったります。今回の記事が実装の助けになれば幸いです。

Dart 3.6の変更点とDart 3.7で追加される言語機能について

こんにちは。エキサイトでアプリエンジニアをしている岡島です。エキサイトホールディングス Advent Calendar 2024の17日目を担当させていただきます。

最近Dart 3.6がリリースされたので、Dart 3.6について見ていきたいと思います。また、DartCHANGELOGで、Dart 3.7.0で追加される言語機能が紹介されていたので、まとめていきたいと思います。

Dart 3.6の新機能

Announcing Dart 3.6. Welcome to Dart 3.6! Today is our last… | by Marya Belanger | Dart | Dec, 2024 | Mediumで紹介されていますが、日本語訳をしながらまとめていきたいと思います!

Pub workspaces機能

モノレポ環境での依存関係の管理を効率化する新機能です。

このPub workspacesの機能によって、グループ化されたパッケージ間の依存関係の競合が発生したときに解決する必要があり、パッケージの使用時に混乱することがなくなります。また、ワークスペース全体を単一の解析コンテキストとして扱われるようになり、Dart言語サーバーの消費メモリ量が大幅に削減され、IDEのパフォーマンスが向上するようです。

Pub workspacesを定義するには、ルートのpubspec.yamlファイルにworkspaceフィールドを追加し、関連するパッケージを記述します。

name: _
publish_to: none
environment:
  sdk: ^3.6.0
workspace:
  - packages/helper
  - packages/client_package
  - packages/server_package

次に、リポジトリ内の任意の場所で実行pub getして、マッピングと残りのファイル管理を完了します。pub ワークスペースを使用するには、すべてのワークスペースパッケージ(依存関係を除く)のSDKバージョン制約が^3.6.0以上である必要があります。

pub.devのダウンロード数表示

pub.devでダウンロード数が確認できるようになりました。

ダウンロード数は、個々のパッケージページにおける以前の「人気スコア」に置き換わるものです。 週ごとのダウンロード数も時系列で示されます。

Digit Separators

長い数値リテラルの可読性を向上させるために、 アンダースコア(_)を数値の区切り文字として使用できるようになりました。

以下、例です。

1__000_000__000_000__000_000
0x4000_0000_0000_0000
0.000_000_000_01
0x00_14_22_01_23_45

Digit Separatorsは数字間でのみ有効であるため、 0._000_1 、1.2e_3100_などは使用できないことに注意してください。 Digit Separatorsを使用するには、パッケージ内の最小SDK^3.6.0以上にする必要があります。

Dart 3.7で追加予定の機能

ここから先はhttps://github.com/dart-lang/sdk/blob/main/CHANGELOG.md#370に記載されている、Dart 3.7.0の言語機能について共有していきます。

ワイルドカード変数 (Wildcard Variables)

ローカル変数やパラメーターとして_という名前を使用することで、 非バインディング変数を作成できるようになります。ワイルドカード変数はバインディングされないため、ワイルドカード変数を複数回宣言しても、名前衝突することなく、宣言できるようになります。

以下ワイルドカード変数を使用できる例:

Foo(_, this._, super._, void _()) {}

main() {
  var _ = 1;
  int _ = 2;

  list.where((_) => true);
}

その他、Nullへの型昇格やAnalyzerやDart formatの変更点がありますが、Dart 3.7の主な機能追加はワイルドカード変数のようです。

最後に

今回はDart 3.6がリリースされたので、こちらの記事を元に記事を書いてみました。このような言語機能追加のMotibationを読んでみると、確かにそうだと思う部分があり、勉強になりました。

最後まで読んでいただきありがとうございました。

IDを付与した要素の子要素がクリックされた際にもGoogle Tagでクリックイベントを測定できるようにする

こんにちは。エキサイトでエンジニアをしている奥川です。今回はGoogle Tag Managerでクリックイベントの計測を行う際のテクニックをご紹介します。

Google Tag でクリックイベントを計測する際は要素の指定をさまざま方法で行います。詳細はGoogleのドキュメントをご覧ください。

support.google.com

今回は組み込み変数 Click ID を用いて、クリックされた要素のIDが特定の値の場合にイベントを発火させる場合のテクニックを紹介します。IDを用いる場合は次のようにDOM要素にIDを指定することで、どの要素がクリックされたかを判別します。

<div id="target">
  Google Tag
</div>

この指定方法の場合、子要素をクリックしてもクリックイベントは計測されません。

<div id="target" style="background: lightgreen; padding: 20px;">
  反応する
  <div style="background: lightcoral; padding: 20px;">
    反応しない
  </div>
</div>

しかし、子要素がクリックされたときにもイベントが発火してほしいケースがあります。例えば、ボタンの中にflexで要素を並べている時などです。

そういった場合は、子要素に対してpointer-events: noneを設定すると要素がポインターイベントの対象にならなくなるため、親要素のクリックイベントを発火させることができます。 developer.mozilla.org

<div id="target" style="background: lightgreen; padding: 20px;">
  反応する
  <div style="background: lightcoral; padding: 20px; pointer-events: none;">
    反応する
  </div>
</div>

Tailwind CSSでキーボード操作時のフォーカスをスタイリングする方法

こんにちは。エキサイトでデザイナーをしている齋藤です。

エキサイトホールディングス Advent Calendar 2024 シリーズ2の7日目を担当します。

qiita.com

今回は、Tailwind CSSでキーボード操作時のフォーカスをスタイリングする方法をご紹介します。

理想状態

まずは理想状態をお示します。

Codepen内でtabキー(Safariではoption+ tab)を使用してボタンを選択しようとしてみてください。

See the Pen Tailwind CSSでfocus-visible by AyumuSaito (@ayumusaito-excite) on CodePen.

それぞれ、「理想状態のボタン」の方は赤い境界線で囲われ、「通常のボタン」ではブラウザ標準の境界線で囲われます。

フォーカス時のスタイルを変えたいシーンとは

ここで、なぜフォーカス時のスタイルを変える場合が発生するのかを整理します。

ボタン要素であればbuttonタグを用いるなど、適切にマークアップすればブラウザ標準の境界線が適用されるため、一見アクセシブルに思えます。

では、以下の場合だとどうでしょうか。Codepen内でtabキーを使用してボタンを選択しようとしてみてください。

See the Pen focus-visible:をスタイリングしない場合 by AyumuSaito (@ayumusaito-excite) on CodePen.

この場合のように、ブラウザ標準の境界線色に対して、ボタンの背景色のコントラスト比が低い場合にフォーカス状態であることがわからなくなってしまいます。

そうすると、キーボードを用いて操作しているユーザーが迷子なりかねないため、アクセシブルとは言えません。

そこで、フォーカス時のスタイルを変えるという動機が生じます。

どうやってスタイリングするか

純粋なCSSの場合、キーボードフォーカス時のスタイリングの際には、疑似クラスである:focus-visibleを用います。

Tailwind CSSの場合、:focus-visibleの代替となる、接頭辞focus-visible:が用意されています。

先の青ボタンがキーボードでフォーカスされた場合に赤い枠線にしたい場合は、以下のようになります。

See the Pen Untitled by AyumuSaito (@ayumusaito-excite) on CodePen.

<button 
type="button" 
class="px-3 py-2 rounded bg-blue-700 text-white focus-visible:ring ring-red-600 focus-visible:outline-none"
>
  青色のボタン
</button>
  1. focus-visible:ringでキーボードフォーカス時に境界線を設けると設定
  2. ring-red-600で境界線色を指定
  3. focus-visible:outline-noneブラウザ標準のスタイルを無効化

まとめ

今回は、Tailwind CSSでキーボード操作時のフォーカスをスタイリングする方法をご紹介しました。

この方法を知っておくと、よりアクセシブルなスタイリングに一歩近づくと思います。

Tailwind CSSを使用される方の一助となれば幸いです。

ご精読ありがとうございました。

htmxのhx-getでパーツ読み込みをした際のtitleタグ置き換えを防止する方法

こんにちは。エキサイトでデザイナーをしている齋藤です。

エキサイトホールディングス Advent Calendar 2024 シリーズ1の16日目を担当します。

qiita.com

今回は、htmxのhx-getでパーツ読み込みをした際に、読み込み元のtitleタグが置き換わってしまう現象を防ぐ方法をご紹介します。

hx-getでパーツ読み込み?

htmxでは、テンプレートエンジンのように、他のHTMLファイルにある要素をパーツとして読み込むことが可能です。

htmxやパーツ読み込みの方法については、以下の記事をご参照ください。

tech.excite.co.jp

発生する現象

例えば以下のコードでindex.htmlにて/parts/index.html#partsを読み込んだとします。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>トップページ</title>

    <script src="https://unpkg.com/htmx.org@2.0.4"></script>
</head>
<body>

    <!-- 自要素を`/parts/index.html`の`#parts`に置き換え -->
    <div
    hx-get="/parts/"
    hx-select="#parts"
    hx-trigger="load"
    hx-target="this"
    hx-swap="outerHTML"></div>

</body>
</html>

/parts/index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>パーツ</title>

    <script src="https://unpkg.com/htmx.org@2.0.4"></script>
</head>
<body>

    <!-- 呼び出される要素 -->
    <div id="parts">...</div>

</body>
</html>

index.htmlを表示したときのtitle

この状態でindex.htmlをブラウザで読み込むと、titileタグが置き換わってしまい、「トップページ」となってほしいところが「パーツ」になってしまいます。

index.htmlをブラウザで表示した際のtitle

どうやって解決するか

titleタグの置き換えを防止したい場合は、レスポンスで受け取った要素をどう扱うかを処理するhx-swapに、ignoreTitle:trueを付与します。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>トップページ</title>

    <script src="https://unpkg.com/htmx.org@2.0.4"></script>
</head>
<body>

    <!-- `hx--swap`に`ignoreTitle:true`を追加 -->
    <div
    hx-get="/parts/"
    hx-select="#parts"
    hx-trigger="load"
    hx-target="this"
    hx-swap="outerHTML ignoreTitle:true"
    ></div>

</body>
</html>

結果

ignoreTitle:trueを追加した結果、期待通りにtitleが「トップページ」となりました。

`ignoreTitle:true`追加後の`title`

まとめ

今回は、htmxのhx-getでパーツ読み込みをした際に、読み込み元のtitleタグが置き換わってしまう現象を防ぐ方法をご紹介しました。

ignoreTitleを使用することで、ちょっとした静的サイトであればテンプレートエンジンの代わりに使用できるかもしれません。

htmxを使用される方の一助となれば幸いです。

ご精読ありがとうございました。

デザイナーの自分が 使用している アクセス解析ツール紹介

こんにちは!エキサイトお悩み相談室でデザイナーをしているサヅカです。

エキサイトホールディングス Advent Calendar 2024 15日目は、サヅカが担当させていただきます。

qiita.com

12月に入り急に寒くなりました。おこたに入って猫を足に乗せて仕事がしたいものです。

この前ふと気付いたのですが、「あれ?最近私デザインしてない。計測ばかりしているな」と思いました。

macに向かい、真っ先に開くのはGoogle AnalyticsかtableauかKarte Blocksです。

ということで、今回はデザイナーの自分がよく使う計測系のツールを紹介させていただきます。

Google Analytics(GA4)

developers.google.com

できることが多すぎて詳細は割愛させていただきますが、GA4ではどこのリンクがどのくらい押されているかを確認することが多いです。

例)サービスサイトのトップページのデザイン改修を行なったとき

改修したページの各リンクのクリック数

レポート→エンゲージメント→イベント

改修したページの各リンクのクリック率(CTR)を出す

レポート→エンゲージメント→ランディングページでセッション数を確認しクリック数で割る

クリック数だけですと、その日たまたまセッションが多くクリック数も増えているだけかもしれません。ですので、クリック率で出すのが良いと思います。

tableau

www.tableau.com

簡単に説明すると、どこからどのくらい売上が発生したかを確認できるツールになります。

使用するにはGA4と同様にページ内にコードを記述する必要がありますので、コーディングの際に私がコードを追加し、tableau側の設定は企画のディレクターにやってもらっています。

金額が出るのでGA4よりもtableauの計測はショックを受けることもしばしば。

「ここのコンテンツ工数かけて改修したのにどうして…」と。そこで新たなWhyが生まれます。

  • 「ユーザーに気付かれていないのでは?」
  • 「別のコンテンツに流れている?」
  • 「GA4で見ると結構クリックされているから興味はあるのでは?」

など。

そこから自然とUXデザイナーのような動きをするようになっていくことも多いです(こういうタイミングでUXデザイナーに転身する人もいるのでしょうか)

Karte Blocks

blocks.karte.io

主にABテストで使用しています。

毎日ABテストしているわけではないのですが、今期は結構テストしていました。困った時にサポートセンターにチャットで聞けるのが本当にありがたい…

User Insight

ui.userlocal.jp

こちらもたまに使用しているのでご紹介。主にヒートマップの確認で使用しています。離脱率や滞在時間なども見れて便利です。

まとめ

割り算までしかできない自分にはこういうツールは使いこなせない!と思っていたのですが、周りのサポートもあり最低限見れると良いかなという部分はなんとか触れるようになりました。

(※難しい計測やもっと細かいデータに関しては、ベテランの企画メンバーやSEO担当者がやっています)

デザイナーはどこまで数字を意識するべきかは会社や部署によると思います。

私個人としては自分が作成や改修したものがどのくらい使われているか知ることは、今や重要なものとなっています。

誰かの参考になりましたら幸いです。

RAGを関連キーワードを押さえながら理解してみる!

こんにちは!エキサイト株式会社、SaaS・DX事業部エンジニアの岩田史門です!

エキサイトHDアドベントカレンダー14日目を担当させていただきます!

はじめに

RAG(Retrieval-Augmented Generation)は、外部データの情報を利用して、より正確で文脈に沿った応答を大規模言語モデル(LLM)に生成させる方法です!

現在開発に携わっている、FanGrowthというプロダクトでも、「FanGrowth AI ウェビナー企画提案」という機能で活用しています!

www.fangrowth.biz

この記事では、関連するキーワードとともに、RAGについて説明します!

RAGの概要

RAGは、下記の2つの処理ステップを組み合わせたアプローチです!

1. Retrieval(情報検索)

ベクトルDBなどから、ユーザーの質問やリクエストに関連する情報を検索します。このステップでは、質問をEmbedingモデルを使って、ベクトル化して、類似するデータを見つけます。類似度の指標として、コサイン類似度というものが有名ですが、こちらを利用する場合は、スコアが1に近いほど類似していることを表します!

2. Augmented Generation(拡張生成)

検索された情報を活用して、生成モデル(例:OpenAIのGPT)にコンテキストを提供し、それに基づいて応答を生成します。

RAGは、ただ単純にモデルが事前に学習している知識だけに頼るのではなく、外部データを活用して最新の情報や特定のドメイン知識を含んだ応答を生成するのが特徴です。そのため社内文書や自社の商品、FAQなどによく活用されています!

キーワードの解説

RAGの概要説明にて、ベクトル化、ベクトルDB、ベクトル検索、Embedding、コサイン類似度などのキーワードが出てきました!

この章では、これらのRAGの話をする上で頻出するキーワードを解説していきます!

ベクトル化

ベクトル、みなさんご想像の通り、高校の時に数学で習ったあれです。

こんなところで活用されています!

ベクトル化とは、言葉や画像などのデータを数値ベクトル(数値の配列)に変換することです。

ベクトル化することにより、機械学習モデルがデータとデータの類似性を計算しやすくなります!

(おまけ)実際に画像をベクトル化してみました

今回は下記の僕が登山したときの画像をベクトル化してみます!

ベクトル化する画像(冬の乗鞍岳に登ったときの僕の写真です)

下記のコードをGoogle Colabで実行するとベクトル化できます!

import numpy as np
import tensorflow as tf

model = tf.keras.applications.EfficientNetB0(include_top=False, pooling="avg")

def image2vec(image_path):
    raw = tf.io.read_file(image_path)
    image = tf.image.decode_jpeg(raw, channels=3)
    image = tf.image.resize(image, [224, 224])
    vec = model.predict(np.array([image.numpy()]))[0]
    return vec

image2vec("/content/アイコン画像_乗鞍岳.jpeg")

実際の結果は、下記のようになり、無事ベクトル化できていますね!

自分でやっておいてなんですが、自分がこんなふうに数値で表されていると思うと、なんだか不思議な感覚になりますねw

画像のベクトル化

ベクトルDB(ベクトルデータベース)

その名の通り、ベクトル形式でデータを保存することができるデータベースです!

効率的に類似度検索を実行できるようになっています!

FanGrowthでは、Amazon OpenSearch Service を利用しています!

ベクトル検索

質問や検索の際のリクエストをベクトル化し、ベクトルDBに保存されたデータのベクトルと類似度スコアを計算して、最も関連性の高いデータを検索する方法です!

例えば、ユーザーが、「うちの会社の就業規則を教えて」と質問すると、この質問がベクトル化されて、関連する文書や情報を検索するといった仕組みです!

Embedding(エンべディング:埋め込み)

Embedding(エンべディング:埋め込み)とは、データ(単語や画像、音声など)を、数値のベクトル形式に変換する技術です!

Embeddingモデルとは、データをEmbeddingしてベクトル化するための機械学習モデルです。

有名なものだと、OpenAI EmbeddingモデルやGoogleが開発したBERTなどがあります!

類似度スコア

類似度スコアは、データ同士の「似ている度合い」を数値で表したものです!

コサイン類似度は、その中でも特に広く使われる代表的なものです!

コサイン類似度

コサイン類似度は、2つのベクトルの角度のコサインの値を計算することで、類似度スコアを計算する方法です!

cos(0o) の時は1、cos(90o) は0、cos(180o)の時は-1になりますよね!

三角関数を思い出しますね、懐かしい...)

そのため、値は-1 ~ 1の間になります!

1の時は、角度が0度なので、ふたつのベクトルが同じ方向を向いており、完全一致しているので、類似度スコアは高いです。

0の時は、角度が90度で、ふたつのベクトルが直行しているので、無関係です。

-1の時は、角度が180度で、ふたつのベクトルが反対方向を向いているため、正反対のデータとなります。

コサイン類似度をイメージするための図

計算式は下記のようになります!

\displaystyle{

\cos(a,b) = \frac{\sum^{n}_{i=1}a_{i}b_{i}}{\sqrt{\sum^{n}_{i=1}a_{i}^2}\sqrt{\sum^{n}_{i=1}b_{i}^2}}

}

(おまけ)実際に例を挙げて計算してみます

ベクトルA = [1,2,3], ベクトルB = [4,5,6]の場合

  1. 計算式の分子のベクトルの内積を計算します![tex:\displaystyle{ (14)+(25)+(3*6)=32 }]

  2. 分母のベクトルAの大きさを計算します! \displaystyle{\sqrt{1^ 2+2^ 2+3^ 2}=\sqrt{14}}

  3. 分母のベクトルBの大きさを計算します! \displaystyle{\sqrt{4^ 2+5^ 2+6^ 2}=\sqrt{77}}

  4. コサイン類似度は、\displaystyle{
\frac{32}{\sqrt{14}*\sqrt{77}}\approx0.97463
}

上記のように、1に近いので、類似度が高いと言えます!

実際には、3次元ではなく、もっとすごい次元のベクトルの計算をしています!

実際のRAGの大まかな処理の流れ(OpenSearchとOpenAIを利用した場合)

この章では、先ほど押さえたキーワードを使いながら、RAGがどのような流れで処理を行うのか、大まかに説明します!

処理の流れ図

  1. 事前にベクトルDBに社内文書を登録しておく 
    例えば、就業規則のPDFなど社内文書をいくつか登録しておきます

  2. ユーザーが質問する
    ユーザーが、「就業時間を教えてください」と質問します。

  3. 質問のベクトル化
    OpenAIのEmbedding APIを使って、ユーザーの質問をベクトル化します

  4. ベクトル検索
    質問ベクトルを、AWS OpenSearchのベクトルDBに問い合わせ、類似するデータを取得します。今回でいえば、就業規則のPDFがヒットします。

  5. 関連するデータを返す
    ヒットした情報を返却します。

  6. 回答生成用の質問(コンテクスト)を準備
    検索結果から重要な情報(今回だと就業時間)を抽出し、生成モデルに質問として渡します。(いつもChatGPTに質問するときのような文章に組み込みます)

  7. LLMにプロンプトを投げて質問
    Open AIのAPI経由で、取得したデータを含んだ質問を送信して質問します。

  8. 回答生成
    OpenAIのモデルを使って、「就業時間は9時から18時です」というような回答を生成します

単純にChatGPTなどのLLMに質問する場合、就業時間は各社によって異なりますし、適切な回答は返ってきませんが、RAGを使うと、外部データを柔軟に活用して、回答を生成できます!

まとめ

今回は、最近とても注目されているRAG(Retrieval-Augmented Generation)という技術について、関連キーワードを解説しながら理解を深めました!

RAGは、ファインチューニングと異なり、モデル自体を訓練し直す必要がなく、とても便利です!

FanGrowthでは、商品情報やウェビナー用の企業データなど、LLMのモデルが知らない情報を扱いたいケースが多いので、今後もRAGを活用して企画生成の分野で価値提供していきたいと思います!

参考文献

Spring AIを利用してECS環境からBedrockに接続する

こんにちは、エキサイト株式会社の平石です。

エキサイトホールディングス Advent Calendar 2024のシリーズ2, 16日目を担当いたします。

今回は、Spring AIを利用してECS環境からAmazon Bedrockに接続する方法をご紹介します。

はじめに

Spring AIとは

Spring AIはSpringからAIモデルを利用するためのフレームワークです。
SpringがAIモデルを提供しているわけではなく、SpringアプリケーションからAIプロバイダーにリクエストを送り、レスポンスを受け取る部分をAPIとして提供してくれます。

今回紹介するAmazon Bedrock以外にも、OpenAIやStabilityAIなど多くのプロバイダーに対応しています。

spring.io

Amazon Bedrockとは

Amazon Bedrockは、AWS上で生成AIを利用できるようにするフルマネージドサービスです。
既存のモデルを容易に利用することもできますし、用途やデータに合わせてカスタマイズすることも可能です。

aws.amazon.com

環境

この記事のソースコードは以下の環境で動作確認をしています。

  • Java 21
  • SpringBoot 3.2.1
  • Gradle 8.5

依存関係

Spring AIを利用するためのGradleの依存関係は以下のとおりです。

allprojects {
    repositories {
        mavenCentral()
        maven { url 'https://repo.spring.io/milestone' }
        maven { url 'https://repo.spring.io/snapshot' }
    }
}

〜〜 略 〜〜

project(':project1:controller') {
    dependencies {
        〜〜 略 〜〜

        implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-SNAPSHOT")
        implementation 'org.springframework.ai:spring-ai-bedrock-ai-spring-boot-starter'
    }
}

Spring AIのライブラリは2024年9月30日時点でMaven Centralにはないため、個別にrepositoryを指定する必要があります。

AWS認証情報の設定

まずは、Bedrockに接続するためのAWS認証情報の設定を行います。

@Configuration
public class BedrockApiConfig {
    @Bean
    @Profile("local")
    public AwsCredentialsProvider awsDefaultCredentialsProvider() {
        return DefaultCredentialsProvider.builder()
                .profileName("sample-profile-name")
                .build();
    }

    @Bean
    @Profile("!local")
    public AwsCredentialsProvider awsContainerCredentialsProvider() {
        return ContainerCredentialsProvider.builder().build();
    }
}

Spring AIに対してAWSの認証情報を渡すためにはAwsCredentialsProviderを実装したクラスのインスタンスを用意する必要があります。

ローカル環境で実行するときには、DefaultCredentialsProviderを用いてprofileNameで指定したプロファイル名で設定したアクセスキーやAWS SSOを用いてBedrockにアクセスできます。

当然、ECS環境内でもローカル環境と同じようにアクセスキーを発行してアクセスすることも可能ですが、より簡単な方法があります。

それは、AWS_CONTAINER_CREDENTIALS_RELATIVE_URIまたはAWS_CONTAINER_CREDENTIALS_FULL_URIという環境変数を用いる方法です。(基本的にはAWS_CONTAINER_CREDENTIALS_RELATIVE_URIがセットされているはず)

コンテナが起動するとAWS_CONTAINER_CREDENTIALS_RELATIVE_URIという環境変数に自動的に値がセットされます。この環境変数にセットされたURIにアクセスするとAWSの各サービスにアクセスするための認証情報を取得できます。

ContainerCredentialsProviderを使うと環境変数を参照し、認証情報を取得する一連の処理を自動でやってくれます。*1

今回は、ECS環境からBedrockにアクセスすることが目的ですが、ローカル環境での動作確認も開発上必要になることも考えられます。
そのため、@Profileを用いてローカル環境かどうかで使うAwsCredentialsProviderを切り替えています。*2

Bedrockに接続するための設定

実際にBedrockに接続するためには、利用するモデルに対応するクラスを利用します。

以下から、ご自身が利用したいモデルに対応するクラスをご確認ください。

Chat Model API :: Spring AI Reference

今回はAnthropic3のClaude 3.5というモデルを利用します。 Claude 3.5に対応するクラスはAnthropic3ChatBedrockApiBedrockAnthropic3ChatModelです。

@Configuration
public class BedrockApiConfig {
    // 略

    @Bean
    public Anthropic3ChatBedrockApi anthropic3ChatBedrockApi(final AwsCredentialsProvider awsCredentialsProvider) {
        return new Anthropic3ChatBedrockApi(
                Anthropic3ChatBedrockApi.AnthropicChatModel.CLAUDE_V3_5_SONNET.id(),
                awsCredentialsProvider,
                Region.AP_NORTHEAST_1.id(),
                new ObjectMapper()
        );
    }

    @Bean
    public BedrockAnthropic3ChatModel bedrockAnthropic3ChatModel(final Anthropic3ChatBedrockApi anthropic3ChatBedrockApi) {
        return new BedrockAnthropic3ChatModel(
                anthropic3ChatBedrockApi,
                Anthropic3ChatOptions.builder()
                        .withMaxTokens(4000)
                        .withAnthropicVersion(Anthropic3ChatBedrockApi.DEFAULT_ANTHROPIC_VERSION)
                        .build());
    }
}

Anthropic3ChatBedrockApiで東京リージョン(Region.AP_NORTHEAST_1.id())でClaude 3.5(Anthropic3ChatBedrockApi.AnthropicChatModel.CLAUDE_V3_5_SONNET.id())を指定した認証情報で利用するように設定しています。

また、BedrockAnthropic3ChatModelではAnthropic3ChatOptionsを用いて、Claudeモデルに関する設定を行っています。

上のソースコードで設定した以外にも設定項目はありますので、詳細は以下をご確認ください。

spring-ai/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic3/Anthropic3ChatOptions.java at main · spring-projects/spring-ai · GitHub

実際に、Bedrockにリクエストを送信するには以下のように記述します。

@RestController
@RequiredArgsConstructor
@RequestMapping("bedrock/sample")
public class BedrockTestController {
    private final BedrockAnthropic3ChatModel bedrockAnthropic3ChatModel;

    @GetMapping
    public String callBedrockApi() {
        final String prompt = "Hello!"

        return bedrockAnthropic3ChatModel.call(prompt);
    }
}

SpringBoot アプリケーションを起動しhttp://localhost:{ポート番号}/bedrock/sampleにアクセスすると、以下のようなレスポンスが得られます。

Hello! How can I assist you today? Feel free to ask me any questions or let me know if you need help with anything.

おわりに

今回は、Spring AIを利用してECS環境からAmazon Bedrockに接続する方法をご紹介しました。

次回は、ECSコンテナとAmazon Bedrockの間でのPrivateLinkを確立する方法をご紹介します。

では、また次回。

参考文献

*1:公式ドキュメントにはDefaultCredentialsProviderでもAWS_CONTAINER_CREDENTIALS_RELATIVE_URIを参照すると書かれていますが、私が実行した限りではうまくいきませんでした

*2:環境変数SPRING_PROFILE_ACTIVEに環境名をセットする必要があります

JavaのSpringdocはAPIのリクエストパラメータをどのように認識しているのか?

こんにちは、エキサイト株式会社の平石です。

エキサイトホールディングス Advent Calendar 2024の10日目を担当いたします。

Springdocライブラリを利用することで、簡単にOpenAPI仕様に則ったAPIドキュメントを自動生成することができます。

しかし、このライブラリを使って開発を行う中で、APIのパラメータをどのように認識しているのか少し気になったので、その調査をしてみました。
今回は、その内容をブログとして残したいと思います。

なお、今回紹介するのはspringdoc-openapi v2.7.0での動作であり、今後変更される可能性はありますのでご注意ください。

共通で使う例

今回は、以下のようなControllerを例に挙げて説明していきます。
なお、@ParameterObjectというアノテーションを付与していない場合には動作が異なります。
今回は、@ParameterObjectが付与されている場合の動きを紹介します。

@RestController
@RequestMapping("hello-world")
public class SampleController {
    @GetMapping
    public String getHelloWorld(
            @ParameterObject SampleRequestDto requestDto
    ) {
        return "Hello java";
    }
}

SampleRequestDtoはリクエストのパラメータをまとめたDTOです。
このDTOにはString型のstringFieldInteger型のintegerFieldが含まれているとします。

通常のクラスを使う場合

SampleRequestDtoを通常のクラスで表現してみます。
この場合、以下のようになるでしょう。

public class SampleRequestDto {

    private final String stringField;

    private final Integer integerField;
    
    public SampleRequestDto2(String stringField, Integer integerField) {
        this.stringField = stringField;
        this.integerField = integerField;
    }
}

この状態でSwagger UI*1を確認してみると以下のようになっています。

Parametersの欄がNo parametersとなっています。
どうやらフィールドとして記述しただけでは、パラメータとして認識されないようです。

Getterを作成してみる

現状では、このDTOを受け取ったControllerはDTO内のフィールドを利用できません。
Getterを追加してみましょう。

    public String getStringField() {
        return stringField;
    }

    public Integer getIntegerField() {
        return integerField;
    }

この状態で、Swagger UIを確認してみます。

今度は、2つのフィールドがパラメータとして認識されています。
どうやら、SpringdocのライブラリはGetterがあるフィールドをパラメータとして認識するようです。

Getterの名前を変えてみる

Getterの名前は何でも良いのでしょうか。
コードを以下のように変更して確認してみます。

    public String getFieldOfString() {
        return stringField;
    }

    public Integer integerField() {
        return integerField;
    }

No parametersに戻ってしまっています。
どうやら、get{フィールド名}という名前でないと認識してくれないようです。

これは、内部でjava.beans.IntrospectorクラスのgetBeanInfoメソッドが使われているからだと考えられます。

https://github.com/springdoc/springdoc-openapi/blob/main/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/extractor/MethodParameterPojoExtractor.java#L290

このソースコードの以下の部分で、readMethodnullでないものをパラメータとして残すような実装をしています。

   .map(PropertyDescriptor::getReadMethod)
    .filter(Objects::nonNull)

getBeanInfoget{フィールド名}のメソッドをreadMethodとして扱うため、この名前のメソッドが存在している時のみ、パラメータとして認識したようです。

Getterだけを作成してみる

Getterだけを作成した場合はどうなるのでしょうか。

    public String getStringField2() {
        return stringField + "2";
    }

内部では、「フィールドの一覧を取得」→「ゲッターがあるかどうかを判定」の順で処理が行われるため、そもそもフィールドが存在していなければパラメータとして認識されることはありません。

staticフィールドを作成してみる

staticを付けたフィールドもパラメータとして認識されるのでしょうか。

    private static final Integer constInteger = 1;

    //(中略)

    public Integer getConstInteger() {
        return constInteger;
    }

どうやら、フィールドとして存在し、getterが存在すればstaticフィールドもパラメータとして認識するようです。
APIのリクエストにconstIntegerを指定してもDTO内の値を書き換えることはできないので、無意味なものではありますが.....。

慣習的に定数は大文字で記述しますので、問題にはならないかと思いますが、staticがついているかどうかは無関係なようです。

@JsonIgnoreを付与してみる

@JsonIgnoreJSONシリアライズして欲しくないフィールドに付与することで、シリアライズ時に無視してくれるようになるアノテーションです。

integerFieldに付与してみます。

    private final String stringField;

    @JsonIgnore
    private final Integer integerField;

@JsonIgnoreを付与したintegerFieldはパラメータとして認識されておらず、@JsonIgnoreは有効なようです。

ここまでのまとめと補足(@ParameterObjectを付与している場合)

  • Springdocは、フィールドとして存在し、かつget{フィールド名}という名前のメソッドがあるものをパラメータとして認識する
    • 補足:戻り値の型とメソッド名が一致していれば「Getter」が何を返しているかまでは考慮しない
  • フィールドとして存在すれば、実際に外部からのリクエストをもとに値をセットしているかどうかは考慮しない
    • 補足:コンストラクタは無関係な模様
  • staticフィールドもGetterがあればパラメータとみなす
  • @JsonIgnoreで特定のフィールドをパラメータとして認識させないようにできる
    • 補足:リクエストパラメータをDTO内で加工したときに、加工後の値を変数に格納したいときに利用できそう

@ParameterObjectを文字通り解釈すれば「パラーメータを格納したオブジェクト」なので、フィールドとして存在しているものをパラメータとしてみなすのは、ある意味自然な動作なのかもしれません。

Javaのレコードを使う場合

Javaのレコードは、通常のクラスで表現するより少ない記述で「データのまとまり」を表現できる特殊なクラスです。

例えば、今回の例のリクエストモデルは以下のように表現できます。

public record SampleRequestDto(
        String stringField,
        Integer integerField
) {
}

このように記述することで、SampleRequestDtoインスタンスを作成するためのコンストラクタや、stringFieldintegerFieldにアクセスするためのメソッドが自動で生成されます。

この状態で、Swagger UIを確認してみましょう。

特に、問題なくパラメータを認識してくれているようです。

ここで、疑問に思う点がありました。 先ほどの通常のクラスの場合で見た時には、フィールドが存在しget{フィールド名}という名前の「Getter」が存在するものをパラメータとしてみなすのでした。

しかし、レコードで生成される「Getter」のメソッド名は{フィールド名}で、パラメータとしてみなされる条件にマッチしません。

これは、どういうことなのでしょうか。

わかってしまえば単純なのですが、Springdocライブラリの内部では@ParameterObjectが付与されたクラスがレコードであるかどうかにより内部で分岐が行われています。

https://github.com/springdoc/springdoc-openapi/blob/main/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/extractor/MethodParameterPojoExtractor.java#L280

           Parameter parameter = field.getAnnotation(Parameter.class);
            boolean isNotRequired = parameter == null || !parameter.required();
            // レコードかどうかによって分岐
            if (paramClass.getSuperclass() != null && paramClass.isRecord()) {
                return Stream.of(paramClass.getRecordComponents())
                        .filter(d -> d.getName().equals(field.getName()))
                        .map(RecordComponent::getAccessor)
                        .map(method -> new MethodParameter(method, -1))
                        .map(methodParameter -> DelegatingMethodParameter.changeContainingClass(methodParameter, paramClass))
                        .map(param -> new DelegatingMethodParameter(param, fieldNamePrefix + field.getName(), fieldAnnotations, true, isNotRequired));

            }

getRecordComponents()でレコード内のコンポーネントの配列を取得しており、getReadMethodメソッドは使用していません。
そのため、get{フィールド名}でないGetterでなくてもパラメータとして認識されたのです。

終わりに

今回は、springdocライブラリによるリクエストパラメータの認識方法が少し気になったため、調査した結果をブログとして記述しました。

レコードやLombok@Valueのようなアノテーションを利用していれば何も意識する必要はないので、知らなくても問題ない内容ではあります。
しかし、リクエストモデル内部で何か処理をしたいと思ったときに役立つかもしれません。

では、また次回。

参考文献

*1:APIドキュメントをYAML形式やJSON形式のドキュメントよりも、見やすい形式で表示してくれる

【Github】Jenkinsで本番環境にリリースした筈が変更が反映されなかった話

はじめに

こんにちは、エキサイト株式会社デザイナーの山﨑です。

エキサイトホールディングス Advent Calendar 2024 13日目は、山﨑が担当させていただきます。

qiita.com

今回は、Jenkinsで本番環境にリリースしたはずなのに、リリース内容が反映されなかった事件について紹介します。

トラブル発生

ウーマンエキサイトでは、一部のリポジトリにおいて、ステージング環境や本番環境へのリリースをJenkinsで行っています。

しかし、今回はそこでトラブルが発生しました。

最新コメントランキングという機能の本番リリースを行った後、cssのスタイルがまるまる適用されていないことが判明したのです。

原因調査

原因を調査したところ、Githubのリリースタグが入れ替わっていたことにより、Jenkinsのリリース対象に古いタグが使われていることが判明しました。

以下はJenkinsのリリース画面です。Jenkins側では、最新のタグがv2.426.0であるかのように見えました。

しかし、Githubを確認すると、v2.426.0は2週間前に発行された過去のタグであり、本来の最新のタグはv2.425.0だったのです。

Jenkinsでリストの一番上に表示されていたため、「これが最新のタグだな」と誤認してしまい、結果として2週間前のタグを本番環境にリリースしてしまいました。

このミスにより、最新コメントランキングのスタイルが反映されないという問題が発生しました。

反省

このトラブルの原因は、おそらくGithub上でタグが並び替わるバグにあると推測されます。

今回の件を教訓に、以下の対策を徹底することにしました:

Githubのリリースタグの並び順が正しいか確認すること

Jenkinsだけでなく、Github上でも最新タグが正しく設定されているかを確認する。タグの順番が正しいと思い込まないこと。

②ステージング環境での事前検証を徹底すること

Jenkinsで本番環境にリリースする前は、ブランチタグでステージ確認を行っていたが、mainにマージした後も必ずステージング環境でリリース対象のタグをテストする。

以上より、リリースミスを未然に防ぎ、安定した運用を実現できるよう努めます。

最後に

最後に、エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!

興味があればぜひ連絡よろしくお願いいたします!🙇

www.wantedly.com

【Dart】ジェネリクス型はなぜ必要なのか

こんにちは。エキサイトでアプリエンジニアをしている岡島です。

エキサイトホールディングス Advent Calendar 2024の12日目を担当させていただきます。

tech.excite.co.jp

以前、上記の記事を書いたときにmapの内部処理で用いられているTやEの文字について、これは何かと気になったので調べたことについて共有していこうと思います。

ジェネリクス型とは

ジェネリクス型とは、特定の型に依存せず、柔軟にコードを記述する方法のことです。List や Map<String, int> のようにint型やString型というように型を指定することなく、ジェネリクス型ではListやMap<K, V>というように任意の型の要素を保持できます。

なぜジェネリクスが必要なのか

ジェネリクス型は、型安全性を保ちながらコードの再利用性を高めるために必要です。それぞれについて詳しく説明していきます。

型安全性の向上

型を指定することで、意図しない型のデータが渡されるのを防ぐ事ができます。

ジェネリクス型を使わない場合

void printItems(List items) {
  for (final item in items) {
    print(item);
  }
}

void main() {
  printItems([1, 2, 3]); // 正常に動作
  printItems(["A", "B", "C"]); // 正常に動作
  printItems([1, "A", true]); // 動作するが、型が混在している
}
  • このコードの問題点
    一見問題ないようにも見えますが、List 内の要素の型が混在しているため、特定の型に依存する処理を行うとエラーが発生する可能性があります。型を定義していないことで、コードの保守性が低くなってしまいます。

ジェネリクス型を使う場合

ジェネリクス型を用いる場合は、型を指定する必要があるので、型安全性が保たれます。意図しない型があるとコンパイルエラーになります。

void printItems<T>(List<T> items) {
  for (T item in items) {
    print(item);
  }
}

void main() {
  printItems<int>([1, 2, 3]); // 型安全に動作
  // printItems<int>(["A", "B", "C"]); // コンパイルエラー
}

型を指定することで、意図しない型のデータが渡されるのを防ぎ、コードの安全性が向上します。

コードの再利用性を高める

ジェネリクス型を用いると、特定の型(例: int や String)に固定されることなく、任意の型に対応することができます。ジェネリクス型を用いて関数を定義した場合、関数を呼び出す際に具体的な型に置き換えられます。

つまり、1つの関数で異なる型に対応できるため、コードの再利用性が高まります。

具体例について見ていきます。

ジェネリクスを使わない場合

こちらの例では、それぞれ異なる型のリストを受け取り、リストの要素を順番にprint出力する関数を定義します。

ジェネリクス型を使わずに実装する場合、扱う型が増えるたびに新しい関数を作成しなければなりません。

void printIntList(List<int> items) {
  for (final item in items) {
    print(item);
  }
}

void printStringList(List<String> items) {
  for (final item in items) {
    print(item);
  }
}

void main() {
  printIntList([1, 2, 3]); 
  printStringList(["A", "B", "C"]); 
}

このコードの問題点

  • 異なる型に対応するためには、新しい関数(printIntList や printStringList)を作成しなければならない
  • リストを出力するロジックが重複している
  • 他の型(例えば、double 型)に対応するには、さらに別の関数を定義する必要がある

ジェネリクスを使う場合

ジェネリクスを使うことで、型に依存しない汎用的な関数を1つだけ書けば、すべての型に対応できます。

void printItems<T>(List<T> items) {
  for (var item in items) {
    print(item);
  }
}

void main() {
  printItems<int>([1, 2, 3]); // int型のリストに対応
  printItems<String>(["A", "B", "C"]); // String型のリストに対応
  printItems<double>([1.1, 2.2, 3.3]); // double型のリストにも対応
}

ジェネリクス型で用いられる名前

ジェネリクス型では、慣例によって以下の文字が用いられるようです。

  • E・・・Element
  • T・・・Type
  • S・・・Second Type
  • K・・・Key
  • V・・・Value

まとめ

ジェネリクスは、コードの柔軟性を高めながらも、型安全性を保つことのできるもので、非常に重要なものだと感じました。個人的に、EやTが何を意味するかが疑問であったため、名前の由来についても知ることができていい機会になりました。

最後まで読んでいただきありがとうございました。

動画加工しやすいのはどっち!?Canva vs Adobe Express

こんにちは!SaaS・DX事業部デザイナーの鍜治本です! 年末年始、イベントや休暇の思い出を形にするコンテンツを作る機会も増える時期。
今回はノンデザイナー向けツールとして人気の高い「Canva」と「Adobe Express」を使い、それぞれの特徴を比較してみました。

デザイナーでも迷う?動画加工ツール選び

動画編集はハードルが高いと思われがち。デザイナーという立場でも動画編集する人ばかりではなく、専門的なソフトを使える人は多くいません。写真を加工するよりも、より知識を要するイメージがありますよね。
でも最近は、初心者でも気軽に扱えるツールが増えてきました。今回は、以下のような状況の方におすすめの内容です

  • 忘年会や新年会の動画を急いで作らなきゃいけない!
  • 休暇中に特別感のあるコンテンツをアウトプットしたい!

そんな方々向けに、動画を作成する視点でそれぞれの特徴を掘り下げてみます。

Canvaの特徴

https://www.canva.com/

Canvaは、初心者でも簡単に使えるテンプレートが充実しており、デザイン性の高い動画やスライドがすぐに作れるのが魅力です。
エキサイトでも組織内で導入しており、デザイナー以外のエンジニアやビジネス職も自由に使っています。

テンプレートの素材を差し替えるだけで、プロ並みの仕上がり

Canvaはテンプレートの豊富さが魅力の一つ。スライドやリール広告、印刷物のチラシまで…幅広いフォーマットに対応しています。
テンプレートのジャンルも豊富で、旅行vlogや日常のストーリー投稿、マーケティングコンテンツに活用できそうなものまで様々です。

テンプレートの幅広さが魅力的

動画の加工機能は最小限(トリミング、色調整、素材追加など)で手軽

動画の加工としては、トリミングや分割といった尺の変更に加えて、色味を変更も可能です。CanvaPro(有料プラン)の機能ですが、動画の背景切り取り機能も備えています。

尺の調整や、動画素材そのものの加工も便利

これらの機能は、手持ちの動画素材さえあればクオリティの高いアウトプットに繋げてくれます。「動画にテキストを載せる」や「エフェクトをちょっと足す」といった用途で力を発揮します。

キーフレームのような本格的な機能はないものの、短時間で手元の素材をサクッと調理したい人向けなツールではないでしょうか。

Adobe Expressの特徴

https://www.adobe.com/jp/express/

クリエイティブソフトをいくつも抱える最大手「Adobe」がリリースしたアプリケーション。各ソフトの特徴を織り交ぜたような機能が魅力です。

クイック機能で本格派の仕上がり

アプリのトップからクイックアクションが展開されており、目的別に簡単に作れるようになっています。
編集できる項目自体はCanvaと同じですが、やりたいことベースで機能と直結しているのはAdobeならではかもしれません。

クイックアクションから目的別ですぐ作れる

字幕やキャラクターアニメーションを手軽に作成

PremireProやAftereffectsを連想させるようなアウトプットが作れるのも大きな特徴です
動画の文字起こし原稿がなくとも、元動画から音声を読み取って字幕入れができます。

自動で文字起こしをしてくれるので、スタイルを選ぶだけで字幕付き動画に
他にも、プリセットキャラクターにナレーションして、喋っているかのようなアニメーションを作成できるのも強みです。(エキサイトHDのmissionを鹿のキャラクターに喋ってもらいました)

Adobe製品ならではの加工技術で作るものは決まっていないが色々試してみたい人が触ってみると、楽しくアウトプットを作れるのではないでしょうか。

まとめ:どちらを選ぶべき?

ざっくり、それぞれの強みや特徴をご紹介しました。用途や目的によって選び方は変わりますが、

  • 手元の素材を活かしてサクッと仕上げたいなら → Canva
  • 完成イメージはないがクオリティの高いものを作って見たいなら → Adobe Express

こんな使い分けが個人的にはしっくりきています。
もちろん今回ご紹介した以外の活用方法もたくさんあり、それぞれの強みを活かせば、誰でも楽しく動画コンテンツを作れますよ!
皆さんもぜひ、この年末に試してみてください〜!

SpringBoot + Thymeleafで、JavaScriptに変数を埋め込む方法

はじめに

こんにちは、新卒2年目の岡崎です。エキサイトHDアドベントカレンダー2024の5日目を担当します。

今までの記事はこちらをご覧ください。

qiita.com

さて。今回は、SpringBoot + Thymeleafで、JavaScriptに変数を埋め込む方法を紹介します。

環境

  • Java 試しに23を使っていますが、21でも問題ないです。
openjdk version "23.0.1" 2024-10-15
OpenJDK Runtime Environment Corretto-23.0.1.8.1 (build 23.0.1+8-FR)
OpenJDK 64-Bit Server VM Corretto-23.0.1.8.1 (build 23.0.1+8-FR, mixed mode, sharing)
  • gradle
------------------------------------------------------------
Gradle 8.11.1
------------------------------------------------------------

Build time:    2024-11-20 16:56:46 UTC
Revision:      481cb05a490e0ef9f8620f7873b83bd8a72e7c39

Kotlin:        2.0.20
Groovy:        3.0.22
Ant:           Apache Ant(TM) version 1.10.14 compiled on August 16 2023
Launcher JVM:  23.0.1 (Amazon.com Inc. 23.0.1+8-FR)
Daemon JVM:    /Users/hiromi.okazaki/.sdkman/candidates/java/23.0.1-amzn (no JDK specified, using current Java home)
OS:            Mac OS X 14.7 aarch64
  • SpringBoot
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.4.0)

依存関係

build.gradleに以下の設定がない場合は、追加してください。

 dependencies {    
    // SpringBootの設定
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    
    // SpringBootでThymeleafを使うための設定
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}

実装

今回の目標は、Controllerで設定した変数を、コンソールで表示することです。

Controller

まずは、Controllerで変数の設定を行います。 今回はsampleMessageという変数に、hello worldを値としてセットしました。

実装例は以下です。

@Controller
@RequiredArgsConstructor
@RequestMapping("sample")
public class SampleController {
    @GetMapping
    public String getSample(
            Model model
    ) {
        model.addAttribute("sampleMessage", "hello world");

        return "page/sample/index";
    }
}

html

次にhtmlの実装を行います。

resources以下を、今回は以下の構成にしました。

resources
|
 -- templates
     |
     -- pages
          |
            -- sample
                |
                -- index.html          

index.htmlの実装は以下の通りです。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">

<head>
    <title></title>
    <meta charset="UTF-8">

    <script th:inline="javascript">
        const sampleMessage = /*[[${sampleMessage}]]*/"sampleです";

        console.log(sampleMessage);
    </script>
</head>

<body>
    sample pageです
</body>

</html>

実装の解説です。 今回はThymeleafを使い、Controllerでモデルにセットした値をコンソールに表示します。 JavaScriptで変数を使うため、scriptタグにth:inline="javascript"を設定します。

<script th:inline="javascript">
    // 中略
</script>

scriptタグの中で変数を使う場合、/*[[${ }]]*/の中に変数をセットします。 今回は変数名をsampleMessageとしているので、/*[[${sampleMessage}]]*/となります。

sampleMessageはイミュータブルな値としたかったので、constを使いました。 const sampleMessage = /*[[${sampleMessage}]]*/;だけだと、初期値がないと判断されるため、'const' variable without initializer is not allowedというエラーメッセージが出ます。

したがって、初期値をセットする必要があります。 /*[[${ }]]*/"ここに初期値をセットする"とすれば、初期値を設定できます。

最後にアプリケーションを起動し、指定したエンドポイントにアクセスします。

ページが表示されました。 ここでコンソールを開いて確認すると、sampleMessageに設定された値が表示されました。

最後に

今回は、SpringBoot + Thymeleafで、JavaScriptに変数を埋め込む方法を紹介しました。 どなたかの参考になれば幸いです。

Alpine.jsでdialogタグを制御する方法

こんにちは。エキサイトでデザイナーをしている齋藤です。

エキサイトホールディングス Advent Calendar 2024 シリーズ1の9日目を担当します。

qiita.com

今回はAlpine.jsでdialogタグを制御する方法をご紹介します。

dialogタグとは

dialogタグはHTML標準のダイアログボックス要素です。2022年3月にFirefoxでもサポートされ、広く使用できるようになりました。

HTML標準ということで、表示されたダイアログは自動的に最上位のレイヤーに移動するためz-indexの調整が不要であったり、escキーで閉じられたりと多くのメリットがあります。

一方で制御にはJavaScriptが必要です。

See the Pen Untitled by AyumuSaito (@ayumusaito-excite) on CodePen.

<button id="show-btn" type="button">ダイアログを表示</button>

<dialog id="dialog">
  <p>ダイアログです</p>
  <button id="close-btn" type="button">閉じる</button>
</dialog>

<script>
  const showTriggerBtn = document.getElementById("show-btn");
  const closeTriggerBtn = document.getElementById("close-btn");
  const dialog = document.getElementById("dialog");

  showTriggerBtn.addEventListener("click", () => {
    dialog.showModal();
  });

  closeTriggerBtn.addEventListener("click", () => {
    dialog.close();
  });
</script>

Alpine.jsで制御する

Alpine.jsを使用して制御すると以下のようになります。

See the Pen Untitled by AyumuSaito (@ayumusaito-excite) on CodePen.

<div x-data>
  <button @click="$refs.dialog.showModal()" type="button">ダイアログを表示</button>

  <dialog x-ref="dialog">
    <p>ダイアログです</p>
    <button @click="$refs.dialog.close()" type="button">閉じる</button>
  </dialog>
</div>

$refsを使用してdialogタグのDOMノードにアクセスしてshowModal()close()を実行しています。

$refsについては以下の記事も合わせてご覧ください。

tech.excite.co.jp

コンテンツの外側を押下しても閉じられるようにする

先の例では「閉じる」ボタンを押下しないと非表示にできませんが、コンテンツの外側を押下しても非表示にできるようにすると操作性が向上します。

外側(赤い部分)を押下しても非表示にできるようにする

See the Pen Alpine.jsでdialogを制御(外側押下でも非表示に) by AyumuSaito (@ayumusaito-excite) on CodePen.

<div x-data>
  <button @click="$refs.dialog.showModal()" type="button">ダイアログを表示</button>

  <dialog @click.self="$refs.dialog.close()" x-ref="dialog">
    <!-- ダイアログのスタイルは.dialog-contentに当てる -->
    <div class="dialog-content">
      <p>ダイアログです</p>
      <button @click="$refs.dialog.close()" type="button">閉じる</button>
    </dialog>
    </div>
</div>

まず、dialogタグに@click.self="$refs.dialog.close()"を付与して、自タグが押下された場合にclose()が実行されるようにします。

このままですと、コンテンツの部分(今回の場合は文章と「閉じる」ボタン)を押下しても非表示になってしまいます。

そこで、コンテンツの部分をwrapすることで@click.selfによる自タグのクリック判定から除外させ、dialogタグがshowModal()のときに背後に挿入される疑似要素(::backdrop)の押下のみが判定されるようにします。

まとめ

今回はAlpine.jsでdialogタグを制御する方法をご紹介しました。

通常の方法では、HTMLとJSが分離するためどの要素に対する処理なのかが一見分かりづらかったり、他要素への影響に気を使う必要がありました。

一方でAlpine.jsでは、HTMLの属性としてJSの処理を記述するため要素との関係性も分かりやすく、x-data単位でスコープされるため他要素への影響も気にせずに実装ができるようになりました。

ぜひ、dialogタグとAlpine.jsの組み合わせをお試しいただければと思います。

ご精読ありがとうございました。