API Search para pacote de serviços legados

Na API Search, há um modelo para a indexação de documentos contendo dados estruturados. É possível pesquisar um índice, organizar e apresentar os resultados da pesquisa. A API é compatível com correspondência de texto completa em campos de string. Documentos e índices são salvos em um armazenamento permanente separado e otimizado para operações de pesquisa. Qualquer número de documentos pode ser indexado na API Search. O App Engine Datastore pode ser mais apropriado para aplicativos que precisam recuperar grupos de resultados muito grandes. Para ver o conteúdo do pacote search, consulte as referências do pacote search.

Visão geral

A API Search é baseada em quatro conceitos principais: documentos, índices, consultas e resultados.

Documentos

Um documento é um objeto com um código exclusivo e uma lista de campos contendo dados do usuário. Cada campo tem um nome e um tipo. Existem vários tipos de campos, identificados pelos tipos de valores que contêm:

  • Campo atômico: uma string de caracteres indivisível.
  • Campo de texto: uma string de texto simples que pode ser pesquisada por palavra.
  • Campo HTML: uma string de caracteres que contém tags de marcação HTML. Somente o texto fora das tags de marcação pode ser pesquisado.
  • Campo numérico: um número de ponto flutuante.
  • Campo de horário - uma valor time.Time, armazenado com precisão de milissegundos.
  • Campo de Geopoint: um objeto de dados com coordenadas de latitude e longitude.

O tamanho máximo de um documento é 1 MB.

Índices

Um índice armazena documentos para recuperação. É possível recuperar um documento pelo código, uma série de documentos com códigos consecutivos ou todos os documentos em um índice. Também é possível pesquisar um índice para recuperar documentos que atendam a determinados critérios nos campos e valores correspondentes, especificados como uma string de consulta. Também é possível gerenciar grupos de documentos colocando-os em índices separados.

Não há limite para a quantidade de documentos em um índice ou o número de índices em uso. O tamanho total de todos os documentos em um único índice é limitado a 10 GB por padrão. Aqueles com o papel Administrador do App Engine podem enviar uma solicitação pela página do App Engine Search no console do Google Cloud para aumentar o tamanho para até 200 GB.

Consultas

A fim de pesquisar um índice, você constrói uma consulta contendo uma string de consulta e talvez opções adicionais. Na string de consulta, são especificadas as condições para os valores de um ou mais campos do documento. Ao pesquisar um índice, você recupera apenas os documentos com os campos que atendem à consulta.

A consulta mais simples, às vezes chamada de "pesquisa global", é uma string que contém apenas valores de campo. Na pesquisa a seguir, uma string é usada para localizar documentos contendo as palavras "rose" e "water":

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

A string a seguir é usada para pesquisar documentos com campos de data que contenham "4 de julho de 1776" ou campos de texto que incluam a string "1776-07-04":

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

Uma string de consulta também pode ser mais específica. Ela pode conter um ou mais termos, cada um nomeando um campo e uma restrição sobre o valor dele. A forma exata de um termo depende do tipo de campo. Por exemplo, supondo que exista um campo de texto chamado "Produto" e um campo numérico chamado "Preço", aqui está uma string de consulta com dois termos:

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

As opções de consulta, como o nome indica, não são necessárias. Com elas, é possível usar diversos recursos:

  • Controlar quantos documentos são retornados nos resultados da pesquisa.
  • Especificar quais campos de documento incluir nos resultados. O padrão é incluir todos os campos do documento original. É possível definir que os resultados incluam apenas um subgrupo de campos, sem que o documento original seja afetado.
  • Classificar os resultados.
  • Criar "campos calculados" para documentos usando FieldExpressions e campos de texto abreviados usando snippets.
  • Dar suporte à paginação por meio dos resultados da pesquisa ao retornar apenas uma parte dos documentos correspondentes em cada consulta, usando deslocamentos e cursores.

Recomendamos que você registre strings de consulta no aplicativo se quiser manter um registro das consultas que foram executadas.

Resultados da pesquisa

Uma chamada Search retorna um valor Iterator, que pode ser usado para retornar o conjunto completo de documentos correspondentes.

Material de treinamento adicional

Além desta documentação, leia a aula de treinamento em duas partes sobre a API Search na Google Developer's Academy. A API Python é usada na aula, mas talvez você considere útil discutir sobre os conceitos de pesquisa.

Documentos e campos

Os documentos são representados por estruturas Go, que incluem uma lista de campos. Eles também podem ser representados por qualquer tipo de implementação da interface FieldLoadSaver.

Identificador do documento

Todo documento em um índice precisa ter um identificador exclusivo ou docID. O identificador pode ser usado para recuperar um documento de um índice sem realizar uma pesquisa. Por padrão, a API Search gera automaticamente um docID quando um documento é criado. É possível definir o docID ao criar um documento. O docID pode conter apenas caracteres ASCII visíveis e imprimíveis (códigos ASCII 33 a 126 inclusos) e não pode ter mais de 500 caracteres. O identificador de documento não pode começar com um ponto de exclamação ('!'), e não pode começar e terminar com sublinhados duplos ("__").

É conveniente criar identificadores de documentos exclusivos, legíveis e significativos, mas não é possível incluir o docID em uma pesquisa. Considere esta situação: você tem um índice com documentos que representam peças, usando o número de série da peça como docID. Será muito eficiente recuperar o documento de uma peça única, mas será impossível pesquisar por uma faixa de vários números de série junto com outros valores de campo, como data de compra. Armazenar o número de série em um campo atômico resolve o problema.

Campos do documento

Um documento contém campos com um nome, um tipo e um único valor desse tipo. Dois ou mais campos podem ter o mesmo nome, mas tipos diferentes. Por exemplo, é possível definir dois campos com o nome "idade": um com tipo texto (o valor "vinte e dois") e outro com o tipo número (valor 22).

Nomes de campos

Os nomes dos campos diferenciam maiúsculas de minúsculas e só podem conter caracteres ASCII. Eles precisam começar com uma letra e podem conter letras, números ou sublinhados. O nome do campo não pode ter mais de 500 caracteres.

Campos com vários valores

Um campo pode conter apenas um valor, que precisa corresponder ao tipo do campo. Os nomes dos campos não precisam ser exclusivos. Um documento pode ter vários campos com o mesmo nome e o mesmo tipo. Essa é uma maneira de representar um campo com vários valores. No entanto, os campos de data e número com o mesmo nome não podem ser repetidos. Um documento também pode conter vários campos com o mesmo nome e diferentes tipos.

Tipos de campo

Existem três tipos de campos para armazenar strings de caracteres. Eles são conhecidos conjuntamente como campos de string:

  • Campo de texto: uma string com comprimento máximo de 1024**2 caracteres.
  • Campo HTML: uma string formatada em HTML com comprimento máximo de 1024**2 caracteres.
  • Campo atômico: uma string com comprimento máximo de 500 caracteres.

Existem também três tipos de campos para armazenar dados não-textuais:

  • Campo numérico: um ponto flutuante de dupla precisão entre -2.147.483.647 e 2.147.483.647.
  • Campo de horário - uma valor time.Time, armazenado com precisão de milissegundos.
  • Campo de Geopoint: um ponto na Terra descrito em coordenadas de latitude e longitude.

Os tipos de campo de string são o tipo de stringintegrado do Go e os tipos HTML e Atom do pacote search. Os campos numéricos são representados pelo tipo float64 integrado do Go. O tipo dos campos de horário é time.Time e os campos de Geopoint sam o tipo GeoPoint do pacote appengine.

Tratamento especial para campos de string e tempo

Quando um documento com campos de tempo, texto ou HTML é adicionado a um índice, ocorre um tratamento especial. É útil entender o que está acontecendo nos bastidores para usar a Search API de forma eficaz.

Tokenização de campos de string

Quando um campo de HTML ou texto é indexado, o conteúdo é tokenizado. A string é dividida em tokens onde houver espaços em branco ou caracteres especiais, como sinais de pontuação, barra invertida etc. O índice inclui uma entrada para cada token. Isso permite pesquisar palavras-chave e frases que compõem apenas parte do valor de um campo. Por exemplo, em uma pesquisa por "noite", um documento com um campo de texto contendo a string "era uma noite escura e tormentosa" será retornado. Em uma pesquisa por "tempo", um documento com um campo de texto contendo a string "isto é um sistema em tempo real" será retornado.

Em campos HTML, o texto dentro das tags de marcação não é tokenizado. Portanto, um documento com um campo HTML que contém it was a <strong>dark</strong> night será retornado para "night", mas não para "strong". Para pesquisar um texto de marcação, armazene-o em um campo de texto.

Os campos atômicos não são tokenizados. Um documento com um campo atômico que contenha o valor "tempo ruim" só será retornado em uma pesquisa para toda a string "tempo ruim". Ele não será retornado em uma pesquisa pelos termos "tempo" ou "ruim" separados.

Regras de tokenização
  • As palavras não são divididas em tokens pelos caracteres e comercial (&) e sublinhado (_).

  • As palavras são sempre divididas em tokens pelos seguintes caracteres de espaço em branco: espaço, retorno de carro, avanço de linha, tabulação horizontal, tabulação vertical, avanço de formulário e NULL.

  • As palavras são divididas em tokens pelos caracteres a seguir, que são tratados como sinais de pontuação:

    !"%()
    *,-|/
    []]^`
    :=>?@
    {}~$
  • Geralmente, as palavras são divididas em tokens pelos caracteres a seguir, mas eles podem ser manipulados de forma diferente dependendo do contexto em que aparecem:

    Caractere Regra
    < Em um campo HTML, o sinal "menor que" indica o início de uma tag HTML, que é ignorada.
    + Uma string de um ou mais sinais de adição é tratada como parte da palavra se aparecer no final dela (C++).
    # O sinal de jogo da velha é tratado como parte da palavra se for precedido de a, b, c, d, e, f, g, j ou x (a# - g# são notas musicais, j# e x# são linguagens de programação e c# é as duas coisas). Se um termo é precedido por '#' (#google), ele é tratado como uma hashtag, e o sinal de jogo da velha se torna parte da palavra.
    ' O apóstrofo é uma letra se preceder a letra "s" seguida de uma quebra de palavra, como em "John's hat".
    . Se um ponto decimal aparecer entre dígitos, ele faz parte de um número, ou seja, é o separador de decimais. Ele também pode ser parte de uma palavra se usado em uma sigla (A.B.C).
    - O traço é parte de uma palavra se usado em uma sigla (I-B-M).
  • As palavras são divididas em tokens pelos outros caracteres de 7 bits que não sejam letras e dígitos ("A-Z", "a-z", "0-9"). Eles são tratados como sinais de pontuação.

  • Todo o resto é analisado como um caractere UTF-8.

Siglas

A tokenização tem regras especiais para reconhecer as siglas. Por exemplo, strings como "I.B.M.", "a-b-c" ou "C.I.A". Uma sigla é uma string de caracteres alfabéticos individuais com o mesmo caractere separador entre todos eles. Os separadores válidos são o ponto, o traço ou qualquer número de espaços. O caractere separador é removido da string quando a sigla é tokenizada. Assim, as strings de exemplo mencionadas acima tornam-se os tokens "ibm", "abc" e "cia". O texto original permanece no campo do documento.

Ao lidar com siglas, observe que:

  • Uma sigla não pode conter mais de 21 letras. Uma string de sigla válida com mais de 21 letras será dividida em uma série de siglas, cada uma com até 21 letras.
  • Se as letras de uma sigla forem separadas por espaços, todas precisarão ter a mesma capitalização. As siglas formadas com pontos e traços podem usar letras maiúsculas e minúsculas.
  • Ao pesquisar uma sigla, insira a forma canônica dela (a string sem separadores) ou a sigla pontuada com o traço ou o ponto (mas não ambos) entre as letras. Portanto, o texto "IBM" pode ser recuperado com qualquer um dos termos de pesquisa: "I-B-M", "I.B.M" ou "IBM".

Precisão do campo de tempo

Ao criar um campo de tempo em um documento, defina o valor como time.Time. Com o objetivo de indexar e pesquisar o campo de tempo, qualquer componente de tempo é ignorado, e a data é convertida para o número de dias desde 01/01/1970 UTC. Isso significa que, ainda que o campo contenha um valor de tempo preciso, a consulta de data só pode especificar um valor de campo de tempo no formato yyyy-mm-dd. Também significa que a ordem de classificação dos campos de tempo com a mesma data não está bem definida. time.Time representa o tempo com precisão de nanossegundos, mas o valor é armazenado na API Search apenas com a precisão de milissegundos.

Outras propriedades do documento

A classificação de um documento é um número inteiro positivo que determina a ordem padrão dos documentos retornados de uma pesquisa. Por padrão, ela é definida no momento em que o documento é criado, pelo número de segundos desde 1º de janeiro de 2011. É possível definir a classificação explicitamente ao criar um documento. Não é uma boa ideia atribuir a mesma classificação a muitos documentos. Nunca defina a mesma classificação para mais de 10.000 documentos. Ao especificar opções de classificação, é possível usar a classificação como uma chave. Quando a classificação é usada em uma expressão de classificação ou de campo, ela é mencionada como _rank. Para mais informações sobre como configurar a classificação, consulte a referência de DocumentMetadata.

O idioma em que o campo está codificado é especificado pela propriedade "Idioma" da estrutura Field.

Como vincular de um documento a outros recursos

É possível usar o docID e outros campos de um documento como links para outros recursos do aplicativo. Por exemplo, se você usa o Blobstore, associe o documento a um blob específico. Basta configurar docID ou o valor de um campo atômico como BlobKey dos dados.

Criar documentos

No exemplo de código a seguir, mostramos como criar um objeto de documento. A estrutura do documento é especificada pelo tipo User, e um valor de User é criado da maneira usual.

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),
	}
	// ...

Como trabalhar com um índice

Como colocar documentos em um índice

Ao colocar um documento em um índice, ele é copiado para o armazenamento permanente, e cada um dos campos é indexado de acordo com nome, tipo e docID.

Veja a seguir um exemplo de código para acessar um índice e colocar um documento nele.

// ...
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")

Quando você coloca um documento em um índice e este já contém um documento com o mesmo docID, o novo documento substitui o anterior. Nenhum aviso é exibido. Chame Index.Get antes de criar ou adicionar um documento a um índice para verificar se determinado docID já existe.

O docID é retornado pelo método Put. Se você não tiver especificado o docID, poderá analisar o resultado para descobrir o docID que foi gerado:

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

Observe que criar uma instância do tipo Index não garante que um índice permanente realmente exista. O índice permanente é criado na primeira vez em que um documento é adicionado a ele usando o método put.

Atualizar documentos

Um documento não pode ser alterado depois de adicionado a um índice. Não é possível adicionar ou remover campos nem alterar o valor de um campo. No entanto, é possível substituir o documento por outro que tenha o mesmo docID.

Como recuperar documentos por docID

Use o método Index.Get para recuperar um documento de um índice usando o 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)
}

Pesquisar documentos pelo conteúdo

Para recuperar documentos de um índice, crie uma string de consulta e chame o Index.Search. O Search retorna um iterador que produz documentos correspondentes em ordem de classificação decrescente.

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)
	}
}

Como excluir um índice

Cada índice consiste em documentos indexados e um esquema de índice. Para excluir um índice, exclua todos os documentos inclusos nele e também o esquema dele.

Para excluir documentos de um índice, especifique o docID do documento a ser excluído no método Index.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)
}

Consistência eventual

Quando você manipula um documento em um índice usando put, update ou delete, a alteração é propagada por vários data centers. Normalmente esse processo é rápido, mas o tempo pode variar. Uma consistência eventual (link em inglês) é garantida pela API Search. Isso significa que, em alguns casos, uma pesquisa ou a recuperação de documentos pode retornar resultados que não refletem as alterações mais recentes.

Esquemas de índice

Todos os índices têm esquemas que mostram os nomes e os tipos de campos que aparecem nos documentos que contêm. Não é possível definir um esquema. Os esquemas são mantidos dinamicamente e são atualizados conforme os documentos são adicionados ao índice. Um esquema simples pode ter a seguinte aparência em um formulário estilo JSON:

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

Cada chave do dicionário é o nome de um campo de documento. O valor da chave é uma lista dos tipos de campo usados com esse nome. Se você usar o mesmo nome de campo com tipos diferentes, o esquema listará mais de um tipo para cada nome, como no exemplo a seguir:

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

Quando um campo aparece em um esquema, ele nunca mais pode ser removido. Não há como excluir um campo, mesmo que o índice não contenha mais nenhum documento com esse nome de campo específico.

No esquema, não há a definição de uma "classe" no sentido da programação de objeto. No que diz respeito à API Search, cada documento é único, e os índices podem conter diferentes tipos de documentos. Se você quiser tratar coleções de objetos com a mesma lista de campos como instâncias de uma classe, isso é uma abstração que precisará ser aplicada no seu código. Por exemplo, é possível garantir que todos os documentos com o mesmo conjunto de campos sejam mantidos no próprio índice. O esquema de índice pode ser visto como a definição da classe, e cada documento no índice é uma instância dessa classe.

Como visualizar índices no console do Google Cloud

No console do Google Cloud, é possível ver informações sobre os índices do aplicativo e os documentos que eles contêm. Clique em um nome de índice para exibir os documentos contidos nele. Você verá todos os campos de esquema definidos para o índice e, para cada documento que contém um campo com aquele nome, você verá o valor desse campo. Também é possível emitir consultas aos dados do índice diretamente do console.

Cotas da API Search

A API Search tem várias cotas gratuitas:

Recurso ou chamada de API Cota gratuita
Armazenamento total (documentos e índices) 0,25 GB
Consultas 1.000 consultas por dia
Adição de documentos a índices 0,01 GB por dia

Esses limites são impostos pela API para garantir a confiabilidade do serviço. Eles são válidos para aplicativos gratuitos e pagos:

Recurso Cota de segurança
Uso máximo da consulta 100 minutos agregados de tempo de execução da consulta por minuto
Máximo de documentos adicionados ou excluídos 15.000 por minuto
Tamanho máximo por índice (número ilimitado de índices permitido) 10 GB

O uso da API é contado de diferentes maneiras, dependendo do tipo de chamada:

  • Index.Search: cada chamada de API conta como uma consulta. O tempo de execução é equivalente à latência da chamada.
  • Index.Put: quando você adiciona documentos a índices, o tamanho de cada documento e o número de documentos contam para a cota de indexação.
  • Todas as outras chamadas da API Search são contadas com base no número de operações que envolvem:
    • Index.Get: uma operação é contada para cada documento realmente retornado, ou uma operação se nada for retornado.
    • Index.Delete: uma operação é contada para cada documento da solicitação, ou uma operação se a solicitação estiver vazia.

A cota sobre a capacidade da consulta é imposta para que um único usuário não monopolize o serviço de pesquisa. Como as consultas podem ser executadas simultaneamente, cada aplicativo pode executar consultas que consumam até 100 minutos de tempo de execução por minuto no relógio. Se executar várias consultas curtas, você provavelmente não atingirá esse limite. porém, se exceder a cota, as consultas subsequentes falharão até a próxima fração de tempo, quando a cota é restaurada. A cota não é imposta rigorosamente em frações de um minuto. Uma variação do algoritmo leaky bucket é usada para controlar a largura de banda da pesquisa em incrementos de cinco segundos.

Veja mais informações na página Cotas. Se o app tentar exceder esses valores, é exibido um erro de cotas insuficientes.

Esses limites são aplicados por minuto, mas o console exibe os totais diários para cada um. Os clientes com suporte Silver, Gold ou Platinum podem solicitar limites de capacidade maiores. Basta entrar em contato com o representante do suporte.

Preços da API Search

As seguintes cobranças são aplicadas ao uso além das cotas gratuitas:

Recurso Custo
Armazenamento total (documentos e índices) US$ 0,18 por GB/mês
Consultas US$ 0,50 por 10.000 consultas
Indexação de documentos pesquisáveis US$ 2,00 por GB

Para mais informações, acesse a página de Preços.