Skip to content

Latest commit

 

History

History
42 lines (27 loc) · 5.16 KB

README.md

File metadata and controls

42 lines (27 loc) · 5.16 KB

Gist de intentos de querys: https://gist.github.com/manurtinez/4447bf6793d13bde6a70878269165e10

En este readme se especifica el proceso de desarrollo que fuimos llevando para construir la app y las dificultades encontradas.

Empezando el proyecto, instancia de docker de ES y levantandolo

El primer paso de todos fue descargar una aplicacion starter de la pagina initialize de spring boot, a forma de tener un punto de partida, con las dependencias ya agregadas (solo necesitamos la de spring data y elasticsearch). Procedimos a levantar el proyecto para ver que minimamente corriera.

Acto seguido, habia que crear los containers necesarios en Docker (o en su defecto, instalar elastic search localmente) para que la aplicacion pudiera luego conectarse. Para esto, creamos un docker compose que unicamente contiene un nodo de elastic y uno de kibana, de forma de tener una interfaz con la cual hacer pruebas.

Esta etapa fue muy de aprendizaje sobre elastic, sus filosofias, su forma de modelar los datos y mas importante, su forma de indizarlos y obtenerlos. Esto se hace mediante requests HTTP convencionales, con body JSON que define que tipo de query se quiere hacer.

Una vez levantado el ambiente de elastic en docker, la app es muy facil de configurar mediante un archivo application.properties para que sepa a que URL y puerto conectarse, entre otras cosas. En este punto, teniamos una conexion exitosa entre la base de datos y la aplicacion (sin ningun codigo escrito aun).

Luego, probamos escribir un par de entidades muy simples, con sus respectivos controllers / services / repositories para probar hacer un CRUD de las mismas, y ver las diferencias entre elastic y las DB relacionales que veniamos manejando.

transladando el modelo

Durante la 'migracion' de modelos a nuestro nuevo esquema, una de las dificultades con la que nos encontramos fué relacionado a la manipulación de datos tipo Date; Sobre la cual es necesario especificar un formato particular, nos costó encontrar este bug dado que si la BBDD queda con datos 'basura' de algun run, al querer configurar lo mencionado, saltan errores de tipos provenientes de los indices del run anterior, ya que la DB no hace un "create" de cero con cada run.

Otra de las dificultades fué la representación de las relaciones entre modelos. En el proyecto original las relaciones se declaraban mas bien bidireccionalmente, pero en este caso, para ElasticSearch los declararemos desde un único lado (caso contrario podría generar un bucle infinito, terminando en error). Respecto a lo mencionado, en el proyecto anterior, además representabamos estas relaciones normalmente con HashSets, mientras que ahora debemos declararles como ArrayLists.

Por ultimo, otro cambio que tuvimos que hacer al modelo fue respecto a la join table "ProductOnSale", la cual llevaba la cuenta de que productos estaban a la venta, con foreign keys haciendo referencia al producto y provider correspondiente. Al no haber concepto de join tables ni foreign keys en elastic, decidimos prescindir de la referencia al product, y tener los ProductOnSale directamente representados como un array dentro de Provider. Teniendo en cuenta que la repeticion de datos no importa mucho en estos tipos de BDs, se puede guardar el objeto completo para luego hacer queries mucho mas rapidas.

Transladando queries

En esta etapa, la idea era transladar algunas de las queries estadisticas que se usaban en el trabajo. Se pueden definir varias implementaciones para estas:

  • Con la annotation @Query
  • Dejando que spring data las implemente segun el nombre
  • Haciendo implementacion customizada

Para solo muy pocas, pudimos dejar que spring data realize la implementacion automaticamente, ya ea mediante el nombre o el @Query. Para el resto de las que pudimos hacer, hubo que implementarlas manualmente. Esto se hace creando una clase Repository custom para la entidad, y hacer que la interfaz extienda al mismo. Hubo que usar la clase middleware RestHighLevelClient, que provee una api de interaccion con elastic search, creando manualmente las queries y condiciones necesarias. Para el caso de las queries normales, por ejemplo, de tipo Boolean (documentoxs que cumplan X condicion), resulta relativamente simple. La dificultad se presento cuando habia que hacer aggregations (mayoria de casos) ya que se necesitaba sacar estadisticas de los documentos, y algunos de ellos estan anidados, requiriendo una nested query para extraer la informacion.

Las siguientes queries quedaron hechas:

  • Category with less products
    public String getCategoryWithLessProducts() throws IOException {
    // Se crea la aggregation, con nombre "less_category", que tiene en cuenta el
    // campo "category.name" (keyword es para indicar el tipo de dato de elastic), y
    // orden por count, ascendente (queremos el primero, menor)
    TermsAggregationBuilder aggregation = AggregationBuilders.terms("less_category").field("category.name.keyword")
    .order(BucketOrder.aggregation("_count", true));
    // Creo un builder para la busqueda, que usa la aggregation anterior y un tamaño
    // de 1 (me esta ignorando ese size, pero se supone que deberia limitarlo)
    SearchSourceBuilder builder = new SearchSourceBuilder().aggregation(aggregation).size(1);
    // Luego se asigna ese builder al request que se va a hacer para el indice
    // "product"
    SearchRequest searchRequest = new SearchRequest("product").source(builder);
    // Se hace la busqueda efectiva y se extraen los resultados, de tipo Terms
    // (porque la aggregation fue terms),
    // para la aggregation con nombre "less_category".
    SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
    Terms terms = response.getAggregations().get("less_category");
    // No pude encontrar forma de traer directamente el documento desde el
    // aggregation, asi que devuelvo el nombre para hacer un get
    // con la key (el nombre, ya que busque sobre category.name)
    return terms.getBuckets().size() > 0 ? terms.getBuckets().get(0).getKeyAsString() : null;
    }
  • Most used delivery method
    public String getMostUsedDeliveryMethod() throws IOException {
    // Hace falta crear una aggregation de tipo nested para hacer aggregate de datos
    // dentro del array purchases (que es de tipo nested)
    NestedAggregationBuilder aggregation = AggregationBuilders.nested("nested", "purchases")
    .subAggregation(AggregationBuilders.terms("top_delivery").field("purchases.deliveryMethod.name.keyword")
    .order(BucketOrder.aggregation("_count", false)));
    SearchSourceBuilder builder = new SearchSourceBuilder().aggregation(aggregation);
    SearchRequest request = new SearchRequest("user").source(builder);
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // La aggregation al ser nested retorna un tipo ParsedNested, el cual tiene la
    // otra aggregation adentro
    ParsedNested nestedAggrResults = response.getAggregations().get("nested");
    // Se extrae la aggregation de adentro de la nested
    Terms deliveryResults = (Terms) nestedAggrResults.getAggregations().asMap().get("top_delivery");
    // Se retorna el nombre del primer resultado, que como esta ordenado de forma
    // decreciente, sera el mas usado
    return deliveryResults.getBuckets().get(0).getKeyAsString();
    }
  • get purchases in period
    public List<Purchase> getPurchasesInPeriod(Date start, Date end) throws IOException {
    // Creo el search request con el index(es) a buscar
    SearchRequest searchRequest = new SearchRequest("purchase");
    // Defino el formato que tendra la fecha
    DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz");
    // Creo el query builder, que define como se va a construir la query efectiva a
    // hacer
    QueryBuilder queryBuilder = QueryBuilders.boolQuery()
    .must(QueryBuilders.rangeQuery("dateOfPurchase").gte(dateFormat.format(start)).lte(dateFormat.format(end)));
    // Creo el search source builder con la query anterior, y se lo asigno al
    // request
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    searchSourceBuilder.query(queryBuilder);
    searchRequest.source(searchSourceBuilder);
    // Se hace la busqueda
    SearchResponse res = client.search(searchRequest, RequestOptions.DEFAULT);
    return getSearchResult(res);
    }
  • get top N users by purchase (esta quedo planteada pero tiene bugs)