API de búsqueda para servicios agrupados antiguos

La API Search proporciona un modelo para indexar documentos que contienen datos estructurados. Puedes buscar en un índice, así como organizar y presentar los resultados de búsqueda. La API admite la coincidencia de texto completo en campos de cadena. Los documentos y los índices se guardan en un almacén persistente independiente optimizado para las operaciones de búsqueda. La API Search puede indexar cualquier número de documentos. El almacén de datos de App Engine puede ser más adecuado para las aplicaciones que necesitan recuperar conjuntos de resultados muy grandes. Para ver el contenido del paquete search, consulta la referencia del paquete search.

Información general

La API Search se basa en cuatro conceptos principales: documentos, índices, consultas y resultados.

Redactar documentos

Un documento es un objeto con un ID único y una lista de campos que contienen datos de usuario. Cada campo tiene un nombre y un tipo. Hay varios tipos de campos, que se identifican por el tipo de valores que contienen:

  • Campo Atom: cadena de caracteres indivisible.
  • Campo de texto: una cadena de texto sin formato que se puede buscar palabra por palabra.
  • Campo HTML: cadena que contiene etiquetas de marcado HTML. Solo se puede buscar el texto que no esté entre etiquetas de marcado.
  • Campo numérico: un número de punto flotante.
  • Campo de hora: un valor time.Time que se almacena con una precisión de milisegundos.
  • Campo de geopunto: un objeto de datos con coordenadas de latitud y longitud.

El tamaño máximo de un documento es de 1 MB.

Índices

Un índice almacena documentos para recuperarlos. Puedes recuperar un solo documento por su ID, un intervalo de documentos con IDs consecutivos o todos los documentos de un índice. También puedes buscar en un índice para recuperar documentos que cumplan determinados criterios en los campos y sus valores, especificados como una cadena de consulta. Puedes gestionar grupos de documentos colocándolos en índices independientes.

No hay límite en el número de documentos de un índice ni en el número de índices que puede usar. El tamaño total de todos los documentos de un índice está limitado a 10 GB de forma predeterminada. Los usuarios con el rol Administrador de App Engine pueden enviar una solicitud desde la página Búsqueda de App Engine de la consola Google Cloud para aumentar el tamaño hasta 200 GB.

Consultas

Para buscar en un índice, se crea una consulta, que tiene una cadena de consulta y, posiblemente, algunas opciones adicionales. Una cadena de consulta especifica las condiciones de los valores de uno o varios campos de documento. Cuando buscas en un índice, solo obtienes los documentos del índice con campos que cumplen la consulta.

La consulta más sencilla, a veces denominada "búsqueda global", es una cadena que contiene solo valores de campo. Esta búsqueda usa una cadena que busca documentos que contengan las palabras "rosa" y "agua":

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

Esta consulta busca documentos con campos de fecha que contengan el 4 de julio de 1776 o campos de texto que incluyan la cadena "1776-07-04":

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

Una cadena de consulta también puede ser más específica. Puede contener uno o varios términos, cada uno de los cuales nombra un campo y una restricción sobre el valor del campo. La forma exacta de un término depende del tipo de campo. Por ejemplo, supongamos que hay un campo de texto llamado "Producto" y un campo numérico llamado "Precio". A continuación, se muestra una cadena de consulta con dos términos:

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

Las opciones de consulta, como su nombre indica, no son obligatorias. Permiten usar varias funciones:

  • Controla cuántos documentos se devuelven en los resultados de búsqueda.
  • Especifica los campos del documento que quieres incluir en los resultados. De forma predeterminada, se incluyen todos los campos del documento original. Puedes especificar que los resultados solo incluyan un subconjunto de campos (el documento original no se ve afectado).
  • Ordena los resultados.
  • Crea "campos calculados" para documentos con FieldExpressions y campos de texto abreviados con fragmentos.
  • Admite la paginación de los resultados de búsqueda devolviendo solo una parte de los documentos coincidentes en cada consulta (mediante desplazamientos y cursores).

Te recomendamos que registres las cadenas de consulta en tu aplicación si quieres mantener un registro de las consultas que se han ejecutado.

Resultados de búsqueda

Una llamada Search devuelve un valor Iterator, que se puede usar para devolver el conjunto completo de documentos coincidentes.

Material de formación adicional

Además de esta documentación, puedes leer la clase de formación de dos partes sobre la API Search en Google Developer's Academy. Aunque la clase usa la API de Python, puede que te resulte útil la explicación adicional de los conceptos de búsqueda.

Documentos y campos

Los documentos se representan mediante estructuras de Go, que constan de una lista de campos. Los documentos también se pueden representar mediante cualquier tipo que implemente la interfaz FieldLoadSaver.

Identificador de documento

Cada documento de un índice debe tener un identificador único o docID. El identificador se puede usar para recuperar un documento de un índice sin realizar una búsqueda. De forma predeterminada, la API Search genera automáticamente un docID cuando se crea un documento. También puedes especificar el docID cuando crees un documento. Un docID solo puede contener caracteres ASCII visibles e imprimibles (códigos ASCII del 33 al 126, ambos incluidos) y no puede tener más de 500 caracteres. Un identificador de documento no puede empezar por un signo de exclamación ("!"), ni empezar ni terminar con dos guiones bajos ("__").

Aunque es práctico crear identificadores de documentos únicos, legibles y significativos, no puedes incluir el carácter docID en una búsqueda. Imagina esta situación: tienes un índice con documentos que representan piezas y usas el número de serie de la pieza como docID. Será muy eficiente recuperar el documento de cualquier parte, pero será imposible buscar un intervalo de números de serie junto con otros valores de campo, como la fecha de compra. Almacenar el número de serie en un campo de átomo se soluciona el problema.

Campos del documento

Un documento contiene campos que tienen un nombre, un tipo y un único valor de ese tipo. Dos o más campos pueden tener el mismo nombre, pero tipos diferentes. Por ejemplo, puede definir dos campos con el nombre "edad": uno con el tipo de texto (el valor "veintidós") y otro con el tipo de número (el valor 22).

Nombres de campos

Los nombres de campo distinguen entre mayúsculas y minúsculas, y solo pueden contener caracteres ASCII. Deben empezar por una letra y pueden contener letras, números o guiones bajos. El nombre de un campo no puede tener más de 500 caracteres.

Campos con varios valores

Un campo solo puede contener un valor, que debe coincidir con el tipo del campo. Los nombres de los campos no tienen que ser únicos. Un documento puede tener varios campos con el mismo nombre y el mismo tipo, lo que permite representar un campo con varios valores. Sin embargo, los campos de fecha y número con el mismo nombre no se pueden repetir. Un documento también puede contener varios campos con el mismo nombre y diferentes tipos de campo.

Tipos de campo

Hay tres tipos de campos que almacenan cadenas de caracteres. En conjunto, los denominamos campos de cadena:

  • Campo de texto: una cadena con una longitud máxima de 1024**2 caracteres.
  • Campo HTML: cadena con formato HTML de 1024**2 caracteres como máximo.
  • Campo Atom: cadena con una longitud máxima de 500 caracteres.

También hay tres tipos de campos que almacenan datos no textuales:

  • Campo de número: valor de punto flotante de doble precisión comprendido entre -2.147.483.647 y 2.147.483.647.
  • Campo de hora: un valor time.Time que se almacena con una precisión de milisegundos.
  • Campo de geopunto: un punto de la Tierra descrito por coordenadas de latitud y longitud.

Los tipos de campos de cadena son el tipo string integrado de Go y los tipos HTML y Atom del paquete search. Los campos de número se representan con el tipo float64 integrado de Go, los campos de hora usan el tipo time.Time y los campos de geopunto usan el tipo GeoPoint del paquete appengine.

Tratamiento especial de los campos de cadena y de tiempo

Cuando se añade a un índice un documento con campos de hora, texto o HTML, se aplican algunos procesos especiales. Es útil entender qué ocurre "bajo el capó" para usar la API Search de forma eficaz.

Tokenizar campos de cadena

Cuando se indexa un campo de texto o HTML, su contenido se tokeniza. La cadena se divide en tokens cada vez que aparecen espacios en blanco o caracteres especiales (signos de puntuación, almohadilla, barra invertida, etc.). El índice incluirá una entrada para cada token. De esta forma, puedes buscar palabras clave y frases que solo incluyan una parte del valor de un campo. Por ejemplo, si buscas "oscuro", se encontrará un documento con un campo de texto que contenga la cadena "era una noche oscura y tormentosa", y si buscas "tiempo", se encontrará un documento con un campo de texto que contenga la cadena "este es un sistema en tiempo real".

En los campos HTML, el texto que se encuentra dentro de las etiquetas de marcado no se tokeniza, por lo que un documento con un campo HTML que contenga it was a <strong>dark</strong> night coincidirá con una búsqueda de "noche", pero no con "fuerte". Si quieres poder buscar texto de marcado, guárdalo en un campo de texto.

Los campos Atom no se tokenizan. Un documento con un campo atom que tenga el valor "bad weather" solo coincidirá con una búsqueda de la cadena completa "bad weather". No coincidirá con una búsqueda de "malo" o "tiempo" por separado.

Reglas de tokenización
  • Los caracteres de subrayado (_) y ampersand (&) no dividen las palabras en tokens.

  • Estos caracteres de espacio en blanco siempre dividen las palabras en tokens: espacio, retorno de carro, salto de línea, tabulación horizontal, tabulación vertical, salto de página y NULL.

  • Estos caracteres se tratan como signos de puntuación y dividen las palabras en tokens:

    !"%()
    *,-|/
    []]^`
    :=>?@
    {}~
  • Los caracteres de la siguiente tabla suelen separar las palabras en tokens, pero se pueden gestionar de forma diferente en función del contexto en el que aparezcan:

    Carácter Regla
    < En un campo HTML, el signo "menor que" indica el inicio de una etiqueta HTML que se ignora.
    + Una cadena de uno o varios signos "más" se trata como parte de la palabra si aparece al final de la palabra (C++).
    # El signo "#" se considera parte de la palabra si va precedido de a, b, c, d, e, f, g, j o x (de a# a g# son notas musicales; j# y x# son lenguajes de programación; c# es ambas cosas). Si un término va precedido por "#" (por ejemplo, #google), se trata como un hashtag y el símbolo se convierte en parte de la palabra.
    ' El apóstrofo es una letra si precede a la letra "s" seguida de un salto de palabra, como en "el sombrero de Juan".
    . Si aparece un punto decimal entre dígitos, forma parte de un número (es decir, el separador decimal). También puede formar parte de una palabra si se usa en un acrónimo (A.B.C).
    - El guion forma parte de una palabra si se usa en un acrónimo (I-B-M).
  • Todos los demás caracteres de 7 bits que no sean letras ni dígitos ('A-Z', 'a-z', '0-9') se tratan como signos de puntuación y dividen las palabras en tokens.

  • Todo lo demás se analiza como un carácter UTF-8.

Acrónimos

La tokenización usa reglas especiales para reconocer acrónimos (cadenas como "I.B.M.", "a-b-c" o "C I A"). Una sigla es una cadena de caracteres alfabéticos individuales con el mismo carácter de separación entre todos ellos. Los separadores válidos son el punto, el guion o cualquier número de espacios. El carácter separador se elimina de la cadena cuando se tokeniza un acrónimo. Por lo tanto, las cadenas de ejemplo mencionadas arriba se convierten en los tokens "ibm", "abc" y "cia". El texto original permanece en el campo del documento.

Cuando trabajes con acrónimos, ten en cuenta lo siguiente:

  • Un acrónimo no puede contener más de 21 letras. Si una cadena de acrónimo válida tiene más de 21 letras, se dividirá en una serie de acrónimos de 21 letras o menos.
  • Si las letras de un acrónimo están separadas por espacios, todas las letras deben ser del mismo tipo. Los acrónimos formados con puntos y guiones pueden usar letras en mayúsculas y minúsculas.
  • Cuando busques un acrónimo, puedes introducir su forma canónica (la cadena sin separadores) o el acrónimo con guiones o puntos (pero no ambos) entre sus letras. Por lo tanto, el texto "I.B.M" se podría recuperar con cualquiera de los términos de búsqueda "I-B-M", "I.B.M" o "IBM".

Precisión del campo de hora

Cuando creas un campo de tiempo en un documento, le asignas el valor time.Time. Para indexar y buscar el campo de hora, se ignora cualquier componente de hora y la fecha se convierte en el número de días transcurridos desde el 1 de enero de 1970 (UTC). Esto significa que, aunque un campo de hora puede contener un valor de hora preciso, una consulta de fecha solo puede especificar un valor de campo de hora con el formato yyyy-mm-dd. Esto también significa que el orden de los campos de hora con la misma fecha no está bien definido. Aunque el tipo time.Time representa el tiempo con una precisión de nanosegundos, la API Search los almacena con una precisión de milisegundos.

Otras propiedades del documento

El rango de un documento es un número entero positivo que determina el orden predeterminado de los documentos devueltos en una búsqueda. De forma predeterminada, la clasificación se asigna en el momento en que se crea el documento y corresponde al número de segundos transcurridos desde el 1 de enero del 2011. Puedes definir el rango de forma explícita al crear un documento. No es recomendable asignar el mismo rango a muchos documentos y nunca deberías asignar el mismo rango a más de 10.000 documentos. Si especifica opciones de ordenación, puede usar el rango como clave de ordenación. Ten en cuenta que, cuando se usa el rango en una expresión de orden o en una expresión de campo, se hace referencia a él como _rank. Consulta la referencia de DocumentMetadata para obtener más información sobre cómo definir el rango.

La propiedad Language de la estructura Field especifica el idioma en el que se codifica ese campo.

Crear enlaces de un documento a otros recursos

Puedes usar el docID de un documento y otros campos como enlaces a otros recursos de tu aplicación. Por ejemplo, si usa Blobstore, puede asociar el documento a un blob específico configurando docID o el valor de un campo Atom en BlobKey de los datos.

Crear un documento

En el siguiente ejemplo de código se muestra cómo crear un objeto de documento. El tipo User especifica la estructura del documento y el valor User se crea de la forma habitual.

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

Trabajar con un índice

Incluir documentos en un índice

Cuando insertas un documento en un índice, se copia en el almacenamiento persistente y cada uno de sus campos se indexa según su nombre, tipo y docID.

En el siguiente ejemplo de código se muestra cómo acceder a un índice e insertar un documento en él.

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

Cuando añades un documento a un índice y este ya contiene un documento con el mismo docID, el nuevo documento sustituye al antiguo. No se muestra ninguna advertencia. Puedes llamar a Index.Get antes de crear o añadir un documento a un índice para comprobar si ya existe un docID específico.

El método Put devuelve un objeto docID. Si no has especificado el docID, puedes examinar el resultado para descubrir el docID que se ha generado:

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

Ten en cuenta que crear una instancia del tipo Index no garantiza que exista un índice persistente. Un índice persistente se crea la primera vez que añades un documento con el método put.

Actualizar documentos

Una vez que hayas añadido un documento a un índice, no podrás cambiarlo. No puedes añadir ni quitar campos, ni cambiar el valor de un campo. Sin embargo, puedes sustituir el documento por otro que tenga el mismo docID.

Recuperar documentos por ID de documento

Usa el método Index.Get para obtener un documento de un índice por su 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)
}

Buscar documentos por su contenido

Para recuperar documentos de un índice, crea una cadena de consulta y llama a Index.Search. Search devuelve un iterador que genera documentos coincidentes en orden de rango descendente.

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

Eliminar un índice

Cada índice consta de sus documentos indexados y de un esquema de índice. Para eliminar un índice, elimina todos los documentos de un índice y, a continuación, elimina el esquema del índice.

Para eliminar documentos de un índice, especifica el docID del documento que quieras eliminar en el 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)
}

Este método puede tardar mucho tiempo si necesitas eliminar un gran número de entradas del índice de búsqueda. Para solucionar este problema, prueba lo siguiente:

  1. Elimina el proyecto y sus dependencias.
  2. Solicita una cuota más alta para eliminar elementos más rápido.

Coherencia eventual

Cuando insertas, actualizas o eliminas un documento en un índice, el cambio se propaga por varios centros de datos. Este proceso suele ser rápido, pero el tiempo que tarda puede variar. La API Search garantiza la coherencia final. Esto significa que, en algunos casos, una búsqueda o una recuperación de uno o varios documentos puede devolver resultados que no reflejen los cambios más recientes.

Esquemas de índice

Cada índice tiene un esquema que muestra todos los nombres y tipos de campos que aparecen en los documentos que contiene. No puedes definir un esquema por tu cuenta. Los esquemas se mantienen de forma dinámica y se actualizan a medida que se añaden documentos a un índice. Un esquema sencillo podría tener este aspecto en formato similar a JSON:

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

Cada clave del diccionario es el nombre de un campo de documento. El valor de la clave es una lista de los tipos de campo que se usan con ese nombre de campo. Si has usado el mismo nombre de campo con diferentes tipos de campo, el esquema mostrará más de un tipo de campo para un nombre de campo, como en este ejemplo:

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

Una vez que aparece un campo en un esquema, no se puede eliminar. No hay forma de eliminar un campo, aunque el índice ya no contenga ningún documento con ese nombre de campo concreto.

Un esquema no define una "clase" en el sentido de la programación orientada a objetos. En lo que respecta a la API de búsqueda, cada documento es único y los índices pueden contener diferentes tipos de documentos. Si quieres tratar colecciones de objetos con la misma lista de campos como instancias de una clase, debes aplicar esa abstracción en tu código. Por ejemplo, puedes asegurarte de que todos los documentos con el mismo conjunto de campos se conserven en su propio índice. El esquema de índice se puede considerar como la definición de la clase, y cada documento del índice sería una instancia de la clase.

Ver índices en la consola Google Cloud

En la Google Cloud consola, puedes ver información sobre los índices de tu aplicación y los documentos que contienen. Al hacer clic en el nombre de un índice, se muestran los documentos que contiene. Verá todos los campos de esquema definidos del índice. En cada documento con un campo de ese nombre, verá el valor del campo. También puedes enviar consultas sobre los datos del índice directamente desde la consola.

Cuotas de la API Search

La API Search tiene varias cuotas gratuitas:

Recurso o llamada a la API Cuota gratuita
Almacenamiento total (documentos e índices) 0,25 GB
Consultas 1000 consultas al día
Añadir documentos a los índices 0,01 GB al día

En la API de búsqueda se establecen estos límites para garantizar la fiabilidad del servicio. Se aplican tanto a las aplicaciones gratuitas como a las de pago:

Recurso Cuota de seguridad
Uso máximo de consultas 100 minutos agregados de tiempo de ejecución de consultas por minuto
Se ha alcanzado el número máximo de documentos añadidos o eliminados 15.000 por minuto
Tamaño máximo por índice (se permite un número ilimitado de índices) 10 GB

El uso de la API se contabiliza de diferentes formas en función del tipo de llamada:

  • Index.Search: cada llamada a la API cuenta como una consulta. El tiempo de ejecución es equivalente a la latencia de la llamada.
  • Index.Put: Cuando añades documentos a los índices, el tamaño de cada documento y el número de documentos se tienen en cuenta para la cuota de indexación.
  • El resto de las llamadas a la API Search se contabilizan en función del número de operaciones que implican:
    • Index.Get: 1 operación por cada documento devuelto o 1 operación si no se devuelve nada.
    • Index.Delete: se contabiliza una operación por cada documento de la solicitud o una operación si la solicitud está vacía.

La cuota de rendimiento de las consultas se impone para que un solo usuario no pueda monopolizar el servicio de búsqueda. Como las consultas se pueden ejecutar simultáneamente, cada aplicación puede ejecutar consultas que consuman hasta 100 minutos de tiempo de ejecución por cada minuto de tiempo real. Si ejecutas muchas consultas cortas, probablemente no alcances este límite. Una vez que superes la cuota, las consultas posteriores fallarán hasta el siguiente periodo, cuando se restaure la cuota. La cuota no se impone estrictamente en intervalos de un minuto, sino que se usa una variación del algoritmo de cubo con fugas para controlar el ancho de banda de búsqueda en incrementos de cinco segundos.

Puedes consultar más información sobre las cuotas en la página Cuotas. Cuando una aplicación intenta superar estas cantidades, se produce un error en el que se indica que la cuota no es suficiente.

Aunque estos límites se aplican por minuto, en la consola se muestran las cantidades máximas diarias. Los clientes que dispongan de asistencia Plata, Oro o Platino pueden solicitar límites de rendimiento más amplios. Para ello, solo tienen que ponerse en contacto con su representante de asistencia.

Precios de la API Search

Se aplican los siguientes cargos al uso que supere las cuotas gratuitas:

Recurso Coste
Almacenamiento total (documentos e índices) 0,18 USD por GB al mes
Consultas 0,50 USD por cada 10.000 consultas
Indexación de documentos disponibles para búsquedas 2,00 USD por GB

Puedes consultar más información sobre los precios en la página Precios.