開発中のRAGシステムで、ある日突然「PDFアップロード時にエラーが発生する」という問い合わせが舞い込んできました。今回は、なぜこのエラーが発生したのか、どのように調査を進め、最終的にどのように解決したのか、そのプロセスを振り返ります。

システムの背景と発端

システム概要

  • 目的:アップロードされたファイル内容をもとに、ユーザの質問に回答するRAGシステム
  • 仕組み:ユーザがファイル(PDF、JSONなど)をアップロードし、その内容を保存し質問があれば適宜検索して回答を生成
  • インフラ:ファイルはAWS S3上に保存。バックエンドはpythonでlangchainなどの各種ライブラリを活用

問題発生の発端

ある日、「PDFファイルをアップロードするとエラーになって使えない」との報告を受けました。
JSONなど他のフォーマットでは正常に動作しているとのことで、PDFに特有の問題かもしれないと感じました。

ログと現象の解析

エラー内容の確認

アプリケーションログを確認したところ、以下のメッセージが目に飛び込んできました。

  • エラー内容langchain_community.document_loaders.s3_file.S3FileLoaderでファイルをloadする際に「HTTP Error 403: Forbidden」が発生。どういった理由から403になったかといった情報は得られず(実は後述するようになくはなかったのですが…)。

一見すると、S3へのアクセス権限の問題のように見えましたが、ログを丹念に追っていくと少し違う様相が…。

S3ログとの不整合

  • S3アクセスログ:PDFアップロード時のS3ログをチェックすると、実際のアクセス記録が残っていない
  • EC2からのアクセス:適当にEC2インスタンスを立ち上げて同じバケットに対してアクセスすると、正常にログが記録されるため、S3バケット自体に問題はなさそう

この時点で、エラーはS3のアクセス制御から発生しているのではなく、別の箇所で403エラーが発生している可能性に行き着きました。

調査の深堀と原因究明

スタックトレースの徹底解析

ログのスタックトレース(と対応するライブラリのソースコード)を改めて確認すると、langchainから呼ばれるunstructuredパッケージ内部で、NLTKを取得する処理中に403エラーが発生していることが判明しました。

スタックトレース部分の抜粋
  File "/usr/local/lib/python3.10/site-packages/langchain_core/document_loaders/base.py", line 30, in load
    return list(self.lazy_load())
  File "/usr/local/lib/python3.10/site-packages/langchain_community/document_loaders/unstructured.py", line 107, in lazy_load
    elements = self._get_elements()
  File "/usr/local/lib/python3.10/site-packages/langchain_community/document_loaders/s3_file.py", line 135, in _get_elements
    return partition(filename=file_path, **self.unstructured_kwargs)
  File "/usr/local/lib/python3.10/site-packages/unstructured/partition/auto.py", line 341, in partition
    elements = partition_pdf(
  File "/usr/local/lib/python3.10/site-packages/unstructured/documents/elements.py", line 605, in wrapper
    elements = func(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/unstructured/file_utils/filetype.py", line 706, in wrapper
    elements = func(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/unstructured/file_utils/filetype.py", line 662, in wrapper
    elements = func(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/unstructured/chunking/dispatch.py", line 74, in wrapper
    elements = func(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/unstructured/partition/pdf.py", line 210, in partition_pdf
    return partition_pdf_or_image(
  File "/usr/local/lib/python3.10/site-packages/unstructured/partition/pdf.py", line 357, in partition_pdf_or_image
    out_elements = _process_uncategorized_text_elements(elements)
  File "/usr/local/lib/python3.10/site-packages/unstructured/partition/pdf.py", line 966, in _process_uncategorized_text_elements
    new_el = element_from_text(cast(Text, el).text)
  File "/usr/local/lib/python3.10/site-packages/unstructured/partition/text.py", line 295, in element_from_text
    elif is_possible_narrative_text(text):
  File "/usr/local/lib/python3.10/site-packages/unstructured/partition/text_type.py", line 74, in is_possible_narrative_text
    if exceeds_cap_ratio(text, threshold=cap_threshold):
  File "/usr/local/lib/python3.10/site-packages/unstructured/partition/text_type.py", line 270, in exceeds_cap_ratio
    if sentence_count(text, 3) > 1:
  File "/usr/local/lib/python3.10/site-packages/unstructured/partition/text_type.py", line 219, in sentence_count
    sentences = sent_tokenize(text)
  File "/usr/local/lib/python3.10/site-packages/unstructured/nlp/tokenize.py", line 134, in sent_tokenize
    _download_nltk_packages_if_not_present()
  File "/usr/local/lib/python3.10/site-packages/unstructured/nlp/tokenize.py", line 128, in _download_nltk_packages_if_not_present
    download_nltk_packages()
  File "/usr/local/lib/python3.10/site-packages/unstructured/nlp/tokenize.py", line 86, in download_nltk_packages
    urllib.request.urlretrieve(NLTK_DATA_URL, tgz_file_path)
  File "/usr/local/lib/python3.10/urllib/request.py", line 241, in urlretrieve
    with contextlib.closing(urlopen(url, data)) as fp:
  File "/usr/local/lib/python3.10/urllib/request.py", line 216, in urlopen
    return opener.open(url, data, timeout)
  File "/usr/local/lib/python3.10/urllib/request.py", line 525, in open
    response = meth(req, response)
  File "/usr/local/lib/python3.10/urllib/request.py", line 634, in http_response
    response = self.parent.error(
  File "/usr/local/lib/python3.10/urllib/request.py", line 563, in error
    return self._call_chain(*args)
  File "/usr/local/lib/python3.10/urllib/request.py", line 496, in _call_chain
    result = func(*args)
  File "/usr/local/lib/python3.10/urllib/request.py", line 643, in http_error_default
    raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 403: Forbidden
  • 気づき:処理中にアクセスされるURLから、NLTKのリソースがS3上にホストされている可能性を感じる

外部情報との照合

「unstructured 403」などのキーワードで検索すると、GitHub上に同様の現象を報告するissue(Issue #3795)が見つかりました。

  • 確認ポイント:ある時期を境に、NLTK取得時の403エラーが多発するようになったとの報告
  • 原因:パッケージ側でのアクセス先変更や認証方法の見直しが行われた可能性

解決策とその適用

対応策の決定

調査の結果、今回利用していたunstructuredのバージョンは0.15.7でしたが、issueでは0.16.11でこの問題が解消されていると記述されていました。

  • 対応unstructuredのバージョンを0.16.11へアップデート

結果の検証

アップデート後、PDFアップロードテストを実施。

  • 検証結果:アップデート後は、以前発生していた403エラーが解消され、PDFファイルも正常に処理されるようになった

結論

今回のPDFアップロードエラーは、一見S3のアクセス権限に起因しているように見えたものの、実際はunstructuredパッケージ内部のリソース取得処理に起因していました。問題発生時は原因が複数のコンポーネント間で分散していることが多く、その背景や相互作用を正しく把握することが、問題解決への近道となります。
今回の経験を通じて、外部依存パッケージの管理やログの徹底分析の重要性を改めて実感しました。