【Rails】 Cloud Natural Language API 活用ハンズオン

# 概要
この記事では、GCPの `Cloud Natural Language API`を用いて、登録したタスクに対して、自動でタグ付けを行うTODOアプリの作成をハンズオン形式で行ってみようと思います。

https://cloud.google.com/natural-language

# 対象者
この章では対象者について説明します。
- Railsを用いたアプリケーションの作成経験がある方

# 動作環境
こちらの章では動作環境について明記していきます。

## ハードウェア
- **PC** : MacBook Air (M1, 2020モデル)
- **RAM** : 8GB

## ソフトウェア
- **OS** : macOS Monterey (バージョン 12.1)

## 開発環境
- **Editor** : Visual Studio Code
- **Language** : Ruby (バージョン 3.1.2)
- **FW** : Rails (バージョン 6.1.7)

# 実装
こちらの章では実装を進めていきます。

:::note warn
**注意**
この記事ではコードの意味について深く言及しません。
chatGPTなどのAIツールが普及しており、初心者でも様々な情報を得られやすい環境が整ってきていると思いますので、疑問点等ありましたら調べていただけると幸いです。
:::

## 雛形作成

まずはアプリケーションの実装を進める前に、雛形を準備をします。
```shell:Terminal
~$ rails new TodoApp
~$ cd TodoApp
~ TodoApp $ yarn add @babel/plugin-proposal-private-methods @babel/plugin-proposal-private-property-in-object
```
起動できるか試しておきます。
```shell:Terminal
~ TodoApp $ rails s
```

下記のURLにアクセスし、画像のように表示されていたら問題ありません。

http://localhost:3000

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/43722a24-db2d-0094-24d5-b898ded93d6d.png)


## Bootstrapの導入
今回はCDNで導入します。
ついでに表示するページも全体の75%にし、少々のpaddingを入れておきます。

```diff_erb: views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>BitMaskTodoApp</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
  </head>

  <body>
+   <div class="p-5 w-75 m-auto">
      <%= yield %>
+    </div>
  </body>
</html>
```

## Modelの作成
Taskモデルを作成していきます。
tagsのカラムには、「`,`」区切りでtagを文字列として、保存していくこととします。

```shell:Terminal
~ TodoApp $ rails g model Task title:string description:text tags:string
```

## Gemを追加
Gemfileに、Gemを追加します。
```diff:Gemfile
+ gem 'dotenv-rails'
```
その後、Teminalにて、上記のGemを読み込みます。
```shell:Terminal
~ TodoApp $ bundle install
```

.envファイルを作成し、取得したAPI_KEYを記述しておきます。
取得方法はこちらの記事を参考にしてみてください。

https://naskanoheya.com/catch-up/ai/

```env:.env
GOOGLE_API_KEY="API Keyの値をこちらに記載"
```

:::note warn
**注意**
githubでプロジェクトを管理する場合は`.gitignore`でgitの管理対象から`.env`を外すようにしてください。API_KEYが漏れると、高確率で攻撃されるので気をつけてください。僕はGoogleアカウントを乗っ取られかけました。
```diff:.gitignore
.env
:::


## モデルの記述を変更
モデルの記述を変更していきます。
あまり見慣れない`after_save`はsaveメソッドの実行後に、実行されるメソッドを指定することができるオプションのことです。なのでcontrollerでタスクの登録が行われた後にタグの追加処理が行われるという順番になります。
API関連の記述については説明をし行ってもキリがないので調べてみていただけますと幸いです。
```rb:task.rb
require 'json'
require 'net/https'

class Task < ApplicationRecord
  after_save :assign_tags

  private

  def assign_tags
    # APIのURL作成
    api_url = "https://language.googleapis.com/v1/documents:analyzeEntities?key=#{ENV['GOOGLE_CLOUD_API_KEY']}"

    # APIリクエスト用のJSONパラメータ
    params = {
      document: {
        type: 'PLAIN_TEXT',
        content: self.description
      },
      encodingType: 'UTF8'
    }.to_json

    # Google Cloud Natural Language APIにリクエスト
    uri = URI.parse(api_url)
    https = Net::HTTP.new(uri.host, uri.port)
    https.use_ssl = true
    request = Net::HTTP::Post.new(uri.request_uri)
    request['Content-Type'] = 'application/json'
    response = https.request(request, params)
    response_body = JSON.parse(response.body)

    # APIレスポンス出力
    if (error = response_body['error']).present?
      raise error['message']
    else
      update_columns(tags: response_body['entities'].map { |entity| entity['name'] }.join(', '))
    end
  end
end

```

## APIのテスト
rails consoleを使ってTaskの保存が完了した後に、APIリクエストが正常に動作し、タグとする文字列の配列を取得した上で、Taskを更新できるか確認します。
```shell:Terminal
~ TodoApp $ rails c
```
下記のように表示されたら問題なくAPIと連携ができておりTaskの作成ができています。
```shell:Terminal
Running via Spring preloader in process 18249
Loading development environment (Rails 6.1.7.6)
3.1.2 :001 > Task.create(title:"test1",description:"ここはタスクの説明です");
   (0.5ms)  SELECT sqlite_version(*)
  TRANSACTION (0.1ms)  begin transaction
  Task Create (0.4ms)  INSERT INTO "tasks" ("title", "description", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "test1"], ["description", "ここ はタスクの説明です"], ["created_at", "2024-02-20 14:47:59.391906"], ["updated_at", "2024-02-20 14:47:59.391906"]]
  Task Update (0.2ms)  UPDATE "tasks" SET "tags" = ? WHERE "tasks"."id" = ?  [["tags", "説明, タスク"], ["id", 2]]
  TRANSACTION (4.0ms)  commit transaction
 => 
#<Task:0x00007f2520b636d0
 id: 1,
 title: "test1",
 description: "ここはタスクの説明です",
 tags: "説明, タスク",
 created_at: Tue, 20 Feb 2024 14:47:59.391906000 UTC +00:00,
 updated_at: Tue, 20 Feb 2024 14:47:59.391906000 UTC +00:00> 
```


## Controllerの作成
Terminalにて下記のコマンドを実行し、Controllerを作成します。
`index`と`new`を記述しているのは先に必要となるviewファイルを作成させるためです。
```shell:Terminal
~ TodoApp $ rails g controller tasks index new
```
Controllerの記述を下記のように変更してください
```rb:tasks_controller
class TasksController < ApplicationController
  def index
    @tasks = Task.all
  end

  def new
    @task = Task.new
  end

  def create
    @task = Task.new(task_params)
    if @task.save
      redirect_to tasks_path, notice: 'Task was successfully created.'
    else
      render :new
    end
  end
  
  def destroy
    @task= Task.find(params[:id])
    @task.destroy
    redirect_to tasks_path, notice: 'Task was successfully destroyed.'
  end

  private

  def task_params
    params.require(:task).permit(:description,:title)
  end
end


```

## Viewファイルを変更
`tasks`配下の該当ファイルを変更していきます。
```erb:tasks/new.html.erb
<div class="w-50">
  <h1>New Task</h1>
  
  <%= form_with(model: @task, local: true) do |form| %>
    
    <% if @task.errors.any? %>
      <div id="error_explanation" class="alert alert-danger">
        <h2><%= pluralize(@task.errors.count, "error") %> prohibited this task from being saved:</h2>
  
        <ul>
          <% @task.errors.full_messages.each do |message| %>
            <li><%= message %></li>
          <% end %>
        </ul>
      </div>
    <% end %>
    
    <div class="mb-3">
      <%= form.label :title,"タイトル",class: 'form-label' %>
      <%= form.text_field :title, class: 'form-control' %>
    </div>
    
    <div class="mb-3">
      <%= form.label :description,"説明",class: 'form-label' %>
      <%= form.text_field :description, class: 'form-control' %>
    </div>
  
    <div class="actions">
      <%= form.submit class: 'btn btn-primary' %>
      
      <%= link_to 'Back', tasks_path,class: 'btn btn-secondary' %>
    </div>
  <% end %>
</div>

```

```erb:tasks/index.html.erb
<p id="notice" class="text-success"><%= notice %></p>

<h1>Tasks</h1>
<%= link_to 'New Task', new_task_path %>
<table class="table">
  <thead>
    <tr>
      <th>タイトル</th>
      <th>説明</th>
      <th>タグ</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @tasks.each do |task| %>
      <tr>
        <td><%= task.title %></td>
        <td><%= task.description %></td>
        <td>
          <% if task.tags.present? %>
            <% task.tags.split(',').each do |tag| %>
              <span class="badge bg-primary"><%= tag.strip %></span>
            <% end %>
          <% else %>
            No tags
          <% end %>
        </td>
        <td><%= link_to 'Destroy', task_path(task), method: :delete, data: { confirm: '本当に削除しますか?' },class:"btn btn-sm btn-danger" %></td>
      </tr>
      </tr>
    <% end %>
  </tbody>
</table>
```

## routes.rbを変更
routes.rbを下記のように変更します。
```rb:routes.rb
Rails.application.routes.draw do
  resources :tasks, only: [:index, :new, :create, :destroy]
end
```

# 動作検証
この章では動作検証を行なっていただくために、結果の画面を貼り付けておきます。

## 一覧画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/af53fdee-4eb3-083c-e040-403715e4c64b.png)

## 新規登録画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/541bb563-4bc3-3eaf-7612-3f0145116ed3.png)

## エラー画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/6f0c783e-be3f-454b-1476-36f64551fcd2.png)

## タスク追加
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/e8562260-edfc-2523-b074-12d4f32de666.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/08d83a83-d9c2-d136-c9a4-01ee0e5c0369.png)

## タスクの削除
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/d6d5a039-a663-3f5b-01ce-680dc2646f3d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/9512fdd4-55da-316e-fa08-b28b2f6a8a76.png)

# 参考文献

https://naskanoheya.com/catch-up/ai/

# おわりに

今回は最近勉強した`Natural Language API`を使ってTodoアプリを作ってみました。
APIを使うと色々なことができるので、Todoアプリ内のタグ付けだけでなく、様々なケースで試してみたいなと思います。
Qiitaの記事にできないようなしょうもない内容も自分のブログで掲載しているので、見ていただけると幸いです。

https://kt-tech.blog