PostgreSQL の全文検索機能で, 実際のテキストデータをゴニョゴニョしてみます.
いわゆるやってみた系の記事です. クエリの効率とかそっちのけです.
やること
口コミやアンケート形式のテキストデータに対して,
- 出現単語の頻度を調べる
 - 文脈の傾向とその推移を調べる
 
学術研究用に livedoor グルメのデータが公開されているので, そちらを利用させていただきます.
- 
livedoor グルメの DataSet を公開
使用するのは ratings.csv(評価データ) と restaurants.csv(店舗データ) です. 
textsearch_ja の準備
用意するもの
- textsearch_ja: 9.0.0
 - MeCab: 本体及び IPA 辞書
 
MeCab のインストール
tar xf mecab-0.996.tar
cd mecab-0.996
./configure --enable-utf8-only
make
sudo make install
IPA 辞書のインストール
tar xf mecab-ipadic-2.7.0-20070801.tar
cd mecab-ipadic-2.7.0-20070801
./configure --with-charset=utf8
make
sudo make install
textsearch_ja のインストール
tar xf textsearch_ja-9.0.0.tar
cd textsearch_ja-9.0.0
make
sudo make install
関数登録
psql -f textsearch_ja.sql
PostgreSQL 9.2以降では, textsearch_ja.sql中の
"LANGUAGE 'C'"の部分を"LANGUAGE 'c'"に変えておく必要があります.
テーブル作成
元のデータの他に, インデックスに使う tsvector 型のカラムも作成しておきます.
また, データ更新時に自動更新されるようトリガを作成しておきます.
create table ratings(
id integer not null,
restaurant_id integer,
user_id text,
total integer,
food integer,
service integer,
atmosphere integer,
cost_performance integer,
title text,
body text,
purpose integer,
created_on timestamp,
body_tsv tsvector);
create trigger tsvector_update before insert or update
on ratings for each row execute procedure
tsvector_update_trigger(body_tsv, 'pg_catalog.japanese', body);
create table restaurants (
id integer not null unique,
name text,
-- 略
);
データ投入
ratings.csv には created_on が 0000-00-00 00:00:00 な行がありそのままではエラーとなるので, 予め取り除いておきます.
copy ratings(
id, restaurant_id, user_id, 
total, food, service, atmosphere, cost_performance, 
title, body, purpose, created_on) 
from '/tmp/ratings.csv' csv header;
copy restaurants from '/tmp/restaurants.csv' csv header;
ビュー作成
ratings と restaurants を restaurant_id をキーにして join します.
そういえば PostgreSQL 9.3 ではマテリアライズドビューが追加されたのでした.
せっかくなので使います.
インデックスはマテビューに対して張ります.
create materialized view rating_restaurants
as select
tbl2.name, tbl1.title, tbl1.body,
tbl1.body_tsv, tbl1.created_on,
tbl1.total, tbl1.food, tbl1.service,
tbl1.atmosphere, tbl1.cost_performance
from ratings tbl1 inner join restaurants tbl2
on tbl1.restaurant_id = tbl2.id;
create index body_tsv_idx on rating_restaurants using gin(body_tsv);
テキスト分析
単語の出現頻度を調べる
select * from ts_stat('select body_tsv from rating_restaurants')
order by nentry desc, ndoc desc, word limit 10;
実行結果はこんな感じ
  word  |  ndoc  | nentry 
--------+--------+--------
 する   | 159589 | 556802
 いる   | 134863 | 384592
 ある   | 133476 | 320285
 店     | 127809 | 277266
 円     |  71713 | 220179
 食べる |  98682 | 183696
 味     |  84546 | 142662
 思う   |  83732 | 135453
 れる   |  74668 | 133128
 なる   |  78962 | 131695
(10 rows)
これだと "する" や "いる" など, 特に意味をなさない単語が上位に来てしまいます.
品詞で絞り込めれば, 有用なランキングが得られそうです.
textsearch_ja をインストールすると ja_analyze という関数が使えるようになります.
文章を mecab で解析した結果が取得でき, 品詞の情報も得られます. それをうまいこと利用すれば品詞で絞り込むこともできそうです.
が, 手元の環境 (PostgreSQL 9.3) では残念ながら ja_analyze を実行するとサーバーとの接続が切れてしまいます. なので今回はおあずけさせて頂きます.
文脈の傾向を調べる
味に関する投稿は何件, 接客に関する投稿は何件, といった件数を取得します.
文脈に関するテーブルを別途作成します.
create table contexts(
context text,
rule tsquery);
insert into contexts values
('food', to_tsquery('おいしい|美味しい|うまい|美味い|まずい|不味い|おいしくない')),
('service', to_tsquery('丁寧|接客|態度|店員|スタッフ|サービス|対応|応対')),
('atmosphere', to_tsquery('雰囲気|居心地|店内|内装|インテリア|外装')),
('cost_performance', to_tsquery('安い|高い|お得|割高|コスト|値段|価格|相場'));
新たに作成した contexts と rating_restaurants を, body_tsv をキーとして join します.
select year, month, coalesce(context, 'others') as tag, count(*) as tag_count
from rating_restaurants left join contexts
on body_tsv @@ rule
group by year, month, tag
order by year, month, tag;
結果はこんな感じです.
 year | month |       tag        | tag_count 
------+-------+------------------+-----------
 2000 |    10 | atmosphere       |        31
 2000 |    10 | cost_performance |        33
 2000 |    10 | food             |        59
 2000 |    10 | others           |       116
 2000 |    10 | service          |        11
 2000 |    11 | atmosphere       |         9
 2000 |    11 | cost_performance |        19
 2000 |    11 | food             |        28
 2000 |    11 | others           |        21
 2000 |    11 | service          |         2
 2000 |    12 | atmosphere       |        14
 2000 |    12 | cost_performance |        12
 2000 |    12 | food             |        25
 2000 |    12 | others           |        20
 2000 |    12 | service          |        10
...
以下略
店の評判の文脈を分類して集計し, 時系列にそって追うことができました.
上のクエリは全店舗を対象にしているので, 結果に特に意味はないのですが.
contexts テーブルの中身を変えれば, ポジネガ分析みたいなこともできそうです.
特定の店舗を対象にして, 評価ポイントの推移とテキストの傾向を比較してゴニョゴニョと.
やっつけで rule を作ったので, 分類の精度はまぁよくありません.
機械学習なテクニックで contexts を自動アップデートするなどの遊びもできそうです.
前準備が大半を占める記事になってしまいました.
参考link:
本家マニュアル
textsearch_ja
MeCab
livedoor tech ブログ