企業マッチングシステム - rfdnxbro/trends-laravel GitHub Wiki
企業マッチングシステム(CompanyMatcher)
概要
CompanyMatcherは、スクレイピングした記事データから企業を自動識別するシステムです。ハードコーディングされたマッピングではなく、データベースベースの動的検索により、拡張性と保守性を実現しています。
アーキテクチャ
基本設計
- Service Layer:
App\Services\CompanyMatcher
- Database:
companies
テーブルのJSONカラムによるパターン管理 - Integration: 各スクレイパー(Qiita、Zenn、はてブ)で統合使用
データベース設計
ALTER TABLE companies ADD COLUMN
url_patterns JSON, -- URLパターンのリスト
domain_patterns JSON, -- ドメインパターンのリスト
keywords JSON, -- キーワードのリスト
zenn_organizations JSON; -- Zenn組織名のリスト
マッチング戦略
CompanyMatcherは以下の優先順序でマッチングを実行します:
1. URLパターンマッチング(最優先)
企業の公式ブログや技術サイトのURLで識別
{
"url_patterns": [
"blog.cybozu.io/",
"speakerdeck.com/cybozuinsideout/",
"developers.freee.co.jp/"
]
}
マッチング例:
https://blog.cybozu.io/entry/2025/07/tech-blog
→ サイボウズhttps://developers.freee.co.jp/entry/new-feature
→ freee
2. ドメインマッチング
企業ドメインでの完全・部分一致
{
"domain_patterns": [
"cybozu.io",
"cybozu.co.jp",
"freee.co.jp"
]
}
3. ユーザー名マッチング
QiitaやZennでの企業アカウント識別
-- 既存カラムを使用
qiita_username VARCHAR(255),
zenn_username VARCHAR(255)
4. キーワードマッチング
記事タイトル内の企業名検出(厳密マッチング)
{
"keywords": [
"サイボウズ",
"cybozu",
"freee"
]
}
5. Zenn組織マッチング
Zenn組織記事の自動検出
{
"zenn_organizations": [
"cybozu",
"freee",
"cookpad"
]
}
マッチング例:
https://zenn.dev/cybozu/articles/new-tech
→ サイボウズ
実装詳細
メインメソッド
public function identifyCompany(array $articleData): ?Company
{
// 1. URLパターンマッチング
if (!empty($articleData['url'])) {
$company = $this->identifyBySpecificUrl($articleData['url']);
if ($company) return $company;
}
// 2. ドメインマッチング
if (!empty($articleData['domain'])) {
$company = $this->identifyByExactDomain($articleData['domain']);
if ($company) return $company;
}
// 3. ユーザー名マッチング
if (!empty($articleData['platform']) && !empty($articleData['author_name'])) {
$company = $this->identifyByUsername($articleData['platform'], $articleData['author_name']);
if ($company) return $company;
}
// 4. キーワードマッチング
$company = $this->identifyByStrictKeywords($articleData);
if ($company) return $company;
return null;
}
動的検索の実装
ハードコーディングを排除し、データベースから動的に検索:
private function identifyBySpecificUrl(string $url): ?Company
{
$companies = Company::whereNotNull('url_patterns')
->where('is_active', true)
->get();
foreach ($companies as $company) {
$patterns = $company->url_patterns ?? [];
foreach ($patterns as $pattern) {
if (str_contains($url, $pattern)) {
return $company;
}
}
}
return null;
}
スクレイパー統合
QiitaScraper
// author_nameを抽出(@記号を削除)
$authorName = ltrim(trim($article['author']), '/@');
// CompanyMatcherで企業を特定
$articleData = array_merge($article, [
'author_name' => $authorName,
'platform' => 'qiita',
]);
$company = $companyMatcher->identifyCompany($articleData);
ZennScraper
// author_nameを抽出(会社名を除去)
if (preg_match('/(.+?)(?:in.+)/u', $authorText, $matches)) {
$authorName = trim($matches[1]);
} else {
$authorName = $authorText;
}
$articleData = array_merge($article, [
'author_name' => $authorName,
'platform' => 'zenn',
]);
$company = $companyMatcher->identifyCompany($articleData);
HatenaBookmarkScraper
$articleData = array_merge($entry, [
'platform' => 'hatena_bookmark',
]);
$company = $companyMatcher->identifyCompany($articleData);
設定・管理
企業データ設定例
// サイボウズの設定例
Company::where('name', 'サイボウズ')->update([
'url_patterns' => [
'speakerdeck.com/cybozuinsideout/',
'blog.cybozu.io/'
],
'domain_patterns' => [
'cybozu.io',
'cybozu.co.jp'
],
'keywords' => [
'サイボウズ',
'cybozu'
],
'zenn_organizations' => [
'cybozu'
]
]);
新企業追加手順
-
企業基本情報の追加
Company::create([ 'name' => '新企業名', 'domain' => 'example.com', 'is_active' => true ]);
-
マッチングパターンの設定
$company->update([ 'url_patterns' => ['tech.example.com/'], 'keywords' => ['新企業名', 'example'] ]);
-
動作確認
php artisan scrape:platform qiita --dry-run
ログとモニタリング
ログ出力
マッチング成功時に詳細ログを出力:
Log::info("特定URLベースで会社を特定: {$company->name}", [
'url' => $articleData['url'],
'article_title' => $articleData['title'] ?? null,
]);
モニタリング指標
- マッチング率: 全記事に対する企業特定成功率
- マッチング手法別統計: URL・ドメイン・キーワード別の成功数
- 精度: 誤判定の発生率
パフォーマンス考慮
最適化ポイント
- 優先順序: 高精度なURLパターンマッチングを最優先
- 早期リターン: マッチング成功時に即座にreturn
- インデックス: JSONカラムにGINインデックス適用を検討
- キャッシュ: 頻繁にアクセスされる企業データのキャッシュ
スケーラビリティ
- 企業数増加に対する線形スケーラビリティ
- パターン数増加時のパフォーマンス影響は軽微
- 必要に応じてElasticsearchへの移行も可能
テスト戦略
ユニットテスト
public function test_URLパターンで企業を正しく特定できる()
{
$company = Company::factory()->create([
'name' => 'テスト企業',
'url_patterns' => ['blog.test.com/']
]);
$matcher = new CompanyMatcher();
$result = $matcher->identifyCompany([
'url' => 'https://blog.test.com/article/123'
]);
$this->assertEquals($company->id, $result->id);
}
統合テスト
実際のスクレイピングと組み合わせたテスト:
php artisan test --filter CompanyMatcherIntegrationTest
今後の拡張
機械学習による精度向上
- 記事内容からの企業推定
- ユーザー投稿パターンの学習
- 誤判定の自動修正
管理UIの実装
- 企業マッチングパターンのWeb管理画面
- マッチング精度のダッシュボード
- 新企業の半自動追加機能
外部API連携
- 企業データベースAPIとの連携
- ソーシャルメディアAPIとの統合
- 技術ブログRSSフィードの活用