ドキュメントとインデックス

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

概要

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

ドキュメント

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

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

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

インデックス

インデックスには取得用にドキュメントを格納できます。ドキュメント ID を 1 つ指定してドキュメントを 1 つ取得したり、連続する ID を指定して一連のドキュメントを取得したり、インデックス内のすべてのドキュメントを取得したりできます。また、フィールドとその値についての条件をクエリ文字列として指定してインデックスを検索し、その条件を満たすドキュメントを取得することもできます。ドキュメントをグループごとに別のインデックスに入れてグループ単位で管理することも可能です。

1 つのインデックスに格納できるドキュメント数と使用できるインデックス数に制限はありません。デフォルトでは、1 つのインデックスに格納できるすべてのドキュメントの合計サイズが 10 GB に制限されていますが、Google Cloud Platform Console の [App Engine] > [検索] ページからリクエストを送信することで、最大 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 種類があり、それらをまとめて「文字列フィールド」と呼びます。

  • テキスト フィールド: 最大 1024**2 文字の文字列。
  • HTML フィールド: 最大 1024**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 に設定します。時間フィールドのインデックス登録および検索を目的とした場合、時間の要素はすべて無視され、日付は 1/1/1970 UTC を起点とした日数に変換されます。つまり、時間フィールドには正確な時間の値を格納できるものの、日付のクエリで時間フィールド値として指定できる値は、yyyy-mm-dd の形式となります。またこれは、日付が同じ時間フィールドの並び順を明確に定義できないことも意味します。time.Time 型はナノ秒単位の精度で時間を表現しますが、Search API はミリ秒単位の精度で時間を格納します。

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

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

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

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

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

ドキュメントの作成

次のコードサンプルは、ドキュメント オブジェクトの作成方法を示したものです。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 に従って作成されます。

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

// ...
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 が保証するのは結果整合性です。これは、ID を使用して検索したり 1 つ以上のドキュメントを取得したりする場合に、最新の変更が結果に反映されていないことがあることを意味します。

インデックス スキーマ

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

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

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

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

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

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

Google Cloud Platform Console でのインデックスの表示

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

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

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

このページは役立ちましたか?評価をお願いいたします。

フィードバックを送信...

Go の App Engine スタンダード環境