PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

ULIZA のドキュメントを PDF から HTML に移植して全文検索できるようにした話(前編)

こんにちは、SaaS 事業部テックリードの丸山です。

私たち SaaS 事業部で取り扱っている商品のひとつに、動画配信プラットフォームの ULIZA があります。ULIZA は 2022 年 4 月に、ユーザーガイドや API 仕様書などのドキュメントを docs.p.uliza.jp にて一般公開しました(それまでは、ULIZA をご契約いただいたお客様だけに配布しておりました)。実はこのドキュメント、元々は PDF 形式だったのですが、今回ドキュメントを一般公開するのに合わせて HTML 形式への移植を行いました。本稿では、その経緯と移行方法についてご紹介したいと思います。

docs.p.uliza.jp

なお、この記事は前編と後編に分かれております。前編では VuePress を使用したドキュメントサイトの構築について紹介し、後編では OpenSearch を使用した全文検索機能の実装と CI/CD パイプラインの構築について紹介します。この記事は前編です後編はこちら

PDF 版における課題

以前の ULIZA のユーザーガイドは、Microsoft Word で書いたものを PDF 形式で出力したものでした*1。また、機能ごとに複数のドキュメントに分割されていて、全部で 50 個ほどの PDF ファイルが存在する状況でした。そのため、以下のような課題がありました。

  • たくさんの PDF ファイルに分かれているため、まず目的の情報がどの PDF ファイルに書かれているのかを調べなければならず、目的の情報にたどり着くのに時間がかかる。
  • 全文検索用のインデックスを作成しようにも、データがプレーンテキストではないため扱いにくい。
  • Microsoft Word で書いているため、バージョン管理が難しい。また、複数のメンバーが同じドキュメントを同時に編集した場合は、マージを手作業で行わなければならない。
  • ファイルによって余白や文字サイズなどのスタイル(Word の設定)が微妙に異なっていたり、ところどころフォントが異なっていたりと、デザインにムラがある。
  • PDF ファイル中のサンプルコードを選択してクリップボードにコピーすると、余計な空白文字が混入したり、逆に文字が欠けたりして、うまくペーストできないことがある。また、Microsoft Word が " を勝手に (左二重引用符)や (右二重引用符)に変換してしまうため*2、ペーストしたコードが正常に動かないことがある。
  • Adobe Reader で PDF ファイル内を検索しても、ヒットしないことがある(単語がページを跨いでいる場合や、長音記号を含む場合などに多い)。

ドキュメントサイトの構築

このような状況を改善すべく、ドキュメントの HTML 形式への移植 を決断しました。なるべく時間をかけずに、見た目の良いドキュメントサイトを構築する必要があったので、サイトの構築には既存のフレームワークを活用することにしました。

フレームワークの選定

ドキュメントサイトを構築するにあたり、以下の条件を満たすフレームワークがないか調査しました。

  • 学習コストが低いこと。
  • Markdown 形式で本文が書けること。
  • 標準のテーマでも見た目がカッコいいこと。
  • 細かいカスタマイズもできる(拡張性がある)こと。

これらの条件を満たすフレームワークとして、以下の 2 つが候補に上がりました。

ULIZA は管理画面の開発に Vue.js を使用していることもあり、最終的には Vue.js での拡張が可能な VuePress を選択することにしました(本稿執筆時点では、VuePress には v1 と v2-beta がありますが、v1 を選択しました)。

VuePress は、Vue.js の生みの親である Evan You 氏を中心に開発されている静的サイトジェネレータで、もともと Vue.js 本体およびそのサブプロジェクトのドキュメントサイトを構築することを目的として作られたという経緯があります*3。ちなみに本稿執筆時点では、Vue.js 本体の公式サイトは後進である VitePress で構築されているようです。VitePress はまだアルファ版なのですが、VuePress v1 よりも軽量&高速という特徴があり、今からフレームワークを選定するのであれば、VitePress も候補に上がるでしょう。VitePress と VuePress の棲み分けやそれぞれの今後の方向性については、現在も以下の issue で議論されているようです。

github.com github.com

VuePress の基本的な使い方

VuePress の使い方は、公式サイトの Getting Started を試せば分かると思いますので詳細は割愛しますが、特定のディレクトリに Markdown 形式で書いたドキュメントファイルを置いておき、yarn run dev を実行すればローカルで開発用 Web サーバを起動してサイトを確認できますし、yarn run build を実行すれば公開用の静的ファイルを生成できます。こうして生成された静的ファイルを、自前の Web サーバや Amazon S3 にアップロードすれば、ドキュメントサイトを公開できます。

v1.vuepress.vuejs.org

Microsoft Word からの移植

VuePress を使ってドキュメントサイトを公開するまでの基本的な要領は掴めたので、肝心のドキュメント本文を書く作業に入っていきます。

冒頭で述べたように、既存のドキュメントは Microsoft Word で書いたものを PDF 形式で出力しています。Pandoc のようなツールを使って、一括で Word から Markdown に変換することも検討したのですが、ツールで変換しても結局最終的には手直しが必要になる点や、1 回移植してしまえば以降は必要のない作業であることから、手作業で Markdown 化することにしました。具体的には、Word 上で Ctrl+A ですべてのテキストを選択してコピーし、テキストエディタに貼り付け、エディタ上で見出しレベルや箇条書きなどの体裁を整えていく、という作業をほとんど手作業(一部、単純な文字列置換などについては、お手製のスクリプトで自動化しました)で実施しました。

特に厄介だったのは表(テーブル)と画像です。表については、Microsoft Word からテキストエディタにそのままコピペするとタブ区切りテキストになるので、タブ文字( \t )を Markdown におけるセル区切り文字である | に一括置換するお手製スクリプトを使うなどして、なるべく手作業の量を減らすようにしました。セル内に改行があるパターンはさらに厄介で、Markdown ではセル内の改行を記述できないため、改行したい位置に <br> タグを手書きしなければなりませんでした。

画像については、Word 文書内に貼り付けてある画像の元データを残していなかったので、Word 文書から埋め込まれている画像を逆抽出できないか試みました。Microsoft Word では出力形式として HTML 形式が選べるのですが、これを行うと、文書に埋め込まれている画像がファイルとして吐き出されてくるので、この画像を使用しました。ただし、Microsoft Word 上で図形ツールを使って描かれた図などについては、この方法では画像化できないため、Microsoft Word の画面のスクリーンショットを撮って画像化するという作業が必要になりました。

画像ファイルを用意する作業は複数のメンバーに手伝ってもらいましたが、それ以外の作業は 1 人で実施しました。1 日にだいたい 100〜150 ページ分ほどの Word 文書を移植するペースで作業は進み、およそ 2 週間で主要なドキュメントについては Markdown 化が完了しました。

テーマのカスタマイズ

VuePress のデフォルトのテーマでは、以下のような見た目に仕上がります。

これでも概ね満足していたのですが、一部に気になるところもありました。

  • デフォルトで用意されているドキュメント内検索機能が貧弱。
    • 単純な部分一致でしか検索できないので、類義語での検索などに対応できない。
    • 検索結果がページ単位で、ページ内の特定の段落へのジャンプができない。
  • すべてのページのフッターに会社のコピーライト表記を入れたい。
  • サービスのロゴとは別に、サイドバーの上部にドキュメントのタイトルを入れたい。

そのため、オリジナルのテーマを作成することにしました。VuePress では テーマの継承 がサポートされているため、デフォルトのテーマを継承しつつ、カスタマイズしたいところだけを部分的に上書きすることができます。

今回作成したオリジナルテーマは、以下のようなファイル構成になっています。

.
├── assets
│   ├── logo.svg          # ブランドロゴ画像
│   └── search.svg        # 検索アイコン画像
├── components
│   ├── Home.vue          # ホームページ
│   ├── PageFooter.vue    # ページフッター(コピーライト表記)
│   ├── SearchBox.vue     # 検索ボックス
│   └── SidebarTitle.vue  # サイドバータイトル(ドキュメントのタイトル表示用)
├── enhanceApp.js         # GTM タグ埋め込み用
├── index.js              # メインファイル
├── layouts
│   ├── 404.vue           # 404 ページ
│   └── Layout.vue        # レイアウト定義
├── package.json
└── styles
    └── palette.styl      # CSS 設定

中心となる index.js は以下のようになっています。ここで継承元のテーマを指定しているほか、プラグインの設定も行っています。こうしておくことで、このオリジナルテーマを使用するドキュメントでは、自動的にこれらのプラグインが適用されるようになります。

index.js

const path = require('path');

module.exports = (options, ctx) => {
  const { themeConfig } = ctx;

  return {
    // VuePress のデフォルトテーマを継承
    extend: '@vuepress/theme-default',

    alias: {
      // デフォルトで用意されている検索ボックスの代わりに独自の検索ボックスを使用
      '@SearchBox': path.resolve(__dirname, 'components/SearchBox.vue'),
    },
    plugins: [
      // デフォルトで用意されている検索機能をオフ
      ['@vuepress/plugin-search', false],

      // その他、以下の VuePress プラグインを使用
      ['vuepress-plugin-sitemap', { /* 省略 */ }],
      ['vuepress-plugin-pdf-export', { /* 省略 */ }],
      ['vuepress-plugin-seo', { /* 省略 */ }],
    ],
  };
};

layouts/Layout.vue では、ページの基本的なレイアウトを指定しています。継承元テーマのレイアウト では、本文エリアの上部と下部、サイドバーの上部と下部にそれぞれ <slot> が用意されているため、その中をカスタマイズするだけであれば、継承元のテーマに手を加えずに済みます。

layouts/Layout.vue

<template>
  <ParentLayout>
    <template #sidebar-top>
      <!-- サイドバー上部に SidebarTitle コンポーネントを挿入 -->
      <SidebarTitle />
    </template>
    <template #page-bottom>
      <!-- 本文エリア下部に PageFooter コンポーネントを挿入 -->
      <PageFooter />
    </template>
  </ParentLayout>
</template>

<script>
import ParentLayout from '@parent-theme/layouts/Layout.vue';
import SidebarTitle from '@theme/components/SidebarTitle.vue';
import PageFooter from '@theme/components/PageFooter.vue';

export default {
  name: 'Layout',
  components: {
    ParentLayout,
    SidebarTitle,
    PageFooter,
  },
};
</script>

components/SearchBox.vue は、この記事の後半で説明する全文検索機能を利用するための検索ボックスで、入力フォームの外観や、実際に検索リクエストを投げて結果をリスト表示するための JavaScript の実装が含まれています(こちらのコードは割愛します)。

また、VuePress 公式の Google Analytics プラグイン は GA4 をサポートしていなかったため、enhanceApp.js に Google タグマネージャのタグを埋め込むためのスクリプトを記述することで対応を行いました。

enhanceApp.js

const GA_MEASUREMENT_ID = 'G-**********';

export default () => {
  // Google analytics integration
  if (process.env.NODE_ENV === 'production' && typeof window !== 'undefined') {

    // avoid duplicated import
    if (window.dataLayer && window.gtag) return;

    // insert gtag `<script>` tag
    const gtagScript = document.createElement('script');
    gtagScript.src = `https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`;
    gtagScript.async = true;
    document.head.appendChild(gtagScript);

    // insert gtag snippet
    window.dataLayer = window.dataLayer || [];
    window.gtag = function () {
      dataLayer.push(arguments);
    };

    gtag('js', new Date());
    gtag('config', GA_MEASUREMENT_ID);
  }
};

オリジナルのテーマを適用した場合は、このような見た目に仕上がりました。


今回は、VuePress を使用したドキュメントサイトの構築について紹介してきました。後編 では OpenSearch を使用した全文検索機能の実装と CI/CD パイプラインの構築について紹介します。

*1:REST API の仕様書だけは以前より HTML 形式で提供していました。この仕様書は OpenAPI で記述された仕様をもとに Redoc を使用して自動生成されています。

*2:Microsoft Word の設定で自動変換を無効化することは可能。

*3:https://github.com/vuejs/vitepress/discussions/548