Mail me on jrochelly@gmail.com

Elasticsearch: Search and N+1 queries' problems

Usually when we have some app that needs a search feature, it contains relationships between tables, for example, a Store app, where you have product related to category, manufacturer, so on and so forth. It's possible the database to suffer N+1 queries by using just simple searches with Elasticsearch or any database. But we can get rid of this with indexes.

Picture we have a Library application. Books and authors are fully bonded in a many-to-many relationship. In book.rb model we have:

 1 class Book < ActiveRecord::Base
 2   has_and_belongs_to_many :authors
 3 
 4   include Tire::Model::Search
 5   include Tire::Model::Callbacks
 6 
 7   mapping do
 8     indexes :id
 9     indexes :title, analyzer: 'snowball'
10     indexes :summary, analyzer: 'snowball'
11     indexes :released_at, type: 'date'
12     indexes :edition
13     indexes :isbn, analyzer: 'keyword'
14   end
15 
16   def self.search(params)
17     tire.search(load: true) do
18       query { string params[:query] } if params[:query].present?
19     end
20   end
21   
22 end

In the controller:

1 def index
2   @books = Book.search(params)
3 end

As you can see above, a normal gem setup. So in view we call authors and there's where the issue lies:

1 By <%= book.authors.map { |a| link_to a.name, author_path(a.id) }.join(', ').html_safe %>

The above author call result in a N+1 queries we want to avoid.

image
Rails console: N+1 queries issue

Solution

What we may do is to add a block with authors indexes, leaving it like this:

 1 mapping do
 2     indexes :id
 3     indexes :title, analyzer: 'snowball'
 4     indexes :summary, analyzer: 'snowball'
 5     indexes :released_at, type: 'date'
 6     indexes :edition
 7     indexes :isbn, analyzer: 'keyword'
 8     indexes :author do
 9       indexes :id
10       indexes :name
11     end
12   end

Still in the model, it will be needed to overwrite the to_indexed_json method so we can include the authors:

1 def to_indexed_json
2   to_json include: :authors
3 end

Also, there's no need to make elasticsearch load from the db anymore. Then let's remove the (load: true) chunk of self.search method.

Reindex everything using the command rake environment tire:import CLASS='Book' FORCE=true. After reloading the page::

image
Rails console: No loadings! Yay!

No data loaded from db. Much faster! I hope this has helped. If you know another way, please, share with us.

Elasticsearch: Busca e problemas de queries N + 1

Geralmente quando temos alguma aplicação que precisa de busca, ela contem relacionamentos entre tabelas, como por exemplo uma App de Loja, onde você tem produto relacionado com categoria, fabricante, etc. Se usarmos o Elasticsearch na aplicação, é possível que o banco de dados sofra com queries N + 1. Podemos acabar com isso definindo alguns índices.

Imagine que temos uma aplicação de uma Livraria. Livros e autores estão inteiramente ligados por um relacionamento N - M. No model book.rb temos:

 1 class Book < ActiveRecord::Base
 2   has_and_belongs_to_many :authors
 3 
 4   include Tire::Model::Search
 5   include Tire::Model::Callbacks
 6 
 7   mapping do
 8     indexes :id
 9     indexes :title, analyzer: 'snowball'
10     indexes :summary, analyzer: 'snowball'
11     indexes :released_at, type: 'date'
12     indexes :edition
13     indexes :isbn, analyzer: 'keyword'
14   end
15 
16   def self.search(params)
17     tire.search(load: true) do
18       query { string params[:query] } if params[:query].present?
19     end
20   end
21   
22 end

No controller temos:

1 def index
2   @books = Book.search(params)
3 end

Como você pode ver acima, uma configuração normal da gem. Então na view, chamamos os autores e é aqui que mora o problema:

1 By <%= book.authors.map { |a| link_to a.name, author_path(a.id) }.join(', ').html_safe %>

A de chamada autores acima faz N + 1 queries, e é isso que queremos evitar:

image
Rails console: Problema de queries N + 1

Solução

O que podemos fazer é adicionar um block com índices de autores, ficando assim:

 1 mapping do
 2     indexes :id
 3     indexes :title, analyzer: 'snowball'
 4     indexes :summary, analyzer: 'snowball'
 5     indexes :released_at, type: 'date'
 6     indexes :edition
 7     indexes :isbn, analyzer: 'keyword'
 8     indexes :author do
 9       indexes :id
10       indexes :name
11     end
12   end

Ainda no model, será necessário sobrescrever o método to_indexed_json para que possamos incluir autores:

1 def to_indexed_json
2   to_json include: :authors
3 end

Também, não precisamos mais fazer o elasticsearch carregar a partir do banco (puxar informações como objetos), então podemos tirar o trecho (load: true) do método self.search.

Reidexamos tudo com o comando rake environment tire:import CLASS='Book' FORCE=true. E ao recarregar a página:

image
Rails console: No loadings! Yay!

Sem informações vindas do banco. Muito mais rápido! Espero que tenha ajudado. Se você conhece outra maneira, por favor, compartilhe.

Saving Google Maps waypoints into database

Currently I'm building an app that shows bus routes and it's important for the project to save the waypoints, instead of just start point and end point. Thus, I'm capable to replicate the route exactly same as saved.

So, let's see the basic code.

 1 <div class="map">
 2   <div id="map-canvas"/>
 3 </div>
 4 <!-- footer -->
 5 <script type="text/javascript"
 6   src="https://maps.googleapis.com/maps/api/js?key=AIzaSyDc7y0hbNmHzHKgaqe7VCl6a--P4VAW2lU&sensor=false">
 7 </script>
 8 <script src="maps.js"></script>
 9 <script>
10   $( document ).ready(function() {
11     initialize(true);
12     calcRoute();
13   })
14 </script>

The above code is used to create a new one, it allows you to edit the route and save it. To be clear I'm using google maps V3.

Now let's go to important thing. Don't forget to call the script as well. Obs.: I also use jQuery, so put it on the line.

The code I'm using takes basically two steps, initialize() which is responsable for the definitions of the map (ie. center, maxZoomLevel, panControl, boundaries, etc.), it also receive one boolean parameter used to define whether the route can be draggleble or not. I set false for the show route page, where you can navigate the map, but cannot edit. The calcRoute() has a simple task: take a default route to show.

Bellow, the map script:

  1 var rendererEditOptions = {
  2   draggable: true
  3 };
  4 var rendererOptions = {
  5   draggable: false,
  6   suppressMarkers: true
  7 };
  8 var ren,
  9     ser = new google.maps.DirectionsService(),
 10     data = {},
 11     map, marker,
 12     palmas = new google.maps.LatLng(-10.204164, -48.3332),
 13     minZoomLevel = 12;
 14 function initialize(edit) {
 15   var mapOptions = {
 16     center: palmas,
 17     zoom: 14,
 18     panControl:false,
 19     streetViewControl:false,
 20     maxZoom: 18,
 21     minZoom: minZoomLevel
 22   };
 23   var map = new google.maps.Map(document.getElementById("map-canvas"),
 24       mapOptions);
 25   // Get's the boolean parameter to set the route to be draggable or not.
 26   if (edit) {
 27     ren = new google.maps.DirectionsRenderer(rendererEditOptions);
 28   } else {
 29     ren = new google.maps.DirectionsRenderer(rendererOptions);
 30   };
 31   ren.setMap(map); // Make map shows up
 32   google.maps.event.addListener(ren, 'directions_changed', function() {
 33     computeTotalDistance(ren.getDirections());
 34   });
 35   // Limit bounds to Palmas
 36   var limitBounds = new google.maps.LatLngBounds(
 37     new google.maps.LatLng(-13.2906, -51.0310167),
 38     new google.maps.LatLng(-4.6734667, -45.2803667)
 39   );
 40   // Listen for the dragend event
 41    google.maps.event.addListener(map, 'drag', function() {
 42      if (limitBounds.contains(map.getCenter())) return;
 43      // When on the bound limit - Move the map back within the bounds
 44      var c = map.getCenter(),
 45          x = c.lng(),
 46          y = c.lat(),
 47          maxX = limitBounds.getNorthEast().lng(),
 48          maxY = limitBounds.getNorthEast().lat(),
 49          minX = limitBounds.getSouthWest().lng(),
 50          minY = limitBounds.getSouthWest().lat();
 51      if (x < minX) x = minX;
 52      if (x > maxX) x = maxX;
 53      if (y < minY) y = minY;
 54      if (y > maxY) y = maxY;
 55      map.setCenter(new google.maps.LatLng(y, x));
 56    });
 57    // Limit the zoom level
 58   google.maps.event.addListener(map, 'zoom_changed', function() {
 59     if (map.getZoom() < minZoomLevel) map.setZoom(minZoomLevel);
 60   });
 61 }
 62 // Shows a default route so we can drag and edit the way we want.
 63 function calcRoute() {
 64   var request = {
 65       origin: "Quadra 101 Norte, Av. Teotônio Segurada, Palmas - TO",
 66       destination: "Quadra 1102 Sul, Av. Teotônio Segurada, Palmas - TO",
 67       travelMode: google.maps.TravelMode.DRIVING
 68   };
 69   ser.route(request, function(response, status) {
 70     if (status == google.maps.DirectionsStatus.OK) { ren.setDirections(response) }
 71   });
 72 }
 73 // Used for editing the saved route
 74 function loadRoute(os){
 75   var wp = [];
 76     for(var i=0;i < os.waypoints.length;i++)
 77         wp[i] = {'location': new google.maps.LatLng(os.waypoints[i][0], os.waypoints[i][1]),'stopover':false }
 78     ser.route({'origin':new google.maps.LatLng(os.start.lat,os.start.lng),
 79     'destination':new google.maps.LatLng(os.end.lat,os.end.lng),
 80     'waypoints': wp,
 81     'travelMode': google.maps.DirectionsTravelMode.DRIVING},function(res,sts) {
 82         if(sts=='OK')ren.setDirections(res);
 83     })
 84 }
 85 function saveWaypoints(){
 86   var w=[],wp;
 87   var rleg = ren.directions.routes[0].legs[0];
 88   data.start = {'lat': rleg.start_location.lat(), 'lng':rleg.start_location.lng()}
 89   data.end = {'lat': rleg.end_location.lat(), 'lng':rleg.end_location.lng()}
 90   var wp = rleg.via_waypoints
 91   for(var i=0;i<wp.length;i++)w[i] = [wp[i].lat(),wp[i].lng()]
 92   data.waypoints = w;
 93   var str = JSON.stringify(data);
 94   // Send data to fields
 95   $('#going_start_location').val(data.start.lat+','+data.start.lng);
 96   $('#going_waypoints').val(JSON.stringify(w));
 97   $('#going_end_location').val(data.end.lat+','+data.end.lng);
 98 }
 99 $(function(){
100   $('#save_going').click(function(e) { saveWaypoints() });
101 });

The initialize() is commented in the important parts and excepts the bounds the rest is kind of standard to show a map, so I will pass it and explain about the loadRoute(os) and saveWaypoints() functions.

The Bounds

The bounds is a way I find really good to limit the area where the user can navigate, He doesn't need to see China once my app is in one state only. On line 36 I set the limit coordinates and add a drag listener to verify if the user dragged until the limit. The code is simple so I won't go into details.

Saving the route

When you save the way points into database using this script, it end like this: [[-10.1977188,-48.338372400000026],[-10.204542,-48.34220600000003],[-10.2108154,-48.3414487],[-10.2108614,-48.3248532001],[-10.2238141,-48.3255121999],[-10.2195964,-48.34521760001]]

Arrays within an array, each waypoints you create on dragging the route has coordinates, this set above is all of them chained into one line.

On line 87 rleg receive legs[0]. This legs[0] means the route you've created. We are going to work with it. The app saves the start/end location and the waypoints as you can see on the lines 88-89 and 90-92 respectively. Lines 90-92 are responsable to make the waypoints look the same you saw in the beginning of this session. On line 93 the JSON.stringfy turn the data to literal string so it can be save into DB. At the end I just set the values to the fields in the form.

Loading the route

Loading the route is basically the inverse of saving, so you need to get the strings saved into db and put them in the function by passing the values through the parameter:

1 <script>
2   $( document ).ready(function() {
3     initialize(true);
4     loadRoute(<%= raw '{"start":{"lat":'+@going.start_location.split(",")[0].to_s+',"lng":'+@going.start_location.split(",")[1].to_s+'},"end":{"lat":'+@going.end_location.split(",")[0].to_s+',"lng":'+@going.end_location.split(",")[1].to_s+'},"waypoints":'+@going.waypoints.to_s+'}' %>);
5   });
6   </script>

As you can see I use Ruby to pass the values.

Well, after all this you can save/load you waypoints.

Medo de ser bom?

É estranho quando você sabe algo mas tem um certo receio de falar. Isso acontece muito comigo, mais frequentemente com o inglês falado. Tenha uma sensação de medo de falar em inglês dando o meu melhor e, ao invés disso, eu acabo falando inglês aportuguesado. E nem é muita coisa, poucas palavras, termos técnicos, etc.

Talvez exista uma pressão invisível por quem não sabe para você não ser um babaca. Mas você não está sendo um babaca, você sabe aquilo, você se esforçou pra isso, não estava brincando e não é coisa exclusiva que só você pode aprender.

Como Fábio Akita diz, não e talvez são a mesma coisa. Se você sabe algo, ficar se expondo é babaquisse, ainda mais se na hora do vamo-ver você não souber nada, mas se alguém te perguntar se você sabe, responda que sim se realmente o souber.

Acostume-se a dizer Sim o tempo todo e encare as consequências.

Não estou dizendo pra fazer todos aqueles favorzinhos que aparecem e que você não ganha nada com isso.

O importante é a mensagem ser entendida. Eu realmente gosto daquelas pessoas que conseguem falar de algo tão técnico de forma clara, usando de palavras comuns para atingir o objetivo, incluindo mais pessoas no entendimento da mensagem. O que no final é o objetivo de uma comunicação, passar a mensagem. Pessoas que falam dessa forma, são as que mais conhecem o que estão falando, a ponto de elevar o nível da conversa, assim como na computação, abstraíndo termos e técnicas mais complicadas.

Tentar complicar o que você quer dizer é só uma forma de mostrar que você não entende muito do que está falando. E por alguns termos técnicos, querer passar a impressão que tem total domínio do assunto.

Rewind: 2013

This was a year of changes for me. I moved to Palmas - TO to work, the job itself, living only with friends and my girlfriend.

I've encountered rocks on the path, big ones, crazy ones, but in the end they were all good because I've grown up.

It all started in October 2012, with a job exam for civil servant, which turns out me and 3 more friends have passed, but we were just called to work at march 2013, pretty much time, isn't it? Well, This was a life changer.

The whole new life helps to grown up a bit more, get more responsibilities, learn new stuff about life. Also, as I worked, I experienced things that made me think more about the kind of future I want. I'm now sure where I wanna go, I'm gonna do everything to get there. Thus, from this year that's about to start I'm gonna be dedicated to improve myself, learn a lot, start cool projects, to not get stuck without action (zombie mode), investing in new material and tools, just for the sake of a better future.

I already have a basic plan for 2014, one of them started this December, the other ones is upcoming.

2013 was for sure a great year for me and I'm gonna work hard to make 2014 better.

Happy new year!

Retrospectiva: 2013

Este foi um ano de mudanças para mim. I me mudei para Palmas - TO a trabalho, o próprio trabalho, viver somente com amigos e minha namorada e mais recentemente apenas com minha namorada.

Encontrei pedras no caminho, algumas grandes, outras malucas, mas no final elas foram todas boas, pois eu cresci.

Tudo começou em Outubro de 2012, com um concurso, que ao final eu e mais 3 colegas (contando com minha namorada) tinham passado, porém só seriamos chamados para entrar em exercício em 2013, muito tempo, não é? De qualquer forma, isso mudou minha vida.

Toda essa nova vida me ajudou a crescer um pouco mais, ter mais responsabilidades, aprender coisas novas da vida. Também, enquanto trabalhava, eu vivi coisas que me fizeram pensar mais sobre o tipo de futuro que eu quero. Eu estou certo agora de onde eu quero ir, e vou fazer tudo para chegar lá. Assim, este ano é todo para eu começar a me dedicar a aprender muito, começar projetos bacanas, à não ficar imóvel (modo zombie), investir em novos materiais e ferramentas, tudo por um futuro melhor.

Eu já tenho um plano básico para 2014, um deles começou em Dezembro os outros estão vindo aí.

2013 foi com certeza um ótimo ano para mim e eu vou trabalhar duro para fazer 2014 melhor.

Feliz ano novo!

Menos

Recentemente percebi uma coisa, por mais simples que seja um objeto/serviço que preciso, busco todas as possibilidades que me dão, praticidade, simplicidade, design e, claro, tenha um bom preço. Mesmo que seja apenas uma carteira, lá estou eu, pesquisando, me informando, tentando pegar a melhor possível.

Isso ocorre também no meu aprendizado, porém, tendo a procrastinar depois de um tempo. Sinto como estivesse parado flutuando sem poder fazer nada, inútil. Acho que isso continua ocorrendo pelo fato de eu querer muitas coisas ao mesmo tempo, acabo ficando sem fazer nada.

Agora, acabei de notar, que devo ter menos coisas que tenha a possibilidade de me tirar o foco. Como faço muita coisa no computador, que é meu principal aliado e também vilão, tenho que diminuar a dependência, e usá-lo de forma mais focada.

Um navegador com várias páginas abertas, tende a te fazer querer ler várias coisas e acaba que você não termina de ler nem uma propriamente.

Minimizar as distrações

Email, redes sociais, e vários outros são hoje apresentados de forma integrada aos sistemas operacionais, trazendo uma enxurrada de notificações a todo instante. Reduzir ou até remover essas notificações, pode trazer um benefício muito grande e certamente aumentará o seu foco e, consequentemente, a produtividade.

É interessante, também, diminuir (se possível) a quantidade de coisas pra fazer. Uma vez que se tem poucas coisas, você pode focar nelas melhor e trazer melhores resultados a tarefa.

São poucas dicas, mas que pode fazer uma enorme diferença se feitas corretamente. Alguma dica?