タスクの重複検出とは、新規タスクの起票や着手時に、過去の完了タスクや既存タスクの中から類似・同一の作業を自動抽出し、二重着手やナレッジの分散を防ぐ仕組みです。タスクや Issue の重複を自動検出しようとするとき、多くの開発者が最初に試みるのが「タイトルや説明文の単語(トークン)が一致しているかを調べる」というルールベースの判定ではないでしょうか。

しかし、短文で文脈が乏しいタスク管理において、単純な単語一致は日常的に使われる一般的な語彙を拾い上げ、膨大な過検出(False Positive)を引き起こします。筆者が開発・運用するタスク管理環境(Taskwarrior と連携し、次に着手すべき1件を浮上させてディスパッチする対話型 CLI ツール)において、直近 14 日間の完了タスクとトークンを共有する重複検出を導入したところ、あるリポジトリでは未完了タスク 32 件中 31 件(96.9%)が重複候補として検出され、画面が重複警告で埋め尽くされる事態に陥りました。

誤検知を減らそうと頻出語の除外や共有単語数の閾値を設けると、今度は本当に検出すべき唯一の本命タスクまで脱落するジレンマに直面しました。そこで「重複か否か」の二値判定による抑止を諦め、情報検索(IR)のアプローチを取り入れて、共有トークンの逆文書頻度(IDF)の和で並べる「順位づけ(Top-K 枠提示)」へと設計を転換しました。本記事では、この実測の失敗から得られた教訓と、短文データにおける軽量な重複スクリーニングの設計原則を解説します。

1. 単純なトークン一致による過検出の現実──32件中31件ヒットの構造

ソフトウェア開発において重複課題の発生は無視できないコスト要因です。バグ報告に関する実証研究(Jahan & Rahman, 2022)1 でも示されているように、バグ報告の約 40% が重複であり、ソフトウェア保守における大きなオーバーヘッドになっていると言われています。この無駄を避けるため、筆者は自作のタスク管理ツールにおいて、着手候補タスクと直近 14 日間の完了タスクの間で固有トークンを共有している場合に dup= として警告行を出力する初期実装を行いました。

しかし、この素朴な単語共有判定を実務環境(2026年10月22日時点)に適用したところ、以下のような過検出の嵐に見舞われました。

管理リポジトリの種別未完了タスク(母数)重複警告が出た件数直近14日の完了タスク(母数)重複警告の発生率
リポジトリ A(個人設定系)323110096.9%
リポジトリ B(業務アプリ系)12105983.3%
ワークスペース524240.0%
筆者のタスク管理ツール10490.0%
合計504325086.0%

調査した全 4 リポジトリの合計で、未完了タスク 50 件中 43 件(86.0%)に重複候補が出力され、その大半が無関係な別件タスクでした。

過検出を引き起こした主要なトークンを調査すると、weekly-retro、SKILL.md、settings.json、run_daily.sh、test_daily.sh といった語が上位を占めていました。たとえば、リポジトリ A で浮上したタスク「tw-day-plan の launchd 発火確認」に対し、同一のスクリプトや設定ファイル名を含むという理由だけで、文脈のまったく異なる「meeting-cheatsheet の失敗調査」など 3 件が無関係に紐づいていました。

短文のタスク管理において単純な単語やトークンの表層一致だけに頼ると、共通のファイル名や定期業務といった定型ノイズに引っ張られ、無関係な別件への誤検知が極めて容易に発生する構造が浮き彫りになりました。

2. 閾値を上げると本命が消える──足切りが招く「再現率(Recall)の破壊」

大量の誤検知を目の当たりにした開発者が次に考えるのは、「閾値を設けてノイズを足切りする」ことでしょう。筆者も同様に、以下のルールによる足切りを試作しました。

  1. チケット番号や Issue 番号といった作業単位の参照の共有は無条件で一致とみなす。
  2. 完了タスクが 20 件以上ある環境では、全体の 5% を超える頻出語を照合から除外する。
  3. 希少トークン 2 個以上 の共有を必須条件とする。

翌日(2026年10月23日)、未完了タスクが 1 件増えて 33 件となっていた環境でこの試作ルールを適用したところ、31 件に出ていた重複警告は 7 件 へと激減しました(約 77% の削減)。一見すると、邪魔なノイズをきれいに削ぎ落とすことに成功したように思えました。

しかし、ここに致命的な副作用が潜んでいました。

そもそもこの重複検出機能を導入するきっかけとなった「本命の実例」(週報の訂正判定修正に関する過去タスクとの重複)を当てる唯一のトークンが、皮肉にも weekly-retro だったのです。過去タスクの記録は「weekly-retro の訂正判定から…」、新規タスクは「週報の訂正判定を直す」と記述されており、日本語の表現揺れによって共通する固有トークンは英数字識別子の weekly-retro 1 語しか存在しませんでした。そのため、頻出語除外(5% 超え)によって照合対象から外され、さらに 2 語以上要求の足切り条件によっても弾かれ、本来最も検出したかった本命の実例が完全に脱落(False Negative) してしまいました。

当時のテストコードにも「動機の実例(weekly-retro 1 語だけの共有)もこの規模では見えなくなる、の確認を兼ねる」と明記されており、足切りの代償として本命が失われた事実が記録されていました。

表層のテキスト類似度を中心に据える従来の手法では、表現の違いやノイズに阻まれて本命を取りこぼすという課題は、先行研究(Jahan & Rahman, 2022)1 でも非類似重複の見逃しとして指摘されています。短文タスクにおいては、「本命を当てる決定的なキーワード」と「日常業務で頻出するノイズ語」が同一の語になってしまう事態が日常的に発生します。二値判定(重複か否か)の枠組みで足切り条件(Threshold cutoff)を厳しくすると、適合率(Precision)は見かけ上向上しても、再現率(Recall)が致命的に破壊されてしまうのです。

3. 「重複判定」から「IDF順位づけ」への転換──二値分類を諦めて情報検索にする

本命が消えてしまう以上、閾値による足切りは採用できません。そこで筆者が下した設計判断は、「足切りをやめ、頻出トークンも消さずに下位へ回す順位づけにする」 というものでした。

機械に「重複しているか否か」を白黒つけさせる二値分類(Classification)を諦め、情報検索(IR)の考え方を導入して「類似している可能性が高い順に並べて上位を提示する」ランキング問題へと定式化し直すアプローチです。

比較軸二値判定(足切り)アプローチ順位づけ(IDF Ranking)アプローチ
設計の目的重複か否かを機械が白黒判定する類似している可能性が高い順に候補を並べる
頻出語の扱い閾値を超えた語を一律除外・足切りスコアの重みを下げるが完全に消去はしない
本命が1語のみの場合複数語要求などの足切りで脱落する希少な語ほど上位に残り、頻出語でも下位に残る
UIでの役割閾値判定により警告の要否を二値化警告を止めず希少トークン上位を提示し、UIの推奨は維持

機械に完全な二値判定を迫るのではなく、情報検索のランキング問題として定式化し直すことで、本命の脱落を防ぎつつ、希少トークンを共有する候補を優先して上位に提示できます。ただし、順位づけによって警告行の総数そのものが減るわけではありません(実測でも 33 件中 31 件に出力されました)。変わるのは提示される「並び順」です。

短文における IDF の決定性と数理仕様

全文検索で広く用いられる TF-IDF において、短文のタスクタイトルでは同一単語の出現頻度 tf はほぼ 1 です。また文長も極端に長くないため、文書ベクトルの長さを正規化するコサイン類似度をあえて計算しなくても、共有トークンの希少度、すなわち 逆文書頻度(IDF: Inverse Document Frequency) の合計値を見るだけで実用上十分な順位づけが得られます(IDF の標準的な体系は Wikipedia 2 参照)。

改修後の実装では、Python 標準ライブラリの math.log を用い、平滑化 IDF 式 IDF = math.log(1 + len(haystacks) / df) を採用しました。

ここで len(haystacks) は比較対象となる完了タスクの総件数、df(Document Frequency)はそのトークンを含む完了タスクの件数です。分母に df をそのまま使いつつ分子を 1 + len(haystacks) とすることで、ゼロ除算を防止しています。これは scikit-learn の TfidfTransformer における smooth_idf オプション 3 と同様の平滑化設計です。また、すべての完了タスクに出現する最頻出語(df = len(haystacks))であっても、スコアは math.log(1 + 1) = math.log(2) ≒ 0.693 となり、重みが完全にゼロ化して照合から消滅するのを防いでいます。

改修後の実装コード

今回の改修では、DUP_MIN_POOL(母数閾値)や min_shared(最小共有単語数)といった足切りロジックをすべて撤廃しました。

トークンの抽出(dup_tokens)は形態素解析器等の重い外部依存を持たず、正規表現で英数字・記号・特定キーワードを抽出する軽量設計としています。また、チケット番号や Issue 番号(#123 や PROJ-456 等)といった確定的参照(STRONG_REF_RE)の一致を最優先キー(第 1 キー)、共有トークンの平滑化 IDF 和を第 2 キー、完了日時を第 3 キーとして降順ソートし、上位 3 件(DUP_MAX = 3)のみを提示する構造に改めました。

引用元: 筆者のタスク管理ツール(非公開)の float.py

ファイル: float.py

    haystacks = [(t, dup_haystack(t)) for t in completed if t.get("uuid") != uuid]
    weights = {}
    for tk in dup_tokens(desc, repo):
        rx = dup_token_re(tk)
        if df := sum(1 for _, h in haystacks if rx.search(h)):
            weights[tk] = (rx, math.log(1 + len(haystacks) / df))
    scored = []
    for t, text in haystacks:
        shared = [(tk, w) for tk, (rx, w) in weights.items() if rx.search(text)]
        if shared:
            strong = any(STRONG_REF_RE.fullmatch(tk) for tk, _ in shared)
            scored.append((strong, sum(w for _, w in shared), t.get("end", ""), t))
    scored.sort(key=lambda x: x[:3], reverse=True)
    return [f"dup={t['uuid'][:8]} {' '.join(t.get('description', '').split())[:60]}" for *_, t in scored[:DUP_MAX]]

このコードにより、頻出語しか共有していないタスクであっても足切りで捨て去られることはなく、他に希少トークンを共有する候補が存在しない場合には下位の枠に残って提示されます。

アルゴリズムの細部や数理的な知識は既存ライブラリや AI アシスタントに委ねても問題ありません。人間がすべての数式や仕様を完璧に記憶していなくても、現場で起きた現象(過検出の多発や本命の脱落)に対して「二値判定をやめて順位づけにする」というアーキテクチャ上の設計判断を下せれば、実務の仕組みは十分に前進させられます。

4. UIの割り切り──実行を止めず「気づかせ」に留める

ランキング方式への転換によって本命の脱落は防げるようになりましたが、UI および運用の観点でもう一つの重要な設計判断が必要になりました。それは 「重複警告が出ても処理を中断させず、推奨を維持する」 という割り切りです。

ワークフローを壊さない推奨維持

初期の検討では、dup= が出たタスクは着手推奨から外す(二重作業を防止するため選択肢のデフォルトから外す)ことも考えられました。しかし、改修後の実装では dup= 行が出力されても、選択肢 1(タスク着手)の推奨(既定選択)は維持し、ラベルに「(重複の可能性あり)」という注記を添えるだけに留めました。

誤検知をゼロにすることは原理的に困難です。重複の疑いが少しでもあるからといってツールの自動推奨を止めてしまうと、「最も浮いたタスクをテンポよく実行に移す」というタスク管理本来のワークフローが停止してしまいます。未コミットの変更があるといった「作業を確実に止めるべき理由」と、重複の疑いのような「人間に気づかせるべき理由」は明確に分離し、後者は推奨を維持したまま注記に留めるのが実務的な落としどころです。

この「強制ブロックではなくサジェストに留める」設計思想は、一般的なプロジェクト管理ツールでも共通しています。たとえば Jira の重複課題防止プラグイン(Find Duplicates)4 や、Redmine における問い合わせチケット推薦システム「あいちゃん」5 においても、課題やチケットの起票を強制的に中断させるのではなく、類似する過去チケットを画面上に提示して担当者の自発的な判断を促す運用が採られています。

表示枠漏れの許容というリアリズム

順位づけの実装後、実環境で改めて検証を行ったところ、興味深い事象が発生しました。

開発の動機となった本命の実例が、実環境ではより希少なトークンを共有する別の 3 件に枠を取られ、上位 3 行(DUP_MAX = 3)の表示枠から漏れてしまったのです。

「画面から漏れてしまったのなら、足切りで消えたのと実質的に同じではないか?」と思われるかもしれません。しかし、両者にはアーキテクチャ上の決定的な違いがあります。足切りはアルゴリズム自身が候補の存在を不可逆的に完全抹消(False Negative)するのに対し、順位づけは「提示優先度の問題」であり、本命タスクもスコアを持って候補群の中に依然として存在し続けています。

このとき本命ペアを押し出した上位 3 件は、特定のシェルスクリプト名や個別ファイル名など、出現頻度がごくわずか(df が 1〜2 件)の希少トークンを共有するタスク群でした。日常的な業務の中で頻繁に登場する中頻度語(weekly-retro)よりも、滅多に使われない希少な固有トークンを共有する候補を優先して上位に並べるのは、情報検索の数理スコアリングとして極めて正常で忠実な挙動です。

本命を無理に画面内に収めようとして表示枠を 5 件、10 件と広げてしまえば、タスクを選択するたびに画面が候補行で溢れかえり、認知負荷が増大してしまいます。だからこそ、筆者は 「表示枠の漏れを許容し、表示件数を 3 件から増やさない」 という判断を下しました。

機械に完璧な判定を求めず、限られた表示領域(Top-3)に最も確率の高い候補を提示し、最終的な見極めを人間に委ねます。順位づけに変えても警告自体の発生率がゼロになるわけではないからこそ、UI 側の割り切りが不可欠となります。

5. 前提が変われば結論はどう変わるか

今回採用した平滑化 IDF による順位づけは、ローカル環境で数十〜数百件のタスクを高速にスクリーニングするための軽量なアプローチです。前提条件が変化すれば、最適なアーキテクチャの結論も変わり得ます。

完了タスクが数千〜数万件に肥大化した場合

完了タスクが数千〜数万件の規模に拡大した場合、毎回のタスク浮上時に全件ループを回して Python 上で IDF を計算するオーバーヘッドは無視できなくなります。その段階では、インメモリでの逐次計算を卒業し、SQLite の FTS5(Full-Text Search)拡張や BM25 をサポートするローカル検索エンジンを活用した、転置インデックスベースのアーキテクチャへの移行が必要になるでしょう。

意味的な言い換え(セマンティック検索)への対応

単語の表層一致に基づく IDF 順位づけでは、シノニム(同義語)や表記揺れ(例: 「修正」と「バグフィックス」、「週報」と「weekly-report」)による重複を見逃す限界があります。意味的な類似性を捉えるには、テキスト埋め込み(Embedding)や LLM によるセマンティック検索が有効な選択肢となります。

ただし、ローカル CLI や即時応答性が求められるディスパッチツールにおいて、外部 API の呼び出しに伴う数百ミリ秒のレイテンシや従量課金コストは小さくありません。実務においては、まず計算コストがほぼゼロの平滑化 IDF で強固なベースラインを構築し、必要に応じてローカルで動作する小型の埋め込みモデルをハイブリッドに組み合わせるのが現実的な発展経路と言えます。

自律的な振り返りループが仕組みを育てる

今回の過検出の発生から足切りの失敗、そして順位づけへの改修に至る一連のプロセスにおいて、開発者自身が過去のアルゴリズムの細部や数式を最初からすべて記憶していたわけではありませんでした。

日々の運用の中で異常(32件中31件のヒット)に気づき、一次計測ログを取り、試作の副作用(本命脱落)を検知し、設計方針を転換する。このように、日々の振り返りや運用の仕組みを通じて問題点が自律的に拾い上げられ、適切な設計判断のもとで改善が適用されていくプロセスこそが、実務のシステムが健全に回っている証拠と言えるでしょう。

よくある質問(FAQ)

Q: なぜコサイン類似度ではなく「IDFの和」だけで順位づけできるのですか?

A: 短文のタスクタイトルでは各単語の出現頻度(TF)がほぼ 1 であり、文長も極端に長くないためです。ベクトルの長さを正規化するコサイン類似度を計算しなくても、共有トークンの IDF 値を足し合わせるだけで、筆者の実務環境では頻出ノイズを下位に沈め、希少な重要単語を共有する候補を優先する実用的な並び順が得られました。

Q: 閾値による足切りを完全に撤廃すると、誤検知がまた画面に溢れませんか?

A: 順位づけによって警告行の発生そのものが消えるわけではありません(実測でも 33 件中 31 件に出力されました)。しかし、上位 3 件にはより希少なトークンを共有する候補が並び、頻出語しか共有していない候補は下位へ回ります。画面が過剰な警告で破綻しないよう、表示件数を上位 3 件に制限しつつ、選択肢の推奨を止めない UI の割り切り(注記に留める運用)とセットで設計することが重要です。

まとめ

短文データの重複スクリーニングにおいて、本記事の実践から得られた教訓は以下の通りです。

  • 単純な単語一致は過検出を引き起こす: 文脈の乏しい短文タスクでは、日常的に使われる共通語によって 8 割以上の過検出が容易に発生する。
  • 足切り閾値は再現率を破壊する: 誤検知を減らすために頻出語除外や複数単語要求を設けると、本命の重要タスクまで脱落するジレンマに陥る。
  • 二値判定を諦めて順位づけに転換する: 重複を白黒判定するのではなく、共有トークンの平滑化 IDF 和でソートして Top-K 件を提示することで、本命を消さずに希少語を優先できる。
  • UI は推奨を維持して注記に留める: 誤検知がゼロにならない以上、重複警告でタスクの推奨を止めず、気づかせに留める割り切りがワークフローの維持に不可欠である。

開発プロセスの標準化、AI Operations の導入設計、アーキテクチャや意思決定プロセスの見直しについて、ぜひ コンタクトフォーム よりお気軽にご相談ください。

Footnotes

  1. Towards Understanding the Impacts of Textual Dissimilarity on Duplicate Bug Report Detection Sigma Jahan, Mohammad Masudur Rahman, arXiv:2212.09976 [cs.SE], 2022年12月20日公開。アブストラクト。ソフトウェアのバグ報告の約40%が重複であり保守の大きなオーバーヘッドとなっていること、およびテキストが非類似な重複ペアでは従来の表層類似度手法の性能が著しく低下すること(見逃し課題)の根拠。 ↩ ↩2

  2. Tf–idf Wikipedia「Tf–idf」。逆文書頻度(IDF)の数理的定義、および平滑化(Smooth IDF)の標準的バリエーション。 ↩

  3. TfidfTransformer — scikit-learn documentation scikit-learn documentation。smooth_idf=True によるゼロ除算防止および定数加算による重みゼロ化防止の設計思想の根拠。 ↩

  4. Find DuplicatesでJiraの重複課題を簡単に防ごう! リックソフト株式会社 澤田, 2021年12月17日公開。Jira における重複課題防止アプリの解説。起票を強制ブロックせず、画面上にサジェスト表示して自発的防止を促す UI 設計の根拠。 ↩

  5. その問い合わせ、AIが解決します!~Redmineチケットレコメンドシステムのご紹介~ フューチャー株式会社 竹林康太, 2018年10月31日公開。Redmine におけるチケットレコメンドシステム。起票の禁止ではなく類似チケットを自動検索・紐付けして回答支援を行う運用の根拠。 ↩