以前のバンドル サービス用の Search API

Search API は、構造化データを含むドキュメントのインデックス作成モデルを提供します。インデックスを検索し、検索結果を整理して表示できます。API では文字列フィールドの全文検索がサポートされています。ドキュメントとインデックスは、検索処理のために最適化された専用の永続ストアに保存されます。Search API は、任意の数のドキュメントのインデックスを作成できます。非常に大きな結果セットを取得する必要があるアプリケーションには、App Engine Datastore のほうが適しています。search パッケージの内容については、search パッケージ リファレンスをご覧ください。

概要

Search API は、ドキュメント、インデックス、クエリ、結果という 4 つの主要なコンセプトに基づいています。

ドキュメント

ドキュメントとは、一意の ID を持つオブジェクトであり、ユーザーデータを含むフィールドのリストが含まれています。各フィールドには名前と型があります。フィールドには複数の型があり、フィールドに含まれている値の種類によって指定されます。

  • Atom フィールド - 非表示の文字列。
  • テキスト フィールド - ワード単位で検索可能な書式なしテキストの文字列。
  • HTML フィールド - HTML マークアップ タグを含む文字列。マークアップ タグの外のテキストのみを検索可能。
  • 数値フィールド - 浮動小数点数。
  • 時間フィールド - time.Time 値(ミリ秒単位の精度で格納)。
  • 地理位置情報フィールド - 緯度と経度の座標が指定されたデータ オブジェクト。

ドキュメントの最大サイズは 1 MB です。

インデックス

インデックスにはユーザーが取得できるようにドキュメントが格納されます。ID を直接指定して単一ドキュメントを取得する、連続した ID を指定して一連のドキュメントを取得する、インデックス内の全ドキュメントを取得するなど、複数の取得方法がサポートされています。また、フィールドとその値についての条件をクエリ文字列として指定してインデックスを検索し、その条件を満たすドキュメントを取得することもできます。ドキュメントをグループごとに別のインデックスに入れてグループ単位で管理することも可能です。

1 つのインデックスに格納できるドキュメント数と使用できるインデックス数に上限はありません。デフォルトでは、1 つのインデックスに格納できるすべてのドキュメントの合計サイズは 10 GB に制限されています。App Engine 管理者のロールを持つユーザーは、Google Cloud コンソールの App Engine Search ページからリクエストを送信することで、最大 200 GB まで増やすことができます。

クエリ

インデックスを検索するには、クエリ文字列のほか、必要に応じて追加のオプションを使用してクエリを作成します。クエリ文字列は、1 つ以上のドキュメント フィールドの値に対し検索条件を指定します。インデックスを検索すると、クエリを満たすフィールドを持つインデックス内のドキュメントのみが戻されます。

「グローバル検索」とも呼ばれる最もシンプルなクエリは、フィールド値のみを含む文字列です。次の検索では、「rose」と「water」という語を含むドキュメントを検索する文字列を使用しています。

index.Search(ctx, "rose water", nil)

次の例は、1776 年 7 月 4 日の日付を含む日付フィールドまたは文字列「1776-07-04」を含むテキスト フィールドを持つドキュメントを検索します。

index.Search(ctx, "1776-07-04", nil)

さらに絞り込むクエリ文字列を指定することもできます。クエリ文字列に、フィールドとフィールド値の制約を指定した語句を 1 つ以上含めることができます。語句の正確な形式は、フィールドの型によって異なります。たとえば、「Product」というテキスト フィールドと「Price」という数値フィールドがある場合は、2 つの語句を含む次のクエリ文字列を指定できます。

// search for documents with pianos that cost less than $5000
index.Search(ctx, "Product = piano AND Price < 5000", nil)

クエリ オプションは、名前が示すとおり必須ではありません。クエリ オプションは次のようなさまざまな機能を有効にします。

  • 検索結果に返すドキュメントの数を制御する。
  • 検索結果に含めるドキュメント フィールドを指定する。デフォルトでは、元のドキュメントのすべてのフィールドが検索結果に含められます。一部のフィールドのみを検索結果に含めるように指定することもできます(元のドキュメントへの影響はありません)。
  • 検索結果を並べ替える。
  • FieldExpressions を使用してドキュメントの「計算フィールド」を作成し、スニペットを使用して要約テキスト フィールドを作成する。
  • (オフセットとカーソルを使用して)各クエリに一致したドキュメントの一部のみを返すことで、検索結果のページ分割をサポートする。

実行されたクエリの記録を保持する場合は、アプリケーションでクエリ文字列をログに記録することをおすすめします。

検索結果

Search を呼び出すと Iterator 値が返されます。この値を使用して、一致する一連のドキュメントをすべて返すことができます。

追加のトレーニング資料

このドキュメントに加えて、Search API に関する 2 部構成のトレーニング クラスが Google Developers Academy に用意されています(クラスでは Python API を使用しますが、Search のコンセプトについての追加の説明は役に立ちます)。

ドキュメントとフィールド

ドキュメントは、フィールドのリストで構成された Go 構造体で表現されます。ドキュメントは、FieldLoadSaver インターフェースを実装する任意の型で表現することもできます。

ドキュメント ID

インデックス内のすべてのドキュメントには、一意のドキュメント ID(docID)が必要です。 この ID を使用することで、検索を行わずにインデックスからドキュメントを取得できます。デフォルトでは、ドキュメントの作成時に Search API が自動的に docID を生成します。また、ドキュメントの作成時に自分で docID を指定することもできます。docID は、表示可能で印刷可能な ASCII 文字(ASCII コード 33 から 126 まで)で構成されている必要があります。また、500 字以内である必要もあります。ドキュメント ID の最初の文字を感嘆符(!)にすることはできず、最初と最後の文字を 2 つのアンダースコア(__)にすることもできません。

読み取りやすく意味のある一意のドキュメント ID を作成すると便利ですが、検索に docID を含めることはできません。次のシナリオを考えてみましょう。部品を表すドキュメントに、部品のシリアル番号を docID として使用したインデックスがあるとします。この場合、非常に効率的に任意の 1 つの部品のドキュメントを取得できますが、他のフィールド値(購入日など)とともに一定範囲のシリアル番号を使用して検索することはできません。この問題は、シリアル番号を Atom フィールドに格納することで解決できます。

ドキュメントのフィールド

ドキュメントには、名前、型、その型の単一の値を持つフィールドが含まれています。同じ名前で型が異なるフィールドを 2 つ以上作成することができます。たとえば、「age」という名前の 2 つのフィールドを、テキスト型(値は「twenty-two」)と、数値型(値は「22」)を指定して定義できます。

フィールド名

フィールド名では大文字と小文字が区別され、ASCII 文字のみを含めることができます。先頭は文字でなければならず、文字、数字、アンダースコアを含めることができます。フィールド名は 500 文字以下にする必要があります。

複数の値を持つフィールド

1 つのフィールドには 1 つの値だけを含めることができ、その値はフィールドの型と一致している必要があります。フィールド名を一意にする必要はありません。ドキュメントには同じ名前と同じ型の複数のフィールドを含めることができ、これによって 1 つのフィールドを複数の値で表すことができます (ただし、日付フィールドと数値フィールドについては、同じ名前のものを複数含めることはできません)。また、名前が同じでフィールド型が異なる複数のフィールドをドキュメントに含めることもできます。

フィールドの型

文字列を格納するフィールドには次の 3 種類があり、それらをまとめて「文字列フィールド」と呼びます。

  • テキスト フィールド: 最大 1,024**2 文字の文字列。
  • HTML フィールド: 最大 1,024**2 文字の HTML 形式の文字列。
  • Atom フィールド: 最大 500 文字の文字列。

さらに、非テキストデータを格納する 3 つのフィールド型があります。

  • 数値フィールド: -2,147,483,647 から 2,147,483,647 までの範囲の倍精度浮動小数点値。
  • 時間フィールド - time.Time 値(ミリ秒単位の精度で格納)。
  • 地理的位置フィールド: 緯度と経度の座標で表される地点。

文字列フィールド型は、Go の組み込み string 型と、search パッケージの HTML 型、および Atom 型です。数値フィールドは、Go の組み込み float64 型で表現されます。時間フィールドでは time.Time 型を使用し、地理位置情報フィールドでは appengine パッケージの GeoPoint 型を使用します。

文字列フィールドと時間フィールドの特別な処理

時間フィールド、テキスト フィールド、または HTML フィールドを持つドキュメントをインデックスに追加するときには、いくつかの特別な処理が行われます。内部で何が行われているかを理解することは、Search API を効果的に使用するのに役立ちます。

文字列フィールドのトークン化

HTML フィールドまたはテキスト フィールドのインデックスを作成すると、その内容がトークン化されます。空白や特殊文字(句読点やハッシュ記号、バックスラッシュなど)がある場所で文字列がトークンに分割されます。インデックスには各トークンのエントリが含められます。これにより、フィールドの値の一部のみを含むキーワードやフレーズを検索できるようになります。たとえば、「dark」を検索すると「it was a dark and stormy night」という文字列を含むテキスト フィールドを持つドキュメントに一致し、「time」を検索すると「this is a real-time system」という文字列を含むテキスト フィールドを持つドキュメントに一致します。

HTML フィールドでは、マークアップ タグ内のテキストはトークン化されないため、it was a <strong>dark</strong> night を含む HTML フィールドは「night」の検索とは一致しますが、「strong」の検索とは一致しません。マークアップ テキストを検索できるようにする場合は、そのテキストをテキスト フィールドに格納してください。

Atom フィールドはトークン化されません。「bad weather」という値を持つ Atom フィールドを含むドキュメントは、「bad weather」の文字列全体を検索した場合にのみ一致します。「bad」や「weather」だけを検索しても一致しません。

トークン化のルール
  • アンダースコア(_)文字とアンパサンド(&)文字では語句はトークンに分割されません。

  • 空白文字(スペース、復帰、改行、水平タブ、垂直タブ、フォーム フィード、NULL)がある場合は必ず語句がトークンに分割されます。

  • 以下の文字は句読点として処理され、語句がトークンに分割されます。

    !"%(
    *,-|/
    []]^`
    :=>?@
    {}~$
  • 次の表の文字がある場合、通常は語句がトークンに分割されますが、出現する文脈によっては異なる処理が行われる可能性があります。

    文字 ルール
    < HTML フィールドでは、「小なり」記号は HTML タグの開始を表すので無視されます。
    + 1 つ以上の「+」記号からなる文字列が語の最後に現れる場合(C++ など)、その文字列は語の一部として扱われます。
    # 「ハッシュ」記号は、a、b、c、d、e、f、g、j、x が前に付く場合、語の一部として扱われます(a# から g# は音楽記号を表し、j# と x# はプログラミング言語を表します。c# はその両方を表します)。前に「#」が付く語(#google など)はハッシュタグとして扱われ、ハッシュは語の一部になります。
    ' アポストロフィは、「s」の文字の前に付き、「s」の後に語の区切りが来る場合(例、「John's hat」)、文字として扱われます。
    . 数字の間に現れる小数点は、数値の一部(小数区切り記号)です。頭字語(A.B.C. など)の中で使用される場合も語の一部になります。
    - ダッシュは、頭字語で使用される場合(I-B-M など)、語の一部です。
  • 英文字と数字(A から Z、a から z、0 から 9)以外のすべての 7 ビット文字は、句読点として処理され、語句がトークンに分割されます。

  • それ以外のすべては、UTF-8 文字列として解析されます。

頭字語

トークン化では、頭字語(「I.B.M.」、「a-b-c」、「C I A」など)を認識するための特別なルールが使用されています。頭字語とは、同じ区切り文字を使用してアルファベット文字を 1 つずつつなげた文字列です。使用可能な区切り文字は、ピリオド、ダッシュ、任意の数のスペースです。頭字語がトークン化されるときには、文字列から区切り文字が削除されます。そのため、前述の文字列の例の場合は「ibm」、「abc」、「cia」というトークンになります。元のテキストはドキュメント フィールドにそのまま残されます。

頭字語を扱う際には、以下の点に注意してください。

  • 頭字語には 21 以上の文字を含めることができません。有効な頭字語の文字列が 21 文字を超えている場合、その頭字語は 21 文字以下の複数の頭字語に分割されます。
  • 頭字語内の文字をスペースで区切る場合は、すべての文字が大文字または小文字で統一されている必要があります。ピリオドまたはダッシュを使用する頭字語は、大文字と小文字が混在していてもかまいません。
  • 頭字語を検索する際は、検索語として頭字語の正規形(区切り文字なしの文字列)あるいは文字の間をダッシュまたはドット(併用不可)で区切った形式のいずれかを入力します。したがって、「I.B.M」のテキストは「I-B-M」、「I.B.M」、「IBM」のどの検索語を使用しても取得できます。

時間フィールドの精度

ドキュメントに時間フィールドを作成するときは、その値を time.Time に設定します。時間フィールドのインデックス作成と検索では、時刻コンポーネントはすべて無視され、日付は 1970 年 1 月 1 日 UTC からの経過日数に変換されます。つまり、時間フィールドに正確な時刻値を含めることは可能ですが、時間クエリには yyyy-mm-dd 形式の時間フィールド値しか指定できません。またこれは、日付が同じ時間フィールドの並び順を明確に定義できないことも意味します。time.Time 型はナノ秒単位の精度で時間を表現しますが、Search API はミリ秒単位の精度で時間を格納します。

その他のドキュメント プロパティ

ドキュメントの「ランク」とは、検索からドキュメントを返すデフォルトの順序を決定する正の整数です。デフォルトでは、ランクはドキュメントの作成時刻(2011 年 1 月 1 日からの経過秒数)をもとに設定されます。ドキュメントを作成するときに、ランクを明示的に設定できます(多数のドキュメントに同じランクを割り当てることはおすすめできません。10,000 個を超えるドキュメントに同じランクを決して指定しないでください)。 並べ替えオプションを指定する場合に、ランクをソートキーとして使用できます。並べ替えの式またはフィールド式でランクを使用するときは、_rank として参照します。ランクの設定方法について詳しくは、DocumentMetadata リファレンスをご覧ください。

Field 構造体の Language プロパティは、フィールドのエンコードに使用する言語を指定します。

ドキュメントから他のリソースへのリンクの作成

ドキュメントの docID と他のフィールドを、アプリケーションの他のリソースへのリンクとして使用できます。たとえば、Blobstore を使用している場合、ドキュメントを特定の blob と関連付けるには、docID または Atom フィールドの値をデータの BlobKey に設定します。

ドキュメントの作成

次のコードサンプルは、ドキュメント オブジェクトの作成方法を示しています。User 型はドキュメント構造を指定し、User 値は通常の方法で構築されています。

import (
	"fmt"
	"net/http"
	"time"

	"golang.org/x/net/context"

	"google.golang.org/appengine"
	"google.golang.org/appengine/search"
)

type User struct {
	Name      string
	Comment   search.HTML
	Visits    float64
	LastVisit time.Time
	Birthday  time.Time
}

func putHandler(w http.ResponseWriter, r *http.Request) {
	id := "PA6-5000"
	user := &User{
		Name:      "Joe Jackson",
		Comment:   "this is <em>marked up</em> text",
		Visits:    7,
		LastVisit: time.Now(),
		Birthday:  time.Date(1960, time.June, 19, 0, 0, 0, 0, nil),
	}
	// ...

インデックスの操作

インデックスへのドキュメントの挿入

インデックスにドキュメントを挿入すると、そのドキュメントは永続ストレージにコピーされ、ドキュメントの各フィールドのインデックスが名前、型、docID に従って作成されます。

次のコード例は、インデックスにアクセスし、インデックスにドキュメントを挿入する方法を示しています。

// ...
ctx := appengine.NewContext(r)
index, err := search.Open("users")
if err != nil {
	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
}
_, err = index.Put(ctx, id, user)
if err != nil {
	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
}
fmt.Fprint(w, "OK")

インデックスにドキュメントを挿入したときに、そのインデックスにすでに同じ docID のドキュメントが含まれている場合、古いドキュメントは新しいドキュメントで置き換えられます。警告は表示されません。ドキュメントの作成やインデックスへの追加を行う前に Index.Get を呼び出すことにより、特定の docID がすでに存在するかどうかを確認できます。

Put メソッドは docID を返します。docID を自分で指定しなかった場合は、結果を参照して、生成された docID を確認できます。

id, err = index.Put(ctx, "", user)
if err != nil {
	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
}
fmt.Fprint(w, id)

Index 型のインスタンスを作成しても、永続インデックスが実際に存在することにはならないので注意してください。永続インデックスは、put メソッドを使用して初めてインデックスにドキュメントを追加したときに作成されます。

ドキュメントの更新

インデックスに追加した後にドキュメントを変更することはできません。フィールドの追加や削除、フィールド値の変更もできません。ただし、ドキュメントを同じ docID を持つ新しいドキュメントに置き換えることはできます。

docID によるドキュメントの取得

Index.Get メソッドを使用して、docID によってインデックスからドキュメントを取得します。

func getHandler(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)

	index, err := search.Open("users")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	id := "PA6-5000"
	var user User
	if err := index.Get(ctx, id, &user); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	fmt.Fprint(w, "Retrieved document: ", user)
}

内容によるドキュメントの検索

インデックスからドキュメントを取得するには、クエリ文字列を作成して Index.Search を呼び出します。Search は、一致するドキュメントをランクの降順で返すイテレータを返します。

func searchHandler(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)

	index, err := search.Open("myIndex")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	for t := index.Search(ctx, "Product: piano AND Price < 5000", nil); ; {
		var doc Doc
		id, err := t.Next(&doc)
		if err == search.Done {
			break
		}
		if err != nil {
			fmt.Fprintf(w, "Search error: %v\n", err)
			break
		}
		fmt.Fprintf(w, "%s -> %#v\n", id, doc)
	}
}

インデックスの削除

各インデックスは、インデックス付けされたドキュメントとインデックス スキーマで構成されています。インデックスを削除するには、インデックス内のすべてのドキュメントを削除してからインデックス スキーマを削除します。

インデックスからドキュメントを削除するには、削除するドキュメントの docIDIndex.Delete メソッドに指定します。

func deleteHandler(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)

	index, err := search.Open("users")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	id := "PA6-5000"
	err = index.Delete(ctx, id)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	fmt.Fprint(w, "Deleted document: ", id)
}

結果整合性

インデックスにドキュメントを挿入したり、インデックス内のドキュメントを更新したり削除したりすると、その変更が複数のデータセンターに伝播されます。通常この処理はごく短時間で行われますが、所要時間は一定ではありません。Search API が保証するのは結果整合性です。つまり、1 つ以上のドキュメントを検索または取得した場合、最新の変更が反映されていない結果が返されることがあります。

インデックス スキーマ

すべてのインデックスは、そのインデックス中に含まれているドキュメントに出現するすべてのフィールド名とフィールド型を示すスキーマを持っています。スキーマを自分で定義することはできません。スキーマは動的に保守され、ドキュメントがインデックスに追加されると更新されます。シンプルなスキーマは、次のような JSON に似た形式です。

{'comment': ['TEXT'], 'date': ['DATE'], 'author': ['TEXT'], 'count': ['NUMBER']}

ディクショナリ内の各キーは、ドキュメント フィールドの名前です。キーに対応する値は、そのフィールド名で使用されているフィールド型のリストです。複数の異なるフィールド型で同じフィールド名を使用している場合は、スキーマに次のように 1 つのフィールド名について複数のフィールド型がリストされます。

{'ambiguous-integer': ['TEXT', 'NUMBER', 'ATOM']}

一度スキーマに現れたフィールドを削除することはできません。あるフィールド名を含むドキュメントがインデックスに存在しなくなった場合でも、そのフィールドを削除する方法はありません。

スキーマは、オブジェクト プログラミングでいう「クラス」を定義するものではありません。Search API に関する限り、すべてのドキュメントは一意であり、インデックスにはさまざまな種類のドキュメントを含めることができます。フィールド リストが同じオブジェクトの集合をクラスのインスタンスとして扱うためには、コード内で抽象化を行う必要があります。たとえば、同じ一連のフィールドを持つドキュメントは、すべて専用のインデックスで保持されるようにします。この場合、インデックス スキーマをクラス定義、インデックス内の各ドキュメントをクラスのインスタンスと見なすことができます。

Google Cloud コンソールでのインデックスの表示

Google Cloud コンソールでは、アプリケーションのインデックスとそれに含まれるドキュメントに関する情報を表示できます。インデックス名をクリックすると、インデックスに含まれているドキュメントが表示されます。インデックスのすべての定義済みスキーマ フィールドが表示され、その名前のフィールドを持つドキュメントごとにフィールドの値が表示されます。また、インデックス データに対するクエリをコンソールから直接発行できます。

Search API の割り当て

Search API には次のようにいくつかの無料の割り当てがあります。

リソースまたは API 呼び出し 無料の割り当て
保存容量の合計(ドキュメントとインデックス) 0.25 GB
クエリ 1,000 クエリ/日
インデックスへのドキュメントの追加 0.01 GB/日

サービスの信頼性を維持するため、Search API には次の制限が設定されています。これらの制限は無料割り当てと有料割り当ての両方に適用されます。

リソース 安全上の割り当て
クエリの最大使用量 クエリ実行時間の合計 100 分/分
追加または削除されるドキュメントの最大数 15,000/分
インデックス 1 つあたりの最大サイズ(許可されるインデックスの数は無制限) 10 GB

API の使用量は、呼び出しのタイプに応じて異なる方法でカウントされます。

  • Index.Search: 各 API 呼び出しが 1 つのクエリとしてカウントされます。実行時間は呼び出しのレイテンシに相当します。
  • Index.Put: インデックスにドキュメントを追加すると、各ドキュメントのサイズとドキュメントの数が、インデックス作成割り当て量に算入されます。
  • その他のすべての Search API 呼び出しは、呼び出しに必要なオペレーションの数に基づいてカウントされます。
    • Index.Get: 実際にドキュメントが返されるたびに 1 オペレーションがカウントされます。また、何も返されなくても 1 オペレーションがカウントされます。
    • Index.Delete: リクエスト内のドキュメントごとに 1 オペレーションがカウントされます。また、リクエストが空の場合も 1 オペレーションがカウントされます。

1 人のユーザーが検索サービスを独占できないように、クエリのスループットに割り当てが設定されています。クエリは同時に実行できるので、アプリケーションごとに、クロック時間 1 分あたりのクエリの実行時間の合計が最大 100 分になるまでクエリを実行することが許可されています。短いクエリを多数実行している場合、この限界に達することはまずありません。この割り当てを超えた場合は、割り当てが元に戻される次のタイムスライスまで、それ以降のクエリは失敗します。厳密に 1 分間のタイムスライスで割り当てを適用しているわけではなく、リーキー バケット アルゴリズムの一種を使用して、5 秒間隔で検索帯域幅を制御しています。

割り当てについて詳しくは、割り当てのページをご覧ください。アプリがこれらの量を超えると、割り当て不足エラーが返されます。

これらの制限は分単位で適用されますが、コンソールには各制限についての日単位の合計数が表示されることに注意してください。シルバー、ゴールド、プラチナ サポートをご契約の方は、サポート担当者に連絡して、スループットの上限を引き上げることができます。

Search API 料金

次の料金が、無料割り当てを超えた使用量に適用されます。

リソース 料金
ストレージの合計(ドキュメントとインデックス) 月額 $0.18/GB
クエリ $0.50/1 万クエリ
検索可能なドキュメントのインデックス登録 $2.00/GB

料金に関する詳細については、料金のページをご覧ください。