API de búsqueda para servicios agrupados en paquetes heredados

La API de búsqueda proporciona un modelo para indexar documentos que contienen datos estructurados. Puedes realizar búsquedas en un índice, y organizar y presentar los resultados de la búsqueda. La API es compatible con la búsqueda de coincidencias de texto completa para los campos de string. Los documentos y los índices se guardan en un almacén persistente independiente optimizado para operaciones de búsqueda. La API de búsqueda puede indexar cualquier cantidad de documentos. Para aplicaciones que necesitan recuperar grandes conjuntos de resultados, puede ser más conveniente usar App Engine Datastore. Para ver el contenido del paquete search, consulta la referencia del paquete search.

Descripción general

La API de búsqueda se basa en cuatro conceptos principales: documentos, índices, consultas y resultados.

Documentos

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

  • Campo atómico: una string de caracteres indivisible
  • Campo de texto: una string de texto sin formato donde se pueden realizar búsquedas palabra por palabra
  • Campo HTML: una string que contiene etiquetas de lenguaje de marcado HTML; solo admite búsquedas sobre el texto que está por fuera de las etiquetas
  • Campo numérico: un número de punto flotante
  • Campo de tiempo: un valor time.Time, que se almacena con milisegundos de precisión
  • Campo de punto geográfico: 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 su recuperación. Puedes recuperar un solo documento por su ID, un rango de documentos con ID consecutivos o todos los documentos de un índice. También puedes buscar en un índice para recuperar documentos que satisfagan determinados criterios en los campos y sus valores, especificados como una cadena de consulta. Puedes administrar grupos de documentos colocándolos en índices separados.

No hay límite para el número de índices o de documentos por índice que puedes usar. El tamaño total de todos los documentos en un solo índice se limita 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 de Google Cloud para aumentar el tamaño hasta 200 GB.

Consultas

Para hacer búsquedas en un índice, debes generar una consulta, que incluye una cadena de consulta y posibles opciones adicionales. Una cadena de consulta especifica condiciones para los valores de uno o más de los campos del documento. Cuando haces búsquedas en un índice, obtienes como resultado solamente los documentos con campos que satisfagan la consulta.

La consulta más sencilla, llamada a veces "consulta global", es una string que contiene solo valores de campos. La siguiente búsqueda usa una string que busca documentos con las palabras “rose” y “water”:

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

En el siguiente ejemplo, se buscan documentos con campos de fecha que contengan la fecha 4 de julio de 1776, o campos de texto que incluyan la string “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 más términos; cada uno nombrará un campo y una restricción al valor de ese campo. La forma exacta de un término depende del tipo de campo. Por ejemplo, si hay un campo de texto llamado "Product" y un campo de número llamado "Price", aquí 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 lo indica, no son necesarias. Estas habilitan diferentes características, entre ellas:

  • Controlar cuántos documentos se muestran en los resultados de la búsqueda
  • Especificar qué campos de los documentos deben incluirse en los resultados: según la configuración predeterminada, se incluyen todos los campos del documento original, pero puedes especificar que los resultados solo incluyan un subconjunto de campos (el documento original no se ve afectado)
  • Ordenar los resultados
  • Crear “campos procesados” para documentos mediante FieldExpressions y campos de texto abreviados mediante fragmentos
  • Admitir la paginación en los resultados de la búsqueda, para lo que se muestra solo una parte de los documentos coincidentes en cada consulta (con desplazamientos y cursores)

Te recomendamos registrar las strings de consulta en tu aplicación si deseas mantener un registro de las consultas que se ejecutaron.

Resultados de la búsqueda

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

Material de capacitación adicional

Además de esta documentación, puedes leer las dos partes de la capacitación acerca de la API de búsqueda en Google Developer's Academy. (Aunque en la clase se usa la API de Python, el análisis adicional de los conceptos relacionados con la búsqueda podría serte útil).

Documentos y campos

Los documentos se representan mediante structs de Go y comprenden una lista de campos. Además, los documentos se pueden representar mediante cualquier tipo si se implementa la interfaz FieldLoadSaver.

Identificador de documento

Cada documento de un índice debe tener un identificador de documento único, o docID. El identificador puede usarse para recuperar un documento de un índice sin realizar una búsqueda. De forma predeterminada, la API de búsqueda genera de forma automática un docID cuando se crea un documento. También puedes especificar el docID por tu cuenta cuando creas un documento. Un docID debe contener solo caracteres ASCII visibles y que puedan imprimirse (códigos ASCII del 33 al 126 inclusive) y no debe tener más de 500 caracteres en total. Los identificadores de documento no pueden empezar con un signo de exclamación (“!”), y no pueden empezar ni terminar con doble guion bajo (“__”).

Aunque es conveniente crear identificadores de documento únicos, legibles y significativos, no puedes incluir el docID en una búsqueda. Considera la siguiente situación: tienes un índice con documentos que representan partes, en el que se usa el número de serie de la parte como docID. Será muy eficiente para recuperar el documento de cualquier parte individual, pero no permitirá buscar un rango de números de serie junto con otros valores de campo, como la fecha de compra. Este problema se soluciona almacenando el número de serie en un campo atómico.

Campos del documento

Un documento incluye campos que tienen un nombre, un tipo y un solo valor de ese tipo. Dos o más campos pueden tener el mismo nombre, pero diferentes tipos. Por ejemplo, puedes definir dos campos con el nombre "edad": uno de tipo texto (valor "veintidós") y el otro de tipo numérico (valor 22).

Nombres de campos

Los nombres de campos distinguen mayúsculas y minúsculas, y solo pueden contener caracteres ASCII. Deben comenzar con una letra y pueden incluir letras, dígitos o guiones bajos. Los nombres de campo no pueden tener más de 500 caracteres de longitud.

Campos con valores múltiples

Un campo solo puede contener un valor, que debe coincidir con el tipo del campo. No es necesario que los nombres de campo sean únicos. Un documento puede tener múltiples campos con el mismo nombre y el mismo tipo, que es una forma de representar un campo con valores múltiples (sin embargo, no es posible repetir campos numéricos y de fecha con el mismo nombre). Un documento también puede contener múltiples campos con el mismo nombre y tipos distintos.

Tipos de campos

Hay tres tipos de campos que almacenan strings de caracteres; nos referimos a ellos de manera colectiva como campos de string:

  • Campo de texto: una string con una longitud máxima de 1,024**2 caracteres
  • Campo HTML: una string de formato HTML con una longitud máxima de 1,024**2 caracteres
  • Campo atómico: una string con una longitud máxima de 500 caracteres

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

  • Campo numérico: un valor de punto flotante de doble precisión entre -2,147,483,647 y 2,147,483,647
  • Campo de tiempo: un valor time.Time, que se almacena con milisegundos de precisión
  • Campo de punto geográfico: un punto del planeta descrito por sus coordenadas de latitud y longitud

Los tipos de campo de string son el tipo string integrado de Go y los tipos HTML y Atom del paquete search. Los campos numéricos se representan con el tipo float64 integrado de Go, los campos de tiempo usan el tipo time.Time y los campos de punto geográfico, el tipo GeoPoint del paquete appengine.

Tratamiento especial de los campos de string y de tiempo

Cuando se agrega a un índice un documento con campos HTML, de hora o de texto, se producen controles especiales. Es útil entender qué es lo que sucede internamente para poder usar la API de búsqueda de manera efectiva.

Asignación de tokens a campos de string

Cuando se indexa un campo HTML o de texto, se asignan tokens a su contenido. La string se divide en tokens interpretando como separadores los espacios o caracteres especiales (signos de puntuación, numeral, barra invertida, entre otros). El índice incluirá una entrada para cada token. Esto te permite buscar palabras clave y frases que correspondan a solo una parte del valor de un campo. Por ejemplo, la búsqueda de la palabra “dark” mostrará un documento con un campo de texto que contenga la string “it was a dark and stormy night”, y la búsqueda de la palabra “time” mostrará un documento con un campo de texto que contenga “this is a real-time system”.

En los campos HTML, el texto dentro de las etiquetas de lenguaje de marcado no contiene tokens, por lo que un documento con un campo HTML que contenga it was a <strong>dark</strong> night coincidirá con la búsqueda “night”, pero no con “strong”. Si quieres ser capaz de buscar texto del lenguaje de marcado, almacénalo en un campo de texto.

No se realiza la asignación de tokens en los campos Atom. Un documento con un campo Atom que tenga el valor "bad weather" solo aparecerá en una búsqueda de la string completa "bad weather", pero no en las búsquedas de las palabras individuales "bad" o "weather".

Reglas de asignación de tokens
  • Los caracteres guión bajo (_) y et (&) no dividen las palabras en tokens.

  • Los siguientes 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 carácter nulo.

  • Los siguientes caracteres se tratan como puntuación y dividen las palabras en tokens:

    !"%()
    *,-|/
    []]^`
    :=>?@
    {}~$
  • Los caracteres de la siguiente tabla por lo general dividen las palabras en tokens, pero se pueden tratar de otra manera según el contexto en el que aparezcan:

    Carácter Regla
    < En un campo HTML, el signo "menor que" indica el principio de una etiqueta HTML que se ignora.
    + Una string de uno o varios signos "más" se trata como parte de la palabra si aparece al final (C++).
    # El símbolo "numeral" se trata como parte de la palabra si aparece después de las letras a, b, c, d, e, f, g, j o x (a# hasta g# son notas musicales; j# y x# son lenguajes de programación; c# entra en ambas categorías). Si a un término lo antecede este símbolo (#google), se lo trata como un hashtag y el numeral se convierte en parte de la palabra.
    ' El apóstrofo se considera una letra si antecede a la letra "s" seguida de un espacio, como en "John's hat".
    . Si un punto decimal aparece entre dígitos, es parte de un número (es decir, el separador decimal). También puede ser parte de una palabra si se lo usa en una sigla (A.B.C).
    - El guión medio es parte de una palabra si se usa en una sigla (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 puntuación y dividen las palabras en tokens.

  • Cualquier otro elemento se analiza como carácter UTF-8.

Acrónimos

La asignación de tokens usa reglas especiales para reconocer acrónimos (strings como “I.B.M”, “a-b-c” o “C I A”). Un acrónimo es una string de caracteres alfabéticos individuales que tiene el mismo separador entre cada uno de los caracteres. Los separadores válidos son el punto, el guión medio o cualquier cantidad de espacios. El carácter separador se quita de la string cuando se asignan tokens a una sigla. Así, las strings de ejemplo mencionadas antes se convierten en los tokens "ibm", "abc" y "cia". El texto original permanece en el campo del documento.

Al trabajar con siglas, ten en cuenta lo siguiente:

  • Una sigla no puede contener más de 21 letras. Una string de sigla válida de más de 21 caracteres se dividirá en una serie de siglas con 21 letras o menos cada una.
  • Si las letras de una sigla están separadas por espacios, todas ellas deben estar en mayúscula o en minúscula. Las siglas que incluyen puntos o guiones medios pueden combinar mayúsculas y minúsculas.
  • Al buscar una sigla, puedes ingresarla en su forma canónica (la string sin separadores) o bien con punto o guión medio (uno de los dos, no ambos) entre las letras. Por lo tanto, el texto "I.B.M" se puede recuperar mediante cualquiera de los siguientes términos de búsqueda: "I-B-M", "I.B.M" o "IBM".

Exactitud del campo de tiempo

Cuando creas un campo de tiempo en un documento, le asignas el valor time.Time. A fin de indexar y buscar el campo de tiempo, se ignora cualquier componente de tiempo y la fecha se convierte en la cantidad de días a partir del 1/1/1970 UTC. Esto significa que, aunque un campo de tiempo puede contener un valor de tiempo preciso, una consulta de fecha solo puede especificar un valor de campo de tiempo en el formato yyyy-mm-dd. Esto también significa que no está bien definido el orden de clasificación de los campos de tiempo con la misma fecha. Si bien el tipo time.Time representa el tiempo con precisión de nanosegundos, la API de búsqueda los almacena solo con precisión de milisegundos.

Otras propiedades de los documentos

La clasificación de un documento es un número entero positivo que determina el orden predeterminado de los documentos que se muestran en una búsqueda. De manera predeterminada, la clasificación se configura cuando se crea el documento con el número de segundos transcurridos desde el 1 de enero de 2011. Puedes configurar la clasificación de manera explícita cuando creas un documento. No conviene que asignes la misma clasificación a muchos documentos y, en ninguna circunstancia, deberías asignar la misma clasificación a más de 10,000 documentos. Si especificas las opciones de ordenamiento, puedes usar la clasificación como clave de ordenamiento. Ten en cuenta que, cuando se usa la clasificación en una expresión de orden o una expresión de campo, se hace referencia a ella como _rank. Consulta la referencia de DocumentMetadata para obtener más información acerca de la configuración de la clasificación.

La propiedad de lenguaje del struct Field especifica el lenguaje que se usa para codificar ese campo.

Cómo establecer vínculos desde un documento a otros recursos

Puedes usar docID y otros campos de un documento como vínculos a otros recursos en la aplicación. Por ejemplo, si usas Blobstore, puedes asociar el documento con un BLOB específico mediante la configuración del docID o el valor de un campo Atom a la BlobKey de los datos.

Crea 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 se construye un valor User 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

Agrega documentos a un índice

Cuando agregas documentos a un índice, este se copia en un 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 y agregarle un documento.

// ...
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 agregas un documento en un índice que ya contiene un documento con el mismo docID, el nuevo reemplaza al antiguo. No se da ninguna advertencia. Puedes llamar a Index.Get antes de crear o agregar un documento a un índice para verificar si ya existe un docID específico.

El método Put muestra un docID. Si no especificaste el docID, puedes examinar el resultado para detectar el docID que se generó:

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. Los índices persistentes se crean la primera vez que les agregas un documento con el método put.

Actualiza documentos

Los documentos no pueden modificarse una vez agregados a un índice. No puedes agregar o quitar campos ni cambiar sus valores. Sin embargo, puedes reemplazar el documento por uno nuevo que tenga el mismo docID.

Recupera documentos por docID

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

Busca documentos por contenido

Para recuperar documentos de un índice, construye una cadena de consulta y llama a Index.Search. Search muestra un iterador que genera documentos coincidentes en orden decreciente.

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

Borra un índice

Cada índice consiste en sus documentos indexados y un esquema de índice. Para borrar un índice, borra todos los documentos que lo integran y luego el esquema de índice.

Puedes borrar documentos de un índice si especificas el docID del documento que deseas borrar 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)
}

Coherencia eventual

Cuando agregas, actualizas o borras un documento de un índice, el cambio se propaga entre múltiples centros de datos. Esto suele ocurrir rápido, pero el tiempo que demora puede variar. La API de búsqueda garantiza la coherencia eventual. Esto significa que, en algunos casos, una búsqueda o una recuperación de uno o más documentos puede mostrar resultados que no reflejan los cambios más recientes.

Esquemas de índice

Cada índice tiene un esquema que muestra todos los nombres y tipos de campo de los documentos que contiene. No puedes definir un esquema por tu cuenta. Los esquemas se mantienen de manera dinámica; se actualizan a medida que se agregan documentos al índice. Un esquema simple puede verse así, en formato JSON:

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

Cada clave del diccionario es el nombre de un campo del documento. El valor de la clave es una lista de los tipos de campo en uso con ese nombre de campo. Si usaste el mismo nombre de campo con diferentes tipos de campo, el esquema mostrará más de un tipo de campo por nombre de campo:

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

Una vez que un campo aparece en un esquema, no se lo puede quitar. No hay forma de borrar un campo, incluso si el índice ya no contiene ningún documento con ese nombre de campo.

Un esquema no define una "clase" en el sentido de programación de objetos. Con relación 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, deberás contemplar esa abstracción en tu código. Por ejemplo, podrías garantizar que todos los documentos con el mismo conjunto de campos se mantengan en su propio índice. El esquema de índice podría verse como la definición de la clase, y cada documento del índice sería una instancia de la clase.

Visualiza índices en la consola de Google Cloud

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

Cuotas de la API de búsqueda

La API de búsqueda incluye varias cuotas sin cargo:

Recurso o llamada a la API Cuota gratuita
Almacenamiento total (índices y documentos) 0.25 GB
Consultas 1,000 consultas por día
Agregar documentos a los índices 0.01 GB por día

La API de búsqueda impone los siguientes límites a fin de garantizar la fiabilidad del servicio. Los límites se aplican por igual a las aplicaciones gratuitas y a las pagas:

Recurso Cuota de seguridad
Uso máximo de consultas 100 minutos agregados de tiempo de ejecución de consultas por minuto
Máximo de documentos agregados o borrados 15,000 por minuto
Tamaño máximo por índice (se permite un número ilimitado de índices) 10 GB

El uso de API se calcula de distintas maneras según el 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 agregas documentos a los índices, se tienen en cuenta el tamaño de cada documento y la cantidad en el cálculo de la cuota de indexación.
  • Todas las demás llamadas a la API de búsqueda se cuentan según la cantidad de operaciones que involucran:
    • Index.Get: Se cuenta 1 operación por cada documento mostrado o 1 operación si no se muestra nada.
    • Index.Delete: Se cuenta 1 operación por cada documento de la solicitud o 1 operación si la solicitud está vacía.

Se impone la cuota de capacidad de procesamiento de consultas para que ningún usuario individual pueda monopolizar el servicio de búsqueda. Debido a que las consultas se pueden ejecutar de manera simultánea, a cada aplicación se le permite ejecutar consultas que consuman hasta 100 minutos de ejecución por minuto de reloj. Si ejecutas muchas consultas cortas, probablemente no alcances este límite. Una vez que excedas la cuota, fallarán las consultas subsiguientes hasta que comience un nuevo período, cuando se restablecerá tu cuota. Los períodos de la cuota no son estrictamente de un minuto; se usa una variación del algoritmo de cubeta con goteo para controlar el ancho de banda de búsqueda en incrementos de cinco segundos.

Puedes obtener más información en la página Cuotas. Cuando una aplicación intenta exceder estas cantidades, se muestra un error de cuota insuficiente.

Ten en cuenta que, aunque estos límites se aplican por minuto, la consola muestra los totales diarios de cada uno. Los clientes con asistencia de nivel Plata, Oro o Platino pueden comunicarse con su representante de asistencia para solicitar el aumento de los límites de capacidad de procesamiento.

Precios de la API de búsqueda

Los siguientes cargos se aplican al uso cuando se superan las cuotas gratuitas:

Recurso Costo
Almacenamiento total (índices y documentos) $0.18 por GB por mes
Consultas $0.50 por 10,000 consultas
Indexación de documentos que se pueden buscar $2.00 por GB

Para obtener más información, consulta la página Precios.