1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【GAS×AI】採用担当が書類選考を自動化して「月57時間の工数削減」に成功した話

Last updated at Posted at 2025-12-04

はじめに

初めまして。
エンジニア採用やHRBPとして、「テクノロジーで人事オペレーションを自動化し、生まれた時間を組織課題の解決に充てる」ことをミッションに活動している採用担当です。

採用担当の皆さん、「書類選考(エントリーシート)の読み込み」にどれくらいの時間を使っていますか?

応募が増えるのは嬉しい悲鳴ですが、全ての書類に目を通し、公平に評価するには膨大な時間が必要です。
今回は、Google Cloud Natural Language API (Sentiment Analysis) と Google Apps Script (GAS) を活用して、書類選考の一次スクリーニングを補助するスコアリングシステムを内製開発し、結果として月間57時間の工数削減に成功した知見を共有します。

解決したかった課題私が直面していた課題

工数過多: 応募増に伴い、ESの読み込みだけで月数十時間を費やしており、コア業務(候補者対応や戦略立案)を圧迫していた。
評価の揺らぎ: 読む人のコンディションや主観によって、合否判定にブレが生じるリスクがあった。
見落としリスク: 多数の応募の中に埋もれた「熱量の高い候補者」を見逃したくない。

開発したツールの概要Googleフォームから応募があった瞬間に、志望動機などのテキストをAPIに投げ、「感情スコア(ポジティブ/ネガティブ)」と「特定キーワード」に基づいて自動で点数付けを行う仕組みです。

これにより、「スコアが高い順(=熱量が高い可能性が高い順)に優先的に目を通す」という運用が可能になり、採用担当者の工数削減と有望層の早期発見を実現しました。

システム構成

1.Google Forms: エントリー受付
2.Google Apps Script: トリガー検知・テキスト加工・ロジック処理
3.Google Cloud Natural Language API: 感情分析
4.Google Sheets: 結果出力・スコア管理

実装コード

エンジニアの方にも安心して引き継げるよう、「保守性」と「セキュリティ」を意識して実装しました。
工夫したポイント
・設定の集約: 列番号やキーワード設定をCONFIGオブジェクトにまとめ、マジックナンバーを排除。
・秘匿情報の分離: APIキーはコードに直書きせず、PropertiesService(スクリプトプロパティ)で管理。

/**
 * 設定値オブジェクト
 * 列番号やAPI設定を一元管理し、メンテナンス性を高めています
 */
const CONFIG = {
  // フォームの列インデックス設定 (0始まり)
  COLUMNS: {
    TIMESTAMP: 0,
    NAME: 1,
    // 抽出対象の列範囲(志望動機、自己PRなど)
    TARGET_RANGES: [
      { start: 6, end: 10 }, 
      { start: 15, end: 17 }
    ]
  },
  SHEET_NAME: "書類選考結果出力先",
  // スコアリング設定
  SCORE_MULTIPLIER: 10,
  BONUS_POINT: 2,
  // 評価キーワード(求める人物像に合わせて設定)
  KEYWORDS: {
    POSITIVE: ["主体性", "改善", "貢献", "挑戦"], // 例
    NEGATIVE: ["受け身", "不安", "教えてほしい"]    // 例
  }
};

/**
 * フォーム送信時に実行されるトリガー関数
 */
function onFormSubmit(e) {
  try {
    // e.values は送信された1件の回答データ(配列)を含む
    const rowData = e.values;
    const name = rowData[CONFIG.COLUMNS.NAME];
    
    // 分析対象テキストの結合処理
    let textSegments = [];
    CONFIG.COLUMNS.TARGET_RANGES.forEach(range => {
      for (let j = range.start; j <= range.end; j++) {
        // 空欄でない場合のみ追加
        if (rowData[j]) textSegments.push(rowData[j]);
      }
    });
    const fullText = textSegments.join(" ");
    
    // APIで分析
    const analysisResult = analyzeSentimentAndScore(fullText);
    
    // 結果を出力 [日時, 名前, 連結テキスト, 感情スコア, 最終スコア]
    const result = [
      rowData[CONFIG.COLUMNS.TIMESTAMP], 
      name, 
      fullText, 
      analysisResult.sentiment, 
      analysisResult.score
    ];
    
    // 出力先シートに結果を追記
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const outputSheet = ss.getSheetByName(CONFIG.SHEET_NAME);
    
    if (!outputSheet) {
      console.error(`シート「${CONFIG.SHEET_NAME}」が見つかりません。`);
      return;
    }
    
    outputSheet.appendRow(result);
    
  } catch (error) {
    console.error(`エラー発生: ${error.message}`);
  }
}

/**
 * Natural Language API 連携部分
 */
function analyzeSentimentAndScore(text) {
  // Script Propertiesからキーを取得(セキュリティ対策)
  // GitHub等でのコード公開時に事故が起きないよう徹底しています
  const apiKey = PropertiesService.getScriptProperties().getProperty('GCP_API_KEY');
  
  if (!apiKey) throw new Error("APIキーが設定されていません。");

  const url = `https://language.googleapis.com/v1/documents:analyzeSentiment?key=${apiKey}`;
  
  const payload = {
    document: { type: "PLAIN_TEXT", content: text },
    encodingType: "UTF8"
  };
  
  const options = {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };
  
  const response = UrlFetchApp.fetch(url, options);
  const result = JSON.parse(response.getContentText());
  
  // 感情スコア取得 (-1.0 ~ 1.0)
  const sentiment = result.documentSentiment ? result.documentSentiment.score : 0;
  
  // キーワード補正
  const bonus = calculateKeywordBonus(text);
  
  return {
    sentiment: sentiment,
    score: (sentiment * CONFIG.SCORE_MULTIPLIER) + bonus
  };
}

// キーワード出現数に基づく補正ロジック
function calculateKeywordBonus(text) {
  let bonus = 0;
  CONFIG.KEYWORDS.POSITIVE.forEach(k => { if (text.includes(k)) bonus += CONFIG.BONUS_POINT; });
  CONFIG.KEYWORDS.NEGATIVE.forEach(k => { if (text.includes(k)) bonus -= CONFIG.BONUS_POINT; });
  return bonus;
}

導入効果と考察

1. 定量的な成果:月57時間の削減
本ツールの導入に加え、フロー全体の自動化(Slack通知連携など)を行った結果、月間約57時間の工数削減に繋がりました。
空いた時間は、候補者の意向醸成や事業部との採用要件すり合わせなど、「人間にしかできない、EQ(感情知能)が求められる業務」に投資できています。

2. 定性的な発見
・「熱量」の可視化: 文章量が多く、ポジティブな表現(「挑戦」「貢献」など)が多い候補者は、実際に面接してもカルチャーマッチ度が高い傾向が見られました。
・バイアスの排除: フラットなスコアが横にあることで、「なんとなく」での不採用を減らし、セカンドオピニオンとしての役割を果たしてくれました。

最後に(今後の展望)

HRは「人」に関わる仕事ですが、だからこそ定量的なデータと効率的なオペレーションが不可欠だと考えています。

今後は、蓄積された面接評価データと今回のESスコアの相関分析を行い、さらなる精度の向上を目指す予定です。
私はこれからも「エンジニアリング × 人事」の領域で、組織課題の解決に取り組んでいきます。

Profile
エンジニアリング組織の採用・組織開発を専門とするHRBP/採用担当。GAS/SQL/Docker等を活用したOps構築から、OKRに基づく採用戦略立案まで一気通貫で担当。
「外から綺麗にアドバイスする人」ではなく、「中で汗をかきながら事業を伸ばす人」であることを大切にしています。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?