Box AIによるメタデータ抽出

Yuko Taniguchi
Box Developer Japan Blog
23 min readMar 6, 2024

--

Image by jemastock on Freepik

企業のドキュメントが進化し続ける状況において、メタデータは、非構造化コンテンツを整理し、その価値を見出して引き出すのに極めて重要な役割を果たします。

このワークショップでは、Box Platform APIを使用したメタデータテンプレートの作成と抽出について詳しく説明します。

注: Box AIによるメタデータ抽出APIはプライベートベータ版のみ利用可能であり、機能は変更される可能性があります。この記事で説明するその他のメタデータ機能はすべて、すでに利用可能です。

このワークショップは、こちらのGitHubリポジトリにあるコードサンプル一式を使用して進めることができます。ここでは、Box Platformの次世代のPython SDKを使用します。

それでは、始めましょう。

概念

Boxメタデータを使用する際には、留意すべき概念が多数あります。

  • メタデータテンプレートは、非構造化ドキュメントに関連付けるデータの構造を表します。テンプレートは企業レベルで作成されます。
  • メタデータインスタンスは、テンプレートとドキュメントまたはフォルダの関連付けを表します。複数のテンプレートのインスタンスを1つのドキュメントに関連付けることができます。
  • 管理者は、メタデータカスケードポリシーを作成して、メタデータインスタンスが自動的にフォルダのコンテンツに適用されるようにすることもできます。
    たとえば、ユーザーがプロジェクトフォルダに同じinvoiceDataメタデータテンプレートを割り当てると、そのプロジェクトフォルダ内のすべてのファイルとフォルダに自動的に適用することができます。

メタデータを使用してコンテンツに構造を指定するメリットは、プロセス、統合、ワークフローの作成が格段に簡単になることです。メタデータベースのクエリを使用した検索結果は、従来の検索クエリによって得られる検索結果と比較した場合、精度が高くなります。

ユースケース

メタデータの使用を図で示すために、企業のシンプルな調達プロセスを考えてみましょう。

  • 会社は商品やサービスの発注書をベンダーに発行する
  • ベンダーは注文を納品し、会社に請求書を送付する
  • 会社は請求書を発注書と照合する
  • 会社は、請求書、発注書、受領した商品やサービスを確認し、(すべて確認できたら) ベンダーに支払いを行う

通常、ベンダーは請求書に発注書番号を記載するため、会社はより簡単にその2つを照合して、処理を完了し、ベンダーに支払うことができます。ここでは、BoxメタデータAPIを使用して、請求書と発注書からメタデータを抽出し、一致しないものを特定します。

以下の例では、5つの発注書と照合する5つの請求書を使用します。ただし、請求書A5555には発注書番号が記載されていません。

メタデータテンプレートを作成する

メタデータを操作するには、使用するメタデータフィールドを定義するためのメタデータテンプレートが必要です。

def create_invoice_po_template(
client: Client, template_key: str, display_name: str
) -> MetadataTemplate:
"""Create a metadata template"""

scope = "enterprise"
fields = []
# Document type
fields.append(
CreateMetadataTemplateFields(
type=CreateMetadataTemplateFieldsTypeField.ENUM,
key="documentType",
display_name="Document Type",
description="Identifies document as an invoice or purchase order",
options=[
CreateMetadataTemplateFieldsOptionsField(key="Invoice"),
CreateMetadataTemplateFieldsOptionsField(key="Purchase Order"),
],
)
)
# Date
fields.append(
CreateMetadataTemplateFields(
type=CreateMetadataTemplateFieldsTypeField.DATE,
key="documentDate",
display_name="Document Date",
)
)
# Document total
fields.append(
CreateMetadataTemplateFields(
type=CreateMetadataTemplateFieldsTypeField.FLOAT,
key="documentTotal",
display_name="Document Total",
description="Total USD value of document",
)
)
# Supplier
fields.append(
CreateMetadataTemplateFields(
type=CreateMetadataTemplateFieldsTypeField.STRING,
key="vendor",
display_name="Vendor",
description="Vendor name or designation",
)
)
# Invoice number
fields.append(
CreateMetadataTemplateFields(
type=CreateMetadataTemplateFieldsTypeField.STRING,
key="invoice",
display_name="Invoice #",
description="Document number or associated invoice",
)
)
# PO number
fields.append(
CreateMetadataTemplateFields(
type=CreateMetadataTemplateFieldsTypeField.STRING,
key="po",
display_name="PO #",
description="Document number or associated purchase order",
)
)
template = client.metadata_templates.create_metadata_template(
scope=scope,
template_key=template_key,
display_name=display_name,
fields=fields,
)
return template

main関数で、テンプレートがすでに存在するかどうかを確認し、存在しない場合は作成します。

def main():
...

# check if template exists
template_key = "rbInvoicePO"
template_display_name = "RB: Invoice & POs"
template = get_template_by_key(client, template_key)
if template:
print(
f"\nMetadata template exists: '{template.display_name}' ",
f"[{template.id}]",
)
else:
print("\nMetadata template does not exist, creating...")
# create a metadata template
template = create_invoice_po_template(
client, template_key, template_display_name
)
print(
f"\nMetadata template created: '{template.display_name}' ",
f"[{template.id}]",
)

この結果は次のとおりです。

Metadata template does not exist, creating...
Metadata template created: 'RB: Invoice & POs' [2257ed5b-c4c3-48b1-9881-875b5291ddfa]

Box AIによるメタデータ抽出を使用してコンテンツをスキャンする

コンテンツをスキャンしてメタデータの候補を取得するメソッドを作成します。

def get_metadata_suggestions_for_file(
client: Client, file_id: str, enterprise_scope: str, template_key: str
) -> IntelligenceMetadataSuggestions:
"""Get metadata suggestions for a file"""
return client.intelligence.intelligence_metadata_suggestion(
item=file_id,
scope=enterprise_scope,
template_key=template_key,
confidence="experimental",
)

次に、main関数で、ファイルを反復処理してコンテンツをスキャンし、メタデータの候補を取得します。

def main():
...

# Scan the purchase folder for metadata suggestions
folder_items = client.folders.get_folder_items(PO_FOLDER)
for item in folder_items.entries:
print(f"\nItem: {item.name} [{item.id}]")
suggestions = get_metadata_suggestions_for_file(
client, item.id, ENTERPRISE_SCOPE, template_key
)
print(f"Suggestions: {suggestions.suggestions}")

結果は異なる場合もありますが、この例の場合は次のようになります。

Item: PO-001.txt [1443731848797]
Suggestions: {'documentType': 'Purchase Order', 'documentDate': '2024–02–13T00:00:00.000Z', 'vendor': 'Galactic Gizmos Inc.', 'invoiceNumber': None, 'purchaseOrderNumber': '001', 'total': '$575'}
Item: PO-002.txt [1443739645222]
Suggestions: {'documentType': 'Purchase Order', 'documentDate': '2024–02–13T00:00:00.000Z', 'total': '$230', 'vendor': 'Cosmic Contraptions Ltd.', 'invoiceNumber': None, 'purchaseOrderNumber': '002'}
Item: PO-003.txt [1443724777261]
Suggestions: {'documentType': 'Purchase Order', 'documentDate': '2024–02–13T00:00:00.000Z', 'total': '1,050', 'vendor': 'Quasar Innovations'}
Item: PO-004.txt [1443739415948]
Suggestions: {'documentType': 'Purchase Order', 'documentDate': '2024–02–13T00:00:00.000Z', 'vendor': 'AstroTech Solutions', 'invoiceNumber': None, 'purchaseOrderNumber': '004', 'total': '920'}
Item: PO-005.txt [1443724550074]
Suggestions: {'documentType': 'Purchase Order', 'documentDate': '2024–02–13T00:00:00.000Z', 'vendor': 'Quantum Quirks Co.', 'invoiceNumber': None, 'purchaseOrderNumber': '005'}

コンテンツのメタデータを更新する

メタデータの候補を取得できたら、その候補を使用してコンテンツのメタデータを更新します。

ここで考慮すべき点は3つあります。

  • すべてのフィールドの候補を取得できない場合や、取得した値が「None」である場合があります。この場合は、最初にデフォルト値を設定してから、候補を結合します。
  • メタデータテンプレートがまだドキュメントに関連付けられていない可能性があるため、メタデータを更新しようとするとエラーが発生する場合があります。
  • メタデータの更新は、従来の更新とは大きく異なり、追加、置換、削除、テスト、移動、コピーなどの操作がサポートされます。

コンテンツのメタデータを更新するメソッドの例を次に示します。

def apply_template_to_file(
client: Client, file_id: str, template_key: str, data: Dict[str, str]
):
"""Apply a metadata template to a folder"""
default_data = {
"documentType": "Unknown",
"documentDate": "1900-01-01T00:00:00Z",
"total": "Unknown",
"vendor": "Unknown",
"invoiceNumber": "Unknown",
"purchaseOrderNumber": "Unknown",
}
# remove empty values
data = {k: v for k, v in data.items() if v}
# Merge the default data with the suggestions
data = {**default_data, **data}

try:
client.file_metadata.create_file_metadata_by_id(
file_id=file_id,
scope=CreateFileMetadataByIdScope.ENTERPRISE,
template_key=template_key,
request_body=data,
)
except APIException as error_a:
if error_a.status == 409:
# Update the metadata
update_data = []
for key, value in data.items():
update_item = UpdateFileMetadataByIdRequestBody(
op=UpdateFileMetadataByIdRequestBodyOpField.ADD,
path=f"/{key}",
value=value,
)
update_data.append(update_item)
try:
client.file_metadata.update_file_metadata_by_id(
file_id=file_id,
scope=UpdateFileMetadataByIdScope.ENTERPRISE,
template_key=template_key,
request_body=update_data,
)
except APIException as error_b:
logging.error(
f"Error updating metadata: {error_b.status}:{error_b.code}:{file_id}"
)
else:
raise error_a

次に、コンテンツのメタデータを格納するようにmain関数の次のコードを更新します。

def main():
...

# Scan the purchase folder for metadata suggestions
folder_items = client.folders.get_folder_items(PO_FOLDER)
for item in folder_items.entries:
print(f"\nItem: {item.name} [{item.id}]")
suggestions = get_metadata_suggestions_for_file(
client, item.id, ENTERPRISE_SCOPE, template_key
)
print(f"Suggestions: {suggestions.suggestions}")
metadata = suggestions.suggestions
apply_template_to_file(
client,
item.id,
template_key,
metadata,
)

発注書のメタデータを確認すると、メタデータが候補で更新されていることがわかります。

メタデータの取得と発注書への適用

請求書にメタデータを適用する

main関数に、メタデータをスキャンして請求書に適用する次のコードを追加します。

def main():
...

# Scan the invoice folder for metadata suggestions
folder_items = client.folders.get_folder_items(INVOICE_FOLDER)
for item in folder_items.entries:
print(f"\nItem: {item.name} [{item.id}]")
suggestions = get_metadata_suggestions_for_file(
client, item.id, ENTERPRISE_SCOPE, template_key
)
print(f"Suggestions: {suggestions.suggestions}")
metadata = suggestions.suggestions
apply_template_to_file(
client,
item.id,
template_key,
metadata,
)

結果は次のとおりです。

Item: Invoice-A5555.txt [1443738625223]
Suggestions: {'documentType': 'Invoice', 'invoiceNumber': 'A5555',
'total': '920'}

Item: Invoice-B1234.txt [1443724064462]
Suggestions: {'documentType': 'Invoice', 'documentDate': None,
'total': '575', 'vendor': 'Galactic Gizmos Inc.', 'invoiceNumber': 'B1234',
'purchaseOrderNumber': '001'}

Item: Invoice-C9876.txt [1443729681339]
Suggestions: {'documentType': 'Invoice', 'invoiceNumber': 'C9876',
'purchaseOrderNumber': '002', 'total': '$230',
'vendor': 'Cosmic Contraptions Ltd.'}

...

ファイルのメタデータを取得する

次のメソッドを使用して、ファイルのメタデータを直接取得できます。

def get_file_metadata(client: Client, file_id: str, template_key: str):
"""Get file metadata"""
metadata = client.file_metadata.get_file_metadata_by_id(
file_id=file_id,
scope=CreateFileMetadataByIdScope.ENTERPRISE,
template_key=template_key,
)
return metadata

先ほど更新したファイルのいずれかのIDでテストします。

def main():
...

# get metadata for a file
metadata = get_file_metadata(client, "1443738625223", template_key)
print(f"\nMetadata for file: {metadata.extra_data}")

結果は次のとおりです。

Metadata for file: {'invoiceNumber': 'A5555', 'vendor': 'Unknown', 
'documentType': 'Invoice', 'documentDate': '1900-01-01T00:00:00.000Z',
'purchaseOrderNumber': 'Unknown', 'total': '920'}

一致しない請求書を検索する

対応する発注書がない請求書がある場合があります。メタデータを照会するメソッドを作成しましょう。

def search_metadata(
client: Client,
template_key: str,
folder_id: str,
query: str,
query_params: Dict[str, str],
order_by: List[Dict[str, str]] = None,
):
"""Search for files with metadata"""

from_ = ENTERPRISE_SCOPE + "." + template_key
if order_by is None:
order_by = [
SearchByMetadataQueryOrderBy(
field_key="invoiceNumber",
direction=SearchByMetadataQueryOrderByDirectionField.ASC,
)
]
fields = [
"type",
"id",
"name",
"metadata." + from_ + ".invoiceNumber",
"metadata." + from_ + ".purchaseOrderNumber",
]
search_result = client.search.search_by_metadata_query(
from_=from_,
query=query,
query_params=query_params,
ancestor_folder_id=folder_id,
order_by=order_by,
fields=fields,
)
return search_result

そしてmain関数で、対応する発注書がない請求書を検索します。

def main():
...

# search for invoices without purchase orders
query = "documentType = :docType AND purchaseOrderNumber = :poNumber"
query_params = {"docType": "Invoice", "poNumber": "Unknown"}
search_result = search_metadata(
client, template_key, INVOICE_FOLDER, query, query_params
)
print(f"\nSearch results: {search_result.entries}")

結果は次のとおりです。

Search results: 
[{'metadata': {'enterprise_1133807781': {'rbInvoicePO':
{'$scope': 'enterprise_1133807781', '$template': 'rbInvoicePO',
'$parent': 'file_1443738625223', 'purchaseOrderNumber': 'Unknown',
'invoiceNumber': 'A5555', '$version': 11}}}, 'id': '1443738625223',
'type': 'file', 'etag': '3', 'name': 'Invoice-A5555.txt'}]

ここからは、興味深いことをいくつか実行できます。

  • ユーザーが、ある請求書番号から対応する発注書を参照する必要がある場合 (あるいは、その逆の場合) に備えて、すべての発注書メタデータを対応する各請求書番号で更新します。
  • 一致しない請求書について、まだ一致していない特定のベンダーの発注書を検索します。これにより、ユーザーは手動でのドキュメント照合が簡単になります。

まとめ

メタデータは単なる技術的な詳細ではなく、企業の業務の遂行方法を変革できる戦略的資産です。メタデータテンプレートは、チームが企業全体で一貫性を維持するのに役立ちます。発注書、契約書、クリエイティブな資産のいずれであっても、事前定義されたテンプレートによってプロセスが効率化され、エラーが最小限に抑えられます。

組織が進化すればするほど、メタデータの重要性は高まります。メタデータは、適応性、他のシステムとの統合、堅牢な情報アーキテクチャの維持をすべて可能にするだけでなく、チームメンバーがシームレスにコラボレーションするために必要なコンテキストを提供します。

ドキュメントと参考情報

APIリファレンス

アイデア、コメント、フィードバックがある場合は、コミュニティフォーラム (英語のみ) にコメントをお送りください。

--

--