このブログにもタグクラウドを実装してみたくなったので タグ ページに実装してみました。折角ですのでタグクラウドの実装にあたって困ったことなどを書いておこうと思います。
各記事毎のタグはデータベースにどのように格納すればいいのか
SQLite に配列型はないので、もともと私は各記事のタグを以下のようにカンマ区切りでタグリストを格納していました。
fbx,Unity,ゲームエンジン,3DCG,CG
パースするときは毎回スプリットしてタグを抽出していました。検索時は SQL で LIKE "%タグ%" 構文で探していました。この方法だと "CG" というタグを検索すると "3DCG" というタグもヒットしてしまうのは問題として認識はしていました。これを解決しないと正しいタグ数を求められないので、対応を検討しました。
別のテーブルで記事 ID とタグの紐づけを管理する方法もあるのですが、そこまでする必要もないように感じたので、文字列として格納する方法で検討しました。単純に区切り文字を前後に入れれば良さそうだと思い、以下のように変更してみました。
[fbx][Unity][ゲームエンジン][3DCG][CG]
このようにすれば、SQL で以下のように LIKE "%[タグ]%" で検索すれば、"CG" と "3DCG" は区別してカウントすることができます。
SELECT COUNT(tag) AS tagCount FROM table WHERE tag LIKE "%[CG]%"
ただ、これだと区切り文字 "[" と "]" の 2 種類が混在することで、タグを抽出する際にスプリットする手間が少し増えてしまいました。結局、最終的には以下のように "|" で区切ることにしました。
|fbx|Unity|ゲームエンジン|3DCG|CG|
以下がカンマ区切りで格納していたタグ文字列を上記の "|" 区切りに変換する SQL になります。SQL は置換操作が結構柔軟にできてありがたいです。
UPDATE table SET tag=REPLACE(CONCAT("|",tag,"|"), ",", "|") WHERE tag!=""
タグクラウドの各タグのフォントサイズの計算
タグクラウドの各タグのフォントサイズについては結構悩みました。最初はそのままタグ数を font-size に px で代入してみたのですが、あまりにも特定のタグに大きさが偏りすぎて醜いタグクラウドになってしまいました。
タグの分布によって美しく見える計算アルゴリズムは異なると思いますが、このブログの場合はタグの記事数が 10 以下はあまり目立たないように、タグの最大記事数の 1/4 程度は目立つようにするとバランスの良い見た目になりました。具体的には、以下のように最低のフォントサイズを 10pt、最大のフォントサイズを最大タグ数の 1/4 にしてみたところ、それっぽい見た目になりました。
$fontSize = min(max($tagCount, 10), $maxTagCount * 0.25);
もっと突き詰められそうな気もしましたが、この辺は結局タグ分布が変わってきたら、局所的な分布でしか有効でないアルゴリズムは破綻してしまうので、この辺でやめてておきました。
タグクラウド表示処理が遅い
実装が完了して、タグクラウド表示処理の時間を計ってみたところ、0.2 秒ほどかかる結果になってしまいました。通常のブログ記事は 0.02 秒ほどで表示できているので、それの 10 倍となると大分遅いです。ちなみに SQLite2 を使っていた段階だと 0.12 秒程度でしたが、から SQLite3 に移行してから 1.7 倍ほど処理が遅くなりました。
まだこの問題は解決できていませんが、最悪、動的にタグ数を計算せず、あらかじめ作ったページを置いておけば表示時間の問題は解決するので、そちらの方向で対処するのが妥当かもしれません。
自作クラスなどが利用されているのでそのまま利用することはできませんが、何かの参考になるかもしれませんので、実際のソースコードを載せておきます。
public function printTagCloud($filepath,$sortByCount=False)
{
$db = new DataBaseAccessor($filepath);
$sql = 'select tag from main_data where tag!="" and visibility=1';
$queryResult = $db->queryWrap($sql);
$tagsText = '';
while($rowData = $queryResult->fetchArray(SQLITE3_ASSOC))
{
$tagsText .= $rowData['tag'].'|';
}
$tags = explode('|', trim($tagsText, '|'));
$uniqueTags = array_unique($tags);
$tagInfos;
$maxTagCount = 0;
foreach($uniqueTags as $tag)
{
$sql = 'select count(tag) as count from main_data where visibility=1 and tag like "%|'.$tag.'|%"';
$queryResult = $db->queryWrap($sql);
$rowData = $queryResult->fetchArray(SQLITE3_ASSOC);
$count = $rowData['count'];
$tagInfos[$tag] = $count;
$maxTagCount = max($maxTagCount, $count);
}
if ($sortByCount)
{
arsort($tagInfos);
}
$html = '';
foreach($tagInfos as $tagName => $tagCount)
{
$fontSize = min(max($tagCount, 10), $maxTagCount * 0.5 * 0.5);
$html .='<a href="'.SITE_URL.'?tag='.$tagName.'" style="font-size:'.$fontSize.'pt">'.$tagName.'('.$tagCount.')</a> ';
}
print($html);
$db->closeWrap();
}