【Laravel】NGワードを精度高く検知する機能を開発してみる

こんにちは！
サーバーサイドエンジニアからフルスタックエンジニアにテラ進化したいすぎちゃんです！ :cat: 

今回はNGワードを**精度高くマスキング**する機能についてお話ししたいと思います！

さて、このNGワードマスキング機能はどのような効果をもたらすでしょうか？

- サービス利用者の心理的安全性の向上
- カスタマーサポート対応の負担軽減
- エンジニアによるデータベースクリーニング作業の削減
([実際に被害にあった人の声](https://qiita.com/nasuB7373/items/10bebd8e9f0b3331f348))

などなど・・・
まさに三方良しの機能と行っても過言ではありません。

実際に配信サービスやオンラインゲームにも、NGワードに対してなんらかの処理をかける機能が搭載されています！

:::note info
- [**ニコニコ生放送**](https://dic.nicovideo.jp/a/%E3%83%8B%E3%82%B3%E3%83%8B%E3%82%B3%E7%94%9F%E6%94%BE%E9%80%81%3A%E9%81%8B%E5%96%B6ng%E3%83%AF%E3%83%BC%E3%83%89%E4%B8%80%E8%A6%A7)
   ⇨ NGワードへの対処は不明。(ニコ厨の方はどんな風になるかご教示いただければ幸いです・・・m(__)m)
- [**SHOWROOM 利用規約**](https://campaign.showroom-live.com/how_to_use_happily/)
⇨ NGワードは機械的に「☆」に変換される。お下劣ワードはもちろん「カフェラテ」ですら「カ☆テ」となってしまい[やりすぎ感が否めない](https://togetter.com/li/1575066?page=3)。
- [**メイプルストーリー**](https://yukireimu.hatenablog.com/entry/2017/11/21/003107)
⇨「悪口は禁止されています」というウィンドウが表示され、そもそもデータを登録できない。
:::




## :cat: 完成形

### 健全な文章
![画面収録-2024-09-10-20.43.35.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/1dfd4cce-9e7b-3332-8724-a36f1a37f122.gif)


上記のデモをご覧の通り、お上品な投稿だとマスキング処理がかかることなく、原文がそのままDBに保管されます。


### NGワードが含まれる文章（お目汚し失礼します）
![画面収録-2024-09-10-20.44.22.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/469d7711-acf7-c77c-22d3-36bbbdcf2242.gif)

お下劣な単語を含む文章の場合、該当の単語が「☆☆☆」に置き換えられた形でデータベースに保存されます。

ただし、健全な文章を投稿した場合とは異なり、「**不適切な言葉が含まれていたため、一部をマスキングして投稿しました**」という警告文を表示し、ユーザーに注意を促す仕組みを取り入れています。

さて、鋭い読者の読者の方々はもうお気づきかもしれませんが、「パチンコ」はNGワードとして判定されず「パ☆」と変換されていません！ :smile:

以前の私だったら、`str_replace($ngWords, '☆', 'ウーマンコミュニケーション')`のように単純な置き換えをして、「ウー☆ミュケーション」のような行き過ぎた規制になってしまうこともありました…。

しかし、今回のシステムでは、NGワードを厳格に規制しながらも、人間らしい会話を最大限に楽しんでもらう工夫を凝らしています！

次の項目では、これを実現する上で重要な「形態素解析」について詳しくお話しします！


## :book: 形態素解析とは？

 > 形態素解析は、自然言語処理（NLP）の一部です。アルゴリズムを有する自然言語で書かれている文を、言語において意味を持つ最小の単位(＝形態素)に細分化し、一つひとつの品詞・変化などを判別していく作業のことを指します。「形態素」は言語学の用語であり、意味を持つ表現要素の最小単位のことなのです。

:::note info
**引用元**
[形態素解析とは｜意味・活用例と日本語の自然言語処理ツールを紹介！](https://aismiley.co.jp/ai_news/nlp-morpho-analysis-japanese/)
:::

形態素解析を用いることで、例えば「カフェラテ」のような一般的な単語が「カ☆テ」に変換されてしまうといったケースを解消できます。 :thumbsup: 


この技術は、文脈を理解し、単語の意味を適切に解析することで、NGワード検出の精度を飛躍的に向上させます。（ちなみに、ニコニコ生放送も同様の手法で誤判定を防いでいるのではないかと推測しています！）

今回は「[GooラボAPI](https://labs.goo.ne.jp/)」を用いてlaravelに形態素解析をサクッと導入しちゃいましょう！

## 1. GooラボAPIの外部連携

GooラボAPIのAPIキーを取得するには、以下のステップを踏む必要がございます！

1. 「[API利用の流れ](https://labs.goo.ne.jp/apiusage/)」に移動
2. 「[gooラボAPI利用登録](https://labs.goo.ne.jp/jp/apiregister/)」に移動
3. 最下部までスクロールし「利用規約に同意しGitHubで登録」をタップ

### 1-1. 「API利用の流れ」に移動

![スクリーンショット 2024-09-10 14.57.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/c4556da8-1d05-b18d-3c55-dfd3f326aa15.png)

トップページに表示されているサイドメニューに「API利用の流れ」があるので、それをタップしてください。

そうすると以下のように表示されるはずです。

![スクリーンショット 2024-09-10 15.27.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/c5f069d7-cbc4-c739-04fa-9c5e09b94aa6.png)


### 1-2. 「gooラボAPI利用登録」に移動

下にスクロールしていくと「2.アプリケーションIDの取得」のセクションが表示されます。

![スクリーンショット 2024-09-10 14.58.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/7992d772-1894-c981-c8f0-4a0069dedadf.png)

「API利用登録ページ」のテキストリンクをタップしてください。
そうすると以下の画像のようにgooラボ利用規約文が表示されます。

![スクリーンショット 2024-09-10 14.59.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/0636d2b6-7f29-38f3-cbbe-ed62c834e85e.png)

### 1-3. 「利用規約に同意しGitHub」で登録をタップ

:::note warn
Githubのアカウントを事前に発行しておきましょう！
:::

gooラボAPI利用登録ページの利用規約にざっと目を通しましょう。
下にスクロールしていくと「利用規約に同意してGitHubで登録」が表示されます。

![スクリーンショット 2024-09-10 14.59.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/63df68da-420a-abcf-8a5f-25c9f81b8932.png)

タップすると、APIキーとなるアプリケーションIDが表示されます！

![スクリーンショット 2024-09-10 15.00.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/8b5fb9d5-84a7-7fec-9aac-ef05f054ea14.png)

これでgooラボAPIとの連携ができるようになりました！

以下のリンクよりAPIの動作確認もできます！

[形態素解析API](
https://labs.goo.ne.jp/api/jp/morphological-analysis/)

![スクリーンショット 2024-09-10 15.39.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/7931cb31-424c-ef16-b02f-7cf6c44708c6.png)


## 2. Laravelで「GooラボAPI」を使った形態素解析をサクッと導入！

[**CreatePost.php**](https://github.com/sugizakikun/docker-laravel/blob/develop/src/app/Http/Services/Posts/CreatePost.php)
```CreatePost.php
<?php

class CreatePost
{
    use NgWordMaskingTrait;

    /**
     * @var GooLabApiClient
     */
    protected $gooApiClient;

    public function __construct(
        GooLabApiClient $gooApiClient
    )
    {
        $this->gooApiClient = $gooApiClient;
    }

    public function execute(int $userId, string $content, ?array $uploadedFiles)
    {
        # 2-1: つぶやきを形態素に変換する
        $formattedWordList = $this->gooApiClient->morph($content);

        # 2-2: 形態素を走査する
        $ngWordMaskingOutput = $this->maskingProcess($formattedWordList);

        $post = $this->createPost($userId, $ngWordMaskingOutput['result']);

        #　省略

        return $ngWordMaskingOutput['hasNgWord'];
    }

    /**
     * @param int $userId
     * @param string $content
     * @return Post
     */
    private function createPost(int $userId, string $content):Post
    {
        return (new Post)->newQuery()
            ->create([
                'author_id' => $userId,
                'content' => $content
            ]);
    }

    
    # ... 省略

```

### 2-1. つぶやきを形態素に分解する


#### [**HttpClient.php**](https://github.com/sugizakikun/docker-laravel/blob/develop/src/app/Util/Common/HttpClient.php)

HttpClient クラスは、HTTPリクエストを送信するためのユーティリティクラスです。特に POST リクエストを送信するための post メソッドが実装されています。


```HttpClient.php
namespace App\Util\Common;

class HttpClient
{
    # ...省略

    public static function post(string $url, array $data)
    {
        // ストリームコンテキストオプションの設定
        $options = [
            'http' => [
                'method'  => 'POST',
                'header'  => "Content-Type: application/json;" .
                    "charset=UTF-8",
                'content' => json_encode($data), // JSONエンコードされたデータを送信
                'ignore_errors' => true // エラーを無視してレスポンスを取得する
            ]
        ];

        $context = stream_context_create($options);

        return  file_get_contents($url, false, $context);
    }
}
```

#### [**GooLabApiClient**](https://github.com/sugizakikun/docker-laravel/blob/develop/src/app/Util/GooLabApiClient.php)

GooLabApiClient クラスは、Gooラボの形態素解析 API を利用するためのクライアントクラスです。このクラスでは、HttpClient クラスを用いて HTTP リクエストを行い、API のレスポンスを処理します。

 `$this->appId`には[1-3](https://qiita.com/sugichaaaan/items/944146ba2ad9abdea696#1-3-%E5%88%A9%E7%94%A8%E8%A6%8F%E7%B4%84%E3%81%AB%E5%90%8C%E6%84%8F%E3%81%97github%E3%81%A7%E7%99%BB%E9%8C%B2%E3%82%92%E3%82%BF%E3%83%83%E3%83%97)で取得したアプリケーションIDを設定してください :smiley: 


```GooLabApiClient.php
<?php

namespace App\Util;

use App\Util\Common\HttpClient;

class GooLabApiClient
{
    /**
     * @param HttpClient $httpClient
     */
    public function __construct(HttpClient $httpClient)
    {
        $this->httpClient = $httpClient;
        $this->prefix = 'https://labs.goo.ne.jp/api';
        $this->appId = env('GOO_LAB_APP_ID');
    }

    /**
     * @param string $sentence
     * @return mixed
     */
    public function morph(string $sentence)
    {
        $endPoint = $this->prefix.'/morph';

        $data = [
            "app_id" => $this->appId,
            "sentence" => $sentence,
        ];

        $jsonString =  $this->httpClient->post($endPoint, $data);
        $jsonData = json_decode($jsonString);
        $wordList = $jsonData->word_list[0];

        return array_map(function ($word) {
            return [
                'notation' => $word[0],
                'morpheme' => $word[1],
                'kana' => $word[2],
            ];
        }, $wordList);
    }
}
```

試しに以下の文章を入力し形態素解析をしてみましょう！

```
シャルパンティエ氏がパンティリアーテで新品のパンティを買った。
```

そうするとmorphメソッドの結果は以下のようになります！

![スクリーンショット 2024-09-10 16.59.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/2f874f96-7a57-b333-189b-998446c4e5ca.png)

このように文章を形態素に分解できていることを確認できました！
（読みやすさの都合上、`morpheme`、`kana`は省略しています。）


### 2-2. 形態素を走査する

形態素解析を活用して、NGワードを精度高くマスキングするための NgWordMaskingTrait トレイトについてご紹介します。

このトレイトは、ユーザーの投稿やコメントなどからNGワードを検出し、適切にマスキングする機能を提供します。

[**NgWordMaskingTrait.php**](https://github.com/sugizakikun/docker-laravel/blob/develop/src/app/Http/Services/Common/NgWordMaskingTrait.php)(お目汚し失礼します)
```NgWordMaskingTrait.php
<?php

namespace App\Http\Services\Common;

trait NgWordMaskingTrait {


    public function maskingProcess(array $formattedWordList): array
    {
        $ngWords = [
            /*かなり多いので省略*/
        ];

        # ①
        $maskedSentence = '';
        $hasNgWord = false; #NGワード存在フラグ

        # ②
        foreach ($formattedWordList as $formattedWord) {
            if(in_array($formattedWord['notation'], $ngWords) ||  in_array($formattedWord['kana'], $ngWords)) {
                $maskedSentence = $maskedSentence.'☆☆☆';
                $hasNgWord = true;
                continue;
            }

            $maskedSentence = $maskedSentence.$formattedWord['notation'];
        }

        # ③
        return [
            'result' => $maskedSentence,
            'hasNgWord' => $hasNgWord,
        ];
    }
}
```


① `$maskedSentence` には空文字をセットし、マスキング結果をここに蓄積する。`hasNgWord` フラグを false に設定し、NGワードが含まれていたかどうかを走査

② foreach ループで、`formattedWordList` 内の各単語を順に処理。単語がNGワードリストに含まれている場合、その部分を「☆☆☆」で置き換え、`hasNgWord` を true に設定。NGワードでない場合は、そのまま単語を `maskedSentence` に追加。

③ マスキングされた文章 (`maskedSentence`) と、NGワードが含まれていたかどうかを示すフラグ (`hasNgWord`) を含む配列を返却


以上の処理によって下ネタを検知しつつ誤検知率を抑えることがができます！

![スクリーンショット 2024-09-10 17.09.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/637a4718-bf1a-10eb-393b-3a03c896df7b.png)


ここまでお読みいただきありがとうございます！

## まとめ

この記事では、下ネタ検知システムにおける形態素解析の重要性と、Laravelで「GooラボAPI」を使った導入方法を紹介しました :cat: 

形態素解析を使うことで、単語の誤判定を防ぎつつ、精度の高いNGワード検出を実現できます！

この方法を使えば、ユーザーが安心してサービスを利用できる環境を簡単に構築できます。これによりサービスの品質を向上させ、ユーザーエクスペリエンスを大きく改善することが可能です。

ぜひ、今回の方法を参考にして、自身のプロジェクトに取り入れてみてください :smile: 

**検知シリーズ**
- [エロ画像検出APIを用いてエロ画像のアップロードを阻止してみる【前編】](https://qiita.com/sugichaaaan/items/738867e4bc3a1a0077d9)
- [エロ画像検出APIを用いてエロ画像のアップロードを阻止してみる【後編】](https://qiita.com/sugichaaaan/items/826bf81a853ef951a541)

**参考記事**
- [NGワード判定システム構築（ウーマンコミュニケーション）](https://qiita.com/ka2ki2004/items/f70ba1c2ce4f7c4ed492)