接下來兩個案例研究使用 Last.fm 資料集——**百萬歌曲資料集(Million Song Dataset, MSD)**官方的歌曲標籤與歌曲相似度資料。MSD 團隊與 Last.fm 合作,提供最大的歌曲層級標籤與預先計算的歌曲相似度研究資料集,且所有資料都關聯到 MSD track,方便連結其他 MSD 資源(音訊特徵、藝人資料、歌詞等)。
匯入 SQLite 資料庫#
資料集同時以 SQLite 資料庫與 JSON 檔提供。載入 SQLite 資料庫用 pgloader 很容易:
$ curl -L -o /tmp/lastfm_tags.db \
http://labrosa.ee.columbia.edu/millionsong/sites/default/files/lastfm/lastfm_tags.db
$ pgloader /tmp/lastfm_tags.db pgsql://appdev@localhost/appdevpgloader 透過 sqlite_master 目錄與 PRAGMA table_info() 取出資料表與索引定義,再以 COPY 協定用串流方式把資料搬進 PostgreSQL。
延伸輸出:pgloader 匯入報表
table name errors read imported bytes total time
----------------------- --------- --------- --------- --------- --------------
fetch 0 0 0 0.000s
fetch meta data 0 8 8 0.028s
Create tables 0 6 6 0.031s
tids 0 505216 505216 9.2 MB 1.893s
tags 0 522366 522366 8.6 MB 1.781s
tid_tag 0 8598630 8598630 135.7 MB 32.614s
COPY Threads Completion 0 4 4 34.366s
Create Indexes 0 5 5 2m14.346s
Index Build Completion 0 5 5 36.976s
----------------------- --------- --------- --------- --------- --------------
Total import time ✓ 9626212 9626212 153.4 MB 3m25.743s看過專案的 demo_tags.py 腳本後會發現,它使用 SQLite 的 64 位元帶號整數 ROWID 系統欄位來關聯資料。我們需要對等的欄位才能讓資料對得起來:
begin;
alter table tags add column rowid serial;
alter table tids add column rowid serial;
commit;有了新欄位就能初探資料。先在使用者自訂標籤中搜尋 Brian Setzer:
select tags.tag, count(tid_tag.tid)
from tid_tag, tags
where tid_tag.tag=tags.rowid and tags.tag ~* 'setzer'
group by tags.tag; tag │ count
═════════════════════════════╪═══════
Brian Setzer │ 1
Setzer │ 13
brian setzer is GOD │ 1
brian setzer orchestra │ 3
rockabilly Setzer style │ 4
setzer is a true guitarhero │ 9
...
(8 rows)
Time: 394.927 ms這個查詢在 tids(track id)與 tid_tag(track 與 tag 的關聯)之間做 join,並以不分大小寫的正規表達式 'setzer' 過濾。從執行時間就能猜到——這個過濾條件目前沒有索引可用。
從 JSON 檔補上歌名與藝人#
MSD 專案也以一組 JSON 編碼的文字檔發布資料,其中有 track id 之外的額外資訊,例如歌名與藝人。track id 長得像 TRVBGMW12903CBB920,對人類來說不是好的歌曲指稱方式,所以下載 JSON 資源並寫個小解析腳本處理:
curl -L -o /tmp/lastfm_subset.zip \
http://labrosa.ee.columbia.edu/millionsong/sites/default/files/lastfm/lastfm_subset.zip新內容載入新的資料表:
begin;
create table lastfm.track
(
tid text,
artist text,
title text
);
commit;作者以他偏好的 Common Lisp 撰寫解析腳本:直接讀取 zip 檔、在記憶體中解壓並解析其中的 JSON 檔,不在客戶端把 JSON 落地寫檔,一邊解析一邊透過 COPY 串流注入 PostgreSQL,全部內容在單一 PostgreSQL 命令中完成。
延伸範例:Common Lisp 的 zip → COPY 載入腳本
(defpackage #:lastfm
(:use #:cl #:zip)
(:import-from #:cl-postgres
#:open-db-writer
#:close-db-writer
#:db-write-row))
(in-package #:lastfm)
(defvar *db* '("appdev" "appdev" nil "localhost" :port 5432))
(defvar *tablename* "lastfm.track")
(defvar *colnames* '("tid" "artist" "title"))
(defun process-zipfile (filename)
"Process a zipfile by sending its content down to a PostgreSQL table."
(pomo:with-connection *db*
(let ((count 0)
(copier (open-db-writer pomo:*database* *tablename* *colnames*)))
(unwind-protect
(with-zipfile (zip filename)
(do-zipfile-entries (name entry zip)
(let ((pathname (uiop:parse-native-namestring name)))
(when (string= (pathname-type pathname) "json")
(let* ((bytes (zipfile-entry-contents entry))
(content
(babel:octets-to-string bytes :encoding :utf-8)))
(db-write-row copier (parse-json-entry content))
(incf count))))))
(close-db-writer copier))
;; Return how many rows we did COPY in PostgreSQL
count)))
(defun parse-json-entry (json-data)
(let ((json (yason:parse json-data :object-as :alist)))
(list (cdr (assoc "track_id" json :test #'string=))
(cdr (assoc "artist" json :test #'string=))
(cdr (assoc "title" json :test #'string=)))))同樣的技巧可以用任何程式語言實作——前提是你選用的 PostgreSQL 驅動程式有暴露 COPY 協定。務必確認這點,並學會用它正確載入資料。以作者使用的 Postmodern(Common Lisp)驅動為例,COPY API 就三個函式:
open-db-writer開啟 COPY 串流、db-write-row推送一列、close-db-writer收尾關閉串流;腳本每解析完一個 JSON 檔就推送一列。
客戶端雖然不落地,PostgreSQL 伺服器端收到 COPY 協定的資料時,當然還是要把資料序列化到磁碟。
初探資料模型#
用幾個互動式查詢滿足好奇心。先看資料集裡最多歌曲的藝人:
select artist, count(*)
from lastfm.track
group by artist
order by count desc
limit 10; artist │ count
═════════════════════════════╪═══════
Mario Rosenstock │ 13
Aerosmith │ 12
Snow Patrol │ 12
Phil Collins │ 12
Shakira │ 11
Radiohead │ 11
Nick Cave and the Bad Seeds │ 11
...
(10 rows)再看 Last.fm 使用者幫 Aerosmith 貼過哪些標籤。這個簡單而經典的查詢展示了整個資料模型如何組合:tags、tid_tag、tids 來自專案的 SQLite 資料庫,track 則是我們從 JSON zip 檔 COPY 進來的新表:
select track.artist, tags.tag, count(*)
from tags
join tid_tag tt on tags.rowid = tt.tag
join tids on tids.rowid = tt.tid
join lastfm.track on track.tid = tids.tid
where track.artist = 'Aerosmith'
group by artist, tags.tag
order by count desc
limit 10; artist │ tag │ count
═══════════╪════════════════╪═══════
Aerosmith │ Radio4You │ 12
Aerosmith │ hard rock │ 12
Aerosmith │ rock │ 11
Aerosmith │ classic rock │ 11
Aerosmith │ 70s │ 10
Aerosmith │ 80s │ 9
...
(10 rows)這裡只列出十列——光是 Aerosmith 一個樂團,資料集就有 464 個不重複標籤。其中一個拼作 favorites;那麼不論哪種拼法,哪些歌曲被使用者標為最愛?
select track.tid, track.title, tags.tag
from tags
join tid_tag tt on tags.rowid = tt.tag
join tids on tids.rowid = tt.tid
join lastfm.track on track.tid = tids.tid
where track.artist = 'Aerosmith'
and tags.tag ~* 'favourite'
order by tid, tag; tid │ title │ tag
════════════════════╪═════════════════════════╪════════════════════════════════
TRAQPKV128E078EE32 │ Livin' On The Edge │ Favourites
TRAVUAJ128E078EDA2 │ What It Takes │ favourite
TRAYKOC128F930D2B8 │ Cryin' │ Favourites
TRAZDPO128E078ECE6 │ Crazy │ all- time favourite
TRAZISI128E078EE2F │ Same Old Song and Dance │ first favourite metalcore song
TRBARHH128E078EDE9 │ Janie's Got A Gun │ favourite
...
(12 rows)對資料集有了概念之後,就可以拿它來解決更有趣的使用情境了。