文档和索引

Search API 提供了一个用于将包含结构化数据的文档编入索引的模型。之后,您可以搜索索引并整理和展示搜索结果。该 API 支持根据字符串字段进行全文匹配。而文档和索引保存在针对搜索操作进行优化的单独持久性存储中。Search API 可以将任意数量的文档编入索引。App Engine Datastore 可能更适合需要检索非常大的结果集的应用。如需查看 search 软件包的内容,请参阅 search 软件包参考文档

概览

Search API 涉及到四个主要概念:文档、索引、查询和结果。

文档

文档是具有唯一 ID 和一列包含用户数据的字段的对象。每个字段都具有名称和类型。字段类型有多种,由其包含的值的种类进行划分,具体如下所示:

  • Atom 字段 - 一个不可分割的字符串。
  • 文本字段 - 一个可以逐字搜索的纯文本字符串。
  • HTML 字段 - 一个包含 HTML 标记标签的字符串,只能搜索标记标签外部的文本。
  • 数字字段 - 一个浮点数。
  • 时间字段 - 一个 time.Time 值,存储时精确到毫秒。
  • 地理位置字段 - 具有纬度和经度坐标的数据对象。

文档的大小上限为 1 MB。

索引

索引存储用于检索的文档。您可以检索单个文档(按其 ID),检索一系列具有连续 ID 的文档,或者检索一个索引中的所有文档。此外,您还可以搜索索引,以检索满足特定字段及字段值标准(由查询字符串指定)的文档。要管理多组文档,您可以将它们放入多个单独的索引中。

索引中的文档数量或者您可以使用的索引数量无限制。单个索引中所有文档的总大小默认限制为 10GB,但通过从 Google Cloud Console 的 App Engine Search 页面提交请求,可能会增加到 200GB。

查询

要搜索索引,您可以构造一个查询,其中应包含一个查询字符串,可能还包含其他一些选项。查询字符串的作用是为一个或多个文档字段的值指定条件。当您搜索某个索引时,系统只会返回该索引中字段满足相应查询的文档。

最简单的查询(有时称为“全局搜索”)是一个仅包含字段值的字符串。以下搜索使用一个字符串来搜索包含字词“rose”和“water”的文档:

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

以下查询搜索的是日期字段包含日期 1776 年 7 月 4 日或文本字段包含字符串“1776-07-04”的文档:

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

此外,您也可以编写更具体的查询字符串。例如,它可以包含一个或多个字词,各对应一个字段并针对该字段值指定了限制条件。字词的确切形式取决于字段类型。例如,假设有一个名为“Product”的文本字段和一个名为“Price”的数字字段,下面是一个包含两个字词的查询字符串示例:

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

顾名思义,查询选项不是必选项。但是,利用这些选项可以实现多种功能,例如:

  • 控制搜索结果返回的文档数。
  • 指定要包含在结果中的文档字段。默认设置是包含原始文档中的所有字段。但您可以让结果仅包含字段子集(原始文档不受影响)。
  • 对结果进行排序。
  • 使用 FieldExpressions 为文档创建“计算字段”,还可使用代码段创建删减的文本字段。
  • 让每个查询仅返回一部分匹配文档(使用偏移和游标),支持搜索结果分页功能。

搜索结果

Search 调用返回一个 Iterator 值,它可用于返回完整的匹配文档集。

额外的培训资料

除了本文档之外,您还可以访问 Google Developer Academy,了解关于 Search API 的两部分培训课程。(虽然该课程使用 Python API,但您可以重点学习有关搜索概念的其他内容。)

文档和字段

文档由 Go 结构体表示,并包含多个字段。 此外,文档也可以由任何实现 FieldLoadSaver 接口的类型来表示。

文档标识符

索引中的每个文档都必须具有唯一的文档标识符 (docID)。标识符可用于从索引检索文档,而不执行搜索。默认情况下,Search API 会在创建文档时自动生成 docID。您还可以在创建文档时自行指定 docIDdocID 只能包含可见且可打印的 ASCII 字符(ASCII 码值介于 33 到 126 之间,包括 33 和 126),且长度不得超过 500 个字符。文档标识符不能以感叹号(“!”)作为开头,且开头和结尾不能使用双下划线(“__”)。

虽然创建可读、有意义且唯一的文档标识符很方便,但您不能在搜索内容中包含 docID。假设有这样一个场景:您有一个索引,其中包含代表部件的文档,并使用部件的编号作为 docID。这样一来,检索文档以找出任何单个部件的效率会非常高,但是将无法搜索一系列序列号以及其他字段值(例如购买日期)。而将序列号存储在原子字段中可以解决此问题。

文档字段

文档包含的字段具有名称、类型,以及该类型对应的单个值。两个或更多字段可以具有相同名称,但类型不能相同。例如,您可以定义两个名为“age”的字段,一个为文本类型(值为“twenty-two”),另一个为数值类型(值为 22)。

字段名称

字段名称区分大小写,且只能包含 ASCII 字符。这些名称必须以字母开头,并且可以包含字母、数字或下划线。请注意,字段名称的长度不能超过 500 个字符。

多值字段

字段只能包含一个值,且该值必须与字段的类型相匹配。但是,字段名称不必是唯一的。因此,文档可以具有多个名称相同和类型相同的字段,以此表示具有多个值的字段。(请注意,同名的日期字段和数值字段不能重复使用。)此外,文档还可以包含多个名称相同但字段类型不同的字段。

字段类型

目前有三种用于存储字符串的字段,我们将其统称为字符串字段,具体包括:

  • 文本字段:最大长度为 1024**2 个字符的字符串。
  • HTML 字段:一个 HTML 格式字符串,长度最大为 1024**2 个字符。
  • Atom 字段:一个字符串,长度最大为 500 个字符。

另外还有三种用于存储非文本数据的字段,具体包括:

  • 数字字段:介于 -2147483647 和 2147483647 之间的双精度浮点值。
  • 时间字段 - 一个 time.Time 值,存储时精确到毫秒。
  • 地理坐标点字段:由纬度和经度坐标描述的地球上的一个点。

字符串字段类型包括 Go 的内置 string 类型以及 search 软件包的 HTMLAtom 类型。数字字段用 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 字段中,系统不会对标记标签中的文本进行词法单元化处理,因此 HTML 字段包含 it was a <strong>dark</strong> night 的文档将会与针对“night”的搜索匹配,而非与针对“strong”的搜索匹配。如果您希望能够搜索标记文本,请将其存储在文本字段中。

系统不会对 Atom 字段进行词法单元化处理。如果某文档中 Atom 字段的值为“bad weather”,则该文档将仅与针对整个字符串“bad weather”的搜索匹配,而不会与仅针对“bad”或“weather”的搜索匹配。

词法单元化处理规则
  • 下划线 (_) 以及“和”符号 (&) 字符不会将字词拆分为词法单元。

  • 下列空格字符始终会将字词拆分为词法单元:空格、回车符、换行符、水平制表符、垂直制表符、换页符和 NULL。

  • 下列字符被视为标点符号,因此会将字词拆分为词法单元:

    !"%()
    *,-|/
    []]^`
    :=>?@
    {}~$
  • 下表中的字符通常会将字词拆分为词法单元,但可以根据字符所在的上下文以不同的方式对其进行处理:

    字符 规则
    < 在 HTML 字段中,“小于”符号表示被忽略的 HTML 标记的开头。
    + 如果字词的末尾出现包含一个或多个“加号”符号的字符串 (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”等字符串)。首字母缩写词是一串单字母字符,且各字符之间使用相同的分隔符。有效分隔符包括英文句点、短划线或任意数量的空格。当首字母缩写词被词法单元化后,分隔符会从字符串中移除。因此,上述示例字符串会变成“ibm”、“abc”和“cia”词法单元。但是,原始文本会保留在文档字段中。

处理首字母缩写词时,请注意以下几点:

  • 首字母缩写词所含的字母数不能超过 21 个。如果一个有效的首字母缩写词字符串包含的字母数超过 21 个,则该字符串将被拆分为一系列首字母缩写词,且每个首字母缩写词包含的字母数不超过 21 个。
  • 如果首字母缩写词中的字母是用空格分隔的,则所有字母必须采用相同的大小写形式。使用英文句点和短划线构造的首字母缩写词可以使用混合大小写的字母。
  • 搜索首字母缩写词时,您可以输入规范形式的首字母缩写词(即不含任何分隔符的字符串),或者输入以短划线或点(但不能同时使用这两者)作为标点来连接各字母的首字母缩写词。因此,检索文本“I.B.M”时,您可以使用“I-B-M”、“I.B.M”或“IBM”中的任何搜索字词。

时间字段精度

在文档中创建时间字段时,可将该字段的值设置为 time.Time。为了将时间字段编入索引并进行搜索,任何时间部分都将被忽略,并且日期将转换为自世界协调时间 (UTC) 1970 年 1 月 1 日以来所经过的天数。也就是说,尽管时间字段可以包含精确的时间值,日期查询也只能以 yyyy-mm-dd 的格式指定时间字段值。这也意味着日期相同的时间字段的排序顺序没有明确定义。虽然 time.Time 类型可表示精度为纳秒的时间,但 Search API 在存储时间时仅会精确到毫秒。

其他文档属性 (Property)

文档的排名是一个正整数,它确定了通过搜索返回的文档的默认顺序。默认情况下,当您创建文档时,系统会将文档的排名设置为自 2011 年 1 月 1 日以来所经过的秒数。您可以在创建文档时明确设置排名。建议您不要为多个文档指定相同的排名,而且绝不能为数量超过 10000 的文档提供相同的排名。如果指定排序选项,则可以将排名用作排序键。请注意,在排序表达式字段表达式中使用排名时,其引用格式为 _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)
	}
}

删除索引

每个索引都包含其索引文档和索引架构。要删除索引,请删除索引中的所有文档,然后删除索引架构。

您可以通过在 Index.Delete 方法中指定要删除的文档的 docID 来删除索引中的文档。

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 会保证最终一致性,因此在某些情况下,如果您搜索或者检索一个或多个文档,则返回结果可能不会反映最近的更改。

索引架构

每个索引都具有一个架构,用于显示索引所含文档中出现的所有字段名称和字段类型。但请注意,用户不能自行定义架构。相反,架构是动态维护的,比如将文档添加到索引时会更新架构。一个简单的架构可能如下所示(采用类似 JSON 的格式):

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

字典中的每个键都是文档字段的名称,而键的值是与该字段名称一起使用的字段类型的列表。如果您将相同的字段名称与不同的字段类型一起使用,则架构将为一个字段名称列出多个字段类型,如下所示:

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

您无法移除显示在架构中的字段。即使索引不再包含具有该特定字段名称的任何文档,也无法删除字段。

架构不会定义对象编程意义上的“类”。就 Search API 而言,每个文档都是唯一的,并且索引可以包含不同类型的文档。如果您要将具有相同字段列表的对象集合视为类的实例,就必须在代码中执行这种抽象。例如,您可以确保具有相同字段集的所有文档都保存在各自的索引中。索引架构可以视为类定义,而索引中的每个文档都是类的实例。

在 Google Cloud Console 中查看索引

在 Cloud Console 中,您可以查看有关应用索引及其所包含文档的信息。点击索引名称会显示索引包含的文档,您将看到为索引定义的所有架构字段。对于具有该名称字段的每个文档,您将看到该字段的值。此外,您还可以直接从 Console 发出对索引数据的查询。

Search API 配额

Search API 提供以下几项免费配额:

资源或 API 调用 免费配额
总存储空间(文档和索引) 0.25 GB
查询 每天 1000 次查询
将文档添加到索引中 每天 0.01 GB

Search API 设定这些限额是为了确保服务的可靠性。这些限额适用于免费应用和付费应用:

资源 安全配额
查询使用量上限 每分钟可提供共计 100 分钟的查询执行时间
添加或删除的文档数量上限 每分钟 15000 个
每个索引的大小上限(索引数量不限) 10 GB

API 用量的计算方式有所不同,具体取决于调用类型:

  • Index.Search:每个 API 调用算作一个查询;执行时间相当于调用的延迟时间。
  • Index.Put:向索引添加文档时,每个文档的大小和文档数量都会计入索引配额。
  • 其他所有 Search API 调用都根据它们涉及的操作数量进行计数:
    • Index.Get:对于实际返回的每个文档,都算作 1 个操作;如果未返回任何文档,也算作 1 个操作。
    • Index.Delete:对于请求中包含的每个文档,算作 1 个操作;如果请求为空,也算作 1 个操作。

查询吞吐量设有配额,以免单个用户独占搜索服务。由于查询可以同时执行,因此允许每个应用运行查询,这些查询按 1 分钟时钟时间计算,最多可以使用 100 分钟执行时间。如果您运行许多简短查询,则可能达不到此限制。一旦超过配额,后续查询将失败,直到下一个时间片段,系统才会恢复您的配额。配额不是严格限制在一分钟时间片段内;系统会使用一个变体漏桶算法,以五秒钟为增量控制搜索带宽。

如需详细了解配额,请参阅配额页面。当应用尝试超过这些数量时,将返回配额不足错误。

请注意,虽然这些限额是按分钟实施的,但控制台显示的是每个限额的每日总计值。拥有白银级、黄金级或白金级支持的客户可以联系支持代表来申请更高的吞吐量限额。

Search API 价格

以下费用适用于超出免费配额的使用量:

资源 费用
总存储空间(文档和索引) 每月每 GB $0.18
查询 每 10000 次查询 $0.50
为可搜索的文档编制索引 每 GB $2.00

如需详细了解价格,请参阅价格页面。