Azure Cosmos DB nos permite ejecutar consultas por medio de SQL (Structured Query Language). Sin embargo, no permite ejecutar consultas mas sofisticadas sobre campos de texto, por ejemplo: búsquedas de texto completo (full-text search).

Azure Search nos permite realizar este tipo de búsquedas, y otras mas sofisticadas (Lucene query, fuzzy, proximity, phonetic, etc). Y actualmente la integración con Azure Cosmos DB (y con otros motores de persistencia) resulta muy fácil por medio de indexadores. Que se encargan de indexar los datos persistidos sobre un índice de Azure Search. Esto se configura para que ocurra cada intervalo de tiempo sobre los registros nuevos.

Vamos a realizar la integración con Azure Comos DB sobre una solución simple.

Creando servicios

Lo primero que debemos hacer es crear los servicios para Azure Cosmos DB y Azure Search.

Vamos hacia el portal de Azure y buscamos el servicio de Cosmos DB:

Capturamos todos los datos para crear el nuevo servicio, la base de datos y la colección la crearemos mas adelante desde una solución de visual studio. Una vez que se crea el servicio debemos poder ver el dashboard con el detalle.

Aquí debemos extraer las llaves de acceso para poder establecer una conexión y poder manipular las colecciones. En el dashboard del servicio vamos al apartado que dice Keys y obtenemos las llaves de acceso.

Para el servicio de Azure Cosmos DB necesitamos: URI y PRIMARY KEY.

Ahora vamos a crear el servicio de Azure Search.

Aqui siemplemente escribimos un nombre para el servicio, y tambien escogemos el costo de acuerdo al nivel de servicio. Una vez creado el servicio debemos poder acceder al dashboard:

Una vez creado el servicio extraemos las llaves que ocuparemos para conectarnos.

Para el servicio de Azure Search necesitamos: PRIMARY KEY y URL de servicio.

Ya que recuperamos las llaves de acceso de cada uno de los servicios vamos al codigo.

Agregando datos a Cosmos DB

Sobre una solución nueva en ASP .NET MVC insertaremos datos a una colección de Azure Cosmos DB y después crearemos un indexador sobre los datos guardados para poder hacer búsquedas con el API de Azure Search.

Nuestra aplicación permitirá guardar productos con estas propiedades:

  • Id:string
  • Name:string
  • Description:string
  • Price:double (nullable)
  • IsDeleted:bool

Primero es necesario establecer nuestras llaves de configuración que recuperamos para Azure Cosmos DB sobre el archivo web.config.

Las llaves de configuración contienen el nombre de la base de datos que en nuestro caso es “productsdb” y el nombre de la colección que es “product”. Adicional también están las llaves que recuperamos desde el dashboard del portal de Azure (CosmosEndpoint y CosmosKey).

Una vez que establecemos los valores en configuración nos debemos de asegurar de que siempre exista la base de datos y la colección antes de realizar cualquier operación. Vamos a crear la base y la colección cuando inicie la aplicación. Esto lo podemos hacer en el Satartup.cs en caso de tener habilitado OWIN (o si es ASP .NET Core) o en el Global.asax.cs.

En nuestro caso colocaremos la inicialización en el método Application_Start del archivo Global.asax.cs 

La ultima línea del metodo DocumentDBRepository<Product>.Initialize() se encarga de inicializar todo lo que requerimos para Cosmos DB, aquí la implementación de lo que hace el método Initialize:

Básicamente obtiene los valores de configuración de CosmosEndpointCosmosKey. Para después crear la base de datos y la colección si no existen. Esta es una alternativa muy pragmática y no eficiente. En realidad, lo que hace es ejecutar un método de lectura y si marca excepción entonces crea la colección o la base de datos. Pero NO es nada eficiente en ambientes productivos ya que si tenemos muchos datos se va a tardar mucho ese proceso. Es solo para fines de la prueba de concepto.

Para mas detalle de la implementación completa de la clase DocumentDBRepository en el repositorio GutHub.

Ahora vamos al proceso de inserción de documentos/entidades.

Este es el formulario donde capturamos los nuevos documentos:

Formulario de captura y búsqueda

El formulario recibe los campos que ya habíamos listado antes y hace una petición por medio de ajax hacia el controlador para persistir el nuevo documento/entidad sobre Azure Cosmos DB.

Esta es la definición de la acción del controlador que recibe un objeto Producto que contiene lo que se capturo. Y de nuevo se ocupa el método estático CreateItemAsync de la clase DocumentDBRepository. Internamente invoca los objetos del SDK de Azure Cosmos DB para persistir el nuevo documento/entidad.

Mas adelante vemos la definición de la clase Producto.

Esto es todo lo que tenemos que hacer del lado de Azure Cosmos DB, solo estamos insertando documentos. Pero todavía no integramos nada con Azure Search para hacer búsquedas.

Integrando Azure Search

Ya tenemos nuestro modulo que se encarga de hacer la inserción de nuevos documentos/entidades. Pero aun ningún documento que tenemos en Cosmos DB existe en el índice de Azure Search. Para esto debemos crear un indexador (Indexer) que es el encargado de realizar el proceso de “importación” de documentos hacia nuestro índice de Azure Search. Este proceso que realiza el Indexador lo realiza de forma automática sobre un intervalo de tiempo definido.

Lo primero que debemos hacer es crear nuestro indexador al momento de que inicia la ejecución de la aplicación. Este indexador solo lo necesitamos crear si no existe.

Vamos a establecer nuestros valores de configuración para el servicio de Azure Search en la misma solución sobre el web.config.

Adicional a las llaves de conexión que recuperamos antes agregamos la llave SearchIndexName que contiene el nombre de nuestro índice donde se hará la inserción de lo que vamos agregando a la colección de Cosmos DB.

Y de nuevo en el método Application_Start del archivo Global.asax.cs agreamos estas lineas que inicializan y crean el indexador.

Agregamos dos líneas mas que inicializan el índice y crean el indexador si es que aun no existe. Veamos la definición de cada uno de los métodos.

El primer método SearchRepository<Product>.Intialize() se encarga de inicializar el índice y su definición basado en el tipo de la entidad Product que se recibe como genérico de la clase SearchRepository.

El método principal CreateIndexAndGetClient es el que se encarga de obtener las llaves de configuración y crear el índice de acuerdo a su definición. La definición del indice se establece en la clase Product.

Esta clase contiene varios atributos sobre cada una de las propiedades, y cada atributo corresponde a distintos objetivos. Los atributos del serializador para json JsonProperty se utilizan nada mas para Cosmos DB, para que guarde el documento/entidad con las propiedades serializadas en camelCase esto es en especial para la propiedad ID.

Adicional tenemos atributos que corresponden a la definición del índice de Azure Search, estos atributos definen la forma de indexar las propiedades, y estos atributos son: Analyzer, IsFilterable, IsSortable y IsSearchable.

En otra entrada explicaba a que se refiere cada uno de estos atributos: aquí.

Como ya lo explicaba antes, es importante tener primero nuestro índice creado para poder crear el indexador, encargado de hacer la importación de documentos/entidades.

Una vez que ya tenemos la creación de nuestro índice vamos a ver el proceso de creación del indexador.

Ahora vamos a revisar el detalle del método SearchRepository<Product>.IntializeIndexer() que originalmente se invoca después del método Intialize cuando el sitio web inicia en el método Application_Start.

El método que contiene la mayoría de la logia para crear el indexador esta en CreateIndexer vamos a revisarlo a detalle.

Primero se genera un nombre basado en el tipo de dato del elemento genérico que recibe la clase, con este nombre se valida si existe el indexador, esto es para asegurarnos de crear el indexador solo una vez.

El objeto que expone los métodos para manipular los indexadores es SearchServiceClient, este objeto expone un método CreateOrUpdate que permite crear o actualizar un Indexer (indexador). El método recibe el nombre, si esta habilitado, un objeto que indica el intervalo de tiempo de ejecución, y el nombre del índice donde hará la importación de documentos/entidades. Adicional recibe el nombre de un DataSource, este DataSource se tiene que crear previamente.

Se crea en el método CreateDataSource. Este DataSource se encarga de establecer la fuente de datos/documentos para realizar la importación hacia el índice. En este caso nuestra fuente de datos proviene de Azure Cosmos DB.

En la implementación del método vemos que recuperamos toda la información de Cosmos DB para generar la cadena de conexión y el nombre de la colección. También establecemos un query que es el encargado de obtener lo ultimo insertado y que no existe aun sobre el índice de Azure Search. Este query se ejecuta cada intervalo de tiempo definido previamente. Este query determina los documentos que fueron agregados o actualizados. Dentro del objeto DataSource se establece una política para determinar la forma en la cual va a detectar nuevos elementos o cambios, para mas detalle aquí.

El método también valida si ya existe previamente el DataSource, a menos que se indique explícitamente que se actualice. Todas las operaciones sobre los DataSource creados los expone el mismo objeto SearchServiceClient. Finalmente se crea el DataSource, o se obtiene y el método lo devuelve.

Al momento de crear el Indexador inicia la ejecución de acuerdo al valor de la propiedad Schedule.StartTime definido con el objeto IndexingSchedule.

Sincronizando registros nuevos

Ya que tenemos nuestro Indexador creado lo único que tenemos que hacer es insertar documentos sobre Cosmos DB, después cada intervalo de tiempo el indexador se encargara de hacer la sincronización de los nuevos registros para indexarlos sobre el índice de Azure Search.

Vamos a agregar un par de productos desde el portal:

 

Captura de producto 1

 

Captura producto 2

 

Si entramos al portal de Azure podemos ver cada vez que se ejecuta el Indexador y también cuantos documentos nuevos agrego al índice.

En el portal de Azure si navegamos al servicio de Azure Search, en el dashboard en la parte de abajo podemos ver información del Indexador.

Dashboard Idexers

También podemos entrar al detalle de Indexador y ver que fue lo que sincronizó en cada ejecución.

Realizando búsquedas

Todo lo que ya se sincronizó al índice de Azure Search se puede buscar. Desde nuestra interfaz tenemos un buscador, vamos a realizar unas pruebas:

Buscando “cellular data”
Buscando “apple game”
Buscando “mac watch”
Buscando “mac”

 

El método que se utiliza para las búsquedas es SearchRepository<Product>.SearchPhrase(phrase)  en el argumento phase recibe el texto a buscar.

Recibe unos argumentos adicionales que permiten indicar si el query esta basado en la sintaxis de Lucene. Y en la respuesta simplemente se obtiene un listado de los documentos que coincidieron con la búsqueda.

El código completo de la solución: GitHub