Document corpus について
mdcxは文書コレクションを検証済みMarkdownに変換し、文章、検索インデックス、来歴情報を暗号化アーカイブにまとめます。コレクション全体をコンテキストに入れず、出典付きの回答を必要とするエージェントやチーム向けです。
主な機能
- 01文書をMarkdownに変換し網羅率を確認
- 02文章・インデックス・来歴情報を一括パッケージ化
- 03単語検索と密検索を組み合わせて検索
- 04出典位置付きの文章を返却
- 05暗号化と任意の署名に対応
活用シーン
- 01エージェントで大規模な文書コレクションを検索
- 02回答を元文書と位置までたどる
- 03暗号化されたインデックス付きコーパスを配布
- 04文書群の変換網羅率を確認
MCP 対応
オプションのMCPサーバーは、パッケージ化された文書コレクションをMCPクライアントに提供し、出典付きの関連文章を返します。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
ツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
mdcx
文書コレクションを検証済みの Markdown に変換し、検索インデックスと来歴情報を含む単一の暗号化アーカイブにまとめ、Model Context Protocol 経由でエージェントに提供します。
目次
- 対象範囲
- 要件
- インストール
- クイックスタート
- 変換
- パッケージ化
- クエリ
- 検索モデル
- キャリブレーション
- 語彙
- 文字起こしの復元
- 式
- 分割箇所をまたぐ引用
- 添付ファイル
- 日付
- 通信文
- 増分パッケージ化
- 常駐変換
- MCP サーバー
- ソース
- ポータブルパス
- 署名
- 暗号化
- パッケージ形式
- 制限事項
- テスト
- コントリビューション
- セキュリティ
- リリース
- 著者
- 引用
- ライセンス
対象範囲
mdcx は、ある制約に対処します。文書コレクションについて質問されたエージェントは、トークンごとに課金され、容量にも上限のあるコンテキストウィンドウに文書を受け取るか、インデックスを保持し、質問に関係する箇所だけを返すコンポーネントにクエリを送る必要があります。mdcx は後者を実装します。
結果を特徴づけるのは、次の 3 つの特性です。
- 忠実性を検証します。 各変換の結果を、元の文書が提示するテキストと比較し、達成した網羅率をファイルごとに記録します。テキストを提示しないファイルは、完全と報告するのではなく、検証不能として記録します。この比較で何を確認できるかはエンジンによって異なり、その内容は検証で説明します。
- コーパスを 1 つの成果物にまとめます。 箇所、インデックス、来歴情報を単一の AES-256-GCM ファイルに格納します。このファイルのヘッダーは鍵なしで読み取ることができ、署名も可能です。
- 回答を引用可能にします。 すべての箇所にソース文書とその位置が記録されるため、位置を示して回答を引用できます。
パイプラインは 3 段階で構成されます。変換では、各文書を読み取れる最も低コストのエンジンで処理し、そのエンジンで不十分な場合にのみ、より高度な処理に進みます。最初にテキストを直接抽出し、次に描画された表を復元する処理を行い、その後にレイアウト解析を行います。テキストを提示しない文書には、光学文字認識を適用します。パッケージ化では、コーパス、インデックス、各箇所の来歴情報を 1 つのファイルに書き込みます。検索では、単語照合と高密度検索を逆順位融合で統合し、コーパス内に質問に関係する内容がない場合は、最も近い箇所を返すのではなく、その旨を明示します。
要件
Python 3.11 以降が必要です。パッケージの検索には、ほかのコンポーネントは必要ありません。
パッケージは単一の SQLite データベースであり、開くときに復号済みのバイト列から読み込まれます。必要な呼び出しが sqlite3 に追加されたのは Python 3.11 であるため、最低要件を 3.11 としています。変換とクロスランゲージ検索にはそれぞれ追加の依存関係が必要です。詳しくはインストールを参照してください。
インストール
クエリと変換では要件が約2桁異なるため、分けてあります。パッケージを読むだけの受け取り側は、Docling も PyTorch もインストールする必要はありません。
| コマンド | 提供する機能 | おおよそのサイズ |
|---|---|---|
pip install mdcx | .mdcx パッケージのクエリと読み取り | 10 MB |
pip install "mdcx[mcp]" | 上記に加えて MCP サーバー | 50 MB |
pip install "mdcx[convert]" | ドキュメント変換 | 1.4 GB |
pip install "mdcx[tables]" | ページに罫線が描画されていない表 | 1.2 GB |
pip install "mdcx[multilingual]" | 言語をまたいだ検索 | 2.5 GB |
pip install "mdcx[all]" | OCR を含む上記すべて | 4 GB |
pip install "mdcx[all-gpu]" | CPU 用の onnxruntime を除く上記すべて | 4 GB |
multilingual エクストラは埋め込みモデルを提供します。このモデルは初回使用時に一度だけダウンロードされ、言語をまたぐクエリの場合にのみ必要です。
tables エクストラは、罫線が描画されていない表に対応します。たとえば、スプレッドシートのスクリーンショットや、配置によって構成されたレイアウトなどです。読み取るのはジオメトリのみで、セルの内容は引き続きドキュメントのテキストレイヤーから取得します。このエクストラがない場合、そのようなページはレイアウトエンジンで処理されます。
CUDA デバイスを搭載したシステム
mdcx[all] ではなく mdcx[all-gpu] をインストールし、onnxruntime-gpu を別途インストールしてください。
onnxruntime と onnxruntime-gpu は同じモジュールを提供するため、共存できません。後からインストールしたほうが有効になります。そのため、CPU 版を固定するエクストラを使うと、アップグレードのたびにアクセラレーション版が置き換えられます。all-gpu は、その固定を除いた all です。
pip では、どちらか一方のディストリビューションで満たされる依存関係を表現できないため、宣言だけでは解決できません。mdcx-convert は起動時にチェックし、ランタイムが対応していないデバイスがマシンに搭載されている場合に通知します。チェックだけを行う場合:
python -c "import onnxruntime as o; assert 'CUDAExecutionProvider' in o.get_available_providers()"
クイックスタート
pip install "mdcx[convert]"
mdcx-convert --input ./Documents --output ./Documents_md
mdcx pack --output ./Documents_md --target corpus.mdcx --key "passphrase"
mdcx search corpus.mdcx "where is the storage temperature stated" --key "passphrase"
変換
mdcx-convert --input ./Documents --output ./Documents_md
出力フォルダーは入力ディレクトリの構造を反映し、元のドキュメントごとに .md ファイルが作成されるほか、実行のインデックスも作成されます。
| オプション | 効果 |
|---|---|
--max-cores N | 同時に実行するプロセス数 |
--gpu-workers N、--cpu-workers N | 計算された割り当てを上書きします |
--serial | 一度に1つのドキュメントを処理します |
--only PATTERN、--limit N | 実行対象を制限します |
--force | キャッシュを無視して再変換します |
--no-docling | ネイティブエンジンのみを使用します |
--formulas | 数式を LaTeX に書き起こします |
--no-gpu | デバイスを使用しません |
--no-lossless | バックアップ JSON を書き込みません |
--no-compact | Markdown にコンバーターの補助情報を残します |
--no-split、--split-threshold N | 長いドキュメントの分割を制御します |
--sample-pages N | ドキュメントごとに N ページを間隔を空けてサンプル変換します |
--timeout SECONDS | 指定時間を超えたドキュメントの処理を中止します |
--recycle-after N | N 件のドキュメントを処理した後、ワーカーを置き換えます |
中断した実行は再開できます。記録済みの作業は、--force を指定しない限り繰り返されません。
長いドキュメントは章に分割され、各章が個別に変換されます。また、元のドキュメントとの対応を記録するインデックスドキュメントが作成されます。章はドキュメントの抽出部分から変換され、ページマーカーには抽出部分の番号ではなく、ドキュメント本来のページ番号が記録されます。フロントマターには first_page と last_page が記載されるため、該当箇所を元のページに照らして引用できます。インデックスドキュメントには type: document_index が記載され、独自のテキストは含まれません。テキストは章に含まれます。--sample-pages を指定すると、ドキュメント全体ではなく、間隔を空けてサンプルページを変換します。フロントマターには抽出したページとドキュメントの総ページ数の両方が記録されるため、サンプルが短いドキュメントだと誤認されることはありません。
レイアウト解析では処理が完了しないドキュメントがあるため、--timeout が用意されています。上限を設定しないと、そのようなドキュメントが実行終了までワーカーを占有します。ドキュメントの処理を中止しても、開始されたスレッドは停止しないため、--recycle-after はワーカーを定期的に置き換えます。
数式
PDF のテキストレイヤーには、数式の構造は含まれていません。分数線は描画された線であり、上付き文字は別の行に置かれたテキストです。そのため、このレイヤーを忠実に抽出すると、分数は1行で表示されます。問題が起きたわけではありません。抽出するための構造が、もともと存在しなかったのです。
--formulas は数式を LaTeX に書き起こします。つまり、テキストではなく画像を読み取ります。これは抽出ではなく認識であり、文書についての異なる種類の主張です。抽出では起こりえない誤りが生じる可能性があるため、既定では実行されず、明示的に指定する必要があります。構造化エンジンが必要です。--no-docling を指定すると無効になり、実行時にその旨が表示されます。また、docling-tools models download-hf-repo docling-project/CodeFormulaV2 で一度だけ取得するモデルも必要です。モデルが見つからない場合、数式を含む最初のページで失敗するのではなく、実行開始時に処理が拒否されます。
処理にはコストがかかります。このマシンで20ページの論文1本を測定したところ、数式を含まない場合は17.7秒、数式56個を書き起こした場合は348.7秒でした。文章の処理には何のメリットもありません。
書き起こし結果はテキストを置き換えず、専用の見出しの下に追加されます。この論文では、構造化エンジンは数式を書き起こしましたが、単語の11%を失いました。一方、ネイティブ抽出はテキストを網羅しましたが、数式は含まれませんでした。つまり、どちらかを選ぶと、二つのうち一方を諦めることになります。記録には、書き起こした数が formulas として報告されます。
検証
各変換結果を、元の文書が提供するテキストと比較します。インデックスには、ファイルごとの達成率が記録されます。スキャンされた図面など、テキストを提供しない文書は、測定対象となるテキストの原本が存在しないため、検証不能として扱われます。
レイアウトエンジンや OCR エンジンとは独立したライブラリで参照テキストを読み取るため、それらの場合、比較対象は文書を異なる方法で読み取った二つの結果です。ネイティブ抽出の場合は異なります。参照テキストと抽出結果は同じ読み取りに基づいており、この比較で測定するのは、その後の構造化と圧縮でどの程度保持されたかです。参照テキストは一度だけ読み取り、両方に使用します。
パッケージ化
mdcx pack --output ./Documents_md --target corpus.mdcx --key "passphrase"
| オプション | 効果 |
|---|---|
--key, --key-file | パスフレーズを指定します。暗号化を参照してください |
--multilingual | 意味もインデックス化します |
--focus QUESTION | パッケージが答えるための質問を指定します。繰り返し指定できます |
--dates FILE | 公開日を指定します。日付を参照してください |
--date-from-mtime | ファイルの更新日時を代わりに使用します |
--shapes | 文字起こし復元用インデックスを構築します |
--expressions | 単語ルールで除外される表現をインデックス化します |
--quotes document|boundary | 分割箇所をまたぐ引用をどのように検出するかを指定します |
--reuse PACKAGE | 既存のパッケージからベクトルを再利用します |
--issuer, --signing-key | 署名を参照してください |
--fast | サイズより速度を優先して圧縮します |
--preset 0-9 | 圧縮レベルを指定し、--fastより優先します |
--compression lzma|zstd | 本文の圧縮方式を指定します |
--outputには、Markdownを含むフォルダー、単一ファイル、または1行につき1レコードの.jsonlファイルを指定できます。これにより、レコードのカタログをまずディスクに書き出すことなくパッケージ化できます。
--fastは低い圧縮プリセットを選択し、--presetはプリセットを直接指定します。封印には書き込みのたびに一定のコストがかかります。配布を目的とするパッケージは一度だけ圧縮して何度もダウンロードされますが、増大するたびに再構築するコーパスでは、バイト数ではなく処理時間がかかります。圧縮レベルは圧縮ストリーム内に記録されるため、どのレベルで書き込まれたパッケージも、どのリーダーでも開けます。それ以外のパッケージの内容は変わりません。
--compressionは本文の圧縮方式を選択します。ヘッダーには方式の名前が記録され、パッケージはヘッダーに記載された方式で読み込まれます。そのため、既存のパッケージには影響せず、必要なモジュールを備えたリーダーでどちらの方式のパッケージも開けます。LZMAはデフォルトで、追加の依存関係は不要です。zstdにはmdcx[zstd]が必要です。どちらが小さくなるかは素材によって異なります。あるコーパスではzstdのほうが小さくなり、別のコーパスでは大きくなりました。一方、zstdは読み戻しが一貫して速く、サーバーが回答を返す前にかかる時間を短縮できます。
packはseconds_indexとseconds_sealを返します。また、seconds_index_by_phaseでは、最初の値をフォルダーの読み込み、パッセージの分割、用語のカウント、形状インデックスの構築、エンコードに分けています。--multilingualを指定すると、エンコードが他の処理より2桁多く時間を要します。コストはパッセージの数ではなく長さによって決まります。モデルの料金はトークン単位です。
したがって、大規模なコーパスの計画に使うべき数値は、処理速度と、その測定に使われたパッセージの長さの両方です。6 GBのカード1枚で、各長さのパッセージを600件エンコードした場合:
| パッセージあたりの語数 | パッセージ数/秒 |
|---|---|
| 10 | 431 |
| 20 | 196 |
| 50 | 84 |
| 90 | 52 |
| 200 | 21 |
パッセージの長さが示されていない処理速度は参考になりません。また、測定用に生成したテキストと実際のテキストを比べても、作業ではなくマシンの性能を比較することになります。たとえば、利用者が、作られた130文字のテキストで毎秒440パッセージという結果と、自分の942文字のパッセージで毎秒25パッセージという結果を比較したとき、このライブラリの欠陥だと受け取ってしまうことがあります。特定のコーパスについて判断するには、フェーズごとの数値を確認してください。
クエリ
mdcx search corpus.mdcx "the question" --key "passphrase"
mdcx info corpus.mdcx
mdcx export corpus.mdcx --target ./restored --key "passphrase"
| オプション | 効果 |
|---|---|
--limit N | 返すパッセージ数 |
--only received|sent | 対応関係の片側に絞り込みます |
--mode auto|lexical|semantic | 検索エンジンを選択します |
--prefer recent | 順位が同等の回答を新しい順に並べます |
infoはパッケージのヘッダーを読み取り、鍵を必要としません。形式、発行者、作成日、ドキュメント数とパッセージ数、整合性、署名、言語、日付、キャリブレーションを確認できます。
--prefer recentはスコアに減衰を適用するのではなく、順位による融合における3つ目のランキングとして加わります。これは絞り込みではなく並べ替えを行うため、回答の質が高い古いドキュメントも引き続き返されます。両方のエンジンが最良のパッセージに同意する場合、日付によって順位は変わりません。意見が分かれた場合は、日付によって順位が決まります。この設定を適用できない場合、たとえば意味インデックスがない場合や、回答に日付付きのパッセージがない場合は、何も変えずに黙って返すのではなく、その旨を応答で伝えます。
検索モデル
単語マッチングと密検索は異なる問いに答えるため、並行して使います。語彙インデックスは、文書にある用語が含まれていることを把握し、密インデックスは、文書が類似した意味を持つことを把握します。単語が共通していないため、ある言語でのクエリから単語マッチングで別の言語の文書にたどり着くことはできません。その境界を越えるのは、意味を表現することです。
この2つはスコアではなく、相互順位によって統合されます。BM25 スコアには上限がなく、測定対象のコーパスに依存します。一方、コサイン類似度は0から1の範囲で、コーパスには依存しません。両者を加算することはできず、正規化すると、根拠のない重み付けが生じます。両方のエンジンが合意できるのは順位です。
文書は、孤立した文章ではなく文書単位で順位付けされます。そのため、複数箇所で質問に答える文書が、一度だけ用語に言及する文書より下位になることはありません。
キャリブレーション
コーパス内に質問に関係するものが何もない場合、返答では、最も近い文章を回答として提示するのではなく、そのことを明示します。この判断のしきい値はコーパスの特性であり、パッケージのビルド時に測定されます。
絶対的なしきい値は使えません。コーパスが回答対象の質問にどれだけ近いかは、そのコーパスに含まれる内容によって異なるため、あるコレクションで設定した値は別のコレクションでは何も示しません。そのため、各パッケージは独自の対応範囲を記録し、それに基づいて判断します。
pack --focus "<question>" は、文章から推定するのではなく、パッケージが回答するために作られた質問から対応範囲を取得します。カタログの抄録のように、文章が質問に似ていない場合に、より適した情報源となります。このようなカタログでは、すべてのレコードが同じ修辞的な形式を共有しています。このオプションを繰り返し指定すると、複数の質問を指定できます。info では、どちらの方法が使われたかを報告します。
mdcx calibrate corpus.mdcx --key "passphrase" --question "..." --question "..."
calibrate は、ソース素材がなくても、既存のパッケージを測定します。この測定には、パッケージ内にあるベクトルと、外部から提供される質問が必要です。測定値の変更に合わせてパッケージを書き換えますが、文書、文章、ベクトルはそのままです。測定時点がパッケージ作成時とは異なることを示すため、由来情報には focus ではなく focus-after が記録されます。署名済みのパッケージには署名キーが必要です。
この測定が導入される前にビルドされたパッケージには測定値がなく、以前のしきい値がそのまま適用されます。
語彙
archive.vocabulary(connection) # term frequencies, and the rule
archive.unknown_terms(connection, text) # terms this corpus has never seen
archive.unfamiliar(connection, text) # the same, with the share and a flag
インデックスには3文字以上の用語が記録され、単語を区切らない文字体系では1文字の用語も記録されます。そのため、表に含まれていない場合には2つの意味があります。コーパスにその用語が一度も出現しなかったか、インデックスがそもそも記録しない用語であるかです。用語の希少性に基づいて重み付けする呼び出し元は、これらを区別する必要があります。存在しない用語には最大の重みが付くためです。unknown_terms はこのルールを適用します。
そのうえで、機能語を除去することはありません。情報を持たない単語は質問によって異なります。また、die casting の die や Les Misérables の les は、検索対象の言語では意味を持ちます。mdcx が明示できるのは、記録しなかった用語です。
キーは、大文字と小文字およびアクセントを正規化しています。そのため、search.tokenize_text でトークン化する呼び出し元は、用語を検索する前に正規化する必要があります。unknown_terms はこれを行います。
単語インデックスは言語を越えませんが、意味インデックスは言語を越えます。言語を越えた質問に対して、unknown_terms はテキストのすべての用語を返します。これはコーパスの内容ではなく、言語を示すものです。unfamiliar はその割合と cross_language フラグを報告します。質問の言語がコーパスの言語と一致するかどうかわからない場合は、こちらを優先してください。
2つのシグナルとその食い違い
archive.assess(connection, text) は両方を返します。
コサイン類似度では同音異義語の語義を区別できません。多言語埋め込みではそれらが同じ場所に配置されるためです。代数のコーパスがグラフ彩色に関する質問と近くなり、関数のグラフに関する文章が返されることがあります。一方、文字どおりの語彙では区別できます。質問の要となる単語が語彙に含まれていないためです。
assess は両方のシグナルを報告し、どちらも優先しません。見慣れない単語を理由にクエリを拒否すべきかどうかは、クエリの目的によります。また、その単語が目的と無関係な場合もあります。
文字認識の誤りの修復
光学文字認識で誤って読み取られた単語は、インデックスに含まれない単語であり、文字どおりの照合では返せません。文章はコーパスに残っていますが、質問からたどり着けず、理由を示すエラーもないまま、空の応答が返ります。
光学文字認識では、見た目が似た文字を取り違えます。そのため、文字を形状ごとにグループ化すると、誤読された単語と元の単語が同じバケットに入ります。バケットは回答に使うには粗すぎるため、フィルターとして使います。候補を提示し、その中から編集距離で判定します。
mdcx pack --output docs --target corpus.mdcx --key "..." --shapes
mdcx shapes corpus.mdcx --key "..." # for a package written without it
インデックスはパッケージのサイズを大きくし、光学文字認識を経たコーパスにのみ役立つため、任意です。
3つの制限があります。このフィルターが対処するのは入力ミスではなく文字認識の誤りです。その利点は文字の形状によるものであり、適用範囲も同様です。ラテン文字では形状の分類が粗いため、提示される候補のかなりの割合は文字認識の誤りではありません。また、クエリの語が1つでも一致すれば文章が返されるため、クエリのどの語も一致しなかった場合にのみ動作します。
別の綴りで文章が見つかった場合、応答ではその綴りを read_as に記録します。そのような文章を文字どおり一致したものとして提示すると、文書に実際には含まれていない単語が含まれているとみなされてしまいます。
この手法は、Xerox の Spitz が1990年代に説明した文字形状コードを用いるものです。当時は完全な文字認識処理を避けるため、文書画像に適用されました。ここでは、変換後のテキストに残った誤りを対象に、すでに変換済みのテキストに適用します。
添付ファイル
フロントマターで indexed: false と指定された文書は、パッケージ内に保持されます。署名および暗号化されたまま、同じファイルに収められ、インデックス、ベクトル、文章数からは除外されます。証明書や座標表など、コーパスに付属するものの、検索対象のテキストではない資料を想定しています。
export はこれをそのまま復元し、MCP の document ツールは名前で返します。検索では見つけられないため、これが取得する唯一の方法です。
pack は、最も多くの文章を提供した文書を報告します。また、1つの文書が全体の3分の1以上を占める場合は、その旨を伝えます。
日付
パッケージには、各著作の年代と、その判断根拠が記録されます。
| 出所 | 意味 |
|---|---|
source | 出版社または目録から提供 |
sidecar | 呼び出し元から提供 |
front-matter | 文書に記載 |
mtime | 著作の年代ではなく、ファイルの更新日時 |
mtime は、--date-from-mtime が指定されない限り使用されません。すべてのファイルに存在し、著作ではなくファイルについての情報だからです。信頼できる情報が見つからない場合、日付は記録されません。
文書に印刷された著作権の年は意図的に使用しません。再版には版の年ではなく、印刷した年が記載されているためです。
mdcx pack --output docs --target corpus.mdcx --key "..." --dates dates.csv
サイドカーの各行は path,date[,provenance] です。info は日付が記録された文書の数と、その日付がカバーする期間を報告します。応答内の各文章には dated と dated_from が含まれます。
書簡
書簡のコレクションでは、pack が各文書の差出・受取の区別を記録し、search --only received|sent でクエリをどちらか一方に絞り込めます。分類には入力のディレクトリ構造が使われます。
増分パッケージング
mdcx pack --output ./docs --target corpus-2.mdcx --key "..." \
--multilingual --reuse corpus-1.mdcx
テキストが変更されていない文章は、同じベクトルになります。--reuse は既存のパッケージからベクトルを読み取り、新しい部分だけをエンコードします。ベクトルは同じ鍵で暗号化済みのパッケージから読み取られます。ベクトルからは、それが表すテキストを近似できるため、中間ストアは作成されません。
再利用時にはキャリブレーションも引き継がれます。--focus で質問を指定したパッケージは、そのパッケージから作成されるパッケージに質問を渡すため、コーパスが拡大しても、キャリブレーションに使ったしきい値が維持されます。--focus を再度指定すると、継承した設定が上書きされ、サマリーにはキャリブレーションが継承されたことが記録されます。
圧縮と暗号化はファイル全体に適用されるため、1つのドキュメントを追加する場合も、コーパスを再構築する場合も、コストは同じです。頻繁に書き込む呼び出し側が書き込み頻度を判断できるよう、pack は seconds_seal を報告します。
データベースは書き込み先のファイル内に構築され、ドキュメントは1つずつ読み込まれます。その後、ブロック単位で1回の処理により圧縮、暗号化、ハッシュ化されます。そのため、パッケージ書き込み時のピークメモリはコーパスのサイズではなくブロックによって決まります。コーパスと、その4倍の大きさのコーパスで測定したところ、ピークメモリの増加は2%でした。
常駐変換
変換エンジンはプロセスごとに1回だけ読み込まれます。ドキュメントごとに1つのプロセスを実行する呼び出し側は、これは作業の分散と障害の分離に一般的な方法ですが、ドキュメントごとにその読み込みコストを負担します。
from mdcx.convert import resident
with resident.warm(report=print) as convert:
for path in queue:
record = convert(path, output_root)
書き込まれるファイルは、mdcx-convert がそのドキュメント用に書き込むものと同じで、チャプターも含まれます。長い PDF は、ドキュメント名のフォルダー内にチャプターごとの Markdown ファイルとして出力され、それらにリンクするインデックス Markdown も同じ場所に作成されます。split=False を指定すると、ドキュメント全体が1つのファイルになります。
record は mdcx-convert が書き込むレコードです。ドキュメントが分割された場合はインデックスレコードとなり、チャプターレコードは record["chapter_records"] の下に格納されます。resident.convert_documents(paths, output_root) はジェネレーターと同じように動作し、ドキュメントごとに1つのレコードを返します。また、resident.cost_of_starting() は起動にかかった秒数を返すため、ログに記録できます。このようなプロセスを複数実行すると、それぞれの起動コストが分散されます。
フォルダー全体を mdcx-convert に渡しても読み込みコストは分散できますが、キューが提供する順序制御、ドキュメントごとの処理、障害の分離は得られません。
数式表現
語彙インデックスは単語から構築され、単語を判定するルールでは記号が除外されます。散文ではこれが適切です。しかし、数式で検索するコーパスでは内容が失われます。pq | b(b+p+q) と pq | b(b-p-q) は符号が1つ異なるだけなのに同じ項に還元され、それぞれ単独で検索すると項がなくなり、何も取得されません。
--expressions は、数式表現を単語とは別の独立したインデックスとして保持します。英数字に記号が混在するトークンは数式表現とみなされ、散文からは何も抽出されません。コーパスに記載された数式表現を含む質問には、その内容で回答できます。コーパスに記載されていない数式表現を含む質問には、単に同じテーマについて論じている文章ではなく、何も返しません。緩やかな検索ではインデックスが解消しようとしている混同が生じるため、照合は完全一致です。
空白を含む数式表現は抽出されません。文中で数式がどこで終わるかを判定するのは別の問題であり、推測すると、散文の断片でインデックスが埋まってしまうためです。
分割箇所をまたぐ引用
引用句は、ある文章で始まり、次の文章で終わることがよくあります。デフォルトの --quotes document は、すべてのドキュメントの正規化済みテキストを保持して、その中を検索します。引用句の長さに制限はありませんが、コーパスのコピーがもう1つ必要になります。1つのパッケージで測定したところ、ファイルサイズの3分の1に相当しました。
--quotes boundary は代わりに、連続する文章の接合部分を、テキストではなくインデックスを保持するテーブルに登録します。1つの分割箇所をまたぐ引用句はフレーズ照合で見つかりますが、接合部分より長い引用句は見つかりません。全体のコピーを使えば、そのような引用句も引き続き見つけられます。ヘッダーには、パッケージがどちらの形式であるかが記録されます。
MCP サーバー
MDCX_FILE=/path/to/corpus.mdcx
MDCX_KEY=passphrase
python -m mdcx.mcp_server
キーはコマンドラインではなく環境変数から渡します。コマンドラインに指定すると、プロセステーブルから見えてしまうためです。
MDCX_FILE と MDCX_KEY には、プラットフォームのパス区切り文字またはコンマで区切って複数のパッケージを指定でき、ひとつのコーパスとして検索されます。すべてのパッケージでひとつのキーを使うことも、同じ順序でパッケージごとにキーを指定することもできます。
以下の3つのツールが公開されます。
| ツール | 戻り値 |
|---|---|
search | 質問に回答する出典情報付きの文章 |
info | 検索を実行せずにコーパスの記録 |
document | 名前またはポータブルパスで指定した文書の全文 |
search の応答には、出典と順位を伴う文章が含まれ、実行できなかった内容も報告されます。コーパス内に質問に関連するものがない場合は warning、別の表記で文章が見つかった場合は read_as、要求された優先設定に対応できなかった場合は prefer_applied、そのパッケージでこれまでに見たことのない質問内の語句をパッケージごとに示す場合は unknown_terms が返されます。
複数のパッケージを提供している場合、similarity はすべてのパッケージをまとめて計算されます。一方、answerable_at は各パッケージで調整された到達値の最小値となるため、適用されるしきい値は、文章の出典となったパッケージではなく、到達範囲が最も狭いパッケージに基づきます。answerable_at_by_package には各パッケージの値が報告されます。
クライアントが切断すると、サーバーは自身のプロセスを終了します。また、MDCX_IDLE_UNLOAD_MINUTES で指定した時間(デフォルトでは30分、無効にするには 0)操作がないと、埋め込みモデルを解放します。次の質問が届くとモデルを再読み込みします。
ソース
mdcx は変換、パッケージ化、回答を行います。データの取得は行わず、独自のネットワークにも依存しません。カタログはプラグインで、mdcx.sources エントリーポイントグループを通じて宣言され、mdcx.sources の契約を満たします。
[project.entry-points."mdcx.sources"]
oapen = "my_package.oapen:Catalogue"
アダプターを含めないのは意図的です。一見シンプルな HTTP クライアントも、実際はそうではありません。ダウンロードの取得方法、ページ分割の表現方法、識別子の形式はカタログごとに異なり、そうした知識はそれを持つ側にあるべきです。既存のパッケージに対する質問への回答には、こうした仕組みは必要ありません。
特定のカタログに依存しない機能は、次のとおりです。
python -m mdcx.sources --check <name>
sources.looks_like(data, "pdf") # by signature, not by declared type
sources.identify(data) # what the bytes are instead
sources.patiently(call) # retries RateLimited, honours Retry-After
sources.conforms(source) # the conformance check, as a function
Candidate.download には、カタログが追加のリクエストなしで把握できる場合、ファイルの取得先となるサーバーが記録されます。Candidate.host からその値を読み取れます。適合性チェックでは、候補の半数を超えるものがひとつのホストに由来する場合に報告されます。
tests/test_sources_kit.py には、ネットワークを一切使わずに実装した、約20行のリファレンスソースが含まれています。
ポータブルパス
すべての文書には、コレクションのルートからの相対パスとして、@/folder/document.md 形式のポータブルパスが付与されます。絶対パスは保存されないため、ディレクトリ構成が異なるマシンでもパッケージを読み取れます。export は、これらのパスからディレクトリ構成を再構築します。
署名
mdcx keygen
mdcx pack --output ./docs --target corpus.mdcx --key "..." \
--issuer "Organisation" --signing-key <hex private key>
mdcx verify corpus.mdcx --public-key <hex public key>
署名には、暗号化された本文のダイジェストとヘッダーを対象とする Ed25519 を使用するため、いずれかを変更すると署名は無効になります。検証に必要なのは公開鍵だけで、パスフレーズは不要です。
暗号化
パッケージの本文は AES-256-GCM で暗号化されます。キーは、パッケージごとのソルトを使い、パスフレーズから scrypt で導出されます。形式、発行者、件数、整合性ダイジェスト、署名、言語、日付、調整情報を含むヘッダーは暗号化された本文の外にあり、キーがなくても読み取れます。そのため、受信者はパッケージを開く価値があるかどうかを判断できます。
パッケージ化の際に空のパスフレーズを指定すると拒否されます。読み込み時は、MDCX_KEY、--key-file、または標準入力から読み取る --key - でパスフレーズを渡してください。--key を使うと、コマンドの実行中、パスフレーズがプロセステーブルに表示されます。
パッケージ形式
.mdcx ファイルは、マジックナンバー、JSON ヘッダー、暗号化された本文で構成されます。本文は圧縮された SQLite データベースで、次の内容が格納されています。
| テーブル | 内容 |
|---|---|
document | ドキュメントごとに 1 行:名前、ポータブルパス、出典、日付、正規化されたテキスト |
passage | パッセージごとに 1 行。ドキュメント内の位置も含みます |
passage_fts | passage に対して宣言された全文検索インデックス |
passage_vector | 意味がインデックス化された場合の、各パッセージの埋め込み |
term_shape | 構築されている場合の、文字起こし復元インデックス |
attachment | コーパスに含めず、コーパスとともに保持するドキュメント |
df | 用語ごとの文書頻度 |
meta | コーパスの記録:言語、キャリブレーション、モデル、件数 |
埋め込みは半精度で保存され、読み込み時に再正規化されます。以前のバージョンで書き込まれたパッケージには、後から追加されたテーブルがありません。パッケージを読み込むすべてのエントリーポイントは、まずその点を確認するため、古いパッケージでも引き続き応答できます。
制限事項
- 変換の忠実度は、ドキュメントから取得できるテキストを基準に測定します。テキストをまったく取得できないドキュメントは検証できず、その旨が記録されます。
- 言語をまたいだ検索には、
multilingualエクストラとそのモデルが必要です。 - 1.27.0 より前に書き込まれたパッケージには、構築時に使用したトークナイザーが含まれています。このトークナイザーは、母音を結合文字として表記する文字体系を、すべての結合文字の位置で分割していました。このようなパッケージはそのまま開いて応答でき、再構築するとそれらの単語全体がインデックス化されます。
- 文字起こし復元フィルターが対処するのは文字起こしの誤りであり、入力の誤りではありません。また、フィルターが提案するもののかなりの割合は誤りではありません。
- パッケージは全体がメモリー上に復号されます。コーパスが利用可能なメモリーを超える場合は、複数のパッケージに分けて保持し、1 つのコーパスとして検索します。サーバーはクエリが届いたときに各パッケージを開きます。
infoは、どのパッケージが開いているか、また何が格納されているかを報告します。パッケージの書き込みにはこの制限はありません。ターゲットファイル内に構築し、ブロック単位で封印します。 - 複数のパッケージを提供する場合、語彙スコアはパッケージ全体を対象に計算されるため、相互に比較できます。複数のパッケージに対して
archive.queryを直接使用する呼び出し元は、すべてに同じcorpusを渡してください。corpus_statistics_overがそれを収集します。そうしないと、パッケージごとに異なるスケールでスコアが算出されます。 - キャリブレーションのしきい値は、コーパスごとに測定されます。この測定が導入される前に構築されたパッケージは、固定しきい値で判定されます。
テスト
pytest
| ファイル | 対象 |
|---|---|
test_formats.py | 形式ごとのエンジン選択とエスカレーション |
test_package_identity.py | コンテナー:スキーマ、暗号化、署名、整合性 |
test_relevance.py | ランキングと、回答の根拠となるパッセージ |
test_multilingual.py, test_encoder.py | エンコード、融合、言語をまたいだ検索 |
test_answer_quality.py | キャリブレーションのしきい値と警告 |
test_dates.py | 日付、その出典、最新性の優先 |
test_shapekey.py | 文字起こしの復元とその限界 |
test_expressions.py | 単語のルールでは区別できない文 |
test_quotations.py | パッセージ間の区切りをまたぐ引用 |
test_sources_kit.py | ソースの契約とその適合性チェック |
test_card_sizing.py | デバイス検出、レーンのサイズ設定、カード上のターン |
test_resident_output.py | チャプターを含む、常駐コンバーターの出力 |
test_packing_cost.py | パッキング時のメモリー使用量と、オリジナルの読み込みコスト |
test_deep_documents.py | インタープリターの再帰制限を超える深さのドキュメント |
test_closed_package.py | 既存パッケージのキャリブレーションと拡張 |
test_incremental.py | パッケージ間でのベクトルの再利用 |
test_server_leaves.py | サーバーのライフタイムとアイドル時の解放 |
test_describes_itself.py | 公開されるすべてのフィールドと引数が文書化されていること |
コントリビューション
CONTRIBUTING.md を参照してください。バグと測定に関する Issue テンプレートは .github/ISSUE_TEMPLATE にあります。
セキュリティ
サポート対象バージョンと脆弱性の報告方法については、SECURITY.md をご覧ください。
リリース
PyPI および MCP レジストリ(io.github.jorgell23-sys/markdown-document-search)に公開されており、Zenodo にアーカイブされています。
CHANGELOG.md に各リリースの変更内容を記載しています。
著作者
Jorge Ellena G. サードパーティ製コンポーネントとそのライセンスについては、NOTICE をご覧ください。
引用
学術研究で mdcx を引用する場合は、CITATION.cff または DOI を使用してください。
https://doi.org/10.5281/zenodo.22015991
ライセンス
Apache License 2.0。LICENSE をご覧ください。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。