Box Platformでのコンテンツの検索

Yuko Taniguchi
Box Developer Japan Blog
18 min readNov 22, 2023
Image by pikisuperstar on Freepik

どのようなコンテンツ管理システムでも、特にコンテンツが構造化されていない場合、ユーザーが探しているものを見つけられるようにするために検索は重要です。

ところが、Boxを使用している多くの開発者は、検索を使用したときに予期しない結果に遭遇します。この記事では、開発者の視点から検索のさまざまな側面を紹介します。

概念

Box APIは、ファイルコンテンツ検索クエリを使用してBox内のコンテンツを検索する方法を提供しています。Boxの検索APIは、サポートされているすべてのSDKとCLIで利用できます。

Boxはファイルシステムではありません。多くの場合、開発者は、パス、ワイルドカード、ファイル名、フォルダ名を使用した場合、一般的なファイルシステム検索と同様にBoxの検索が動作することを期待しています。

Box内の検索は、インデックス付きのデータベース検索となります。名前、説明、タグ、コメントに加え、最初の10 KBまでのコンテンツにインデックスが作成されます。ファイルまたはフォルダが作成、更新、削除されるたびに、インデックスは非同期的に更新されます。

つまり、検索インデックスは必ずしも最新の状態とは限らず、更新には数分かかる場合があります。

始めましょう

Boxアプリの次のツリー構造について考えます。

- workshops
- search
- apple
- apple1.txt
- apple2.txt
- apple3.txt
- apple banana
- apple.txt
- banana.txt
- apple pineapple banana
- apple.txt
- banana.txt
- pineapple.txt
- banana
- banana.txt
- banana apple
- apple.txt
- banana.txt
- pineapple
- pineapple.txt

また、次のPythonスニペットでは、いくつかの簡単な出力メソッドとBoxクライアントを使用して処理を開始します。

""" Searching Box exercises"""
import logging
from typing import Iterable

from boxsdk.object.item import Item

from utils.config import AppConfig
from utils.box_client import get_client

logging.basicConfig(level=logging.INFO)
logging.getLogger("boxsdk").setLevel(logging.CRITICAL)

conf = AppConfig()

def print_box_item(box_item: Item):
"""Basic print of a Box Item attributes"""
print(f"Type: {box_item.type} ID: {box_item.id} Name: {box_item.name}, ")

def print_search_results(items: Iterable["Item"]):
"""Print search results"""
print("--- Search Results ---")
for item in items:
print_box_item(item)
print("--- End Search Results ---")

if __name__ == "__main__":
client = get_client(conf)

簡易検索

検索方法の例を次に示します。appleを検索するとどうなるでしょうか。

def simple_search(query: str) -> Iterable["Item"]:
"""Search by query in any Box content"""

return client.search().query(query=query)

if __name__ == "__main__":
client = get_client(conf)

# Simple Search
search_results = simple_search("apple")
print_search_results(search_results)

結果は次のとおりです。

--- Search Results ---
--- End Search Results ---

多くの場合、この演習に対する開発者の最初の反応は、結果が返されないということは検索が機能していないことを意味するというものです。

しかし、インデックス作成は非同期処理であり、更新には数分かかる場合があることを忘れないでください。サンプルファイルをアップロードしたばかりであれば、これは想定内です。

数分後には、次のようになります。

--- Search Results ---
Type: folder ID: 208850093677 Name: apple banana,
Type: folder ID: 208858841669 Name: apple,
Type: folder ID: 208856751058 Name: apple pineapple banana,
Type: folder ID: 208848037313 Name: banana apple,
Type: file ID: 1220477661707 Name: apple.txt,
Type: file ID: 1220476606374 Name: apple.txt,
Type: file ID: 1220478477851 Name: apple.txt,
Type: file ID: 1220478610566 Name: apple1.txt,
Type: file ID: 1220479548719 Name: apple2.txt,
Type: file ID: 1220481540143 Name: apple3.txt,
--- End Search Results ---

以下が見つかりました。

  • ファイルとフォルダの両方
  • 名前にappleが含まれる項目
  • apple1apple2apple3は含まれる
  • pineappleは含まれない

次はapple bananaを使用して、同様の簡易検索を試してみましょう。

# Expanded Search
search_results = simple_search("apple banana")
print_search_results(search_results)

結果は次のとおりです。

--- Search Results ---
Type: folder ID: 231320711952 Name: apple banana
Type: folder ID: 231318527838 Name: apple pineapple banana
Type: folder ID: 231320108594 Name: banana apple
Type: folder ID: 231319410565 Name: banana
Type: folder ID: 231318889313 Name: apple
Type: file ID: 1337960845864 Name: banana.txt
Type: file ID: 1337971324252 Name: banana.txt
Type: file ID: 1337959496665 Name: banana.txt
Type: file ID: 1337968972110 Name: apple.txt
Type: file ID: 1337956847194 Name: banana.txt
Type: file ID: 1337966423041 Name: apple.txt
Type: file ID: 1337967294253 Name: apple.txt
Type: file ID: 1337963451641 Name: apple1.txt
Type: file ID: 1337967213245 Name: apple2.txt
Type: file ID: 1337962062207 Name: apple3.txt
--- End Search Results ---

検索範囲が広がったことに注目してください。今回は、applebanana、またはその両方を含む項目が返されています。

完全一致検索

引用符を使用すると、この任意の一致パターンを除外し、より正確な文字列検索を行うことができます。

# "Exact" Search
search_results = simple_search('"apple banana"')
print_search_results(search_results)

結果は次のとおりです。

--- Search Results ---
Type: folder ID: 231320711952 Name: apple banana
--- End Search Results ---

検索演算子の使用

検索演算子であるANDORNOTを使用すると、検索対象を絞り込むことができます。以下に例を示します。

  • apple NOT bananaの場合は、「apple」を含むが「banana」は含まない項目が返されます。
  • apple AND pineappleの場合は、「apple」と「pineapple」の両方を含む項目が返されます。
  • pineapple OR bananaの場合は、「pineapple」または「banana」を含む項目が返されます。

予期しない検索結果

bananaの複数形からbを除いた場合、実際には6つの言語でpineappleとなることをご存知でしょうか。

ananasを検索します。

# More Searches
search_results = simple_search('ananas')
print_search_results(search_results)

結果は次のとおりです。

--- Search Results ---
Type: file ID: 1337971411200 Name: pineapple.txt
Type: file ID: 1337965525302 Name: pineapple.txt
--- End Search Results ---

ananasはどこから来たのでしょうか。

検索では、名前だけでなく、説明、タグ、コメント、コンテンツも調べることを思い出してください。

pineapple.txtでは、説明とコンテンツにananasという単語が含まれています。

コンテンツとファイルの説明の両方にananasが存在する

検索する属性の指定

検索メソッドを変更して、開発者がどの属性で検索を実行するかを指定するためのパラメータを受け入れるようにします。

def simple_search(query: str, content_types: Iterable[str] = None) -> Iterable["Item"]:
"""Search by query in any Box content"""

return client.search().query(query=query, content_types=content_types)

次にもう一度、名前でのみananasを検索してみます。

# Search only in name
search_results = simple_search(
"ananas",
content_types=[
"name",
],
)
print_search_results(search_results)

注: Pythonでは、文字列は文字の反復です。必ずcontent_typesをリストとして渡してください。

どのファイルの名前にもananasが存在しないため、空の結果が返されます。

--- Search Results ---
--- End Search Results ---

検索対象に説明を含めて、再度ananasを取得します。

# Search in name and description
search_results = simple_search(
"ananas",
content_types=[
"name",
"description",
],
)
print_search_results(search_results)

結果は次のとおりです。

--- Search Results ---
Type: file ID: 1337965525302 Name: pineapple.txt
Type: file ID: 1337971411200 Name: pineapple.txt
--- End Search Results ---

返される内容の指定

ここまで、検索で返されるコンテンツの種類は指定していませんでした。内容に応じてファイルやフォルダを取得する場合があります。しかし、結果はファイルまたはフォルダのみできます。

result_typeパラメータを受け入れるように検索メソッドを変更しましょう。

def simple_search(
query: str, content_types: Iterable[str] = None, result_type: str = None
) -> Iterable["Item"]:
"""Search by query in any Box content"""

return client.search().query(
query=query, content_types=content_types, result_type=result_type
)

appleを検索しますが、foldersのみが返されるようにします。

# Search for folders only
search_results = simple_search("apple", result_type="folder")
print_search_results(search_results)

結果は次のとおりです。

--- Search Results ---
Type: folder ID: 231320711952 Name: apple banana
Type: folder ID: 231318889313 Name: apple
Type: folder ID: 231320108594 Name: banana apple
Type: folder ID: 231318527838 Name: apple pineapple banana
--- End Search Results ---

検索場所の指定

検索場所を指定することもできます。これまでは、コンテンツ全体を検索してきました。次の新しいパラメータを受け入れるように検索メソッドを変更します。

def simple_search(
query: str,
content_types: Iterable[str] = None,
result_type: str = None,
ancestor_folders: Iterable["Folder"] = None,
) -> Iterable["Item"]:
"""Search by query in any Box content"""

return client.search().query(
query=query,
content_types=content_types,
result_type=result_type,
ancestor_folders=ancestor_folders,
)

サンプルコンテンツでは、名前にbananaが含まれるすべてのフォルダにbanana.txtファイルがあります。

bananaを検索して、親フォルダ名を出力します。

# Search banana
search_results = simple_search("banana")

print("--- Search Results ---")
for item in search_results:
print(
f"Type: {item.type} ID: {item.id} Name: {item.name} Folder: {item.parent.name}"
)
print("--- End Search Results ---")

結果は次のとおりです。

--- Search Results ---
Type: folder ID: 231319410565 Name: banana Folder: search
Type: folder ID: 231320711952 Name: apple banana Folder: search
Type: folder ID: 231320108594 Name: banana apple Folder: search
Type: folder ID: 231318527838 Name: apple pineapple banana Folder: search
Type: file ID: 1337959496665 Name: banana.txt Folder: apple pineapple banana
Type: file ID: 1337971324252 Name: banana.txt Folder: banana
Type: file ID: 1337956847194 Name: banana.txt Folder: banana apple
Type: file ID: 1337960845864 Name: banana.txt Folder: apple banana
--- End Search Results ---

banana appleフォルダとapple bananaフォルダのみでbananaを検索し、ファイルだけを返すように検索を変更します。

フォルダIDはBoxアカウント固有です。正しいIDを使用していることを確認してください。

# Ancestor Search
folder_apple_banana = client.folder("231320711952")
folder_banana_apple = client.folder("231320108594")
search_results = simple_search(
"banana",
ancestor_folders=[folder_apple_banana, folder_banana_apple],
result_type="file",
)

print("--- Search Results ---")
for item in search_results:
print(f"Type: {item.type} ID: {item.id} Name: {item.name} Folder: {item.parent.name}")
print("--- End Search Results ---")

結果として、指定したフォルダでのみファイルが見つかります。

--- Search Results ---
Type: file ID: 1337960845864 Name: banana.txt Folder: apple banana
Type: file ID: 1337956847194 Name: banana.txt Folder: banana apple
--- End Search Results ---

検索を絞り込むために使用できるパラメータは他にも多数あります。

以下のパラメータを試して、何が見つかるかを確認してみてください。

  • file_extensions
  • created_at_range
  • updated_at_range
  • size_range
  • trash_content
  • sort
  • direction

まとめ

検索APIは強力ですが、主にユーザーがBox内のコンテンツを見つけられるように設計されているため、すべてのユースケースに適しているとは限りません。

  • Boxはファイルシステムではないため、パスはありません。
  • これはインデックス検索のため、コンテンツのインデックスが作成されるまでに数分かかる場合があります。
  • 名前、説明、タグ、コメント、コンテンツにインデックスが作成されるため、開発者に対して予期しない結果が返されることがしばしばあります。

ドキュメントと参考情報

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

--

--