ぽっぺんしゃんにょろりんこ

匿名・非追跡型アダルト動画検索エンジンの設計ノート

求人市場の闇に挑んだ!ドメインパワーが低い方が有利な検索エンジンを作った話

こんにちは、にょろりんこの備忘録技術ブログです。

今日は50回記念として、SaePornsを作る前に作成していた「ドメインパワーが低い方が有利な求人検索エンジン」の作成と挫折の話を書こうと思います。

日本のいびつな求人マーケット

改めて考えると、日本の求人マーケットって歪だと思いませんか?

だって、どこもかしこも求人サイトや転職エージェントのCMばかり。要は、求人広告費をじゃぶじゃぶ使った企業に応募が集まるという構図なんですよね。

本業の事業再生コンサルでも、いつも口をすっぱくして言っているんですが、求人広告費にお金をじゃぶじゃぶ使うのって、相当イケてないです。そのお金は、求人広告費なんかに使うよりも、従業員の給与や、働きやすい職場の整備費用に充てる方が1億倍マシです。

学校の机とティアラ

もちろん、求人広告費を使えば応募は増えます。でもね、その人が定着するかどうかは話が別です。人材使い捨て上等!という採用戦略なら、それでもいいでしょう。でも、まともな企業であれば、仕事を通じて従業員が成長し、そのスキルアップが会社の成長にも繋がる、そういう構造を作っていかないといけない。

結局、「従業員のために給与を高くしよう!」「従業員が働きやすい環境に投資しよう!」というホワイト企業は埋もれてしまい、給与を下げて求人広告費に充てる→給与が低いので転職して辞めていく→以下ループ。というブラックスパイラルに陥る企業ばかりになってしまいます。

儲かるのはビ○リーチやリ○クルートだけ。まさに「社長は(求人広告費をじゃぶじゃぶ使って従業員の給与を下げることに)本気だ!」って感じで、本末転倒なんですよね。こういう現状が日本の採用市場にはあるわけです。

だったら、そういうちゃんと従業員のことを考えている会社にスポットライトが当たる。ちゃんと採用できるようにしよう。というコンセプトのもとに作成を進めていたのがドメインパワーが低い方が上位に表示される。あべこべ求人検索サイトなわけです。

キラリと光る中小企業やスタートアップはどうしたって、求人コストをじゃぶじゃぶ使う大企業や、プロダクトにコストを使わず求人費用にばかりコストを使うスタートアップによりも露出が減ってしまいます。

クラウドライターにコストをかけて、札束殴り合いのコンテンツマーケティングしている暇があったら、ユーザーや従業員に本当の価値を届けることにコストをかけるべき。そう考える会社にスポットライトを当てよう。というコンセプトであべこべ求人サイトを作成をしていました。このコンセプト自体は、自分で言うのもなんですが、そんなに悪くないんじゃないかなと思います。

実際にどうやって作ったのか

構造としては、Googleしごと検索がやっているように、各企業サイトや求人ページに埋め込まれたJsonLDのJobPostingデータをクローリングしてインデックス化する仕組みです。

JobPostingというのは、求人情報を構造化して検索エンジンに伝えるためのフォーマットで、ポジション名、会社名、勤務地、給与、仕事内容などがJSONで記述されています。これを読み取ってデータベースに入れ、通常の求人検索エンジンのように表示するわけですが、あべこべ検索サイトではスコアリングが逆でした。

普通の求人検索だと、ドメインパワーが強い順、つまりSEOが強くて広告費をかけているページが上位に来ます。でも私が作ったのは、ドメインパワーが低い順にソートする。

つまり、無名だけど面白い会社、まだSEO施策をしていない中小企業やスタートアップの公式採用ページが、上位に来るようにしたんです。結果として、「Indeedビズリーチでは見つからないけど、公式採用ページにはしっかり掲載されている求人」が表に出てくる検索エンジンになっていました。

これは面白い!と思いました。

広告宣伝費を使っていないから、イ○ディードやビ○ズリーチでは表示されないけど、ちゃんと公式ページには掲載されている求人がたくさんある。でも、今の日本の求人マーケットでは、そういう求人はほとんど日の目を見ない。

求人広告費をじゃぶじゃぶ使っている企業の求人ばかりが上位に表示される構造。求職者から見える世界が、広告費をかけられる企業だけで埋め尽くされるこの状況は、冷静に考えて相当ヤバいと思うんです。

もちろん求人広告もビジネスだから否定はしません。でも、本当に従業員を大切にしている会社が埋もれてしまうのは、社会的にもったいない。そんな思いから、私はこのあべこべ求人検索サイトで、「広告費をかけなくても、真摯に求人を出している企業がちゃんと見つかる世の中」を作りたいと考えていました。

なぜうまくいかなかったのか

そんな理想を掲げて作った、あべこべ求人検索サイト。でも、現実は甘くありませんでした。

一番の課題は、求人ページを自動で見つけるのが想像以上に難しかったことです。Googleしごと検索のように、JobPostingが埋め込まれたページをフルクローリングできれば理想でした。でも、Googleは世界中のウェブをクロールしてインデックスを作る体制があるからこそ可能なだけで、個人開発レベルでは、どのページに求人情報が埋まっているかを探し当てるだけでも膨大なリソースが必要でした。

また、Indeedや求人ボックスのようなパターンは、求人情報を企業側に投稿してもらう方式が基本なのですが、TVCMをガンガンやっていることからもわかるように、この方式を採用するには、サイトそのものの知名度がとても重要になります。要は札束殴り合いですね。

また、この形式、つまり、「求人コンテンツを媒体側に記入させる」形式は、その求人情報自体がIndeedや求人ボックスの資産になっていくわけで、これはちょっとアンフェアな考え方とも思いました。

企業は応募を集めたくて求人媒体に投稿する。結果として、求人媒体はどんどんデータを貯め込み、プラットフォームとして強大化する。これは、企業とプラットフォームがWIN=WINの関係とは言い難いですね。ここは今回のテーマと違うのであまり細かく書かないのですが、WIN=WINの関係でないと長期的な長続きはしません。

※一方、Googleしごと検索はあくまで検索結果に表示するだけで、求人コンテンツの所有権は企業側に残る。ここが構造的に大きな違いです。キラリと光る中小企業やスタートアップが、良い人材を採用できる、そういう世界をフェアな形で実現するにはグーグル型のクローリング方式が必要ではありました。

でも、私が作ったあべこべ求人検索サイトはGoogleほどのクロール体制も知名度もありません。じゃあどうするかというと、グーグルサーチコンソールのように、企業にインデクシングリクエストを出してもらう必要がある。「うちの求人ページを登録して!」というリクエストを出して!お願いするわけです。

でも、知名度ゼロの検索サイトに、わざわざ求人ページを登録してくれる企業はほとんどありませんでした。

結局、

  • 求人ページをフルクローリングできるインフラがない
  • 企業からの投稿を集める知名度もない

この二重苦で、サービスとして成り立たないという現実にぶち当たりました。

それでも、この思想には価値がある

結果として、あべこべ求人検索サイトは世の中に出ましたが、すぐに消しました。求人検索エンジンという領域は、GoogleIndeedのような巨大インフラを持つ企業か、莫大な広告費を投下できる企業じゃないと勝負にならない。それが痛いほどわかりました。

でも、それでも私は、この思想自体には価値があると思っています。求人広告費をじゃぶじゃぶ使って上位表示される求人ではなく、広告に頼らなくても、従業員にちゃんと向き合っている会社にスポットライトを当てる。そういう求人情報検索エンジンがあったら、きっと求職者も企業も、そして社会も、もっと幸せになるはずです。

今はこの思想を別の形で活かしています。

SaePornsも、その一つ。こっちは求人ではなく動画検索だけど、「大手だけが優遇される世界はつまらない」という思いは同じです。

いつかまた、このあべこべ求人検索の思想を活かせる場面があれば、技術とお金と人脈を総動員して、もう一度挑戦してみたいと思っています。

現在作成中のあなたを追跡しないアダルト動画の検索エンジンSaePornsはこちら!よかったら見て行ってください。※18歳未満の方はご利用いただけません。

sae-porns.org

それではみなさんよい開発ライフを。

評価されない職場で消耗しないために|社会評価とのズレと上司の責任

こんにちは、にょろりんこの備忘録雑記ブログです。

今日は、本業の事業再生コンサルティングで実際にあった話を共有します。

あるクライアント企業から、「従業員が次々と辞めていく」「従業員満足度が下がっている」という相談を受けました。

スタッフにヒアリングしていくと、多くの人がこう感じていました。

「会社から正当に評価されていない」と。

よくよく調べてみると、具体的には、残業時間が多い人や休日出勤が多い人ほど出世していくという評価制度になっていました。長時間会社にいる=頑張っている、という発想です。

これは端的に言って、やめたほうがいい評価基準だと思います。何をやめたほうがいいかというと、長時間働いていること自体を評価するのをやめようということです。

では、そもそも評価とは何か?何をもって評価するべきなのでしょうか。

今回は、ITエンジニアを含む全てのビジネスパーソンが生き残るために知っておくべき、会社の評価と社会の評価のズレ、そして上司の責任について書いていきます。

帽子をかぶるティアラ

そもそも、会社の評価と社会の評価の2軸がある

評価には、会社がする評価と、社会がする評価の2軸があります。

会社の評価とは、その組織の中でどれだけ上司や経営陣に認められているか、ということです。例えば、上司への報告が早い、気が利く、残業している、そういったことで評価が上がる会社もあります。

一方で、社会の評価とは、その人が転職市場や業界全体、顧客やユーザーからどれだけ価値ある存在として認められているか、ということです。

そして、この2つのうち本当に重要なのは社会からの評価です。

ビジネスシーンをサバイブしていく上で重要なのは、会社という肩書がなくなった時のあなたの市場価値です。会社からの評価がいくら高くても、その会社の外に出た瞬間に通用しない能力しか身についていなければ、市場価値はゼロに等しいと言わざるを得ません。

長時間残業している、休日出勤しているというだけの人に市場価値が無いのは明らかです。出勤して、ただエクセルを開いて閉じているだけの人に、市場価値があるはずがありません。

では、そんな市場価値が無いことを高く評価している会社は、今後どうなるのでしょうか。

会社は、会社の評価=社会の評価にするようにすべき

本来、会社が目指すべきは、会社内での評価と社会での評価が一致するようにすることです。つまり、会社の中で高く評価される人材が、そのまま社会でも通用する人材であるべきなのです。

しかし現実には、上司への忖度や、社内の空気を読む力ばかりが評価される会社も少なくありません。そうなると、社員は会社の中でしか通用しないスキルばかりを身につけることになります。

会社は常に市場の声を聴き、競合に勝ち続けなくてはなりません。そういった時流の中で、社会の評価が低い人材ばかりいる組織は生き残れるわけがありません。会社は、外の変化にどんどん取り残されて、やがて消滅してしまいます。社員を社会からの評価を得られるように育てることは、会社の競争力を高めることにつながります。だからこそ、会社は評価基準を社会評価と一致する方向に寄せるべきなんです。

しかし、現実的には、そうした理想的な評価制度を持つ会社ばかりではありません。

会社から評価されないと思っている人へ

もし今、会社から評価されないと感じている人がいるなら、まず確認してほしいことがあります。

それは、あなたが評価されていない理由が、社会でも通用しないからなのか、それとも会社特有の評価基準に合っていないだけなのかということです。例えば、成果やスキル不足で評価されないなら、社会でも通用しない可能性があります。その場合は、今の会社であっても、他の会社であっても、努力して能力を磨く必要があるでしょう。

しかし、もし評価されない理由が、上司への忖度が足りない、飲み会に参加しない、残業をしないといった理由であれば、それは会社独自のローカルルールでしかありません。

社会ではむしろ、効率的に働き、無駄な残業をせず、成果を出す人が評価されます。だからこそ、「会社で評価されない」ことで必要以上に落ち込むのではなく、その評価基準が社会評価と一致しているかを冷静に見極めることが大切です。

しかし、例えば営業のように売上という明確な数字で評価される仕事ならば、会社の評価と社会の評価が一致しているかどうかは比較的分かりやすいでしょう。

一方で、ITのようなクリエイティブな職業では、会社の評価と社会の評価が本当に合っているかを見極めにくい側面があります。その際に、見極めのヒントとなるのが、あなたを評価する上司やマネージャーが、評価に責任を持っているかどうかです。

評価を見極めるポイントは上司の責任感

会社が正しく評価しているかどうかを見極める方法の一つは、上司が評価に責任を持っているかどうかです。例えば、上司が「売上で判断するから」とだけ言っている場合は要注意です。

売上というのは、あくまで社会からの評価であり、上司自身が部下の行動や取り組みをしっかり見ていなくても判断できる指標だからです。つまり、「売上で評価する」という言葉の裏には、上司が自分の評価責任を放棄している可能性があります。

もちろん売上は重要です。

しかし、上司の本当の仕事は、部下の行動や努力を見て、市場に出す前に「これは売れる」「これは価値がある」と判断することです。

売れるかどうかを市場任せにするなら、上司はいらなくなります。

事前に評価し、責任を持つ。これができているかどうかが、その会社が正しく評価しているかを見極めるポイントです。

では、売上以外の数値に責任を持つとは具体的にどういうことでしょうか。一つ事例を紹介します。

売上じゃない数値を売上につながると責任を持てるか

評価を考える上で大切なのは、売上以外の数値を、売上につながると信じて責任を持てるかどうかです。

ある学習塾の社長が、こんなことを言っていました。「先生たちのKPIは、生徒を授業中にどれだけ褒めたかにしなさい」と。

もちろん、褒めることが直接売上になるとは限りません。しかし、その社長は、褒めることで生徒のやる気が上がり、成績が上がり、最終的に塾の評判が上がって売上につながると考えていたのです。これは、売上という最終成果だけに逃げず、売上につながる途中の行動指標をKPIに設定し、責任を持つ上司の姿勢です。

売上だけで評価するのは簡単です。でも、それは上司が部下を正しく見なくても済むからです。

売上に至るまでの行動や工夫をKPIにして、それが成果につながると責任を持つこと。これこそが、上司や経営者に求められる本当の役割だと思います。

しかし、現実にはKPIすら決めないまま、「まずやってみよう」とだけ言って始めるマネジメントも少なくありません。

「まずやってみよう」の罠

「まずやってみよう」という言葉は、現場にポジティブさを与える一方で、大きな罠にもなり得ます。

なぜなら、事前に何をもって成果とするかを決めずに始めた仕事は、ほぼ確実にゴールポストが動くからです。例えば、やってみた結果、「ここまでできるなら、これもやって」と要求がエスカレートしていくことがあります。

ただ、「思っていたのと違うからやり直して」という指摘については、これは必ずしも悪いことではありません。なぜなら、売れるデザインかどうかを市場に出す前に判断するのが上司の仕事だからです。

もし売れないデザインだった場合、リテイクを出すのはむしろ当然であり、上司として責任を持って判断している証拠でもあります。

問題は、評価基準が曖昧なまま進めさせておいて、後から上司の感覚だけで評価を変えるようなケースです。

重要なのは、ゴールポストを動かさないことです。チーム内で事前にゴールイメージを確定させ、メンバー全員がそのゴールを共有している状態を作ることが重要です。

「まずやってみよう」という言葉は、一見前向きに聞こえますが、これは悪いマネジメントです。なぜなら、何をもって成功とするのか決めないまま仕事を始めることは、ゴールのないマラソンを走らせるようなものだからです。

走り切ったと思ったら、「実はゴールはまだ先だった」「ゴールの場所を変えた」、上司のフラッシュアイデアでゴールが動く。こんな状況では、現場のモチベーションは下がり、上司への信頼も失われます。

仕事を始める前に、何がゴールで、どこまでやれば十分なのか必ず明文化して共有する。これができない上司は、チームを疲弊させるだけです。そして、この「まずやってみよう」という無責任さが、さらに深刻になると「朝令暮改」という問題に繋がります。

朝令暮改が横行する職場の弊害

朝令暮改」という言葉があります。

方針を頻繁に変えることを指し、変化対応や経営スピードなどで、ポジティブに使う人もいますが、これは悪です。

なぜなら、朝令暮改が横行する職場では、部下たちはこう考えるようになります。「どうせまた途中で方針が変わるだろうから、全力でやっても無駄だ」と。結果として、部下は常に朝令暮改のためにパワーを残して仕事をせざるを得なくなるのです。

上司や経営者はよく言います。「もっと良いアイデアが出たら、そっちに舵を切るのは当然だ」と。

しかし、私はそうは思いません。その「もっと良いアイデア」とやらを最初に出しておくことこそが、「上司や経営者の仕事」だからです。

経営とは、善手と悪手の戦いではありません。善手と最善手の戦いです。常に最善手を選び続けることが求められます。

朝令暮改を前提としている経営やマネジメントは単なる甘えであり責任放棄なわけです。だからこそ、私たちはそんな環境の中でも、自分のキャリアと心を守る術を身につける必要があります。

今日からできる、自分を守るための行動

ここまでをもとに、ビジネスパーソンのサバイブ術をまとめます。

まず、もし今、評価されないと感じたら、その評価が社会の評価と一致しているかを確認してみてください。

社会の評価と一致しているならば頑張る必要がありますが、一致していないなら、いつでも転職という選択肢がありますし、一致していない会社で長く働くことは、そもそも貴方の社会的価値を棄損させます。

ただ、完璧な職場が存在しないことも事実なので、会社の評価と社会の評価が完璧にイコールではないとはよくあるでしょう。

その場合は次のことをチェックしてみてください。

  • 上司は責任を持って評価しているか。市場や売上だけに丸投げしていないか。
  • 上司は自分の仕事に責任を持っているか。朝令暮改を当然と思っていないか。

IT業界の場合は特に、「ゴールイメージも決めないまま、まずはやってみて、後でゴールを決める」という朝令暮改型マネジメントが横行しています。

自分のキャリアと心を守るために、評価基準と上司の姿勢を冷静に見極めてみてください。

それが、ビジネスの荒波を生き抜くサバイブ術になります。

それではみなさん、よいビジネスライフを。

※私にょろりんこが本業の傍ら作成している「あなたを追跡しないアダルト動画の検索エンジンSae-Pornsはこちら」よかったら見ていってください。(18歳未満の方はご利用いただけません)

sae-porns.org

 

 

動的ウェブサイトで、動画タイトルの文字列を使って動的サイトマップを作る②

こんにちは、にょろりんこの備忘録的技術ブログです。

前回の記事では、動画サイト運営において

  • 無機質なURL(例: /video.php?id=1234)ではSEO評価が上がらない
  • 日本語タイトルをURLスラッグ化して、よりSEOフレンドリーにする

という課題感と、その解決方法として静的ページ用のサイトマップ生成までの実装を解説しました。

www.n-rinko.com

今回は、その続きを書いていきます。

テーマは、「動画DBからタイトルを取得し、動的ページ用のサイトマップを生成する」です。

これができると、

  • 動画ページがGoogleにインデックスされやすくなる
  • タイトル入りURLでCTRも改善する
  • SNSシェアでも「何の動画か」がパッと分かる

といった、運営者として絶対に捨てがたい効果が得られます。

実際に、私が運営している「あなたを追跡しないアダルト動画検索エンジン SaePorns」でもこの方法を導入したところ、インデックスされるまでのスピードが向上した(ように思えます)。

では、コードを見ながら解説していきましょう。

DB接続処理

まずは、動画データを格納している MySQLデータベースに接続していきましょう。
いたって普通のDB接続モジュールですが、順番的に非常に重要な箇所なので、念のためフルコードで記入します。

try {
    $pdo = new PDO(
        "mysql:host=" . DB_SERVERNAME . ";dbname=" . DB_NAME . ";charset=utf8mb4",
        DB_USERNAME,
        DB_PASSWORD,
        [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]
    );
} catch (PDOException $e) {
    fwrite(STDERR, "DB 接続エラー: " . $e->getMessage() . "\n");
    exit(1);
}

これで、動画テーブルからデータを取得する準備が整いました。

次に、実際に動画データを取得していく処理を見ていきましょう。

動画レコードの取得とサイトマップ分割数の計算

サイトマップ生成には「動画データ」と「分割数」 の両方が必要なので、それを準備します。

// videos テーブルから全件取得
$stmt   = $pdo->query("SELECT id, url, title, thumbnail_url, embed_url, duration, updated_at FROM videos");
$videos = $stmt->fetchAll();

$totalUrls     = count($videos);
$totalSitemaps = (int)ceil($totalUrls / $maxUrlsPerSitemap);
echo "総動画 URL: {$totalUrls}, 分割サイトマップ数: {$totalSitemaps}\n";

ここでは、$pdo->query() を使って videos テーブルから必要なカラムを全件取得し、fetchAll() で結果を配列として $videos に格納しています。

そして、count($videos) で取得した動画レコード数を $totalUrls に代入。

さらに、sitemap.xml は 1ファイルにつき最大50,000件 というGoogleの仕様があるため、ceil() を使って切り上げ、必要な分割数を $totalSitemaps に計算しています。

最後に、総動画数と分割サイトマップ数を echo で出力。

この処理で、サイトマップ生成に必要な 「動画データ」と「分割数」 の両方が準備できました。

次は、分割サイトマップを生成する処理に進んでいきます。

サイトマップインデックスXMLの準備

次に、複数のサイトマップファイルをまとめる インデックスXML の準備をしていきます。

前述のとおり、サイトマップにはグーグルの仕様だと上限5万件(5万URL)という制約があります。

ですので、それを束ねる元になるインデックスXMLが必要になるわけですね。

これで理論上25億件(5万件×5万件)までのサイトマップ作成が可能になります。

25億件を超えたらどうなるんだ?とお考えの方もいらっしゃるかと思いますが、これがいったんGoogleの仕様上の上限(らしい)です。

とはいえ、このあたりは個人開発の規模をはるかに超えているので、もしそんな状況になったら、その時に考えればいいかなと思います。

XMLヘッダの定義

$xmlIndex = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";

これはXMLファイルの最初に必須となる宣言で、バージョン1.0、文字エンコーディングUTF-8を指定しています。

<sitemapindex> タグの開始

$xmlIndex .= "<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n";

sitemap-index.xml のルート要素となる <sitemapindex> タグを開き、その中で sitemaps.org のスキーマURLを xmlns 属性としてセットしています。

このタグがあることで、Googleなどの検索エンジンが「これは複数サイトマップをまとめたインデックスファイルですよ」と正しく認識してくれるわけですね。

このインデックスXMLには、後ほど

  • 静的ページ用の sitemap-static.xml
  • 動的に分割生成される sitemap1.xml, sitemap2.xml, …

など、全てのサイトマップファイルを登録していきます。

これで、インデックスXMLの準備が整いましたので、次はまず①で作成した静的URLを登録していきます。

静的サイトマップをインデックスXMLに追加

インデックスXMLの準備ができたら、まずは 静的ページ用のサイトマップ(sitemap-static.xml) を追加していきます。

$xmlIndex .= "  <sitemap>\n";
$xmlIndex .= "    <loc>{$baseUrl}/sitemap-static.xml</loc>\n";
$xmlIndex .= "    <lastmod>" . date('Y-m-d') . "</lastmod>\n";
$xmlIndex .= "  </sitemap>\n";

ここでは、まず <sitemap> タグを使って、静的サイトマップをインデックスXMLに登録しています。

<loc> タグには sitemap-static.xml のURLを指定しており、これによってGoogleに「静的ページ用のサイトマップはここにありますよ」と伝えることができます。

また、<lastmod> タグには date('Y-m-d') でスクリプト実行日の年月日を設定し、最終更新日を記録しています。

ここまでで、静的URLをサイトマップに登録する準備が整いました。

次は動的URLの順番なのですが、まずはslugifyという関数を用意してした準備をします。

slugify 関数の定義

動画タイトルをURL用の文字列(スラッグ:slug)に変換するために、ここで専用の関数を定義しておきます。

// slugify 関数(URL 向けテキスト整形)
function slugify(string $text): string {
    $text = mb_strtolower($text, 'UTF-8');
    $text = preg_replace('/[^\p{L}\p{Nd}]+/u', '-', $text);
    $text = preg_replace('/-+/', '-', $text);
    return trim($text, '-');
}

この slugify 関数では、まず mb_strtolower を使って文字列をすべて小文字に変換しています。

これによって、URLに大文字小文字が混在することで起こる表記ゆれを防ぐことができます。

次に、preg_replace('/[^\p{L}\p{Nd}]+/u', '-', $text) で、アルファベット(\p{L})や数字(\p{Nd})以外の文字をハイフンに置換しています。
これにより、日本語や記号が含まれる文字列でも、URLとして安全に使える文字列に変換できます。

そのあと、preg_replace('/-+/', '-', $text) で、連続したハイフンを1つにまとめています。

最後に、trim($text, '-') で文字列の先頭と末尾のハイフンを削除し、不要なハイフンがURLに残らないようにしています。

この slugify 関数を使うことで、動画タイトルをSEOフレンドリーでURLセーフな形に変換することができます。

次は、このslugifyを使って、実際に動画ページURLを生成する処理を見ていきましょう。

動画用サイトマップを分割して生成する準備

ここからは、実際に動画ページ用のサイトマップを生成していく処理に入ります。

まずは、Googleの仕様(1サイトマップあたり最大5万URL)に合わせて、動画データを分割して処理するためのループを作っています。

// 動画用サイトマップを分割して生成
for ($i = 0; $i < $totalSitemaps; $i++) {
    $batch      = array_slice($videos, $i * $maxUrlsPerSitemap, $maxUrlsPerSitemap);
    $filename   = "sitemap" . ($i + 1) . ".xml";
    $filepath   = "{$sitemapDir}/{$filename}";
    $urlOnSite  = "{$baseUrl}/{$filename}";

    $xml  = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
    $xml .= "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:video=\"http://www.google.com/schemas/sitemap-video/1.1\">\n";

この for ループでは、array_slice を使って、動画データを最大5万件ずつのバッチに切り分けています。

そして、サイトマップのファイル名を "sitemap1.xml", "sitemap2.xml" のように連番で決定し、実際に書き込むファイルパスと、公開URL用のパスも変数に格納しています。

最後に、XMLファイルとして必要なヘッダ情報、つまり、<urlset> タグ(video sitemap extensionも含む)をセットして、1つのサイトマップファイルを生成するための準備が整います。

ここまでで、「どのファイルに、どの動画データを入れるか」 の土台が完成しました。

次は、このバッチデータを使って実際に <url> 要素を生成していく処理に進みます。

各動画レコードをURL要素としてサイトマップに追加

ここからは、いよいよ動画ページのURLを実際にサイトマップXMLに書き込んでいく処理です。

foreach ($batch as $vid) {
    $slug       = slugify($vid['title']);
    $loc        = htmlspecialchars("{$baseUrl}/video/{$vid['id']}-{$slug}", ENT_QUOTES, 'UTF-8');
    $lastmod    = date('Y-m-d', strtotime($vid['updated_at']));
    $thumb      = htmlspecialchars($vid['thumbnail_url'], ENT_QUOTES, 'UTF-8');
    $title      = htmlspecialchars($vid['title'], ENT_QUOTES, 'UTF-8');
    $desc       = $title;
    $embed      = htmlspecialchars($vid['embed_url'], ENT_QUOTES, 'UTF-8');
    $duration   = htmlspecialchars($vid['duration'], ENT_QUOTES, 'UTF-8');

    $xml .= "  <url>\n";
    $xml .= "    <loc>{$loc}</loc>\n";
    $xml .= "    <lastmod>{$lastmod}</lastmod>\n";
    $xml .= "    <changefreq>daily</changefreq>\n";
    $xml .= "    <priority>0.8</priority>\n";
    $xml .= "    <video:video>\n";
    $xml .= "      <video:thumbnail_loc>{$thumb}</video:thumbnail_loc>\n";
    $xml .= "      <video:title>{$title}</video:title>\n";
    $xml .= "      <video:description>{$desc}</video:description>\n";
    if (!empty($vid['embed_url'])) {
        $xml .= "      <video:player_loc allow_embed=\"yes\">{$embed}</video:player_loc>\n";
    }
    if (!empty($vid['duration'])) {
        $xml .= "      <video:duration>{$duration}</video:duration>\n";
    }
    $xml .= "    </video:video>\n";
    $xml .= "  </url>\n";
}

この foreach ループでは、分割された動画データのバッチを1件ずつ処理しています。

まず、slugify($vid['title']) を使って、動画タイトルをSEOフレンドリーなスラッグに変換しています。

これにより、URLが

/video/1234-動画タイトルのスラッグ

のようになり、ユーザーにもGoogleにもわかりやすい構造になります。

続いて、

  • <loc> には、生成した動画ページURLをセット
  • <lastmod> には、動画レコードの更新日をY-m-d形式でセット
  • <changefreq> は「daily」(毎日更新の可能性あり)
  • <priority> は「0.8」(やや高めの重要度)

を設定しています。

そして、ここからが video sitemap extension のタグ群です。

  • <video:thumbnail_loc> に動画サムネイルURL
  • <video:title> に動画タイトル
  • <video:description> に動画説明(ここではタイトルと同じ)
  • <video:player_loc> に埋め込みプレイヤーURL(embed_urlが存在する場合のみ)
  • <video:duration> に動画の再生時間(存在する場合のみ)

をそれぞれセットし、最後にタグを閉じています。

このループを通じて、1つの動画ページに必要なサイトマップ情報が全てXMLとして出力されるわけです。

次は、この生成したXMLをファイルとして書き出し、インデックスXMLに登録する処理を見ていきましょう。

サイトマップXMLファイルの書き出しと完了メッセージ

次に、生成したサイトマップXMLを実際のファイルとして書き出す処理を行います。

$xml .= "</urlset>\n";
file_put_contents($filepath, $xml);
echo "作成: {$filename}\n";

ここでは、まず </urlset> タグを追加して、XMLファイルの末尾を正しく閉じています。

そのあと、file_put_contents を使って、生成した $xml の文字列データを先ほど指定した $filepath に保存しています。

これによって、実際に

  • sitemap1.xml
  • sitemap2.xml
  • ...

というファイルがサーバー上に書き出され、Googleなどの検索エンジンからアクセス可能になります。

最後に、echo で

作成: sitemap1.xml

のように、どのファイルを作成したかを出力しています。

このメッセージはcronバッチなどで実行ログを確認するときに便利ですね。

ここまでで、1つのサイトマップファイルを生成して書き出す処理が完了しました。

次は、この生成したファイルをサイトマップインデックスXMLに登録する処理を見ていきましょう。

生成したサイトマップをインデックスXMLに登録する

最後に、今作成したサイトマップファイルを sitemap-index.xml に登録 していきます。

// インデックスにも追加
$xmlIndex .= "  <sitemap>\n";
$xmlIndex .= "    <loc>{$urlOnSite}</loc>\n";
$xmlIndex .= "    <lastmod>" . date('Y-m-d') . "</lastmod>\n";
$xmlIndex .= "  </sitemap>\n";

ここでは、インデックスXML用の変数 $xmlIndex に

  • <sitemap> タグを追加
  • <loc> には、今作成したサイトマップファイルの公開URLをセット
  • <lastmod> には、スクリプト実行日の年月日をセットしています。

この処理によって、

のように、作成した全てのサイトマップファイルが sitemap-index.xml に登録され、Googleなどの検索エンジンに「このサイトには複数のサイトマップファイルがありますよ」と伝えることができます。

ここまでで、分割生成した各サイトマップを インデックスXMLにまとめる処理 が完了しました。

次は、インデックスXMLを最終的にファイルとして書き出す処理を見ていきましょう。

インデックスXMLの書き出しとスクリプト終了

最後に、サイトマップインデックスXMLをファイルとして書き出す処理を行います。

// サイトマップインデックス出力
$xmlIndex .= "</sitemapindex>\n";
file_put_contents("{$sitemapDir}/sitemap-index.xml", $xmlIndex);
echo "作成: sitemap-index.xml\n";

exit(0);

ここでは、まず </sitemapindex> タグを追加して、インデックスXMLの末尾を正しく閉じています。

続いて、file_put_contents を使って、これまで $xmlIndex に作成してきたインデックスXML

sitemap-index.xml

というファイル名で $sitemapDir に保存しています。

最後に、echo で

作成: sitemap-index.xml

というメッセージを出力し、スクリプトが正常に完了したことをログに残しています。

exit(0); でスクリプトを終了し、処理全体が終了です。

これで、動的URLを含むSEOフレンドリーなサイトマップ生成処理の解説は完了です。

もし運用環境に導入する場合は、cronに組み込んで定期更新すると、Googleインデックス速度の改善に繋がるかもしれません。ぜひ試してみてください。

まとめ

ここまで、読んでくださってありがとうございました!

オフィスのティアラ

①・②と「動的ウェブサイトで、動画タイトルの文字列を使って動的サイトマップを作る」をやってきました。

今回紹介したように、

  • 動画タイトルをslug化してURLに含める
  • 動的URLを含むサイトマップを自動生成する

という仕組みを導入することで、SEO評価が上がるだけでなく、SNSシェア時にもURLが直感的に分かりやすくなるというメリットがあります。

正直、最初に実装する時は「めんどくさい…」と思いましたが、一度仕組みを作ってしまえば、あとはcronで定期実行するだけ。

私はこの仕組みを導入してから、Googleインデックスまでの速度が体感的に早くなった気がしています。(もちろんアルゴリズム次第なので保証はできませんが…)

ぜひ、あなたの開発プロジェクトでも試してみてください。

今回の記事が参考になれば嬉しいです。

今回のコードが実際に動いている「追跡されないアダルト動画の検索エンジン」Sae-Pornsはこちら!よかったら見ていってくださると嬉しいです。
※18歳未満の方はご利用いただけません。

sae-porns.org

それでは、皆さま、よい開発ライフを。

 

【現実】IT副業の闇。キラキラフリーランスウェブデザイナー講座は詐欺まがい

こんにちは、にょろりんこ備忘録雑記ブログです。

ちょうど前回の記事をX(旧ツイッター)にポストしようとした時のことなんですけど、「キラキラ副業ウェブデザイナー講座」みたいな広告が流れてきました。

うっかりクリックしてしまったのですが、いやあ、ひどいもんです。

曰く、

  • 未経験でも月30万
  • 3ヶ月でフリーランスデビュー
  • スキマ時間で自由に働ける

・・・現実を知っている人からしたら、突っ込みどころしかありません。

競合の視点が抜けている

何が酷いかというと、こういった広告って競合の視点がまったく抜け落ちているんです。

だって考えてみてください。

なぜ、ちょっと勉強しただけのあなたが「何年も現場で鍛えられてきたプロのデザイナー」や「法人チームで組織的に案件を取っている制作会社」に勝てると思うのでしょうか?

「未経験でも月30万稼げます!」なんていう甘い言葉に、この現実的な競合の存在は一切書かれていない。それが一番、恐ろしいところだと思います。

マーケットの本質は競争

マーケットの本質が競争である以上、何かをはじめるときには、この競合の存在を確認することが本当に大切です。

例えば、あなたが結婚相談所の職員だったとして、アラフォー無職の女性会員が「私、年下で年収1000万以上のキラキラ男子と結婚したいんです!」と言ってきたら、どう思いますか?

普通に考えたら、「いや、それはちょっと厳しいですよ・・・」って言いますよね。

なぜか?

それは、競合がいるからです。

その年下で年収1000万以上のキラキラ男子は、他にも若くて可愛くて、もっと条件の良い女性から山ほどアプローチを受けているわけです。

つまり、マーケットには常に競争が存在していて、その競争の中で自分がどうポジションを取るかを考えない限り、現実的にはうまくいかないということです。

これは逆も同じで、アラフォー無職の男性が20代の女性と結婚したいと言ってきたら、それもまた厳しい。

確かにラッキーパンチが当たることもあります。だから、確率はゼロではありませんが、それは大局的にはありえないわけです。

宝くじに当たる人だっている、というレベルの話です。

ノートを使うティアラ

話を副業キラキラウェブデザイナーに戻しましょう。

これが、競合の存在がある限り、ちゃんと広告どおりのキラキラ高収入になるわけがないのは、明らかなわけですよ。

正義気取りではなく私が生き残るため

よくこういうことを言うと、「うわっ正義マン出たよ、余計なお世話、承認欲求乙」と言われます。

確かに、正義のためというのは無くはありません、しかし、ここで重要なのは、私が開発を続けていくためには、健全で自由な市場が必要だということです。

業界の隅っこで糊口をしのいでいる私ですが、そんな私だからこそ思うんです。「業界で生きていくためには、業界そのものが健全である必要がある。」と。

例えば、エステ業界を見てください。

一部のサロンが悪質なローン契約を組ませたり、高額コースを押し売りした結果、業界全体が「エステ=怪しい」というイメージを持たれるようになってしまいました。

そうなると、真面目にやっている良心的なサロンまで疑われて、経営が苦しくなるわけです。

同じことが、Webデザイン業界でも起こりうる。

もし業界全体が、「バカを騙すだけの詐欺まがいビジネス」だと思われてしまったら、真面目にやっている人間まで信用されなくなります。

結局、それで一番困るのは、私のような弱小プレイヤーなんですよ。

だから、例えば、誰かを騙して儲けてる人がいたり、逆に、誰かが騙されそうになっているのを見かけたとき、私は「騙されてますよ」と言わざるを得ません。

もちろん、正義感も多少はありますが、それ以上に、業界全体が汚れてしまったら、私がどれだけ真面目にやっても生きていけなくなるからです。

絶対に無理でないところが悪質。詐欺まがい。

何べんも言いますが、絶対に無理だと言っているわけではありません。

ピチピチギャルと結婚する普通のアラフィフおじさんが、この世に0人ということはないでしょう。

だから、この話は悪質なんです。詐欺まがいだけど詐欺ではない。絶対に当たらない宝くじは詐欺だが、極超低確率で当たるなら詐欺ではない。

私は、この業界をギャンブル業界にしたくないんです。

というか、もしギャンブル業界になってしまったら、世間からは「怪しい業界」だと思われ、私のような弱小は生き残れなくなります。

真面目にやっていても、業界全体の信用が落ちれば、真面目な人まで食えなくなるのが世の常なわけです。

稼げないのは努力が足りないから?

このように書くと、それはその人の努力が足りなかっただけ。という反論を主に業者サイドから聞きます。

これは一理あります。まあ、一理もないものはこの世にあんまりありませんが。

でも、だったら何度も言いますが、その努力・学習コストは看護師資格や電験三種に向けた方が安定的にも経済的にも勝率が高いです。

ウェブデザイナーになるのに免許はいりません。つまり参入障壁がむちゃくちゃ低いんです。ということは競合も多く、単価も下がりやすい世界。

死屍累々のレッドオーシャンがどこまでも広がります。

そこで生き残るのは、才能か、血の滲むような努力か、運か、その全部です。

実績ゼロからでもクラウドソーシングで案件取ればいい?

いや、何遍も言いますが、専業&法人が渦巻くWebデザイン業界で、実績ゼロのあなたに回ってくる案件ってどんなのですか?

答えは簡単で、

  • 誰でもできる超低単価案件
  • 時給換算したら数百円以下
  • クライアントから無茶振りされる修正地獄案件

こういう案件ばかりです。

最初の実績作りだとしても、生活費を稼ぐどころか、心がすり減って終わる可能性のほうが高いですよ。

専業&法人がレッドオーシャンにちょっと勉強しただけの副業フリーランスがどうやって勝つんでしょうか。現実的にはまず無理です。

もちろん、何回も言いますが、無職アラフォー婚活女子が年収1000万オーバーのキラキラ3K男子と結婚できる可能性はゼロではありません。

ただ、それは再現性が限りなくゼロで、だから悪質・詐欺まがいなわけですよ。

とにかくスタバでキメながらキラキラ仕事したいの!

そういう気持ち、分からなくはないです。

もし、それでも「スタバでキメながらキラキラ仕事がしたい!」というなら、一つだけ方法があります。

それは、専業や法人に勝てる圧倒的なセンスや実績を持つことです。

例えば、

  • この人に任せればコンバージョンが100%になる
  • 広告費を費やせば費やすだけ無限に儲かるLPが作れる
  • 見た人の全てが「芸術だ!」と感嘆するデザインが作れる

そんなレベルのスキルと成果を出せるなら、キラキラどころか、クライアントから頭を下げてお願いされる立場になります。

でも、そこまで行くには、ちょっと副業講座を受けたくらいでは無理というのが現実です。

というか、そのレベルに達しようと、天性のセンスを持った人が努力を続けているのがウェブデザイン業界なわけです。最終的には生き方の問題ですから、私のような他人がとやかく言うべきではありません。

しかし、それほど厳しい業界だということを、あえて教えずに(大金受講料をとって)飛び込ませるなら、それは悪質・詐欺まがいであり、詐欺まがいが増えると、業界全体が悪いものになり、私のような弱小が生きていけなくなるわけです。これは困ります、だから私は言うんです。

全体のまとめ

今回は、「キラキラ副業ウェブデザイナー講座」の広告がいかに現実離れしているか、そしてそれが業界全体に与える悪影響についてお話ししました。

キラキラ広告は、

  • ちょっと勉強しただけで月30万稼げる
  • 3ヶ月でフリーランスデビューできる
  • スキマ時間で自由に働ける

などと甘い言葉を並べますが、そこには競合という視点が完全に抜け落ちています。

マーケットには常に競争があります。未経験者が何年も修行を積んできた専業や法人に勝つのは、宝くじに当たるレベルの確率です。

もちろん、宝くじに当たる人もいれば、アラフィフ無職おじさんがピチピチギャルと結婚できる可能性もゼロではありません。でも、それを「当たり前の未来」かのように広告で煽るのは、詐欺まがいと言わざるを得ません。

そして、そういう詐欺まがいビジネスが蔓延ると、業界全体が怪しいものだと思われてしまう。結局、困るのは、業界の隅っこで真面目に糊口をしのいでいる私のような弱小プレイヤーです。

だから、これは単なる批判ではなく、業界で静かに生きる私自身のサバイバル戦略でもあるのです。

「甘い話は存在しない」と。

私が口糊をしのいで作成している「あなたを追跡しないアダルト動画の検索エンジンSaePorns」はこちら。よかったら見ていってください。
※18歳未満の方はご利用いただけません。

sae-porns.org

動的ウェブサイトで、動画タイトルの文字列を使って動的サイトマップを作る①

こんにちは、にょろりんこの備忘録的技術ブログです。

今回のシリーズでは、動画タイトルの文字列をそのまま使って、動的サイトマップSEOフレンドリーなURLを自動生成する方法を書いていきます。

地図を見るティアラ

なぜこんなことが必要かというと、こんな現象でお困りではないですか?

  • 動画サイトを運営しているけど、Googleのインデックスが全然進まない
  • 動画ごとにURLはあるけど、/video.php?id=1234 みたいな無機質なURLでSEO評価が上がらない
  • 動画タイトルをURLに含めたいけど、日本語タイトルをどうやってURLセーフに変換すればいいかわからない
  • SNSやLINEでURLを共有しても、何の動画かパッと見て分からない

こういう悩み、私もずっとありました。

では、こんな時にどうすればいいかというと、動画タイトルをもとにSEOフレンドリーなスラッグ(slug)を自動生成し、そのスラッグ付きURLをサイトマップに登録するという方法を使います。

これによって、

  • URLを見ただけでどんな動画か分かる
  • SNSでシェアしても怪しさが減る
  • Googleのインデックス評価も上がる

という、一石三鳥の効果が得られます。

今回使う言語はPHPです。

なぜPHPかというと「サーバーサイドでサクッと実行できる」「cronに組み込むだけでバッチ処理として動かせる」からです。もちろん、Node.js や Python でも同じことはできますが、今回は私が実際に運用しているPHPスクリプトを紹介します。

以下実際にコード見ていきましょう。

排他ロックの設定準備

このような自動処理プログラムの場合、まずは他の処理と競合しないように排他ロックの設定が重要になってきます。

排他ロック設定は、cronバッチを安定稼働させるための必須テクニックですね。

$lockFile = __DIR__ . '/generate_sitemap.lock';
$fp = fopen($lockFile, 'c');
if (!$fp) {
    fwrite(STDERR, "ロックファイルを作成できませんでした: {$lockFile}\n");
    exit(1);
}

上記コードでは、ロックファイルのパスを変数$lockFileにセットしfopenでそのファイルを作成または開いて、排他ロックをかける準備をしています。

ここまでで、排他ロックを設定する準備ができました。次は、実際にこのロックを取得して二重起動を防ぐ処理に進みます。

排他ロック取得と二重起動防止

if (!flock($fp, LOCK_EX | LOCK_NB)) {
    // すでに別プロセスが動いている
    echo "サイトマップ生成プロセスは既に実行中です。終了します。\n";
    exit(0);
}

上記コードでは、すでに他のプロセスがロックを取得していないか確認し、もし取得できなければ「すでに実行中」と表示して、二重起動を防ぐためにスクリプトを終了しています。

ここまでで、排他ロックの取得が完了しました。次は、スクリプト終了時にロックを解除する後処理を登録していきます。

スクリプト終了時の後処理

// スクリプト終了時にロック解除・ファイル削除
register_shutdown_function(function() use ($fp, $lockFile) {
    flock($fp, LOCK_UN);
    fclose($fp);
    @unlink($lockFile);
});

上記コードでは、スクリプトが終了するときに自動でロックを解除し、ファイルポインタを閉じて、最後にロックファイルを削除する処理を登録しています。これにより、次回実行時に古いロックが残って動かなくなる問題を防いでいます。

ここまでで、排他ロックに関する一連の設定が完了しました。次は、実際にサイトマップを生成する処理に進んでいきます。

古いサイトマップの削除

実際にサイトマップを作成する前に、まずは古いサイトマップを削除しておく必要があります。実際のコードは以下です。

$old = glob($sitemapDir . '/sitemap*.xml');
if ($old) {
    foreach ($old as $f) {
        if (is_file($f)) {
            unlink($f);
            echo "古いサイトマップ削除: " . basename($f) . "\n";
        }
    }
}

上記コードでは、glob 関数で既存のサイトマップファイルを全て取得し、unlink で削除しています。これにより、前回生成された古いサイトマップを一掃してから新しいサイトマップを作成できるようにしています。

ここまでで、サイトマップを生成する前のクリーンアップが完了しました。次は、静的ページ用のサイトマップを作成する処理に進みます。

静的ページURLリストの定義

サイトマップには、動的な動画ページだけでなく、トップページや利用規約ページなどの静的ページも含める必要があります。以下のコードでは、その静的ページのURLや優先度、更新頻度を配列で定義しています。

$staticUrls = [
    ['url' => "{$baseUrl}/",       'priority' => '1.0', 'changefreq' => 'monthly'],
    ['url' => "{$baseUrl}/terms",   'priority' => '0.5', 'changefreq' => 'yearly'],
    ['url' => "{$baseUrl}/pc",      'priority' => '0.7', 'changefreq' => 'monthly'],
];

SaePornsは、コンテンツページとトップページ、規約ページ、管理者用のプロセスコントロールページを含めた、3種類の固定ページが存在します。

上記コードでは、その3つのURLに対して、それぞれの優先度(priority)と更新頻度(changefreq)を設定しています。

優先度はグーグル公式にも「この値がインデックス順位を保証するものではない」とされていますが「クローラビリティ向上」「サイト全体構造のヒント」を与えるという意味で設定しておくことが推奨されています。

正直なところ、管理者用ページはグーグルなどの検索エンジンにクロールさせる必要性も無いと思いますが、ここらへんはいずれABテストをして考えていこうと思います。

ここまでで、静的ページ用のURLリストが準備できました。次は、このリストをもとに静的ページ用サイトマップを生成する処理に進みます。

XMLヘッダとルート要素の定義

$xmlStatic  = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
$xmlStatic .= "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n";

この2行では、静的ページ用サイトマップXMLファイルとしての基本構造を宣言しています。

1行目では、XML宣言としてバージョン1.0と文字エンコーディングUTF-8を指定しています。

2行目では、サイトマップのルート要素である <urlset> タグを定義し、その中で sitemaps.org で定められているスキーマURLを xmlns 属性としてセットしています。

ここまでで、XMLファイルを正しく認識させるためのヘッダ部分が完成しました。

静的ページ用サイトマップの生成と保存

foreach ($staticUrls as $page) {
    $loc        = htmlspecialchars($page['url'], ENT_QUOTES, 'UTF-8');
    $lastmod    = date('Y-m-d');
    $freq       = $page['changefreq'];
    $prio       = $page['priority'];
    $xmlStatic .= "  <url>\n";
    $xmlStatic .= "    <loc>{$loc}</loc>\n";
    $xmlStatic .= "    <lastmod>{$lastmod}</lastmod>\n";
    $xmlStatic .= "    <changefreq>{$freq}</changefreq>\n";
    $xmlStatic .= "    <priority>{$prio}</priority>\n";
    $xmlStatic .= "  </url>\n";
}
$xmlStatic .= "</urlset>\n";
file_put_contents($sitemapDir . '/sitemap-static.xml', $xmlStatic);
echo "固定ページ用サイトマップ作成: sitemap-static.xml\n";

このブロックでは、先ほど定義した $staticUrls 配列をもとに、静的ページ用のサイトマップXMLを作成しています。

まず foreach で $staticUrls に含まれる各ページ情報を取り出し、htmlspecialchars を使ってURLをエスケープしています。更新日時には、スクリプト実行日の Y-m-d 形式の日付をセットし、変更頻度(changefreq)や優先度(priority)も配列から取得してそれぞれXML要素として組み立てています。

ループが終わったあと、</urlset> タグでXMLを閉じ、file_put_contents で sitemap-static.xml というファイル名で保存しています。最後に、固定ページ用サイトマップが作成されたことを示すメッセージをechoしています。

ここまでで、静的ページ用サイトマップの生成と保存が完了しました。

長くなってきたので、今回はここまで。

次は、いよいよ、動画ページなど動的URLのサイトマップ生成処理に進みます。

このコードは、実際に「あなたを追跡しないアダルト動画検索エンジンSaePorns」で運用されています。よかったら見ていってください。

※18歳未満の方はご利用いただけません。

sae-porns.org

 

Boodigoの失敗から学ぶ!アダルト動画検索エンジン 成功の条件と戦略

こんにちは、にょろりんこの備忘録的技術ブログです。

みなさんは Boodigo(ブーディゴ) って知っていますか?

BoodigoはBoodigo, LLC社により2014年9月15日に公開されたアダルト専門の検索エンジンです。当時は「アダルト版Google」とも呼ばれ、ニッチながらも業界内では期待を集めました。しかし、残念ながら今はサービスを終了しており、その名前を知る人も少なくなりました。

 

ティアラとパソコン

では、なぜ Boodigo は失敗(サービス終了)してしまったのか?そして、Sae-Pornsは同じ轍を踏まないために、今のアダルト検索エンジンには何が必要なのか?

なにせ、情報が少ないのですが、今回はこのテーマを、仮説を立てつつ、にょろりんこの視点で分解・解説してみます。

仮説1:市場が狭すぎた

Boodigoが失敗した理由のひとつは、市場がニッチすぎたことではないでしょうか。

アダルト検索エンジンというと聞こえは良いですが、実際にはユーザーの多くはPornhubやXvideosなど大手ポータルサイト内の検索だけで事足りる、もしくはGoogle検索で女優名や作品コードを入れるだけで十分、という現実があります。

つまり、「アダルトコンテンツ全体を網羅検索したい」というニーズが思ったほど大きくなかったという仮説です。

まあ、ここはデータ無いんでなんとも言えないのですが、実際、ユーザーの検索行動として、まずPornhubで検索、無ければXvideosや別サイトを試す、さらにGoogle検索で直リンクを探すといった、全てを網羅的に探すという検索行動は想像に難くありません。

だから、ポイントはBoodigoがユーザーのそういった検索ニーズを活かせなかった、ということだと思います。

本来なら、Boodigoが、PornhubにもXvideosにもないマイナー作品を横断検索できる、複数サイトを一括検索できる、という利便性を提供できれば、一定の需要があったはずです。

しかし、現実には、クロールしているサイトが限定的だったり、コンテンツのクリーニングが甘かったりなどで、ユーザーが求める「代替サイト探索ツール」として成立しなかった可能性が高いでしょう。

この行動パターンを踏まえると、Sae-Pornsでは「大手サイトには無いマイナー作品やローカルアップロード作品」「複数サイト横断検索と日本語化による網羅性強化」を追求することで、単なる水平検索ではなく、「最後の砦」になる検索体験を提供できるはずです。

つまり、Boodigoが市場規模不足で負けたのではなく、「ポータルを横断する真の便利さ」を実現できなかったから不要と判断されたと捉える方が、戦略上は建設的です。

ここらへん、Sae-Pornsではちゃんと手を打っています。

具体的には、

・日本語タイトル化
→ マイナー海外作品でも、日本語検索クエリで引き当て可能にする。

・MythoMaxによる自然翻訳
英語圏サイトの独特なタイトル表現も、AV風の自然な日本語キャッチコピーに変換。

・Sudachi形態素解析によるタグ生成
→ タイトルをただの文字列として扱わず、タグ単位で構造化。類義語展開や関連ワード検索にも対応。

・対象サイトの戦略的絞り込みとインデックス品質管理
→ どこでもクロールするのではなく、ユーザーが実際に視聴可能なサイト、コンテンツが生きているサイトを中心にインデックス。

これにより、Sae-Pornsは「無いものは無い」で終わらない検索体験や「複数サイト横断検索 × 日本語化 × タグ生成」の三位一体で、ただの水平検索エンジンではなく、日本語ユーザーに最適化された「アダルト動画の探索エンジン」 になることを目指しています。

このように、Boodigoの失敗を「市場が無かった」で片付けず、「市場はあるが、必要とされるUXが提供されなかった」と再定義し、そこに技術的・設計的打ち手を入れることが、Sae-Pornsの差別化戦略です。

仮説2:収益化モデルの不在・高コスト構造

Boodigoが失敗した理由としてもう一つ考えられるのが、収益化モデルの不在です。

アダルト検索エンジンというのは、トラフィックは一定数稼げる可能性があっても、成人広告のCPC(クリック単価)やCPM(インプレッション単価)は、一般広告に比べて圧倒的に低い、Google Adsenseなどの一般DSPネットワークは使えない、成人向けアフィリエイト広告単価自体が安い上に、転換率も安定しない、という現実があります。

つまり、検索エンジンを運営するコスト(クローラー運用、サーバー、開発工数)に対して、広告収益だけではペイしづらい構造なのです。

さらに、Boodigoはポルノ制作会社や元Google社員チームが立ち上げたとはいえ、「広告ネットワークも自前で作る必要がある」「アダルト広告主開拓という別事業」が必要という二重苦を抱えていたことは容易に想像できます。

その結果「トラフィックがあってもマネタイズが追いつかない」「追加投資や拡大フェーズに乗れない」「投資家からの資金調達も難しい(成人領域は嫌忌される)」という典型的な「ニッチ市場 × 収益化不在」の罠に陥った可能性が高いと考えられます。

だからこそ、Sae-Pornsではこの構造的課題に対して、「収益多角化で攻める」というよりも「徹底した低コスト運用で守る」というアプローチを取っています。

具体的には、以下です。

・インフラコスト最適化
→ 大規模クローリングをせず、ターゲットを絞ったURL抽出とメタデータ解析でDBサイズを最小化

・AI翻訳・タグ生成も自前サーバー運用
クラウドAPI課金ではなく、自前推論サーバーを活用

・人件費ゼロ運営
→ プログラム自動化により、運用は限りなく無人

さらに、収益面では、アダルト産業を煽情的で「悪いもの」と捉えるのではなく、「性を含めた自己表現や娯楽を支える文化的コンテンツ」として尊重する姿勢を明確に打ち出すことで、

  • 性教育系メディア
  • フェムテック商品
  • 性に肯定的な心理カウンセリングサービス

といった、共感型スポンサーや広告主との連携を模索しています。

つまり、Boodigoが高コスト構造と「単純な成人広告モデル」で行き詰まったのに対し、Sae-Pornsは「低コスト構造 × 共感型スポンサー開拓」という別ルートで持続可能性を高める戦略を採用しています。

このように、収益多角化が難しいなら「徹底的にコストを下げ、思想に共感する小さなスポンサーで十分黒字化する」という選択肢も、ニッチ市場では合理的です。

仮説3:検索品質・データクレンジング不足

Boodigoがユーザーに支持されなかった理由として、検索結果の品質が低かった可能性は大いにあります。

アダルト検索エンジンにおいては、

・クロール可能サイトが限定的
→ 動画プレイヤーがJS動的生成でHTMLにURLが無い

・リンク切れコンテンツの混入
→ 削除済みや無効なページが検索結果に大量残存

・スパムやリダイレクト広告サイトの混入
→ 詐欺広告サイトやマルウェア配布ページが混在

といった問題が致命傷になりやすいです。

ユーザーは「見つからない」よりも「リンク切れページが大量に出る」「詐欺サイトに飛ばされる」ことに強いストレスを感じ、結果としてサービス自体を信頼しなくなります。

Boodigoは、全体を網羅する水平検索を志向したがゆえに、こうした不要データのクレンジングや品質管理が追いつかなかったのではないでしょうか。

この点を想定し、Sae-Pornsでは以下の対策を採用しています。

・対象サイトを限定する
→ 最初から一定基準を満たす数十サイトに絞ることで、リンク切れ率を最小化

・定期クロールによる死活監視(クレンジング)
→ DBにあるURLが本当に存在するか、再生可能かを周期チェック

メタデータの正規化と翻訳補正
→ タイトル統一、無意味文字列除去、日本語キャッチコピー生成でDB品質を向上

つまり、Sae-Pornsは水平検索の「」ではなく、垂直特化型の「」を重視しています。この戦略により、以下のようなUX提供が可能となります。

  • そもそも検索結果にスパムが混入しない
  • リンク切れが極小化され、ユーザーの信頼感が高まる
  • タイトル日本語化やタグ生成により、Googleや大手ポータルにない検索体験を提供

Boodigoが「量」を求めて品質管理を後回しにしたのに対し、Sae-Pornsは最初からデータクレンジングを中心に据えた設計で、少数精鋭DBでも戦えるUXを目指しています。

仮説4:非追跡型であるがゆえの収益化ジレンマ

Boodigoは、SaePornsと同じくプライバシー保護を重視した検索エンジンでした。

今となっては実体は不明ですが、これは、アダルト検索において「誰にも知られたくない」という強いニーズに応える理想的な思想です。

しかし、その非追跡型設計こそが、収益化モデルとのジレンマを生んだのではないでしょうか。

アダルト広告市場は、もともとCPCやCPMが低く、一般DSPも使えないため収益源が限定されています。

そんな中で、

  • リターゲティング不可
  • パーソナライズ広告不可

という「プライバシー重視設計」を選択すると、残るのはバナー型クリック課金やサイト広告主契約など、単価の低いモデルのみ。

つまりBoodigoは、ユーザーの匿名性を最大化しつつ、広告収益の効率を最大化するという、ビジネスモデルとしては両立が極めて難しい戦略を取らざるを得なかった。

これは、DuckDuckGoがプライバシー検索+キーワード連動広告(非追跡でも成り立つ規模)で成長できたのとは対照的です。

アダルト検索市場はそもそも規模が小さいため、非追跡型のままでは収益が成り立たなかった可能性があります。

この教訓からSae-Pornsでは、「プライバシー保護は当たり前その上で、運営コストを極小化し、収益規模が小さくても黒字化できる構造にする」という戦略を取っています。

Boodigoが失敗したのは、プライバシーを追求したからではなく、「プライバシーと広告モデルの矛盾を乗り越える運用コスト戦略が無かった」ということかもしれません。

仮説5:思想が弱く、単なるツールになってしまった

Boodigoが失敗した理由として、ビジネス哲学(存在意義)が弱かったことも挙げられるかもしれません。

Boodigoは当時、「アダルト版Google」というキャッチーなポジショニングで登場しました。

しかし、振り返ってみると「アダルト検索を便利にする」 以上の思想が無かった「なぜBoodigoを使うべきか」というストーリーが提示されなかったという印象が否めません。

結果として、

  • メディアやユーザーが熱狂するようなビジョンが無い
  • ブランドコミュニティが育たない
  • 「使わなくても困らないツール」で終わってしまった

という、プロダクトが道具止まりになる典型パターンに陥った可能性があります。

テスラアップルが単なる車、単なるスマホを売っているわけではないように、ユーザーは「このサービスを使うことが自分の価値観を体現している」と思えるプロダクトにこそ熱狂します。

もしBoodigoが、「性表現を文化として尊重する」「検閲や規制から自由を守るプラットフォームである」といった、強い思想や哲学を掲げることに成功していたら、単なる検索ツールではなく、「Boodigoを使うこと自体が表明になる」ようなサービスになれたかもしれません。

この点、Sae-Pornsでは

  • アダルトコンテンツを文化的表現として可視化する
  • 翻訳やタグ生成によって「知」として整理する

という思想を明確にし、単なる便利ツールではなく、性表現の尊厳価値を可視化するプラットフォームを目指しています。

Boodigoが道具止まりで終わったからこそ、Sae-Pornsは思想と機能を両立するサービスを作りたいと思っています。

まとめ:Boodigoの教訓から何を学ぶか

ここまで、Boodigoの失敗要因について仮説をいくつか立ててきました。

  • 市場が狭すぎた
  • 収益化モデルの不在・高コスト構造
  • 検索品質・データクレンジング不足
  • 非追跡型であるがゆえの収益化ジレンマ
  • 思想が弱く、単なるツールになってしまった

正直、Boodigo自体の情報が少なく、実際の運営状況やチーム体制がどうだったのか、広告収益がどれくらいあったのかも外からはわかりません。

なので、今回書いたことはあくまで仮説であり、にょろりんこの推測に過ぎない部分も多いです。

ただ、それでも確実に言えるのは、

  • 検索エンジンを作る」というのは途方もない技術と運用コストがかかる
  • 市場規模に対して開発コストが大きすぎると、どうやってもペイしない

というシンプルだけど重い事実です。

そしてSae-Pornsでは、Boodigoの轍を踏まないように「低コスト運用を徹底する」「思想と機能を両立させ、「ただのツール」ではない存在を目指す」という方針で進めています。

Boodigoが失敗したからこそ、今、私たちが学べることはたくさんあります。

いやほんと、情報が少ないので・・・もし当時の運営チームや詳しい関係者のインタビュー記事などがあれば、誰か教えてください(泣)。

それではみなさん、よい開発ライフを。

開発中の「あなたを追跡しないアダルト動画の検索エンジン、SaePorns」はこちら
※18歳未満の方はご利用いただけません。

sae-porns.org

 

Pythonで作る形態素解析API:SudachiPyとFlaskで最速分割サーバー構築②

こんにちは、にょろりんこの備忘録的技術ブログです。

今回は、Pythonで作る形態素解析API構築シリーズの第2回 をお届けします。

前回の記事では、SudachiPyをFlaskと組み合わせ、POSTでテキストを送信すると名詞や動詞、形容詞などの基本トークンを返す最小構成API を作成しました。

www.n-rinko.com

これだけでも、検索クエリ生成やタグ付け処理の基盤として十分活用できますが、実際の運用では以下のような機能が欲しくなってきます。

  • 名詞だけでなく、複合語(例:癒し系彼女)や文節単位のフレーズも抽出したい
  • 名詞同士や英単語同士のバイグラム(2単語連結)を生成したい
  • 動詞・形容詞は原形(辞書形)に統一しておきたい
  • 類義語(シノニム)展開もAPI側で済ませたい

そこで今回は、これらの要望をすべて盛り込んだ最速分割サーバー②完全版コードを紹介します。

次章から、この改良版APIコードを処理の流れとともに丁寧に解説していきます。

実際のコード

まずはエンドポイントの設定

# APIエンドポイント
@app.route('/extract', methods=['POST'])
def extract():
    data = request.get_json()
    if not data or 'title' not in data:
        return jsonify({"error": "title が指定されていません"}), 400

この部分では、/extract というPOST用APIエンドポイント(APIのアクセス先)を定義しています。リクエストからJSONデータを取得し、title キーがなければ400エラーを返して処理を終了します。

タイトル(文字列)のクリーニング

text = data['title']
cleaned = re.sub(r'[。、・「」『』【】()]', '', text)

この部分では、リクエスJSONから title のテキストを取り出し、re.sub() を使って句読点や括弧など不要な記号を削除しています。これにより、形態素解析で余計なトークンが生成されるのを防いでいます。

SudachiPy Tokenizerの生成と解析

#Tokenizerを都度生成

tokenizer_obj = dictionary.Dictionary(dict="full").create()
mode = tokenizer.Tokenizer.SplitMode.C
tokens = tokenizer_obj.tokenize(cleaned, mode)

この部分では、SudachiPyのTokenizerオブジェクトを都度生成し、解析モードを SplitMode.C(最も細かく分割するモード)に設定して、先ほどクリーニングしたテキストを形態素解析しています。都度生成することで、スレッドセーフにAPIを運用できます。

ここまでで、クリーニングされたタイトル文字列を形態素に分解することができました。

名詞トークンの抽出

word_tokens =
for t in tokens:
    if t.part_of_speech()[0] == "名詞":
        noun = normalize_noun(t.surface())
        if is_valid_token(noun, "名詞"):
            word_tokens.append(noun)

この部分では、形態素解析で得られたトークンの中から品詞が「名詞」のものだけを抽出しています。normalize_noun() で名詞を正規化(末尾助詞の除去など)し、is_valid_token() で妥当性をチェックしてから word_tokens リストに追加しています。

ここまでで、テキストから有効な名詞トークンのリストを作ることができました。

英単語の抽出と名詞トークンへの追加

english_tokens = re.findall(r'\b[a-zA-Z]{3,}\b', text)
word_tokens.extend(english_tokens)

この部分では、元のテキストから3文字以上の英単語を正規表現で抽出しています。そして、その英単語リストを先ほどの名詞トークンリスト word_tokens に追加しています。

ここまでで、日本語の名詞トークンだけでなく、英単語も検索語句として扱えるようになりました。

SaePornsでは英語表現はMythomaxで日本語化されているため、あまり使わないコードですが、一般的なニュース記事解析や技術文書解析など、英単語をそのままキーワードとして活用したい場合には役立つ処理です。

例えば、プログラミング言語名や技術用語、ブランド名などは無理に翻訳せず英語のまま検索対象にした方が精度が高まるため、この処理を残しています。

動詞・形容詞の原形変換と抽出

normalized_forms =
for t in tokens:
    pos_major = t.part_of_speech()[0]
    if pos_major in {"動詞", "形容詞"}:
        base = convert_base(t.normalized_form())
        if base != t.surface() and is_valid_token(base, pos_major):
            normalized_forms.append(base)

この部分では、トークンの品詞が「動詞」または「形容詞」の場合、normalized_form() で辞書形(原形)に変換しています。さらに、convert_base() を通して「為る」を「する」に変換するなどの正規化を行い、元の表層形と異なり、かつ有効な単語であれば normalized_forms リストに追加しています。

ここまでで、動詞や形容詞を検索やタグ付けに使いやすい形(原形)に統一することができました。

文節分割関数の定義

def split_bunsetsu(tokens) -> list[list]:
    bunsetsu, current = ,
    for t in tokens:
        if t.part_of_speech()[0] in {"名詞", "動詞", "形容詞", "形状詞"}:
            if current:
                bunsetsu.append(current)
            current = [t]
        else:
            current.append(t)
    if current:
        bunsetsu.append(current)
    return bunsetsu

この部分では、形態素解析で得られたトークン列を文節単位に分割する split_bunsetsu() 関数を定義しています。

処理内容としては、トークンの品詞が「名詞」「動詞」「形容詞」「形状詞」のいずれかの場合に文節の切れ目とみなし、それまでの current を bunsetsu に追加して新しい文節を開始します。それ以外の品詞は現在の文節に追加されます。

一般的には、文節は自立語(名詞・動詞・形容詞・形容動詞)で始まりますが、SudachiPyの環境では「形容動詞」が「形状詞」という名前で扱われているため、ここでも 形状詞 を指定しています。

ここまでで、形態素トークンのリストを文節単位で分割する準備が整いました。

文節フレーズの生成

bunsetsu_phrases =
for seg in split_bunsetsu(tokens):
    phrase = ''.join(t.surface() for t in seg)
    if len(phrase) > 1 and is_valid(phrase):
        bunsetsu_phrases.append(phrase)

この部分では、先ほど定義した split_bunsetsu() 関数を使ってトークン列を文節単位に分割し、それぞれの文節内のトークンを結合してフレーズを作成しています。

生成したフレーズは、2文字以上で is_valid() による妥当性チェックを通過したものだけを bunsetsu_phrases リストに追加しています。

ここまでで、解析結果から文節単位の自然なフレーズ候補を取得できるようになりました。具体的には、例えば「制服の少女が微笑んでいる」というタイトルの場合、「制服」「少女」「微笑んで」「いる」といった文節に区切られたフレーズが抽出できます。

オフィスでくつろぐティアラ
名詞+助詞+名詞の複合語生成

compound_phrases =
for i in range(len(tokens) - 2):
    t1, t2, t3 = tokens[i], tokens[i+1], tokens[i+2]
    if (t1.part_of_speech()[0] == "名詞" and
        t2.surface() in {"の", "な"} and
        t3.part_of_speech()[0] == "名詞"):
        phrase = t1.surface() + t2.surface() + t3.surface()
        if is_valid(phrase):
            compound_phrases.append(phrase)

この部分では、形態素解析結果から 「名詞 + の/な + 名詞」 のパターンを検出し、複合語として compound_phrases に追加しています。

例えば、「制服の女性」「素敵な彼女」「夜の誘惑」といった日本語特有の名詞連結表現を複合語として抽出できます。

ここまでで、単純な単語抽出だけでなく、文中で意味のある名詞複合語もタグ候補として取得できるようになりました。

名詞トークン同士のバイグラム生成

noun_bigrams =
for i in range(len(word_tokens)):
    for j in range(i + 1, len(word_tokens)):
        for a, b in [(word_tokens[i], word_tokens[j]), (word_tokens[j], word_tokens[i])]:
            big = f"{a} {b}"
            if is_valid(big):
                noun_bigrams.append(big)

この部分では、名詞トークンリスト word_tokens の中から 2つの名詞を組み合わせたバイグラム(2単語連結) を生成しています。

特徴として、トークンのペアを順番を変えて両方向(a b と b a)で生成しis_valid() による妥当性チェックを通過した組み合わせだけを noun_bigrams に追加するというフローで行っています。

例えば、名詞トークンに「女子校生」「誘惑」「放課後」が含まれていれば、

「女子校生 誘惑」

「誘惑 女子校生」

「女子校生 放課後」

「放課後 女子校生」

「誘惑 放課後」

「放課後 誘惑」

といったバイグラムが生成され、検索タグや関連語句として活用できます。

ここまでで、名詞同士を組み合わせた検索ワード候補を作成できるようになりました。

英単語同士のバイグラム生成

english_bigrams =
for i in range(len(english_tokens)):
    for j in range(i + 1, len(english_tokens)):
        big = f"{english_tokens[i]} {english_tokens[j]}"
        if is_valid(big):
            english_bigrams.append(big)

この部分では、抽出した英単語リスト english_tokens の中から 2つの英単語を組み合わせたバイグラム(2単語連結) を生成しています。

特徴として、トークンのペアを順方向(i → j)のみで生成is_valid() による妥当性チェックを通過した組み合わせだけを english_bigrams に追加するということをしています。

例えば、英単語リストに「sexy」「girl」「teacher」が含まれていれば、

「sexy girl」

「sexy teacher」

「girl teacher」

といった英語バイグラムが生成されます。

ここまでで、英単語同士を組み合わせた検索ワード候補も作成できるようになりました。

名詞トークンの類義語(シノニム)展開

synonyms = set()
for w in word_tokens:
    for syn in get_synonyms(w):
        synonyms.add(syn)

この部分では、名詞トークンリスト word_tokens に含まれる各単語について、get_synonyms() 関数を使って Word2Vecモデルから類義語(シノニム)候補を取得 しています。

特徴として、各単語に対して get_synonyms(w) を呼び出し、得られた類義語を synonyms セットに追加。セットを使うことで、重複を自動的に排除しています。

例えば、「女子大生」に対して「JD」といった類義語がWord2Vecモデルから取得されれば、これらも検索タグや関連語句として活用可能になります。

ここまでで、名詞トークンに類義語を加え、より幅広い検索語句候補を生成できるようになりました。

バイグラムの類義語置換展開

bigram_synonyms = set()
for bg in noun_bigrams + english_bigrams:
    parts = bg.split()
    mapped = [get_synonyms(p)[0] if get_synonyms(p) else p for p in parts]
    new_bg = ' '.join(mapped)
    if new_bg != bg and is_valid(new_bg):
        bigram_synonyms.add(new_bg)

この部分では、生成済みの 名詞バイグラム noun_bigrams と英単語バイグラム english_bigrams に対して、それぞれの単語を類義語に置き換えた新しいバイグラムを生成しています。

特徴として、各バイグラムをスペースで分割して2単語に分ける。各単語について get_synonyms() で類義語を取得し、あれば最初の類義語に置換。元のバイグラムと異なり、かつ is_valid() で妥当性がある場合だけ bigram_synonyms に追加ということをしています。

例えば、バイグラムが「女子大生 誘惑」だった場合、「女子校生」の類義語が「JD」、「誘惑」の類義語が「誘惑行為」であれば、「JD 誘惑」「女子大生 誘惑行為」などの新しいバイグラムが生成されます。

ここまでで、既存バイグラムに類義語置換を加えた、多様な検索語句候補を生成できるようになりました。

最終結果の統合とAPIレスポンス生成

final = set()
final.update(word_tokens)
final.update(normalized_forms)
final.update(bunsetsu_phrases)
final.update(compound_phrases)
final.update(noun_bigrams)
final.update(english_bigrams)
final.update(synonyms)
final.update(bigram_synonyms)
final.add(text)

output = sorted({ normalize(w) for w in final if is_valid(w) })

return jsonify({"words": output})

この部分では、これまでに生成してきた各種トークンやフレーズをすべて final というセットに統合しています

具体的には、「名詞トークン」「動詞・形容詞の原形トークン」「文節フレーズ」「名詞複合語」「名詞バイグラム」「英単語バイグラム」「類義語」「バイグラム類義語」「元テキスト」これらすべてをセットに追加し、重複を排除します。

その後、「normalize() 」で正規化、「is_valid()」 で最終的な妥当性チェックを行い、ソートして output リストを作成し、最終的には「{"words": [...]}」というJSON形式でレスポンスを返却しています。

まとめ

ここまでで、クライアントから送られたテキストに対し、形態素解析から類義語展開まで含めた検索タグや関連語句候補の完全セットを一括で返すAPIが完成しました。

検索エンジンの快適さは、いかに早く、そしてユーザーが求めるワードを網羅的に拾えるか、にかかっています。だから、SAEPORNSでは今回紹介したような形態素解析APIと、事前に生成したインデックスを組み合わせて、高速かつ関連性の高い検索を実現しています。

もちろん、ここの部分はまだまだ改善余地があるので、今後もアルゴリズムの精度向上やインデックス構造の最適化を進めていきます。

このコードが実際に使われている「あなたを追跡しないアダルト動画検索エンジンSaePorns」はこちら!※18歳未満の方はご利用いただけません。

sae-porns.org

それではみなさんよい開発ライフを