開発book-knowledge-base

書棚ページの見通しが悪くなっていた。Kindleからインポートした蔵書がどれか画面から判別できず、漫画のシリーズものが1巻ずつカードを占有して★4.5以上の棚を埋めていた。1日かけて4つの改善を順に積んだ。

Kindle蔵書識別バッジを足す

kindle_library テーブルに521件入っているうち、amazon_metadata 側ともASINで突き合うのは467件あった。APIをASIN JOINに直して is_kindleis_kindle_unlimited の2フラグを返すようにし、shelf.vue のカード左下に黄色い「K」バッジを描画させた。

最初はWHERE句のカラム名を am. プレフィックス付きにregexで一括書換えしようとして、置換が脆かったのでやめた。SQL本体を手で書き直したほうが速い、と気づくのに数分かかった。

ブラウザで開くと、Kindle蔵書249件に黄色の「K」が乗っていて、フィルタで絞り込みも効いた。

シリーズもの集約でカードを1つにまとめる

カイジ、インベスターZ、BLOODY MONDAYのように1巻〜N巻が独立カードで並ぶと、★4.5以上の棚が同じシリーズで埋まる。タイトルから巻数を除去して正規化キーを作り、N巻あるシリーズは「📚 N巻」の紫バッジ付きカード1枚に集約。クリックでモーダルを開いて全巻を順序通り並べる方針にした。

最初の実装ではカードを物理的にスタック表示する案を出してきたが、デザインがうるさくなるので止めて、バッジ+モーダルの形に落ち着いた。

開いたページではカイジ13巻、インベスターZ 20巻が正しくまとまっている。「これでよさそう」と思った直後、ユーザーから「BLOODY MONDAYとかもそうじゃないですか。なんでカイジとインベスターZだけなんですか」と差し戻された。

BLOODY MONDAYのregexバグ

原因を追わせると、巻数除去regexの先頭が \s+ になっていた。

// 修正前: スペース必須でマッチしない
const stripVolume = (t: string) => t.replace(/\s+\d+$/, '')

// 修正後: スペース無しでもマッチ
const stripVolume = (t: string) => t.replace(/\s*\d+$/, '')

【極!単行本シリーズ】【】 ごと先に削った後、タイトルが「BLOODY MONDAY9巻」とスペース無しで巻数が直結する。\s+ だと0文字スペースに一致しないので、巻数が残ったまま別カードとして数えられていた。\s* に1文字直したらBLOODY MONDAYが「📚 11巻」として集約され、★4.5以上の件数も 63 → 55 に減った。

regex は「最低1文字必要」と「0文字でもいい」で挙動が真逆になる典型例だった。前段で文字を削る処理を入れたら、後段のregexがその削り方に依存する。

漫画フィルタを「初期非表示」で入れる

ユーザーから「漫画をタグで分けられるようにしてほしい」と言われ、kindle_library.tagscomic を真実として is_comic をAPIで返し、フロントで初期非表示にした。除外94件、表示500冊 → 406冊。★5.0が8→3件、★4.5以上が165→117件に絞られて、棚が一気に実用書中心になった。

紙本のチ。が漫画扱いされない

絞り込んだ画面を見ると、チ。が★4.5以上の棚に残っていた。Pythonの kindle_library_tagger.py でtagsを付けているので判定漏れかと思ったが、調べたら理由は単純で、チ。は紙本でKindle Unlimited対象外、つまり kindle_library に1行も入っていなかった。タグを付ける入口にすら立っていない。

API側の判定を二段構えに変えた:

  1. kindle_library.tagscomic があれば漫画扱い(既存)
  2. amazon_metadata.title のパターンマッチでも漫画扱い(追加)

「チ。」「進撃の巨人」など紙本だけで持っている漫画のtitle正規表現を足したら、除外件数が 94 → 102件に増えて、★4.5以上の棚から漫画が完全に消えた。残ったのは『コンピュータシステムの理論と実装』『デザインの小骨話』など実用書だけ。

イラスト技法書も漫画枠で除外

「スカートの描き方、髪の描き方、ちょっぴりHな〜、漫画の基礎デッサンも漫画枠で除外してほしい」とユーザー追加要望。漫画ではないが棚に出すと並びが乱れる類の本だ。title正規表現に描き方系・基礎デッサン系のパターンを足して除外 102 → 105件。★4.5以上から技法書が消えて、棚の意味がさらに鮮明になった。

漫画フィルタは「漫画かどうか」ではなく「初期表示から外したい本かどうか」のフラグに育っている。名前は is_comic のまま残しているが、実質は「棚を読書ログとして見たいときに邪魔になる本」のフラグ、と捉え直した。

/books 一覧をKindle/その他で分割

最後にユーザーから「/books の一覧でKindle取り込みとそれ以外を上下に分けたい。これからKindle蔵書を積み増していくので、直近のKindleインポートが上に並んでいる形にしたい」と依頼。

ページを2セクションに割った:

  • 📱 Kindle 蔵書から取り込み: 黄色ヘッダー、3冊、createdAt 降順
  • 📕 その他(PDF / 紙書籍 OCR / Notion インポート): グレーヘッダー、265冊

Kindle側に直近インポートの『明治維新とは何だったのか』『戦略質問』『31歳でFIRE』が並んでいて、これからKindleインポートを進めるたびに上のセクションが伸びる形になった。

学び

  • regex を二段で動かすときは、前段の削除が後段のマッチ条件にどう影響するか想像する。\s+\s* の1文字差で BLOODY MONDAY が落ちた
  • 「漫画」というラベルは、API的には「初期表示から外したい本」のフラグに自然に拡張される。名前と中身が乖離してきたら一度立ち止まる
  • データの真実が2つ(Kindle蔵書テーブルと Amazon メタデータテーブル)あるとき、フラグはどちらに置いても漏れる。両方からORで拾うのが安全
  • 「Kindleからインポートしたぞ」という事実を画面のどこかに残す。これからKindleを軸にしていく予定が決まっているなら、識別子は早めに足したほうが後の整理が効く