UUUM攻殻機動隊(エンジニアブログ)

UUUMのエンジニアによる技術ブログです

秋の開発合宿を開催しました!

f:id:c14043:20190930155209j:plain

こんにちは!今回開発合宿のブログ担当する、インターンの鷲見です!

システムユニットでは年2回、開発合宿を行なっています。

今回行なった場所は湯川原にある「おんやど恵」という旅館です。開発合宿(旅行)ではよくお世話になっているのでところです。

  • 都内から二時間ほどで行ける
  • 開発合宿用のプランがある
  • 24時間使える会議室がある
  • Wi-fi環境が整っている
  • 温泉と足湯

といった環境がおんやど恵は整っているので、開発合宿をするのにとても便利です。

www.onyadomegumi.co.jp

旅館紹介

実際にどんな旅館なのか紹介していきます!

会議室

こちらが会議室です。一人、一人スペースが用意されているので、場所に困ることはありません。また成果発表用にプロジェクターも用意してもらいました!

f:id:c14043:20190930122526j:plain

温泉

温泉は夜、早朝といつでも入ることができるので最高ですね!

f:id:c14043:20190930122402j:plain

中庭には足湯があり、入りながら開発もできるのでとても快適です!

f:id:c14043:20190930122103j:plain

食事

食事はコースで出てきます。味もボリュームも十分あるので大満足です。

f:id:c14043:20190930123726j:plain

このお肉と味噌組み合わせはとても美味でした!

f:id:c14043:20190930123737j:plain

開発は何をしたのか?

今回の開発合宿はテーマは「自由開発」です。

日頃業務ではやらないことに挑戦したり、技術をより理解すること、PMの方達はチームが力を最大限発揮するための研究にたくさんの時間を使うことができました。

f:id:c14043:20190930125118j:plain

お昼の2時ごろから着々と取り掛かっていきます。

深夜の追い込み

食事をした後、会議室組は深夜の追い込みをかけてきます。

f:id:c14043:20190930125905j:plain

支給品も届き、仕上げに取り掛かっていきます。

f:id:c14043:20190930143758j:plain

翌朝

朝に少し作業した後、成果発表を順番にしていきます。中には徹夜して限界を迎えてしまう人も。。。

f:id:c14043:20190930154959j:plain

皆さん短い時間の中で高い成果を上げていました。。。

日頃の業務外の技術を使って作られた作品が多かったので、発表はとても面白いものとなりました。

f:id:c14043:20190930160150j:plain

まとめ

2日間に渡り開発に集中できる素晴らしい環境を整えてくださったおんやど恵の皆様ありがとうございました。

すばらしい環境だからこそ、短い時間の中でもそれぞれ成果を出すことができました。

開発と発表を通して、それぞれ知識を共有できたのでとても有意義な時間を過ごせてよかったです。

最後は集合写真終わろうと思います。

f:id:c14043:20190930160745j:plain

Athena + Glue + (Terraform)でいい感じにファイル上のデータを集計しよう

システムユニットのt_u_a_kです。ブログ登場は初めてです。私は業務で少々大きめのデータの集計ということをやっていますが、その際にはAWSのAthenaとGlueを試しました。手軽でよかったので紹介します。

AthenaとGlueについて

まずAthenaについてですが、これはS3上のデータに対するクエリサービスです。データベースに対するクエリサービスではなく、S3上のテキストファイル(もしくはそれらを圧縮したりしたもの)に対してデータ構造を定義し、いわゆるSQLを使って普通にクエリが書けます。この時点でAthenaのようなサービスや仕組みに触れたことがない人にとっては「は?」って感じですね。AthenaはPrestoというFacebookが開発したクエリエンジンを使っていて、大きなデータでも爆速で結果が返ってきます。Presto公式ではFacebook自身が300PB以上のデータの分析に利用していると書いています。ここまで来るとでかすぎて想像もつきませんが、通常の利用であれば本当に驚くほどのスピードでクエリの結果が返ってきます。

aws.amazon.com

次にGlueです。GlueはいわゆるETLサービスというやつです。最近だとデータウェアハウスを構築するところも多いと思いますが、いろんなところにいろんな形で存在しているデータを変換したり移動させたりいい感じに集めるときに使うサービスというのが非常に雑な説明です。Glueにはデータカタログという機能があり、これがAthenaと一緒に使える機能です。Athenaはクエリサービスと書きましたが、Glueで定義したデータカタログをいわゆるデータベースやテーブルと見てクエリをすることができます。これではわかりにくいと思うので下で説明していきます。

aws.amazon.com

今回使うデータ

例えばこんなデータがあったとします。

# hikakin.csv
video_id,title,channel_id,views,date
zW4HJkuFFtc,【ランキング】ヒカキンがガチでウマいと思うセブンの商品トップ3!【2019年5月編】,UCZf__ehlCEBPop-_sldpBUQ,1826863,20190518
VY0EXQqSeRc,顔面が大変なことになりました…,UCZf__ehlCEBPop-_sldpBUQ,1650048,20190517
MgeD8MB6fyg,金属アレルギー検査したらまさかの結果が…【1900万円の時計のその後】,UCZf__ehlCEBPop-_sldpBUQ,1906878,20190515

皆様ご存知のヒカキンさんのチャンネルHikakinTVの動画について、2019年5月のある時点でのデータです。1行目が動画IDや動画タイトルといったヘッダー、2行目以降がデータになっています。他にもこのような似たデータを用意しました。0214mexはじめしゃちょーさんでtsuriyoka釣りよかでしょうさんの動画に関するデータです。釣りよかでしょうさんに関してはサブチャンネルのデータも混ざっています。これらがそれぞれhikakin.csvhajime.csvtsuriyoka.csvという名前で存在している、もっというとUUUM所属の各チャンネルごとにこのようなデータがあると仮定して、Athenaでクエリをかける準備をしていきます。 (※0214mexなどを見ても何のこと?と思う方もいらっしゃるかもしれませんが、はじめしゃちょーさんのYouTubeチャンネルについてはhttps://www.youtube.com/user/0214mexというURLでもアクセスできるようになっており、他のチャンネルでもこれに似た形でURLが表記されることがあります。)

# 0214mex.csv
video_id,title,channel_id,views,date
dmpYE9u3cr4,"【50,000枚】3年間集めた遊戯王カード全部売ったら金持ちになったwwwww",UCgMPP6RRjktV7krOfyUewqw,1280160,20190520
xUFNcNQ24Ho,家に隠されたドラゴンボールがガチで見つからない件。,UCgMPP6RRjktV7krOfyUewqw,1055697,20190519
STA6C9ftvmw,コンビニ弁当をミキサーでドロドロにしても味が分かる説,UCgMPP6RRjktV7krOfyUewqw,969160,20190517
# tsuriyoka.csv
video_id,title,channel_id,views,date
d0XrWwauyQY,むねお船は初心者に優しいらしい,UC4QadOSsJu54Qs8z99shRiQ,136100,20190521
dwXSckt1lM4,庭でイカを料理してたら変なの来た,UC4QadOSsJu54Qs8z99shRiQ,215405,20190520
1AuvYVUMNGs,リフォームした家の壁に缶スプレーでらくがきしてみた!,UCD7-Ocp4InwPKzwiq_U-Abg,230155,20190519
ew0HdXG3SKQ,ドラム缶でオリジナルBBQコンロを作る!,UCD7-Ocp4InwPKzwiq_U-Abg,168260,20190521
j5aQ_Y3_I_U,新居キッチンの紹介します!,UCRT-74ovdMKOFBC96w_gfyQ,105864,20190521
YC1OnPK2fWE,ミルフィーユカツで新潟のB級グルメ作ってみた!,UCRT-74ovdMKOFBC96w_gfyQ,73117,20190520

S3へのデータの保存

AthenaはS3上のデータに対するクエリサービスなので、まずはデータをS3に置かなければいけません。今回は

  • 2019年5月のデータ
  • チャンネルごとに別ファイル

となっているので、仮にデータが増えるとすれば別の月のデータだったり、違うチャンネルでファイルが作られるということが考えられます。このような場合にはS3に保存する際に

  • s3://my-bucket/year=2019/month=5/channel=hikakintv/hikakin.csv
  • s3://my-bucket/year=2019/month=5/channel=0214mex/hajime.csv
  • s3://my-bucket/year=2019/month=5/channel=tsuriyoka/tsuriyoka.csv

というふうに/key=value/のパーティションを作ってあげることで、Athenaでクエリをかける際にまるでyearchannelといったカラムがあるようにクエリをすることができます。channelのカラムが1チャンネル1つに対応していないじゃないかとかchannelキーに対するvalueのルールがおかしいんじゃないかといった意見もあるかと思いますが、今回tsuriyoka.csvに複数チャンネルのデータが入ってしまっているため、ファイルの中を見る前はパーティションを厳密にチャンネル単位で分割することができません。パーティションについては細かくすればするほどクエリの条件を細かく設定できるので料金面で有利になる一方、ファイルをS3に設置するコストが高くなってしまうなどもあるので、どの程度の粒度で分析や集計を行いたいかによってパーティション設計は考えていくべきだと思います。自動的にS3にファイルが流れてくるような状況から作るのであれば、パーティション設計はますます重要です。

docs.aws.amazon.com

データ構造を定義する

今回のデータを見たときに、どういうクエリがかけるでしょうか。(本来はどういうことを知りたいかが先に来るべきですが今回はちょっと置いておきます。) 例えば、

select sum(views) as total_views
from s3上のデータ
where year = 2019 and month = 5 
and channel in ('hikakintv`, 'mex0214`)

とか

select videoid, views
from s3上のデータ
where year = 2019 and month = 5 
order by views desc
limit 5

などがあるでしょうか。別の年月やチャンネルが増えればもっといろんなパターンが出てくるとは思いますが、共通しているのはs3上のデータをテーブルとして、CSVの各カラムやパーティションをselect文に入れたいということです。これをやるためのデータ構造をGlueで定義してみましょう。

Glueでデータカタログを定義する

AthenaでクエリをするためにGlueでデータカタログを定義しますが、データカタログを定義するとはデータベースとテーブル(およびその中のカラム)を定義することです。Glueにおけるデータベースとはいろんな場所にあるデータを統一的に扱うためのメタデータストアになりますが、データベースが必ずどこかのバケットと紐づくなどは無いので、実際あまり意識する必要はありません。必要に応じて作っておけばいいものになります。今回作成したデータベースについても名前以外は何も決めていない状態です。

f:id:xxuxa_k:20190928063135p:plain

次はテーブルです。今回のようにAthenaでのクエリを目的としたテーブル作成においては、テーブルとバケットが1:1だと思っておくのがよいと思います。テーブルに指定する内容は大きくわけてスキーマとそれ以外のオプションになります。オプションの項目としてはどのようにデータを解釈させるかを決めるHadoopのフォーマットなどがあります。かなり細かく指定できるようですが、私もすべては把握できていません。いろんな形式のデータを使ってみたりすることでだんだんわかるようになってくるのだと思います。今回作ってみたblog_test_tableは以下のようなデータカタログ設定になっています。

f:id:xxuxa_k:20190929052618p:plain

f:id:xxuxa_k:20190928063532p:plain

Terraformの利用

↑の画像に出したデータカタログの内容をAWSコンソール上から設定することは可能です。しかし、設定の初期などはパーティションを変えることがあったり、なぜかAthena側でデータカタログのロードにうまくいかなかったりと、最初から作り直したほうがいろいろと楽な場合が多いです。(個人的にはHadoopだったり分散処理に関する知識が足りないと十分にエラー調査していくことが難しいのかなと思っています。) 作り直すたびにAWSコンソール上でポチポチやっていたこともあったのですがつらすぎるので、データカタログはTerraform化してしまいましょう。テーブルの追加も作り直しもterraform apply一撃で済むようになることでかなり作業が楽になるのと、クエリを書いて調査・分析をするという本質的な作業に時間を割くことができるようになります。もちろんTerraformの代わりにCloud Formationもありだと思います。 データカタログ部分のterraformコードはこのような感じです。もともとはv0.11系で動くように書いていたのですが、今回ブログを書くにあたってv0.12.9で動作するように書き換えました。(terraform 0.12updateコマンドがあるので基本的には自動で書き換えてくれましたが、一部自分で直すような感じになりました。) ポイントとしてはstorage_descriptor.ser_de_infoかなと思います。S3に保存してあるファイルの形式によってここが変わってきます。今回はCSVを使っていますが、TSV、JSON、Hadoop形式などにも対応していて、形式に合わせてSerDeというシリアライズ/デシリアライズライブラリにオプションを与えることになります。

locals {
  blog_database_name   = "test_catalog_db"
  blog_bucket_location = "s3://xxx/yyyy/..../"
}

resource "aws_glue_catalog_database" "test_catalog_db" {
  name         = local.blog_database_name
  description  = ""
  location_uri = ""
  parameters   = {}
}

resource "aws_glue_catalog_table" "blog_test_table" {
  name          = "blog_test_table"
  description   = ""
  database_name = local.blog_database_name

  table_type = "EXTERNAL_TABLE"

  parameters = {
    EXTERNAL                 = "TRUE"
    has_encrypted_data       = "false"
    classification           = "csv"
    "skip.header.line.count" = "1"
  }

  partition_keys {
    name    = "year"
    type    = "int"
    comment = ""
  }
  partition_keys {
    name    = "month"
    type    = "int"
    comment = ""
  }
  partition_keys {
    name    = "channel"
    type    = "string"
    comment = ""
  }

  storage_descriptor {
    input_format  = "org.apache.hadoop.mapred.TextInputFormat"
    location      = local.blog_bucket_location
    output_format = "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat"

    ser_de_info {
      name                  = "blog_test_ser_de"
      serialization_library = "org.apache.hadoop.hive.serde2.OpenCSVSerde"

      parameters = {
        quoteChar     = "\""
        separatorChar = ","
      }
    }

    columns {
      name    = "video_id"
      type    = "string"
      comment = ""
    }
    columns {
      name    = "title"
      type    = "string"
      comment = ""
    }
    columns {
      name    = "channel_id"
      type    = "string"
      comment = ""
    }
    columns {
      name    = "views"
      type    = "int"
      comment = ""
    }
    columns {
      name    = "date"
      type    = "string"
      comment = ""
    }
  }
}

データカタログ部分についてはこれだけです。CSVの形式の変更などがあったときもコード変更とterraformの適用だけです。S3バケットもterraform化できていればより完璧です。 docs.aws.amazon.com

Athenaでクエリする

あとはAthenaでクエリをするだけです。AWS CLIからだと結構柔軟に設定できますが、今回はAthenaのコンソール上からやります。忘れてはいけないことですが、S3にデータを追加したら、MSCK REPAIR TABLE test_catalog_db.blog_test_table;というSQLを流しておきましょう。これをやらないとAthenaが新たに追加されたファイルを認識できなくなります。

f:id:xxuxa_k:20190928065421p:plain

select * from blog_test_tableをしてみると、3.22秒と出ました。仮にこれがRDSであればどう考えても遅すぎますが、Athenaはあまりに小さいデータに対してはRDBMSよりもパフォーマンスが悪くなります。逆に大きいデータに対してはとんでもないスピードで結果を返してきます。今回についてはクエリを投げられるようになることが目的なのでここまでにしておきますが、実際にやったことはバケットとデータカタログの設定だけなのでかなり簡単にCSVのようなデータに対してもクエリが投げられることがわかったと思います。もちろん形式はCSVだけでなく、多様な形式をサポートしています。

f:id:xxuxa_k:20190928080139p:plain

料金について

Athenaはスキャンしたデータに対する従量課金のため、スキャンデータを減らすことが利用料金を減らす一番の近道です。同時にスキャンデータを減らすことは処理速度を上げることにもつながります。 データは圧縮形式でも扱えるため圧縮してS3に入れる、Parquetなどの列形式へ変換するなどの方法も役に立つと思います。

まとめ

AthenaとGlueのデータ集計での利用について書いてみました。今回ほど小さいファイルであればSQLiteを使うのもありだと思いますが、実際にはそこそこのサイズがあると思うので、ファイル上のデータについて単純なクエリをしたいということだとこれが一番簡単な気がします。AWSマネージドなので管理コストも最小限です。

最後に

やっと1回ブログ書けた、、、。システムユニットでは一緒に働いてくれる方を募集中です。

recruit.uuum.co.jp

Railsプロジェクトを引き継いでから安定させるまでに行ったこと

nazoです。現在は厳密には攻殻機動隊(システムユニット)ではないのですが、開発に関する大きな動きがありましたのでまとめてみたいと思います。

何の話?

LMND というシステムがあるのですが、これの開発を急ピッチで進めたいという事で急遽移動することになり、快適な開発環境になるまでに行ったことについての話になります。

LMNDのサービスの詳細についてはここでは割愛させて頂きますが、システム的にはRails4系にMySQLという、少し古いものの一般的な構成で作られたものになっております。2018年9月に吸収合併を行ったのですが、その際に開発体制が完全内製に切り替わってしまったため、引き継ぎがほとんどない状態から体制が切り替わってしまったという状態になります。社内の開発者が先に割り当てられていたのですが、困っているので助けてくれということで私が途中から入ることになりました。

作り直すかどうか

事前情報では「やばかったら作り直してもいいよ」とは言われたのですが、作り直しというのは一見楽しそうですが難しい問題を多く抱えています。

  • サービスをやり直すわけではないのでデータは既存のまま使えるようにしないといけない
  • 機能が多く、かつそれらが契約上廃棄できるものではない場合、それらを全て同様に移植しなければならない

一方で、それでも作り直さないといけないという場合も存在します。

  • コードの汚さがあまりにどうしようもなく、何をするにもほぼスクラッチで開発するしかないような状態
  • フレームワークなどが一般的ではないまたは独自のもので、今後のアップデートに対応することがほぼ不可能な状態
  • 予算が十分にあり、かつ作り直しのビジョンが明確にある場合

ただし予算があるケースの場合以外は、最終的には全て作り直すとしても一部分から作り直すとか新しいものと古いものを共存できるようにするとか、様々な手法があると思います。規模の大きなアプリケーションの場合、そのアプリケーションが解決しようとしているもの(事業)が何かを適切に見極め、交通整理を行うことでどこをどう作り直すべきなのかが見えてくると思います。

今回のプロジェクトはRails製で、コードが読みにくい箇所が多いもののRailsそのものを魔改造しているというわけでもなく、作りながら書き直すことが十分に可能なレベルでしたので、基本的には作り直さないという方針を採ることにしました。

続きを読む

polidogさんをお招きして社内勉強会をしました

こんにちは、今年新卒で入社したエンジニアの中村です。

6月28日(金)にパーティーハード株式会社のpolidogさん(@polidog)をUUUMにお招きし、Symfony使いの為の勉強会を行いました!!

4月からSymfonyを使い始め、苦しんでいた私にとってもすごく有意義な時間となりました。

f:id:chibiProgrammer:20190702114021j:plain
写真右奥がpolidogさんです

Symfonyとは、Webアプリケーションフレームワークで、 弊社ではクリエイターポータルサイト「CREAS」の開発にSymfonyを使っています。 普段の開発では柔軟なSymfonyアプリケーションを作ろうと意識しても、 実際どう設計すればいいのか分からないことが多く、今回の勉強会で意識するべきところを教えて頂きました。

以下勉強会の資料になります。

speakerdeck.com

弊社のエンジニアもSymfonyに興味がある人が多く、勉強会の後の質疑応答では沢山質問にお答えしていただきました。 以下、質疑応答で出た内容になります!

  • SymfonyのJob Queue はないのか?

    - BCCResqueBundle
    - JMSJobQueueBundle
    
  • Security.ymlが解決されるタイミングは?

  • Security.bundleってcomposerとかじゃなくてSymfonyで提供されているのか?

    - composerである。Symfonyのコアチームが提供している。
    
  • 1コントローラーつき1メソッド(コンストラクタを除く)とした場合、CRUD等の機能はどのように分けているか?

    - ディレクトリで切っていって、名前空間で分けている。
    

今回の勉強会でリファクタを強く意識しましたが、polidogさん曰く 頑張ってリファクタしすぎるのは良くない、動いているコードが最終的には正解だと思っているが、 変化させるタイミングの時にちょっとづつ綺麗にしていくのが大事とのことだったので、 日頃から少しづつ意識して今あるコードを綺麗にしていきたいなと思いました。

今回はSymfonyについて色々知ることができ、とても楽しい会となりました!

polidogさん、お忙しい中お越しいただきありがとうございました!

www.wantedly.com

AWS Summit Tokyo 2019 に行ってきました。

こんにちは、エンジニアのオークボです。
会社のカンファレンス参加制度を利用して先日、幕張メッセにて行われたAWS Summit Tokyoに参加をしてきました。この記事はカンファレンスでの忘備録兼まとめを書きたいと思います。

aws.amazon.com

youtu.be ダイジェストはこちら

経緯

今回カンファレンスに参加したきっかけは、業務で毎日AWSを触るため、もっと知見を深めたいと思ったのと、以前社内ブログに、先輩エンジニアのめるさんが RubyKaigi2019 に行ってきた。という記事を上げていたのを見て触発された部分もあったからです。 

system.blog.uuum.jp

内容

以下が私の参加したセッションのリストです。

- AWS環境における脅威検知と対応
- Edge Servicesを利用したDDoS防御の構成(AWS WAF/Shield)
- 「ネットワークデザインパターンDeep Dive」
- Security Best Practices on S3
- Startup Architecture of the year 2019 ピッチコンテスト
- サービスメッシュは本当に必要なのか、何を解決するのか
- Amazon SageMaker で実現する大規模データのための分散学習とワークフロー
- Fate/Grand Orderにおける大規模なデータベース移行と負荷試験

ピックアップメモ

こちらは、一番面白かった「AWS環境における脅威検知と対応」 のメモです。

  • 脅威の検知をするためには以下の4つのサービスを組み合わせて使うと良い

    • Amazon GuardDuty
    • AWS Config
    • AWS CloudTrail
    • Security Hub
  • Security Hubは セキュリティ関連等の状態を一覧表示できるのでとても便利

    • 現状パブリックプレビューとなっていてお試しができる様子
    • 脅威が見つかったインスタンスなどに対する各種アクションが行える。以下はslackに通知をするカスタムアクションのサンプル
  • セキュリティワークショップという擬似的に攻撃を行って各種対応の訓練などができるツールもある

aws.amazon.com

github.com

awssecworkshops.jp

このように、どのセッションも普段の業務に直結するような内容が話されていましたし、AWSを利用している各社の事例が紹介されていました。

感想

どのセッションも超満員で熱気がすごかったです。インフラ関連ツールを展示をしている企業さまなどが多く、今まで知らなかった有意義な情報の交換ができました。
セッションは、サイレントセッション(イヤホンとレシーバーにて講演を聞く方式)は初めてでしたが、広い会場で皆がイヤホンをして画面を見上げているのはなんとも不思議な体験でした。
AWS Summit をより楽しむには、もっとAWSを活用してゆき、自分の中に疑問を貯めていくと良いのだろうなと感じました。

まとめ

以上が、簡単でしたが、AWS Summit Tokyo 2019に参加をしてきたレポートでした。
来年はパシフィコ横浜にて開催されるようです。一年後に向けてもっとAWSを活用して、勉強して行くぞ~。


最後に

UUUM では、AWSなどのインフラ関連技術に興味があったり、カンファレンスに参加するのが大好き! 技術大好き! というようなエンジニアを募集しています!! 詳しくは以下のリンクをご覧下さいませ。

www.wantedly.com

「僕」でもわかるSEO対策、基礎の基礎の基礎

こんにちは UUUMでエンジニアしてるながいです。

題名の通り「僕」でもわかるSEO対策です。ちなみに「僕」はまだパソコンを初めて買ってから1年のキラキラ✨✨✨✨✨✨、ペーペーの「僕」です!

なんでこの記事を書くかというと個人的に気になるから、それだけです。サイトのSEO対策をしてみたかったので、、、

SEO対策すればそれなりのサービスでも人が来るんじゃない?という安直な考えからきています。

逆に言えばすごいサービスでも人に知られなければ使われることはないわけですし。多分、たぶん、tabun、、、

さて!

そもそもSEOってなんの略?

これよくSEOって耳にはしますけど実際なんの略なの?ってところから

こういう使ってるけど知らないとか、読み方違う系の言葉が無限にこの界隈にはありますよね。

ちなみに僕はREADMEを「レドメ」ってなんなんって4ヶ月くらい思いながら読んでました!

Search Engine Optimization:検索エンジン最適化

らしいです。これ知ってる人どれくらいいるのかな、、、僕はもちろん知りませんでした!!

まぁでも読んで字のごとくですね、SEOってそういうものだよね確かにって感じがしますね。

Googleとかの検索エンジン上位に表示されるように自分のサイトを最適化して行こう!って感じですね。

どういうアルゴリズムでGoogleは検索上位を作ってるの?

Googleは色々な要素で上位に表示する項目を決定しているようです。その数は200以上!!!

そして最も重要な要素と言えるのが「被リンク」と「コンテンツ」と言われるものらしいです。

まず被リンクってなんだよって感じです。これは自分のサイト以外に表示されている自分のサイトのリンクいわゆるURLの事のようです。

例えば自分のサイトを誰かがブログでシェアしてくれたりしたとします。その際に、リンクを貼ってシェアすることになると思うのですがそれがいわゆる「被リンク」と言われるもののようです。

しかしこの被リンクには良質なものと低質なものがあるようで、自然な被リンク、例えばシェアなどが良質なものとされるようです。低質なものというのは関係のないリンクのみが羅列掲載された相互リンク集などのコンテンツを指すようです。

この低質なリンクというのはSEO効果を下げてしまうようなので、良質な被リンクだけを増やすようにした方がようさそうですね。とはいえシェアしてもらうにはコンテンツが重要ですので被リンクに関しては余計なことをしなければ良いという感じでしょうか。

となるとエンジニアリングサイドで対策できるのは「コンテンツ」に関してかと思われます。具体的にはそのコンテンツのどこを評価しているのでしょうか?

  • Expertise(専門性)

  • Authoritativeness(権威性)

  • Trustworthiness(信頼性)

どうやらGoogleはこの3つを評価すると明言しているらしいです。

ちなみに日本で使われてる検索エンジンの90%以上がGoogleらしいのでとりあえずGoogleでの対策を押さえておけばいいと思います。というか他のエンジンの対策してる人はいるんでしょうか。

まぁ要するにこのGoogleの評価基準から考えるとコンテンツはユーザーファーストでなおかつ、ユーザーにとって質の高いコンテンツを評価しますよってことかと思います。

具体的にどんな感じでコンテンツを作ったら良いかを次に考えて行きましょう。

どんなコンテンツにすれば良いの?

まずどのキーワードでページをアルゴリズムに評価されるかを考えなければいけません。

原則として1ページ1キーワードという概念があるようです。

例えば「UUUM 所属youtube」って検索した人の上位に次ページを出したいなと思ったらUUUMに所属するyoutuberの一覧が載ってるページを見たいわけで、多数のyoutuberのそれぞれの所属事務所が見たいわけではないはずです。

なのでUUUM所属のYouTuberというキーワードでコンテンツを作らないと「UUUM 所属YouTuber」には引っかからなくなるのです。

そしてさらにコンテンツ量(文字数)が大事なようです。文字数の少ないページは低品質なページとみなされるようです。

確かに検索してて読み応えのないページってあんまり上にほうに出てこないですよね。

具体的には800~1000文字以上あれば低品質なページとはみなされないようです。

比較的難易度の高いキーワードでも2,000~4,000文字程度のコンテンツが上位に表示されているらしいです。

「UUUM 所属YouTuber」だったらそんなに難易度の高いキーワードとも取れないきがするので1200文字くらいでいいんじゃないでしょうか?(適当です)

HTMLタグの重要性について

さてコンテンツの作り方の方針が決まったところで細かなHTMLのタグなどの対策をしていきましょう。

重要なのはtitleタグh1タグだろうと素人ながらに軽く知っています。実際やはりそのようですね。しかしここでもう一つ超重要タグがあるようです。それがmetaタグ

とりあえず一つずつ追っていきましょう。

titleタグ

これは検索したるしたらそもそもみなさんみる見出しっぽいあれになるやつです。Googleくんもここを重要に見てるらしいですよ。

<title>ここにページのタイトルを記述</title>

ポイントは簡潔でキーワードを含めること

そして30文字程度にすること。目立たせること。

また他サイトとの重複を避けることも大切です。

以上を踏まえて、先ほどの「UUUM 所属YouTuber」の例だとしたら、「【UUUM所属】のYouTuber一覧!6000人越え!?人気YouTuberが勢揃い!!

とかでいいんじゃないでしょうか?(適当です)

h1タグ

これもまた重要なポイントです。

<h1>ここに見出しを記述</h1>

そして1ページ1キーワードの原則どおり1ページに対してh1タグは1つしかつけてはいけません。

というのが常識かと思ったのですがGoogleは何個あっても問題はないと声明しているらしいです。

ケースバイケースといったところでしょうか。

そして内容は「キーワード」を入れた、コンテンツに合った「簡潔な文」で作成しましょう。

これは超重要ですね。

SEOを知らないとなんか文字で隠したいしh1にしとくかとかいうノリでつけがちですが、h~系のタグは意識してつける必要が非常にありそうです。

metaタグ

これは僕も見落としていたのですがmetaタグもめちゃくちゃ重要ですね。

metaタグにも色々あると思うのですがこれはmeta description についてです。

<meta name="description" content="ここにページの説明を記述" />

よく検索した結果一覧を見るとtitleのしたに内容の軽い説明見たいのがありますよね。それがこれです。

あの文を見て僕は結構クリックするか決めるのでそういう意味合いが強いかと思います。

直接的に表示順位とは関係はなさそうですね。

クリックしたくなるような説明書きをかければ良さそうです。

ただ一つ注意点があり、pcとspでは表示される文字数に違いがあるようです。

SP 50文字程度
PC 120文字程度

のようですね。どちらに重きを置きのか、もしくはスマホとPCで変更できる機能を作るのも吉ですね。

まとめ

ここまで調べたことを書いてきましたがとりあえずはこの辺りに注意をしてサイトを作れれば初心者としては十分なSEO対策ができるのではないでしょうか?

もちろんSEOというのは奥がめちゃくちゃ深いものなので突き詰めていけばキリはないと思います。

手っ取り早くサイトの閲覧数を稼ぐために最低限のことはしておきたいですよね。

エンジニアとしては作ることが楽しいですけど、こういうコンテンツも重要視していかなければどんなにいいサービスも作ってもらえなくなってしまいます。

エンジニアとしていろんな方面へ手を出してみることが大事ですね〜

www.wantedly.com

インターンで初プルリクエストを出してマージされるまで

こんにちわ。2019年4月15日にUUUMにインターンとして入社したれとるときゃりーです。

インターンに入るまで、個人でサービスを作るなどして開発はしていましたが、実際の会社に入ってコードを書くのは初めてでした。

ちょうどUUUMに入って任された一つ目のタスクが終わったので、振り返りを込めてブログに執筆していこうと思います。

f:id:retoruto-carry:20190529170529p:plain
プルリクがマージされたところ

前提条件

laravel+nuxt.jsのプロダクトにアサインされました。

どのような実装を任されたか

外部のRSSを受け取って、それをサービス内で表示するようにします。

※ RSSとは

RSSは、ニュースやブログなど各種のウェブサイトの更新情報を配信するための文書フォーマットの総称である。( wikipediaより引用)

どのように実装したか

※ コードは一部省略しています

実装の方針

  • バックエンド側は、RSSを受け取り、タイトルやサムネイル画像のURLなど必要な部分のみを抜き出してAPIとして返します

  • フロント側は、バックエンド側のAPIを叩いて表示します

バックエンド側(laravel)

RSSを受け取る

ライブラリを使えば簡単でした。willvincent/feedsを使用しました。

サムネイル画像は簡単に取得できませんでした。記事中の一番最初に出てくるimgタグの中のsrcを取ってくる必要がありました。

画像のパース部分は、最初は正規表現で実装していました。

// 記事の本文の中からimgタグを抽出して画像URLを取得
$pattern = '/<img.*?src\s*=\s*[\"|\'](.*?)[\"|\'].*?>/';
if (preg_match($pattern, html_entity_decode($feedItem->get_content()), $match)) {
    $image = $match[1];
} else {
    $image = null;
}

しかしこの実装だと、記事側でのタグの表記揺れがありうまくいかないときがありました。

正規表現はつらいのでやめました😇

HTMLパーサーのライブラリammadeuss/laravel-html-dom-parserを導入して解決しました。

// 記事の本文の中からimgタグを抽出して画像URLを取得
$image = HTMLDomParser::str_get_html(html_entity_decode($feedItem->get_content()))->find('img')[0]->attr['src'];

テスト

テストは書いたことがなかったので、かなり苦戦しました。 他のテストを参考に、laravelとPHPUnitのドキュメントを見ながら書きました。

上述した、willvincent/feedsをモックする必要があったので、中身を少し読む必要がありました。

個人でサービスを作っている中では、ライブラリのコードをまったく見たことなかったので新鮮でした。

「Qiitaなどで調べるより、直接コードを見たほうが早いので癖をつけたほうが良い」ということを先輩に教えてもらいました。

エディタのコードジャンプ機能も、この時教えてもらいました。(今まで知らなくて1年位やってきたので便利すぎて泣きました😭)

書いたテストはこんな感じです。

<?php
namespace Tests\Unit\ControllerTest\Api\V1;
use Mockery;
use Tests\TestCase;
use App\Http\Controllers\Api\V1\HogeController;
use Feeds;
class HogeControllerTest extends TestCase
{
    private $hogeController;
    /**
     * {@inheritdoc}
     */
    public function setUp()
    {
        parent::setUp();
        $this->hogeController = new hogeController();
    }
    /**
     * Test Get List Hoge
     *
     * @dataProvider listHogeDataProvider
     * @param array $hoges データセット
     * @param array $expect 期待値
     * @return void
     */
    public function testFetchListHoge(array $hoges, array $expect)
    {
        $fetchedTopics = [];
        foreach ($hoges as $hoge) {
            $simplePieItem = $this->mock('SimplePie_Item');
            $simplePieItem->shouldReceive('get_content')->andReturn($creatorTopic['content']);
            $simplePieItem->shouldReceive('get_title')->andReturn($creatorTopic['title']);
            $fetchedTopics[] = $simplePieItem;
        }
        $feed = $this->mock('SimplePie');
        $feed->shouldReceive('get_items')->withAnyArgs()->andReturn($fetchedTopics);
        $feed->shouldReceive('get_title')->andReturn('title');
        Feeds::shouldReceive('make')->with(Mockery::any())->andReturn($feed);
        $response = $this->json('GET', 'api/v1/hoge');
        $response->assertJson($expect);
    }
    /**
     * @return array
     */
    public function listHogeataProvider()
    {
        return [
            "正常" => [
                [
                    [
                        'content' => '&lt;figure class=&quot;block-image&quot;&gt;'
                            . '<img src="https://example.com/uploads/img1.png" alt="image" width="auto" height="auto">'
                            . '&lt;/figure&gt;',
                        'title' => 'title1',
                    ],
                    ...
                ],
                [
                    'title' => 'title',
                    'hoges' => [
                        [
                            'title' => 'title1',
                            'image' => 'https://example.com/uploads/img1.png',
                        ],
                        ...
                    ],
                    ...
                ]
            ],
            

        ];
    }
    /**
     * @param $class
     * @return Mockery\MockInterface
     */
    private function mock($class)
    {
        $mock = Mockery::mock($class);
        $this->app->instance($class, $mock);
        return $mock;
    }
}

DataProviderを利用することで綺麗にまとまりました。とても便利ですね。

フロント側(nuxt.js)

記事の表示

プロジェクト内はアトミックデザインを意識している構成でした。

アトミックデザインは、詳しくなかったのでいろいろと調べてみましたが、結局良くわかっていません。

変更前

もともと記事を表示するためのカードがあったので、これを再利用したいと考えました。

CardList.vue

<template>
  <div>
    <ul>
      <li v-for="post in posts" :key="post.id">
        <card v-bind="{ post }" />
      </li>
    </ul>
  </div>
</template>

<script>
import Card from "~/components/molecules/Card";

export default {
  name: "CardList",
  components: { Card },
  props: {
    posts: {
      type: Array,
      default: () => []
    }
  }
};
</script>

Card.vue

<template>
  <a :href="`posts/${post.id}`">
    <div v-lazy:background-image="post.image" />
    <div>
      <text-title :value="post.title" />
      <div>
        <text-date :value="publishDate" />
      </div>
    </div>
  </a>
</template>

<script>
import moment from "moment";
import TextTitle from "~/components/atoms/TextTitle.vue";
import TextDate from "~/components/atoms/TextDate";
import { Post } from "~/utils/entities";
import timeFormat from "~/config/timeFormat";

export default {
  name: "Card",
  components: {
    TextTitle,
    TextDate
  },
  props: {
    post: {
      type: Object,
      default: () => new Post({}),
      validator: obj => Post.keys === obj.keys
    }
  },
  computed: {
    publishDate() {
      return moment(this.post.publish_date, "YYYY/MM/DD").format(timeFormat);
    }
  }
};
</script>

変更後

変更前は、Postという記事のオブジェクトの構造に依存していました。そのため、CardList側でStringやNumberに分解してCardに渡してあげることで、Cardを再利用可能なものにしました。

CardList.vue

<template>
  <div>
    <ul>
      <li
        v-for="content in parsedContents"
        :key="content.id"
      >
        <card
          :link="content.link ? content.link : 'posts/' + content.id"
          :image="content.image"
          :title="content.title"
          :publish-date="content.publishDate"
        />
      </li>
    </ul>
  </div>
</template>

<script>
import Card from "~/components/molecules/Card";
export default {
  name: "CardList",
  components: { Card },
  props: {
    contents: {
      type: Array,
      default: () => []
    }
  }
};
</script>

Card.vue

<template>
  <nuxt-link-and-atag-wrapper :to="link">
    <div v-lazy:background-image="image" />
    <div>
      <text-title :value="title" />
      <div>
        <text-date :value="publishDate" />
      </div>
    </div>
  </nuxt-link-and-atag-wrapper>
</template>

<script>
import TextTitle from "~/components/atoms/TextTitle";
import TextDate from "~/components/atoms/TextDate";
import Tag from "~/components/atoms/Tag";
import NuxtLinkAndAtagWrapper from "~/components/molecules/NuxtLinkAndAtagWrapper";
export default {
  name: "Card",
  components: {
    NuxtLinkAndAtagWrapper,
    TextTitle,
    TextDate,
    Tag
  },
  props: {
    link: {
      type: String,
      default: ""
    },
    image: {
      type: String,
      default: ""
    },
    title: {
      type: String,
      default: ""
    },
    publishDate: {
      type: String,
      default: ""
    },
    categoryId: {
      type: Number,
      default: null
    }
  }
};
</script>
クリックすると内部または外部リンクに飛んでくれるコンポーネントを作成

渡されたリンクが、内部リンク("/hoge"など)か、外部リンク("https://example.com"など)を判断して、aタグかnuxt-linkでラップしてくれるコンポーネントを作りました。

関数型コンポーネントを利用しています。

NuxtLinkAndAtagWrapper.vue

<script>
export default {
  functional: true,
  render: function(h, { props, children, data }) {
    function isInternalLink(path) {
      return !/^https?:\/\//.test(path);
    }
    if (isInternalLink(props.to)) {
      return h("nuxt-link", data, children);
    } else {
      delete data.attrs.to;
      data.attrs.href = props.to;
      return h("a", data, children);
    }
  }
};
</script>

テスト

このプロジェクトのnuxt.js内ではほとんどテストが書かれていませんでした。

フロントのテストを書くのも初めてだったので、いろいろ調べながら書きました。

テスト対象 TextDate.vue

<template>
  <p>{{ value | readableDate }}</p>
</template>

<script>
import moment from "moment";
import timeFormat from "~/config/timeFormat";
export default {
  name: "TextDate",
  filters: {
    readableDate(str) {
      if (str == "") return "";
      return moment(str, "YYYY/MM/DD").format(timeFormat);
    }
  },
  props: {
    value: {
      type: String,
      default: ""
    }
  }
};
</script>

テスト TextDate.spec.js

import { shallowMount } from "@vue/test-utils";
import TextDate from "@/components/atoms/TextDate.vue";

describe("TextDate.vue", () => {
  let textDate;

  test("Setup correctly", () => {
    textDate = shallowMount(TextDate, {
      propsData: {
        value: "2019-01-01 00:00:00"
      }
    });
    expect(true).toBe(true);
  });

  test("props", () => {
    textDate = shallowMount(TextDate, {
      propsData: {
        value: "2019-01-01 00:00:00"
      }
    });
    expect(textDate.props().value).toBe("2019-01-01 00:00:00");
  });

  test("日時が人間に読みやすいように変換されて表示される", () => {
    textDate = shallowMount(TextDate, {
      propsData: {
        value: "2019-01-01 00:00:00"
      }
    });
    expect(textDate.text()).toBe("2019/01/01");
  });

  test("空文字を渡すと、何も表示されない", () => {
    textDate = shallowMount(TextDate, {
      propsData: {
        value: ""
      }
    });
    expect(textDate.text()).toBe("");
  });
});

フロントエンドのテスト項目は、どのようなものが良いのか、正直イマイチわかっていません....。

感想

まず、プロジェクトにアサインされてから、実際に中身を読むと分からないことが多くて大変でした。

理解するためには、テストやアトミックデザイン、Vuex、DI、Trait、サービスコンテナなど、使ったことのない技術や概念について勉強する必要がありました。

また、他の人が書いたコードを読むと勉強になることがとても多く楽しいです。

これは個人でコードを書いていてもあまり得られない経験です。

いろいろ調べて悩みながらやっていたら、実装に2週間半くらいかかってしまいました・・・。もうちょっと早く実装できるようになりたいです。

まだまだ分からないことがたくさんあるので、勉強していきたいです。

これからも頑張っていきます〜💪

www.wantedly.com