Texdoc のスコア計算を理解したい話

2017-11-06   #Lua  #TeX Live  #Texdoc 

Texdoc は与えられたキーワードに対して適切なドキュメントを表示するため,TEXMF ツリーの中で発見したドキュメントに対して順位付けを行います.この順位付けのために用いられるスコアは,公式ドキュメントによると「単純なヒューリスティクスに基づいて計算される」ことになっているのですが,あまり詳しいことは述べられていません.そこで,Texdoc の内部で具体的にどのようなスコア計算が行われているのか,Lua で書かれたソースコードから追ってみようと思います.

追跡の方法

実際にスコア計算のロジックについて見ていく前に,どのようにして Texdoc のソースコード構成を把握し,またその挙動について追跡を行ったのかということを簡単に説明しておきます.

ソースコードの見つけ方と構成

Texdoc の実装は texlua で動作するように書かれた2,300行程度の Lua スクリプトです1.Texdoc を実行する際に,一番最初に読まれるのは texdoc.tlu というファイルで,その所在は kpsewhich で簡単に確認できます.

$ kpsewhich texdoc.tlu

Texdoc のソース・スクリプトファイルは,すべてこの texdoc.tlu と同じディレクトリに水平に配置されていますが,実際にコードを読んでみると,これらのファイル(モジュール)は対等な関係ではありません.スコア計算と密接に関連しているファイルだけを取り出してその構成をまとめると,概ね次のようになっています.

  • texdoc.tlu: main.tlu のラッパー(kpse ライブラリの初期化)
    • main.tlu: メインプログラム
      • config.tlu: 設定の管理
      • search.tlu: ドキュメントの検索と順位付け
        • score.tlu: スコア計算
      • view.tlu: ドキュメントの表示(順位付け結果の使用)

デバッグ情報

ソースコードを読むだけでプログラムの挙動を完全に把握するのは大変なので,実際に Texdoc を動かしてその挙動を確かめながら作業をすすめることも大切です.その際に便利なのが Texdoc の -d オプションで,これを付けて Texdoc を実行すると内部で行われているスコア計算の途中経過などをターミナルに吐き出させることができます.

スコア計算

スコアの使われ方

Texdoc のスコアは,基本的には見つかったドキュメントの順位付けのために用いられます.この順位付けは docfile_order() を比較関数とするソート処理によって実現していますが,この関数は以下の項目をこの順序で検査して初めて差があったときに上位のドキュメントを決定します.

  1. スコア
  2. 拡張子
  3. ファイル名(辞書順)
  4. ファイルの位置

すなわち,この順位付けにおいて最も重要な項目がスコアで,その値が高ければその時点で優先的に表示されることが確定します.次に重要なのが拡張子で,内部変数 ext_list の中で前に出てくるものほど優先されることになります.ext_list のデフォルト値(built-in)は

ext_list = "pdf", "htm", "html", "txt", "ps", "dvi", ""

です.そして,スコアと拡張子を比較しても差がない場合はファイル名やファイル位置に基づいて順位が決定されます.

ところで,スコアにはもう1つの用途があり,Texdoc がドキュメントの品質を評価する際にも利用されます.あるドキュメントのスコアが $s$ のとき,このドキュメントの品質は次のように評価されます.

  • good: $s > 0$
  • bad: $-100 < s \leq 0$
  • killed: $s \leq -100$

Texdoc では -l オプションを付けた場合は基本的に “good” と判定されたドキュメントのみがリスト表示されます.Texdoc に “bad” と評価されたドキュメントは,-s オプションを付けたときに限りリストに載せられることになります.

スコア計算の手順

スコア計算は,基本的にはユーザ入力の「キーワード」と各ドキュメントの名称のパターンマッチによって行われます.この際に用いられるドキュメントの名称は,具体的には Texdoc の用語で「ショートネーム」と呼ばれるファイル名とその直上ディレクトリの組み合わせ(<directory name>/<file name> の形)を小文字化したものです.

各ドキュメントには,始めに初期値としてスコア-10が与えられます.そして,以下に説明する各段階でそのショートネームやメタデータに応じたスコアが加点または減点されて,最終的なスコアが決定されます.

1. パターンに基づくスコア付け:ヒューリスティック・スコアの計算

Texdoc のスコア計算は,まずドキュメントのショートネームとキーワードのパターンマッチによってスコアの基礎点とも言えるヒューリスティック・スコアを算出するところから始まります.

ヒューリスティック・スコアは-10から10の範囲に収まる数値で,以下この節内で列挙する条件に基づいて決定されます.ただし,ユーザ入力のキーワードがエイリアスであった場合,ヒューリスティック・スコアは原則として10になるようです.

  • ショートネームまたはファイル名がキーワードと完全に一致し,ロケール2も一致する場合:5点
  • ショートネームまたはファイル名がキーワードと完全に一致する場合:4点
  • ショートネームまたはファイル名にキーワードが含まれる場合:1点

ここで「ショートネームまたはファイル名がキーワードと完全に一致」しているかの判定(is_exact() 関数)では,拡張子に対しては十分な柔軟性があります3.一方で「ショートネームまたはファイル名にキーワードが含まれる」かどうかの判定(is_subword() 関数)では,キーワードがデリミタ4で区切られて存在している必要があります(これらのルールは以降の項目でも同様です).

さて,ユーザ入力のキーワードにスラッシュが含まれていない場合は,キーワードに suffix_list に含まれる接尾辞をそれぞれ取り付けた「派生キーワード」が生成され,これらについてもパターンマッチが試行されます.

  • ショートネームまたはファイル名が派生キーワードと完全に一致する場合:3点
  • ショートネームまたはファイル名に派生キーワードが含まれる場合:2点5

なお,TeX Live 2017 に付属する texdoc.cnf によって suffix_list は次のように設定されています.

suffix_list = "doc", "-doc", "_doc", ".doc", "/doc", "manual", "/manual", "-manual", "userguide", "/user_guide", "-guide", "-user", "-man", "notes", "-info", "ref"
  • 上記の条件をすべて満たさない場合:-10点

ドキュメントがここまでに挙げた条件を満たしている場合,それぞれ対応するスコア(複数の条件を満たす場合はその最大値)が与えられるわけですが,最終的なヒューリスティック・スコアが決定される前に少し調整が入ります.

まず,ここまでで正のスコアを得ているドキュメントのうち,拡張子が badext_list に登録されているものと,ベースネームが badbasename_list に登録されているものは,それまでの獲得スコアとは無関係にスコアが0.1に下げられます.ここで badext_listbadbasename_list のデフォルト値(built-in)は次のようになっています.

badext_list = "txt", ""
badbasename_list = "readme", "00readme"

一方,ドキュメントの直上ディレクトリがキーワードと完全に一致する場合は,1.5点のボーナス・スコアが加算されます.

2. texlive.tlpdb に基づくスコア付け

もしヒューリスティック・スコアが最低の-10となった場合,TeX Live のパッケージデータベースである texlive.tlpdb の情報に基づいて救済のスコアが与えられるようです.

例えば,一般的な Texdoc の設定において

$ texdoc jsarticle

とすると jsclasses/jsclasses.pdf という,キーワード “jsarticle” をまったく含まないドキュメントが表示されますが,これはこの救済スコアが与えられる影響です.

しかし,救済スコアは必ず0以下の値となるため,正のヒューリスティック・スコアが付くドキュメントより優先順位が高くなる可能性は大変低いと考えられます.

3. メタデータによる加点

各ドキュメントについて,texlive.tlpdb からドキュメントの種類に関する属性情報(メタデータ)が得られた場合には,これまでの得点に1.5点のボーナスが追加されます.ただし,ドキュメントの種類が “readme” であった場合はボーナスは0.1点です.

4. texdoc.cnf に基づく調整

texdoc.cnf に記述可能な設定項目に adjscore というものがあり,これよってユーザが直接スコア計算に介入することが可能です.Texdoc ではスコア計算の最後の過程として,これらの設定に基づくスコアの調整を行います.

texdoc.cnf における adjscore の設定シンタックスは簡単には次のようになっています.

adjscore <pattern> = <score>
adjscore(<keyword>) <pattern> = <score>

(<keyword>) が指定されていないものは,すべてのキーワードに対して適用されるグローバルな設定で,<pattern> がショートネーム中に含まれる場合に対応する <score> が加算されます.一方,(<keyword>) が指定されている場合は,ユーザ入力のキーワードが <keyword> と一致する場合に限りスコアの調整が行われます.

以下に,参考のため TeX Live 2017 に含まれる texdoc.cnf の adjscore 設定を掲載しておきます.

## General adjustments

# Makefile are never documentation, just as documents in src or source subdir
# -1000 should be enough to kill them
adjscore /Makefile = -1000
adjscore /src/     = -1000
adjscore /source/  = -1000

# licence files aren't very likely to contain relevant documentation, but it
# feels wrong to totally kill them
adjscore copying = -10
adjscore license = -10
adjscore gpl     = -10

# tex-virtual-academy provides a lot of spurious matches
adjscore /tex-virtual-academy-pl/ = -50

# test and example files are not likely the best documentation
adjscore test     = -3
adjscore tests    = -3
adjscore example  = -3
adjscore examples = -3
adjscore sample   = -3
adjscore samples  = -3
adjscore /images/ = -3

# readme's usually get a negative score because they have a bad extension,
# but they're still slightly better than other results with negative scores
adjscore readme = 0.1

# uncomment this to make the man pages have a greater priority
#adjscore .man1. = 5
#adjscore .man5. = 5

## Specific adjustments

# 'texdoc' may look like "tex's documentation" but it isn't
# similar problem with 'tex-*'
adjscore(tex) texdoc   = -10
adjscore(tex) tex-gyre = -10
adjscore(tex) tex-ps   = -10

# avoid too many results to be shown for 'latex'
# package names
adjscore(latex) cjw-latex                  = -10
adjscore(latex) cweb-latex                 = -10
adjscore(latex) duerer-latex               = -10
adjscore(latex) guide-to-latex             = -10 # only useful with the book
adjscore(latex) latex-web-companion        = -10
adjscore(latex) ocr-latex                  = -10
adjscore(latex) tufte-latex                = -10
# file names
adjscore(latex) Content_LaTeX_Package_Demo = -10
adjscore(latex) example_latex              = -10
adjscore(latex) test_latex                 = -10

# beamer
adjscore(beamer) beamer-tut-pt/tutorialbeamer = +10
adjscore(beamer) beamer-FUBerlin              = -3
adjscore(beamer) beamer-tut-pt                = -10
adjscore(beamer) presentations                = -10

# misc
adjscore(context) circuitikz = -10
adjscore(context) /gnuplot/  = -3
adjscore(context) context.man1 = +2
adjscore(symbols) /staves/   = -5

# catalogue info missing in the tlcontrib version of the package :-(
adjscore(pgf) pgfmanual.pdf = +5

# context version is found first
adjscore(fixme) /third/  = -6

画期的な Texdoc

これまで見てきたように,Texdoc は表示するドキュメントを決定するにあたり,かなり ad-hoc で複雑なスコア計算をしていることがわかりました.Texdoc は世界の TeX Live ユーザがドキュメントを探すのに使用しているツールなので,(La)TeX パッケージの開発者としては同梱ドキュメントの表示における優先順位をコントロールしたいところですが,現状の Texdoc にはこれを陽に制御する方法はなく6,またスコア計算が複雑でその挙動を正確に推定するのも容易ではありません.

そもそも,Texdoc がなぜこれほど複雑なスコア計算をしなければならないかといえば,世の中のドキュメントの中に「本質的」でないものがあまりにも多いからです.よくよく考えてみれば,本当に表示されるべき本質的なドキュメントといえば,アレしかないはず……そう,ナントカです.

というわけで,どのようなキーワードに対しても本当に表示されるべき本質的なドキュメントを表示する画期的な Texdoc を開発してみました.

使い方は本家 Texdoc と同様で,次のようにすればただちに真に本質的なドキュメントが表示されるはずです.

$ texlua sctexdoc.lua <keyword>

なお,オプションの体系も本家 Texdoc とまったく同様です.☃


  1. Data.tlpdb.lua というデータベースのキャッシュ・ファイルは除きます. [return]
  2. ショートネーム中に -<lang> の形で言語の略称が含まれる場合に,その略称からそのドキュメントのロケールが判断されます. [return]
  3. 具体的には,まず拡張子を除いたベースネームとキーワードの直接比較が行われ,一致しない場合にはパラメタ ext_list に含まれる拡張子をキーワードにそれぞれ結合して一致するものがないかが調べられます. [return]
  4. ここでのデリミタは Lua の正規表現において %p にマッチする記号を指します. [return]
  5. 派生キーワードの完全一致はキーワードとの完全一致よりも1点低い一方で,派生キーワードの部分一致はキーワードとの部分一致よりも1点高いのは不可解ですが,理由は不明です.おそらくテキトーに決められたのでしょう.ヒューリスティックなので. [return]
  6. ユーザ・サイドではエイリアスやスコア補正の設定を行うことで実現可能です. [return]