コンテンツにスキップ
HaneDOCSEN

アプリを作る

文書を扱う

アプリから文書をアップロードし、全文検索で探し、ページの本文を読み、抜き出した値を確かめる方法です。

このページでは、アプリから文書サービスを使うコードを説明します。文書は原本とバージョンで保存され、マルウェアの検査と検索の索引づくりは非同期で進みます。考え方は 文書 にあります。

  • PDF と UTF-8 のテキスト。Office の書類(Word・Excel・PowerPoint)は原本のまま保管し、索引と抽出には PDF に変えて使います。
  • 1 ファイル 10 MiB まで。ZIP を送ると、中のファイルを 1 件ずつ文書にします。
  • 公開範囲は、既定では本人だけ(private)です。チームで使う文書は project にします。
  • 同じ公開範囲に同じ中身(SHA-256 が同じ)の文書があれば、新しい文書を作らずに受け取りの記録だけを足します。

アップロードは、準備・原本の送信・確定の 3 段です。BFF でブラウザから受けた本文の SHA-256 を計算してから送ります。

  1. 準備する

    const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", body));
    const sha256 = Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join("");
    const session = await unwrap(
    hane.POST("/v1/documents/upload-sessions", {
    body: { filename: "lease-C-1001.pdf", content_type: "application/pdf", size: body.byteLength, sha256, visibility: "project" },
    }),
    );

    既存の文書に新しいバージョンを足すときは、document_id も渡します。

  2. 原本を送る

    await unwrap(
    hane.PUT("/v1/documents/upload-sessions/{id}/content", {
    params: { path: { id: session.id } },
    body: "",
    bodySerializer: () => body, // バイト列をそのまま送る
    headers: { "content-type": "application/pdf" },
    }),
    );

    Content-Type は準備で指定した content_type と同じにします。

  3. 確定する

    const done = await unwrap(hane.POST("/v1/documents/upload-sessions/{id}/complete", { params: { path: { id: session.id } }, body: {} }));
    console.log(done.document.id, done.version.version, done.deduplicated);

    ZIP のときは complete の代わりに POST /v1/documents/upload-sessions/{id}/expand を呼び、ファイルごとの結果(受け付けた・まとめた・受け付けなかった)を受け取ります。

確定の時点で、検査は予約されただけです。バージョンの 2 つの状態を見て、画面の出し方を決めます。

状態 値 意味
inspection_status queued・scanning・clean・infected・failed マルウェアの検査。clean の原本だけをダウンロードできる
index_status queued・indexed・needs_ocr・needs_conversion・failed 検索の索引。indexed になったら探せる

UTF-8 のテキストは確定と同時に索引に入り、検査を待たずに探せます。文字を埋め込んだ PDF は、本文を取り出した後に探せます。スキャンした PDF は OCR を設定した環境で読み、設定がなければ needs_ocr のままです。

const v = await unwrap(hane.GET("/v1/documents/{id}/versions/{version}", { params: { path: { id, version } } }));
if (v.inspection_status === "clean") {
// GET /v1/documents/{id}/versions/{version}/content で原本を取れる(documents:download が要る)
}
const hits = await unwrap(hane.POST("/v1/documents/search", { body: { query: "C-1001 満了", limit: 20 } }));
for (const h of hits.items) console.log(h.title, h.page_start, h.snippet);
  • 空白で区切ったすべての語を含む箇所(ページの中の 800 文字のまとまり)を、一致の強い順に返します。語は 8 つまでです。
  • 読める文書のいまのバージョンだけを返し、感染を検出したバージョンは返しません。
  • 副作用がないので、Idempotency-Key は要りません。

mode で探し方を選べます。

mode 探し方 提供状況
keyword(既定) 全文検索 使える
summary ページごとの要約の索引。ページをまたいだ語も見つかる 使える
semantic・hybrid 意味検索と、全文検索との組み合わせ プレビュー 有効にしていない環境では CONFLICT

当たったページの本文は、原本を渡さずに読めます。

const pages = await unwrap(
hane.GET("/v1/documents/{id}/versions/{version}/pages", {
params: { path: { id: hit.document_id, version: hit.version }, query: { from: hit.page_start, to: hit.page_end } },
}),
);

抽出の定義(ExtractionSchema)を置くと、文書から型の項目の値を抜き出して、抽出の候補にします。候補は人が原本と並べて確かめると、台帳の値(確定)になります。

apiVersion: documents/v1alpha1
kind: ExtractionSchema
metadata:
name: lease-contract
spec:
target: LeaseContract
review:
assignment: fleet-desk
documents:
types: [lease-contract]
fields:
contract_no: { labels: [契約番号] }
start_date: { labels: [リース開始日, 開始日] }
term_months: { labels: [リース期間, 契約期間] }
// 要確認の候補
const facts = await unwrap(hane.GET("/v1/documents/facts", { params: { query: { status: "needs_review" } } }));
// 候補の値で確定する(value を渡すと直してから確定する)
await unwrap(
hane.POST("/v1/documents/facts/{id}/confirm", {
params: { path: { id: fact.id }, header: { "If-Match": ifMatch(fact.version) } },
body: {},
}),
);

退けるときは /reject、書類から取れず相手に問い合わせるときは /inquire を使います。どれも利用者のトークンで呼び、確かめた人として利用者が記録されます。